| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- <?php
- namespace App\Services;
- use App\Http\helper\NumConfig;
- use App\Models\RecordScoreInfo;
- use App\Models\UserCoupon;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Log;
- /**
- * 优惠券服务
- *
- * 职责:
- * 1. 自动发放优惠券(根据配置策略)
- * 2. 获取用户可用优惠券列表
- * 3. 使用优惠券(充值成功后:增加金币 + 标记已用)
- */
- class CouponService
- {
- /** @var int 优惠券赠送金币的原因码 */
- const SCORE_REASON = 55;
- /**
- * 获取用户可用优惠券列表,同时触发自动发放逻辑
- *
- * @param int $userId
- * @return array ['list' => [...], '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'];
- }
- }
- }
|