|
@@ -0,0 +1,378 @@
|
|
|
|
|
+<?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'];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|