find(); if (!$agent) { return 0; } return max(0, round((float) $agent->voucher_released - (float) $agent->voucher_used, 2)); } public function consume($userId, $amount, $orderId = 0) { $amount = round((float) $amount, 2); if ($amount <= 0) { return true; } $agent = AgentModel::where('user_id', $userId)->lock(true)->find(); if (!$agent) { error_stop('兑换券账户不存在'); } $available = max(0, round((float) $agent->voucher_released - (float) $agent->voucher_used, 2)); if (bccomp((string) $available, (string) $amount, 2) < 0) { error_stop('兑换券可用额度不足'); } $agent->setInc('voucher_used', $amount); PartnerRewardService::write([ 'order_id' => $orderId, 'buyer_id' => $userId, 'user_id' => $userId, 'role' => 'taster', 'reward_type' => 'voucher_consume', 'amount' => -$amount, 'base_amount' => $amount, 'ext' => ['formula' => "订单抵扣兑换券 {$amount}", 'order_id' => $orderId], 'status' => 1, ]); return true; } public function refundConsume($order, $refundFee = null) { $ext = $order['ext'] ?? []; if (is_string($ext)) { $ext = json_decode($ext, true) ?: []; } $totalVoucher = round((float) ($ext['voucher_fee'] ?? 0), 2); if ($totalVoucher <= 0 || empty($ext['voucher_consumed'])) { return; } $userId = intval($order['user_id'] ?? 0); $orderId = intval($order['id'] ?? 0); // 幂等防重:检查是否已退回过兑换券,避免多次退款事件重复返还额度 $alreadyRefunded = (float) RewardModel::where('order_id', $orderId) ->where('reward_type', 'voucher_consume_refund') ->sum('amount'); $remainVoucher = max(0, round((float) $totalVoucher - $alreadyRefunded, 2)); if ($remainVoucher <= 0) { return; } $fee = $refundFee !== null ? min($remainVoucher, round((float) $refundFee, 2)) : $remainVoucher; if ($fee <= 0) { return; } $agent = AgentModel::where('user_id', $userId)->lock(true)->find(); if ($agent) { $agent->voucher_used = max(0, round((float) $agent->voucher_used - $fee, 2)); $agent->save(); PartnerRewardService::write([ 'order_id' => $orderId, 'buyer_id' => $userId, 'user_id' => $userId, 'role' => 'taster', 'reward_type' => 'voucher_consume_refund', 'amount' => $fee, 'base_amount' => $fee, 'ext' => ['formula' => "订单退款退回兑换券 {$fee}", 'order_id' => $orderId], 'status' => 1, ]); if (is_object($order) && method_exists($order, 'save')) { $ext['voucher_refunded'] = round($alreadyRefunded + $fee, 2); if (bccomp((string) $ext['voucher_refunded'], (string) $totalVoucher, 2) >= 0) { $ext['voucher_consumed'] = 0; } $order->ext = $ext; $order->save(); } } } public function consumeByOrder($order) { $ext = $order['ext'] ?? []; if (is_string($ext)) { $ext = json_decode($ext, true) ?: []; } $fee = round((float) ($ext['voucher_fee'] ?? 0), 2); if ($fee <= 0 || !empty($ext['voucher_consumed'])) { return; } $this->consume(intval($order['user_id']), $fee, intval($order['id'] ?? 0)); if (is_object($order) && method_exists($order, 'save')) { $ext['voucher_consumed'] = 1; $order->ext = $ext; $order->save(); } } public function grant($order, $buyerId, $rules = null) { $exists = VoucherPlan::where('order_id', $order['id'])->find(); if ($exists) { return $exists; } $times = (isset($rules['voucher_times']) && is_numeric($rules['voucher_times'])) ? (float) $rules['voucher_times'] : (float) PartnerConfig::get('partner_voucher_times', 1.5); $days = (isset($rules['voucher_days']) && is_numeric($rules['voucher_days'])) ? intval($rules['voucher_days']) : intval(PartnerConfig::get('partner_voucher_days', 365)); $days = $days > 0 ? $days : 365; $rewardService = new PartnerRewardService(); $qualifiedPayFee = $rewardService->getQualifiedPayFee($order); $total = round((float) $qualifiedPayFee * $times, 2); if ($total <= 0) { return null; } $daily = round($total / $days, 2); // 生产标准模式:下单次日生效起算,按 365 天线性释放 $startDate = date('Y-m-d', strtotime('+1 day', $this->orderTime($order))); $plan = VoucherPlan::create([ 'user_id' => $buyerId, 'order_id' => $order['id'], 'total_amount' => $total, 'daily_amount' => $daily, 'released_amount' => 0, 'released_days' => 0, 'total_days' => $days, 'start_date' => $startDate, 'status' => 1, 'createtime' => time(), ]); $agent = AgentModel::where('user_id', $buyerId)->find(); if ($agent) { $agent->setInc('voucher_total', $total); } PartnerRewardService::write([ 'order_id' => $order['id'], 'buyer_id' => $buyerId, 'user_id' => $buyerId, 'role' => 'taster', 'reward_type' => 'voucher_grant', 'amount' => 0, 'base_amount' => $total, 'ext' => [ 'formula' => "计奖实付{$qualifiedPayFee} × {$times} = {$total},{$days}天释放", 'qualified_pay_fee' => $qualifiedPayFee, 'order_pay_fee' => $order['pay_fee'] ?? 0 ], 'status' => 1, ]); return $plan; } /** * 临时分钟级释放兑换券:每 1 分钟对所有释放中的计划释放一期 * * @return float 本次释放总金额 */ public function releaseMinute() { $plans = VoucherPlan::where('status', 1)->select(); $totalReleased = 0; foreach ($plans as $plan) { if ($plan->released_days >= $plan->total_days || bccomp((string) $plan->released_amount, (string) $plan->total_amount, 2) >= 0) { $plan->status = 2; $plan->save(); continue; } $leftDays = $plan->total_days - $plan->released_days; if ($leftDays <= 1) { $amount = round((float) $plan->total_amount - (float) $plan->released_amount, 2); $plan->status = 2; } else { $amount = (float) $plan->daily_amount; } if ($amount <= 0) { $plan->status = 2; $plan->save(); continue; } $plan->released_amount = round((float) $plan->released_amount + $amount, 2); $plan->released_days += 1; if (bccomp((string) $plan->released_amount, (string) $plan->total_amount, 2) >= 0) { $plan->status = 2; } $plan->save(); $agent = AgentModel::where('user_id', $plan->user_id)->find(); if ($agent) { $agent->setInc('voucher_released', $amount); } // 唯一负数 order_id 规避与历史冲突;settle_date 设为 NULL 规避 uk_daily_user_type 唯一索引 $virtualOrderId = -(intval($plan->id) * 1000 + (intval($plan->released_days) % 1000)); PartnerRewardService::write([ 'order_id' => $virtualOrderId, 'buyer_id' => $plan->user_id, 'user_id' => $plan->user_id, 'role' => 'taster', 'reward_type' => 'voucher_release', 'amount' => $amount, 'base_amount' => $amount, 'settle_date' => null, 'ext' => [ 'formula' => "兑换券释放 {$amount}(第{$plan->released_days}/{$plan->total_days}期)", 'plan_id' => $plan->id, 'order_id' => $plan->order_id, 'released_days' => $plan->released_days, ], 'status' => 1, ]); $totalReleased = bcadd((string) $totalReleased, (string) $amount, 2); } return (float) $totalReleased; } public function releaseByDate($settleDate) { // 幂等防重:检查该结算日期是否已释放过兑换券 $existingCount = RewardModel::where('settle_date', $settleDate) ->where('reward_type', 'voucher_release') ->count(); if ($existingCount > 0) { return (float) RewardModel::where('settle_date', $settleDate) ->where('reward_type', 'voucher_release') ->sum('amount'); } $plans = VoucherPlan::where('status', 1) ->where('start_date', '<=', $settleDate) ->select(); $byUser = []; foreach ($plans as $plan) { $amount = $this->releaseOne($plan, $settleDate); if ($amount <= 0) { continue; } $uid = intval($plan->user_id); if (!isset($byUser[$uid])) { $byUser[$uid] = ['amount' => 0, 'plans' => []]; } $byUser[$uid]['amount'] = bcadd((string) $byUser[$uid]['amount'], (string) $amount, 2); $byUser[$uid]['plans'][] = $plan->id; } $totalReleased = 0; foreach ($byUser as $userId => $info) { PartnerRewardService::write([ 'order_id' => PartnerRewardService::dailyOrderId($settleDate), 'buyer_id' => $userId, 'user_id' => $userId, 'role' => 'taster', 'reward_type' => 'voucher_release', 'amount' => $info['amount'], 'base_amount' => $info['amount'], 'settle_date' => $settleDate, 'ext' => [ 'formula' => "兑换券释放 {$info['amount']}", 'plan_ids' => $info['plans'], ], 'status' => 1, ]); $totalReleased = bcadd((string) $totalReleased, (string) $info['amount'], 2); } return (float) $totalReleased; } public function releaseOne($plan, $settleDate) { if ($plan->status != 1) { return 0; } if ($plan->released_days >= $plan->total_days) { $plan->status = 2; $plan->save(); return 0; } if ($plan->start_date > $settleDate) { return 0; } $leftDays = $plan->total_days - $plan->released_days; if ($leftDays <= 1) { $amount = round((float) $plan->total_amount - (float) $plan->released_amount, 2); $plan->status = 2; } else { $amount = (float) $plan->daily_amount; } if ($amount <= 0) { $plan->status = 2; $plan->save(); return 0; } $plan->released_amount = round((float) $plan->released_amount + $amount, 2); $plan->released_days += 1; $plan->save(); $agent = AgentModel::where('user_id', $plan->user_id)->find(); if ($agent) { $agent->setInc('voucher_released', $amount); } return $amount; } public function closeByOrder($orderId) { $plan = VoucherPlan::where('order_id', $orderId)->find(); if (!$plan || $plan->status == 0) { return; } $plan->status = 0; $plan->save(); $agent = AgentModel::where('user_id', $plan->user_id)->lock(true)->find(); if ($agent) { $releasedAmount = round((float) $plan->released_amount, 2); $totalAmount = round((float) $plan->total_amount, 2); $currentReleased = round((float) $agent->voucher_released, 2); $currentUsed = round((float) $agent->voucher_used, 2); $newTotal = max(0, round((float) $agent->voucher_total - $totalAmount, 2)); $newReleased = max(0, round($currentReleased - $releasedAmount, 2)); $overdraft = 0; if ($newReleased < $currentUsed) { $overdraft = round($currentUsed - $newReleased, 2); $newReleased = $currentUsed; } $agent->voucher_released = $newReleased; $agent->voucher_total = $newTotal; $agent->save(); if ($overdraft > 0) { \think\Log::record("[VoucherService] 订单 {$orderId} 退款关闭计划,用户 {$agent->user_id} 已透支兑换券额度: {$overdraft}", 'warning'); } } } public function reduceByRefund($orderId, $refundItemFee) { $plan = VoucherPlan::where('order_id', $orderId)->find(); if (!$plan || $plan->status == 0) { return; } $times = (float) PartnerConfig::get('partner_voucher_times', 1.5); $reduceTotal = round((float) $refundItemFee * $times, 2); if ($reduceTotal <= 0) { return; } $reduceTotal = min($reduceTotal, (float) $plan->total_amount); $plan->total_amount = max(0, round((float) $plan->total_amount - $reduceTotal, 2)); $leftDays = max(1, $plan->total_days - $plan->released_days); $plan->daily_amount = round(($plan->total_amount - $plan->released_amount) / $leftDays, 2); if ($plan->total_amount <= $plan->released_amount) { $plan->status = 2; } $plan->save(); $agent = AgentModel::where('user_id', $plan->user_id)->lock(true)->find(); if ($agent) { $agent->voucher_total = max(0, round((float) $agent->voucher_total - $reduceTotal, 2)); $agent->save(); } } public function rollbackRelease($settleDate) { $rows = RewardModel::where('settle_date', $settleDate) ->where('reward_type', 'voucher_release') ->where('status', 1) ->select(); foreach ($rows as $row) { $planIds = $row->ext['plan_ids'] ?? []; if (!$planIds && !empty($row->ext['plan_id'])) { $planIds = [$row->ext['plan_id']]; } foreach ($planIds as $planId) { $plan = VoucherPlan::get($planId); if (!$plan) { continue; } $daily = (float) $plan->daily_amount; $plan->released_amount = max(0, (float) $plan->released_amount - $daily); $plan->released_days = max(0, $plan->released_days - 1); if ($plan->status == 2 && $plan->released_days < $plan->total_days) { $plan->status = 1; } $plan->save(); } $agent = AgentModel::where('user_id', $row->user_id)->find(); if ($agent) { $agent->voucher_released = max(0, (float) $agent->voucher_released - (float) $row->amount); $agent->save(); } } } protected function orderTime($order) { $paid = $order['paid_time'] ?? 0; if (is_numeric($paid) && $paid > 0) { return intval($paid); } if (is_string($paid) && $paid !== '') { $ts = strtotime($paid); if ($ts) { return $ts; } } return time(); } protected function dailyOrderId($settleDate, $planId) { // 避免 uk_order_user_type 日结撞车:负日期 + 计划尾巴 return -intval(date('Ymd', strtotime($settleDate))) * 100 + intval($planId) % 100; } }