[...], 'new_issued' => [...]] */ public function getCouponList($userId) { // 1. 先清理已过期的优惠券 UserCoupon::expireByUser($userId); // 2. 自动发放符合条件的优惠券 $newIssued = $this->autoIssue($userId); // 3. 返回当前可用列表 $list = UserCoupon::getAvailableList($userId); return [ 'list' => $list, 'new_issued' => $newIssued, ]; } /** * 自动发放优惠券(根据 config/coupon.php 中的 auto_issue_rules) * * 发放逻辑: * - 遍历所有规则 * - 检查用户是否满足发放条件 * - 检查用户是否已持有同名有效券(防重复) * - 满足条件则发放 * * @param int $userId * @return array 本次新发放的优惠券列表 */ public function autoIssue($userId) { $rules = config('coupon.auto_issue_rules', []); $enabled = config('coupon.enabled_rules', []); $allEnabled = in_array('*', $enabled, true); $newIssued = []; foreach ($rules as $rule) { // 仅处理当前生效的规则('*' 表示全部生效) if (!$allEnabled && !in_array($rule['coupon_name'], $enabled, true)) { continue; } // 防重复:已持有同名有效券则跳过 if (UserCoupon::hasSameCoupon($userId, $rule['coupon_name'])) { continue; } // 检查发放条件 if (!$this->checkCondition($userId, $rule['condition'])) { continue; } // 发放优惠券 $now = date('Y-m-d H:i:s'); $couponData = [ 'user_id' => $userId, 'coupon_name' => $rule['coupon_name'], 'coupon_type' => $rule['coupon_type'], 'coupon_value' => $rule['coupon_value'], 'min_recharge' => $rule['min_recharge'] ?? 0, 'max_bonus' => $rule['max_bonus'] ?? 0, 'bonus_coins' => 0, 'order_sn' => '', 'status' => UserCoupon::STATUS_UNUSED, 'issued_at' => $now, 'used_at' => null, 'expire_at' => date('Y-m-d H:i:s', strtotime("+{$rule['valid_days']} days")), ]; try { $id = UserCoupon::issueToUser($couponData); $couponData['id'] = $id; $newIssued[] = $couponData; Log::info('Coupon auto-issued', [ 'user_id' => $userId, 'coupon_name' => $rule['coupon_name'], 'coupon_id' => $id, ]); } catch (\Exception $e) { Log::error('Coupon auto-issue failed', [ 'user_id' => $userId, 'coupon_name' => $rule['coupon_name'], 'error' => $e->getMessage(), ]); } } return $newIssued; } /** * 检查用户是否满足发放条件 * * @param int $userId * @param array $condition ['type' => '...', '...' => ...] * @return bool */ protected function checkCondition($userId, array $condition) { $type = $condition['type'] ?? ''; switch ($type) { // 新人条件:注册天数 <= N 且无充值记录 case 'new_user': $days = $condition['days'] ?? 3; return $this->isNewUser($userId, $days); // 累计充值 >= N 元 case 'recharge_total': $amount = $condition['amount'] ?? 0; return $this->hasRechargeTotal($userId, $amount); // 最近充值距今 >= N 天(且有过充值记录) case 'inactive_days': $days = $condition['days'] ?? 7; return $this->isInactive($userId, $days); // VIP 等级 >= N case 'vip_level': $level = $condition['level'] ?? 1; return $this->hasVipLevel($userId, $level); default: Log::warning('Unknown coupon condition type', ['type' => $type]); return false; } } /** * 判断是否为新用户:注册天数 <= N 且无充值记录 * * @param int $userId * @param int $days 注册天数阈值 * @return bool */ protected function isNewUser($userId, $days) { $user = DB::connection('write')->table('QPAccountsDB.dbo.AccountsInfo') ->where('UserID', $userId) ->select('RegisterDate') ->first(); if (!$user || !$user->RegisterDate) { return false; } // 注册天数 <= 阈值 $registerDate = strtotime($user->RegisterDate); $diffDays = (time() - $registerDate) / 86400; if ($diffDays > $days) { return false; } // 无充值记录 $hasRecharge = DB::connection('write')->table('agent.dbo.order') ->where('user_id', $userId) ->where('pay_status', 1) ->exists(); return !$hasRecharge; } /** * 判断累计充值是否达到阈值 * * @param int $userId * @param float $amount 阈值(元) * @return bool */ protected function hasRechargeTotal($userId, $amount) { $total = DB::connection('write')->table('QPAccountsDB.dbo.YN_VIPAccount') ->where('UserID', $userId) ->value('Recharge'); return ($total ?? 0) >= $amount; } /** * 判断用户是否处于不活跃状态:最近一次充值距今 >= N 天 * 前提:用户至少有过一次充值记录 * * @param int $userId * @param int $days * @return bool */ protected function isInactive($userId, $days) { $lastRecharge = DB::connection('write')->table('agent.dbo.order') ->where('user_id', $userId) ->where('pay_status', 1) ->max('pay_at'); if (!$lastRecharge) { return false; // 从未充值,不适用"回归"逻辑 } $lastTime = strtotime($lastRecharge); $diffDays = (time() - $lastTime) / 86400; return $diffDays >= $days; } /** * 判断 VIP 等级是否达到阈值 * * @param int $userId * @param int $level * @return bool */ protected function hasVipLevel($userId, $level) { // 使用 VipService 获取用户当前VIP等级 $userRecharge = DB::connection('write')->table('QPAccountsDB.dbo.YN_VIPAccount') ->where('UserID', $userId) ->value('Recharge') ?? 0; $vipLevel = VipService::calculateVipLevel($userId, $userRecharge); return $vipLevel >= $level; } /** * 验证优惠券是否可用于本次充值 * * @param int $couponId * @param int $userId * @param float $payAmt 充值金额(元) * @return array ['valid' => bool, 'coupon' => object|null, 'error' => string] */ public function validateCoupon($couponId, $userId, $payAmt) { $coupon = UserCoupon::getUserCouponById($couponId, $userId); if (!$coupon) { return ['valid' => false, 'coupon' => null, 'error' => 'Coupon not found']; } if ($coupon->status != UserCoupon::STATUS_UNUSED) { return ['valid' => false, 'coupon' => $coupon, 'error' => 'Coupon already used or expired']; } if (strtotime($coupon->expire_at) < time()) { return ['valid' => false, 'coupon' => $coupon, 'error' => 'Coupon expired']; } if ($payAmt < $coupon->min_recharge) { return [ 'valid' => false, 'coupon' => $coupon, 'error' => "Minimum recharge {$coupon->min_recharge} required for this coupon", ]; } return ['valid' => true, 'coupon' => $coupon, 'error' => '']; } /** * 计算优惠券赠送的金币数(单位:分) * * @param object $coupon 优惠券记录 * @param float $payAmt 充值金额(元) * @return int 赠送金币数(分) */ public function calcBonusCoins($coupon, $payAmt) { if ($coupon->coupon_type == UserCoupon::TYPE_FIXED) { // 固定金额 $bonusYuan = $coupon->coupon_value; } else { // 百分比:赠送金额 = 充值金额 * 百分比 $bonusYuan = $payAmt * ($coupon->coupon_value / 100); // 上限限制 if ($coupon->max_bonus > 0 && $bonusYuan > $coupon->max_bonus) { $bonusYuan = $coupon->max_bonus; } } // 转换为分(1元 = 100分) return (int) round($bonusYuan * NumConfig::NUM_VALUE); } /** * 使用优惠券:标记已用 + 发放金币 * * 在充值回调中调用此方法。 * * @param int $couponId * @param int $userId * @param float $payAmt 充值金额(元) * @param string $orderSn * @return array ['success' => bool, 'bonus_coins' => int, 'error' => string] */ public function useCoupon($couponId, $userId, $payAmt, $orderSn) { // 验证 $validation = $this->validateCoupon($couponId, $userId, $payAmt); if (!$validation['valid']) { return ['success' => false, 'bonus_coins' => 0, 'error' => $validation['error']]; } $coupon = $validation['coupon']; // 计算赠送金币 $bonusCoins = $this->calcBonusCoins($coupon, $payAmt); if ($bonusCoins <= 0) { return ['success' => false, 'bonus_coins' => 0, 'error' => 'Bonus amount is zero']; } // 标记优惠券已使用(乐观锁:status=unused 才更新) $marked = UserCoupon::markUsed($coupon->id, $orderSn, $bonusCoins); if (!$marked) { return ['success' => false, 'bonus_coins' => 0, 'error' => 'Coupon already used']; } // 发放金币到用户账户 try { $reason = config('coupon.score_reason', self::SCORE_REASON); RecordScoreInfo::addScore($userId, $bonusCoins, $reason); // 实际增加 GameScoreInfo.Score DB::connection('write')->table('QPTreasureDB.dbo.GameScoreInfo') ->where('UserID', $userId) ->increment('Score', $bonusCoins); Log::info('Coupon bonus coins added', [ 'user_id' => $userId, 'coupon_id' => $coupon->id, 'order_sn' => $orderSn, 'bonus_coins' => $bonusCoins, 'pay_amt' => $payAmt, ]); return ['success' => true, 'bonus_coins' => $bonusCoins, 'error' => '']; } catch (\Exception $e) { Log::error('Coupon add coins failed', [ 'user_id' => $userId, 'coupon_id' => $coupon->id, 'error' => $e->getMessage(), ]); return ['success' => false, 'bonus_coins' => 0, 'error' => 'Failed to add bonus coins']; } } }