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; } $fee = $refundFee !== null ? min($totalVoucher, round((float) $refundFee, 2)) : $totalVoucher; if ($fee <= 0) { return; } $userId = intval($order['user_id'] ?? 0); $orderId = intval($order['id'] ?? 0); $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, ]); } } 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) { $exists = VoucherPlan::where('order_id', $order['id'])->find(); if ($exists) { return $exists; } $times = (float) PartnerConfig::get('partner_voucher_times', 1.5); $days = intval(PartnerConfig::get('partner_voucher_days', 365)); $days = $days > 0 ? $days : 365; $total = round((float) $order['pay_fee'] * $times, 2); if ($total <= 0) { return null; } $daily = round($total / $days, 2); $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' => "实付{$order['pay_fee']} × {$times} = {$total},{$days}天释放"], 'status' => 1, ]); return $plan; } public function releaseByDate($settleDate) { $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; } }