Browse Source

fix:验货员

super-yimizi 2 months ago
parent
commit
4b36826592

+ 326 - 54
application/api/controller/Lottery.php

@@ -1,85 +1,357 @@
 <?php
 
-
 namespace app\api\controller;
 
-
 use app\common\controller\Api;
-use think\Cache;
-use think\Request;
-use app\services\lottery\LuckLotteryRecordServices;
-use app\services\lottery\LuckLotteryServices;
-
+use app\common\Service\LotteryService;
+use app\common\Service\LotteryChanceService;
+use app\common\model\lottery\LotteryActivity;
+use app\common\model\lottery\LotteryPrize;
+use app\common\model\lottery\LotteryDrawRecord;
+use app\common\model\lottery\LotteryWinRecord;
+use app\common\model\lottery\LotteryUserChance;
+use app\common\library\Auth;
+use think\Exception;
 
-class LuckLottery extends Api
+/**
+ * 抽奖API控制器
+ */
+class Lottery extends Api
 {
+    protected $noNeedLogin = ['activityList', 'activityDetail'];
     protected $noNeedRight = ['*'];
-    protected $services;
 
-    public function __construct(LuckLotteryServices $services)
+    /**
+     * 获取抽奖活动列表
+     */
+    public function activityList()
+    {
+        $page = $this->request->param('page/d', 1);
+        $limit = $this->request->param('limit/d', 10);
+        $status = $this->request->param('status/d', 1); // 默认只返回进行中的活动
+
+        $where = [];
+        if ($status !== '') {
+            $where['status'] = $status;
+        }
+
+        // 只返回进行中且在时间范围内的活动
+        $now = time();
+        $where['start_time'] = ['<=', $now];
+        $where['end_time'] = ['>=', $now];
+
+        $activities = LotteryActivity::where($where)
+                                    ->field('id,name,description,cover_image,type,status,start_time,end_time,lottery_type,guide_image,guide_text,intro_content')
+                                    ->order('createtime desc')
+                                    ->page($page, $limit)
+                                    ->select();
+
+        $list = [];
+        foreach ($activities as $activity) {
+            $item = $activity->toArray();
+            
+            // 获取奖品信息
+            $prizes = LotteryPrize::where('activity_id', $activity->id)
+                                  ->where('status', 1)
+                                  ->where('type', '>', 1) // 排除未中奖类型
+                                  ->field('id,name,type,image,probability')
+                                  ->order('sort_order asc')
+                                  ->select();
+            $item['prizes'] = $prizes;
+            
+            // 统计信息
+            $item['total_participants'] = LotteryUserChance::where('activity_id', $activity->id)->count();
+            
+            $list[] = $item;
+        }
+
+        $this->success('获取成功', [
+            'list' => $list,
+            'total' => LotteryActivity::where($where)->count()
+        ]);
+    }
+
+    /**
+     * 获取活动详情
+     */
+    public function activityDetail()
+    {
+        $activityId = $this->request->param('activity_id/d');
+        if (!$activityId) {
+            $this->error('参数错误');
+        }
+
+        $activity = LotteryActivity::find($activityId);
+        if (!$activity) {
+            $this->error('活动不存在');
+        }
+
+        $detail = $activity->toArray();
+
+        // 获取奖品列表
+        $prizes = LotteryPrize::where('activity_id', $activityId)
+                              ->where('status', 1)
+                              ->field('id,name,type,image,description,probability,total_stock,remain_stock,win_prompt,sort_order,unlock_people_num')
+                              ->order('sort_order asc')
+                              ->select();
+        
+        // 检查奖品是否已解锁(如果开启按人数解锁)
+        if ($activity->unlock_by_people) {
+            $currentPeopleCount = LotteryUserChance::where('activity_id', $activityId)->count();
+            foreach ($prizes as &$prize) {
+                $prize['is_unlocked'] = $prize->isUnlocked($currentPeopleCount);
+            }
+        } else {
+            foreach ($prizes as &$prize) {
+                $prize['is_unlocked'] = true;
+            }
+        }
+
+        $detail['prizes'] = $prizes;
+
+        // 统计信息
+        $detail['total_participants'] = LotteryUserChance::where('activity_id', $activityId)->count();
+
+        // 如果用户已登录,返回用户相关信息
+        if (Auth::instance()->isLogin()) {
+            $userId = Auth::instance()->id;
+            $detail['user_chances'] = LotteryService::getUserChances($activityId, $userId);
+            $detail['user_draw_count'] = LotteryDrawRecord::getUserDrawCount($activityId, $userId);
+            $detail['user_win_count'] = LotteryDrawRecord::getUserWinCount($activityId, $userId);
+        }
+
+        $this->success('获取成功', $detail);
+    }
+
+    /**
+     * 执行抽奖
+     */
+    public function draw()
+    {
+        $activityId = $this->request->param('activity_id/d');
+        if (!$activityId) {
+            $this->error('参数错误');
+        }
+
+        $userId = Auth::instance()->id;
+        if (!$userId) {
+            $this->error('请先登录');
+        }
+
+        try {
+            $result = LotteryService::drawLottery($activityId, $userId);
+            $this->success('抽奖成功', $result);
+        } catch (Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取用户抽奖机会
+     */
+    public function getUserChances()
     {
-        parent::__construct();
-        $this->services = $services;
+        $activityId = $this->request->param('activity_id/d');
+        if (!$activityId) {
+            $this->error('参数错误');
+        }
+
+        $userId = Auth::instance()->id;
+        if (!$userId) {
+            $this->error('请先登录');
+        }
+
+        $chances = LotteryChanceService::getUserChanceDetail($activityId, $userId);
+        $this->success('获取成功', $chances);
     }
+
     /**
-     * 抽奖活动信息
-     * @param Request $request
+     * 获取用户抽奖记录
      */
-    public function LotteryInfo(Request $request)
+    public function getDrawRecords()
     {
-        $id = $request->get('id','1');
-        if (intval($id) <= 0) return  $this->error('请上传抽奖ID');
-        $lottery = $this->services->getLottery($id, '*', ['prize'], true);
-        if (!$lottery) {
-            $this->error('未获取到抽奖活动');
-        }
-        $lottery = $lottery->toArray();
-        $lotteryData = ['lottery' => $lottery];
-        $lotteryData['lottery_num'] = $this->auth->lottery_num ?? 0;
-        $this->success('获取成功', $lotteryData);
+        $activityId = $this->request->param('activity_id/d');
+        $page = $this->request->param('page/d', 1);
+        $limit = $this->request->param('limit/d', 20);
+
+        $userId = Auth::instance()->id;
+        if (!$userId) {
+            $this->error('请先登录');
+        }
+
+        $where = ['user_id' => $userId];
+        if ($activityId) {
+            $where['activity_id'] = $activityId;
+        }
+
+        $records = LotteryDrawRecord::where($where)
+                                   ->with(['activity', 'prize'])
+                                   ->order('createtime desc')
+                                   ->page($page, $limit)
+                                   ->select();
+
+        $list = [];
+        foreach ($records as $record) {
+            $item = [
+                'id' => $record->id,
+                'activity_id' => $record->activity_id,
+                'activity_name' => $record->activity->name ?? '',
+                'prize_id' => $record->prize_id,
+                'prize_name' => $record->prize->name ?? '',
+                'prize_type' => $record->prize->type ?? 0,
+                'prize_image' => $record->prize->image ?? '',
+                'is_win' => $record->is_win,
+                'draw_time' => $record->draw_time,
+                'trigger_type_text' => $record->trigger_type_text
+            ];
+
+            // 如果中奖,获取中奖记录详情
+            if ($record->is_win && $record->winRecord) {
+                $item['win_record'] = [
+                    'id' => $record->winRecord->id,
+                    'deliver_status' => $record->winRecord->deliver_status,
+                    'deliver_status_text' => $record->winRecord->deliver_status_text,
+                    'deliver_time' => $record->winRecord->deliver_time,
+                    'exchange_code' => $record->winRecord->exchange_code
+                ];
+            }
+
+            $list[] = $item;
+        }
+
+        $this->success('获取成功', [
+            'list' => $list,
+            'total' => LotteryDrawRecord::where($where)->count()
+        ]);
     }
 
     /**
-     * 参与抽奖
-     * @param Request $request
-     * @return mixed
+     * 获取用户中奖记录
      */
-    public function luckLottery(Request $request)
+    public function getWinRecords()
     {
-        $id = $request->get('id','1');
-        if (!$id) {
-            $this->error('请上传抽奖ID');
+        $page = $this->request->param('page/d', 1);
+        $limit = $this->request->param('limit/d', 20);
+
+        $userId = Auth::instance()->id;
+        if (!$userId) {
+            $this->error('请先登录');
         }
-        if ($this->auth->lottery_num <=0){
-            $this->error('抽奖次数不足');
+
+        $records = LotteryWinRecord::getUserWinRecords($userId, $page, $limit);
+
+        $list = [];
+        foreach ($records as $record) {
+            $item = [
+                'id' => $record->id,
+                'activity_id' => $record->activity_id,
+                'activity_name' => $record->activity->name ?? '',
+                'prize_id' => $record->prize_id,
+                'prize_name' => $record->prize_name,
+                'prize_type' => $record->prize_type,
+                'deliver_status' => $record->deliver_status,
+                'deliver_status_text' => $record->deliver_status_text,
+                'deliver_time' => $record->deliver_time,
+                'exchange_code' => $record->exchange_code,
+                'createtime' => $record->createtime
+            ];
+
+            // 根据奖品类型返回特定信息
+            $prizeValue = $record->prize_value_data;
+            switch ($record->prize_type) {
+                case LotteryPrize::TYPE_RED_PACKET:
+                    $item['amount'] = $prizeValue['amount'] ?? 0;
+                    break;
+                case LotteryPrize::TYPE_COUPON:
+                    $item['coupon_id'] = $prizeValue['coupon_id'] ?? 0;
+                    break;
+                case LotteryPrize::TYPE_GOODS:
+                    $item['goods_id'] = $prizeValue['goods_id'] ?? 0;
+                    $item['goods_sku_id'] = $prizeValue['goods_sku_id'] ?? 0;
+                    break;
+            }
+
+            $list[] = $item;
+        }
+
+        $this->success('获取成功', [
+            'list' => $list,
+            'total' => LotteryWinRecord::where('user_id', $userId)->count()
+        ]);
+    }
+
+    /**
+     * 设置中奖记录收货地址
+     */
+    public function setWinRecordAddress()
+    {
+        $winRecordId = $this->request->param('win_record_id/d');
+        $receiverName = $this->request->param('receiver_name');
+        $receiverMobile = $this->request->param('receiver_mobile');
+        $receiverAddress = $this->request->param('receiver_address');
+
+        if (!$winRecordId || !$receiverName || !$receiverMobile || !$receiverAddress) {
+            $this->error('参数不完整');
+        }
+
+        $userId = Auth::instance()->id;
+        if (!$userId) {
+            $this->error('请先登录');
         }
-        $uid =$this->auth->id;
-        $key = 'lucklotter_limit_' . $uid;
-        if (Cache::get($key)) {
-            $this->error('您求的频率太过频繁,请稍后请求!');
+
+        $winRecord = LotteryWinRecord::where('id', $winRecordId)
+                                     ->where('user_id', $userId)
+                                     ->find();
+        if (!$winRecord) {
+            $this->error('中奖记录不存在');
         }
-        Cache::set('lucklotter_limit_' . $uid, $uid, 1);
 
-        $arrUserinfo = $this->auth->getUser()->toArray();
-        list($nError,$arrData) = $this->services->luckLottery($arrUserinfo, $id);
-        if ($nError === 1 ) {
-            $this->error($arrData);
+        if ($winRecord->deliver_status != LotteryWinRecord::DELIVER_STATUS_PENDING) {
+            $this->error('该奖品已处理,无法修改地址');
+        }
+
+        try {
+            $winRecord->setDeliveryAddress($receiverName, $receiverMobile, $receiverAddress);
+            $this->success('设置成功');
+        } catch (Exception $e) {
+            $this->error('设置失败:' . $e->getMessage());
         }
-        $this->success('', $arrData);
     }
 
     /**
-     * 获取抽奖记录
-     * @param Request $request
-     * @return mixed
+     * 获取活动排行榜(中奖次数)
      */
-    public function lotteryRecord(Request $request)
+    public function getRanking()
     {
-        $uid      = $this->auth->id;
-        $pageSize = (int)$this->request->param('pageSize', 10);
-        $page     = (int)$this->request->param('page', 1);
-        $lotteryRecordServices = new LuckLotteryRecordServices();
-        $arrData = $lotteryRecordServices->getRecord($uid,$page,$pageSize);
-        $this->success('', $arrData);
+        $activityId = $this->request->param('activity_id/d');
+        $page = $this->request->param('page/d', 1);
+        $limit = $this->request->param('limit/d', 50);
+
+        if (!$activityId) {
+            $this->error('参数错误');
+        }
+
+        // 统计用户中奖次数
+        $ranking = LotteryWinRecord::where('activity_id', $activityId)
+                                   ->field('user_id, count(*) as win_count')
+                                   ->with('user')
+                                   ->group('user_id')
+                                   ->order('win_count desc')
+                                   ->page($page, $limit)
+                                   ->select();
+
+        $list = [];
+        $rank = ($page - 1) * $limit + 1;
+        foreach ($ranking as $item) {
+            $list[] = [
+                'rank' => $rank++,
+                'user_id' => $item->user_id,
+                'nickname' => $item->user->nickname ?? '匿名用户',
+                'avatar' => $item->user->avatar ?? '',
+                'win_count' => $item->win_count
+            ];
+        }
+
+        $this->success('获取成功', $list);
     }
 }

+ 357 - 0
application/common/Service/LotteryChanceService.php

@@ -0,0 +1,357 @@
+<?php
+
+namespace app\common\Service;
+
+use app\common\model\lottery\LotteryActivity;
+use app\common\model\lottery\LotteryCondition;
+use app\common\model\lottery\LotteryUserChance;
+use app\common\model\lottery\LotteryDrawRecord;
+use think\Exception;
+
+/**
+ * 抽奖机会服务类
+ * 处理用户获得抽奖机会的逻辑
+ */
+class LotteryChanceService
+{
+    /**
+     * 订单完成后检查并分发抽奖机会
+     * @param array $orderInfo 订单信息
+     * @param int $userId 用户ID
+     * @return array 获得的抽奖机会信息
+     */
+    public static function checkAndGrantChanceForOrder($orderInfo, $userId)
+    {
+        $grantedChances = [];
+        
+        // 获取所有正在进行的抽奖活动
+        $activities = LotteryActivity::getRunningActivities();
+        
+        foreach ($activities as $activity) {
+            try {
+                $chances = static::processActivityForOrder($activity, $orderInfo, $userId);
+                if ($chances > 0) {
+                    $grantedChances[] = [
+                        'activity_id' => $activity->id,
+                        'activity_name' => $activity->name,
+                        'chances' => $chances
+                    ];
+                }
+            } catch (Exception $e) {
+                // 记录错误但不影响其他活动的处理
+                trace('抽奖机会分发失败: ' . $e->getMessage(), 'error');
+            }
+        }
+        
+        return $grantedChances;
+    }
+
+    /**
+     * 充值完成后检查并分发抽奖机会
+     * @param array $rechargeInfo 充值信息
+     * @param int $userId 用户ID
+     * @return array 获得的抽奖机会信息
+     */
+    public static function checkAndGrantChanceForRecharge($rechargeInfo, $userId)
+    {
+        $grantedChances = [];
+        
+        // 获取所有正在进行的抽奖活动
+        $activities = LotteryActivity::getRunningActivities();
+        
+        foreach ($activities as $activity) {
+            try {
+                $chances = static::processActivityForRecharge($activity, $rechargeInfo, $userId);
+                if ($chances > 0) {
+                    $grantedChances[] = [
+                        'activity_id' => $activity->id,
+                        'activity_name' => $activity->name,
+                        'chances' => $chances
+                    ];
+                }
+            } catch (Exception $e) {
+                // 记录错误但不影响其他活动的处理
+                trace('抽奖机会分发失败: ' . $e->getMessage(), 'error');
+            }
+        }
+        
+        return $grantedChances;
+    }
+
+    /**
+     * 处理订单相关的活动
+     */
+    private static function processActivityForOrder($activity, $orderInfo, $userId)
+    {
+        // 检查用户是否符合活动参与资格
+        if (!static::checkUserQualification($activity, $userId)) {
+            return 0;
+        }
+
+        // 获取活动的参与条件
+        $conditions = LotteryCondition::getValidConditions($activity->id);
+        $totalChances = 0;
+
+        foreach ($conditions as $condition) {
+            // 跳过充值条件
+            if ($condition->type == LotteryCondition::TYPE_RECHARGE) {
+                continue;
+            }
+
+            $chances = static::processConditionForOrder($condition, $orderInfo, $userId);
+            $totalChances += $chances;
+        }
+
+        // 如果获得了抽奖机会,记录到数据库
+        if ($totalChances > 0) {
+            static::grantChanceToUser($activity->id, $userId, $totalChances, [
+                'trigger_type' => 'order',
+                'order_id' => $orderInfo['id'] ?? 0,
+                'order_amount' => $orderInfo['total_amount'] ?? 0,
+                'granted_time' => time()
+            ]);
+        }
+
+        return $totalChances;
+    }
+
+    /**
+     * 处理充值相关的活动
+     */
+    private static function processActivityForRecharge($activity, $rechargeInfo, $userId)
+    {
+        // 检查用户是否符合活动参与资格
+        if (!static::checkUserQualification($activity, $userId)) {
+            return 0;
+        }
+
+        // 获取活动的参与条件
+        $conditions = LotteryCondition::getValidConditions($activity->id);
+        $totalChances = 0;
+
+        foreach ($conditions as $condition) {
+            // 只处理充值条件
+            if ($condition->type != LotteryCondition::TYPE_RECHARGE) {
+                continue;
+            }
+
+            $chances = static::processConditionForRecharge($condition, $rechargeInfo, $userId);
+            $totalChances += $chances;
+        }
+
+        // 如果获得了抽奖机会,记录到数据库
+        if ($totalChances > 0) {
+            static::grantChanceToUser($activity->id, $userId, $totalChances, [
+                'trigger_type' => 'recharge',
+                'recharge_amount' => $rechargeInfo['amount'] ?? 0,
+                'granted_time' => time()
+            ]);
+        }
+
+        return $totalChances;
+    }
+
+    /**
+     * 处理订单条件
+     */
+    private static function processConditionForOrder($condition, $orderInfo, $userId)
+    {
+        $chances = 0;
+
+        switch ($condition->type) {
+            case LotteryCondition::TYPE_GOODS:
+                if ($condition->validateOrder($orderInfo)) {
+                    $chances = $condition->getRewardTimes();
+                }
+                break;
+
+            case LotteryCondition::TYPE_ORDER_AMOUNT:
+                if ($condition->validateOrder($orderInfo)) {
+                    $chances = $condition->getRewardTimes();
+                    
+                    // 如果可重复获得,根据金额倍数计算次数
+                    if ($condition->canRepeat()) {
+                        $multiple = floor($orderInfo['total_amount'] / $condition->condition_value);
+                        $chances *= $multiple;
+                    }
+                }
+                break;
+
+            case LotteryCondition::TYPE_ACCUMULATE:
+                if ($condition->validateAccumulateCondition($userId, $condition->activity_id)) {
+                    // 检查是否已经因为累计消费获得过机会
+                    if (!static::hasGrantedForAccumulate($condition->activity_id, $userId)) {
+                        $chances = $condition->getRewardTimes();
+                    }
+                }
+                break;
+        }
+
+        return $chances;
+    }
+
+    /**
+     * 处理充值条件
+     */
+    private static function processConditionForRecharge($condition, $rechargeInfo, $userId)
+    {
+        $chances = 0;
+        
+        if ($condition->validateOrder(['type' => 'recharge', 'amount' => $rechargeInfo['amount']])) {
+            $chances = $condition->getRewardTimes();
+            
+            // 如果可重复获得,根据金额倍数计算次数
+            if ($condition->canRepeat()) {
+                $multiple = floor($rechargeInfo['amount'] / $condition->condition_value);
+                $chances *= $multiple;
+            }
+        }
+
+        return $chances;
+    }
+
+    /**
+     * 检查用户资格
+     */
+    private static function checkUserQualification($activity, $userId)
+    {
+        // 检查用户群体限制
+        switch ($activity->user_limit_type) {
+            case LotteryActivity::USER_LIMIT_ALL:
+                return true;
+            case LotteryActivity::USER_LIMIT_LEVEL:
+                return static::checkUserLevel($userId, $activity->user_limit_value);
+            case LotteryActivity::USER_LIMIT_TAG:
+                return static::checkUserTag($userId, $activity->user_limit_value);
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * 检查用户等级
+     */
+    private static function checkUserLevel($userId, $limitValue)
+    {
+        if (empty($limitValue)) {
+            return true;
+        }
+        
+        $user = \app\common\model\User::find($userId);
+        return $user && in_array($user->level, (array)$limitValue);
+    }
+
+    /**
+     * 检查用户标签
+     */
+    private static function checkUserTag($userId, $limitValue)
+    {
+        if (empty($limitValue)) {
+            return true;
+        }
+        
+        // 这里需要根据实际的用户标签系统实现
+        // 暂时返回true
+        return true;
+    }
+
+    /**
+     * 检查是否已因累计消费获得过机会
+     */
+    private static function hasGrantedForAccumulate($activityId, $userId)
+    {
+        $userChance = LotteryUserChance::getUserChance($activityId, $userId);
+        if (!$userChance) {
+            return false;
+        }
+        
+        $getDetail = $userChance->get_detail_data;
+        foreach ($getDetail as $detail) {
+            if (($detail['trigger_type'] ?? '') === 'accumulate') {
+                return true;
+            }
+        }
+        
+        return false;
+    }
+
+    /**
+     * 给用户分发抽奖机会
+     */
+    private static function grantChanceToUser($activityId, $userId, $chances, $detail)
+    {
+        return LotteryUserChance::addChance($activityId, $userId, $chances, $detail);
+    }
+
+    /**
+     * 手动给用户增加抽奖机会(管理员操作)
+     * @param int $activityId 活动ID
+     * @param int $userId 用户ID
+     * @param int $chances 机会次数
+     * @param string $reason 原因
+     * @param int $adminId 管理员ID
+     * @return bool
+     */
+    public static function manualGrantChance($activityId, $userId, $chances, $reason, $adminId)
+    {
+        $activity = LotteryActivity::find($activityId);
+        if (!$activity) {
+            throw new Exception('活动不存在');
+        }
+
+        $detail = [
+            'trigger_type' => 'manual',
+            'reason' => $reason,
+            'admin_id' => $adminId,
+            'granted_time' => time()
+        ];
+
+        return static::grantChanceToUser($activityId, $userId, $chances, $detail);
+    }
+
+    /**
+     * 获取用户在指定活动中的抽奖机会详情
+     * @param int $activityId 活动ID
+     * @param int $userId 用户ID
+     * @return array
+     */
+    public static function getUserChanceDetail($activityId, $userId)
+    {
+        $userChance = LotteryUserChance::getUserChance($activityId, $userId);
+        
+        if (!$userChance) {
+            return [
+                'total_chances' => 0,
+                'used_chances' => 0,
+                'remain_chances' => 0,
+                'get_detail' => []
+            ];
+        }
+
+        return [
+            'total_chances' => $userChance->total_chances,
+            'used_chances' => $userChance->used_chances,
+            'remain_chances' => $userChance->remain_chances,
+            'last_get_time' => $userChance->last_get_time,
+            'last_use_time' => $userChance->last_use_time,
+            'get_detail' => $userChance->get_detail_data
+        ];
+    }
+
+    /**
+     * 批量初始化用户抽奖机会(活动开始时)
+     * @param int $activityId 活动ID
+     * @param array $userIds 用户ID数组
+     * @param int $initChances 初始机会数
+     * @return bool
+     */
+    public static function batchInitChances($activityId, $userIds, $initChances = 1)
+    {
+        $activity = LotteryActivity::find($activityId);
+        if (!$activity) {
+            throw new Exception('活动不存在');
+        }
+
+        return LotteryUserChance::batchCreateChances($activityId, $userIds, $initChances);
+    }
+} 

+ 453 - 0
application/common/Service/LotteryService.php

@@ -0,0 +1,453 @@
+<?php
+
+namespace app\common\Service;
+
+use app\common\model\lottery\LotteryActivity;
+use app\common\model\lottery\LotteryPrize;
+use app\common\model\lottery\LotteryCondition;
+use app\common\model\lottery\LotteryDrawRecord;
+use app\common\model\lottery\LotteryWinRecord;
+use app\common\model\lottery\LotteryUserChance;
+use think\Db;
+use think\Exception;
+use think\Cache;
+
+/**
+ * 抽奖服务类
+ * 核心抽奖逻辑处理
+ */
+class LotteryService
+{
+    /**
+     * 执行抽奖
+     * @param int $activityId 活动ID
+     * @param int $userId 用户ID
+     * @param int $triggerType 触发类型
+     * @param int $triggerOrderId 触发订单ID
+     * @param float $triggerAmount 触发金额
+     * @return array 抽奖结果
+     * @throws Exception
+     */
+    public static function drawLottery($activityId, $userId, $triggerType = 1, $triggerOrderId = null, $triggerAmount = null)
+    {
+        // 1. 验证活动有效性
+        $activity = LotteryActivity::find($activityId);
+        if (!$activity || !$activity->isRunning()) {
+            throw new Exception('活动不存在或未开始');
+        }
+
+        // 2. 验证抽奖时间
+        if (!$activity->isInDrawTime()) {
+            throw new Exception('不在抽奖时间内');
+        }
+
+        // 3. 验证用户资格
+        if (!static::validateUserQualification($activity, $userId)) {
+            throw new Exception('用户不符合参与条件');
+        }
+
+        // 4. 检查用户抽奖机会
+        $userChance = LotteryUserChance::getUserChance($activityId, $userId);
+        if (!$userChance || !$userChance->hasChance()) {
+            throw new Exception('没有抽奖机会');
+        }
+
+        // 5. 检查用户参与次数限制
+        if (!static::checkUserDrawLimit($activity, $userId)) {
+            throw new Exception('已达到参与次数上限');
+        }
+
+        // 6. 防重复抽奖检查(基于订单)
+        if ($triggerOrderId && static::hasDrawnForOrder($activityId, $userId, $triggerOrderId)) {
+            throw new Exception('该订单已参与过抽奖');
+        }
+
+        // 7. 使用Redis锁防止并发
+        $lockKey = "lottery_lock_{$activityId}_{$userId}";
+        $lock = Cache::store('redis')->handler()->set($lockKey, 1, 'NX', 'EX', 10);
+        if (!$lock) {
+            throw new Exception('操作太频繁,请稍后再试');
+        }
+
+        try {
+            // 8. 开始抽奖流程
+            return static::processDrawLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
+        } finally {
+            // 释放锁
+            Cache::store('redis')->handler()->del($lockKey);
+        }
+    }
+
+    /**
+     * 处理抽奖核心逻辑
+     */
+    private static function processDrawLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
+    {
+        // 开启事务
+        Db::startTrans();
+        
+        try {
+            // 1. 获取可用奖品
+            $prizes = static::getAvailablePrizes($activity);
+            if (empty($prizes)) {
+                throw new Exception('暂无可抽取的奖品');
+            }
+
+            // 2. 执行抽奖算法
+            $selectedPrize = static::executeLotteryAlgorithm($prizes);
+
+            // 3. 减少库存
+            if (!$selectedPrize->decreaseStock()) {
+                throw new Exception('奖品库存不足');
+            }
+
+            // 4. 消耗用户抽奖机会
+            $userChance = LotteryUserChance::getUserChance($activity->id, $userId);
+            if (!$userChance->useChance()) {
+                throw new Exception('抽奖机会使用失败');
+            }
+
+            // 5. 创建抽奖记录
+            $isWin = $selectedPrize->type != LotteryPrize::TYPE_NO_PRIZE;
+            $winInfo = $isWin ? static::buildWinInfo($selectedPrize) : [];
+            
+            $drawRecord = LotteryDrawRecord::createRecord(
+                $activity->id,
+                $userId,
+                $selectedPrize->id,
+                $isWin,
+                $triggerType,
+                $triggerOrderId,
+                $triggerAmount,
+                $winInfo
+            );
+
+            // 6. 如果中奖,创建中奖记录
+            $winRecord = null;
+            if ($isWin) {
+                $winRecord = LotteryWinRecord::createWinRecord(
+                    $drawRecord->id,
+                    $activity->id,
+                    $userId,
+                    $selectedPrize->id,
+                    $selectedPrize->name,
+                    $selectedPrize->type,
+                    static::buildPrizeValue($selectedPrize)
+                );
+
+                // 自动发放奖品
+                if ($selectedPrize->deliver_type == LotteryPrize::DELIVER_AUTO) {
+                    static::autoDeliverPrize($winRecord, $selectedPrize);
+                }
+            }
+
+            // 7. 更新活动统计
+            static::updateActivityStats($activity, $isWin);
+
+            // 提交事务
+            Db::commit();
+
+            // 8. 返回抽奖结果
+            return static::buildDrawResult($drawRecord, $selectedPrize, $winRecord);
+
+        } catch (Exception $e) {
+            Db::rollback();
+            throw $e;
+        }
+    }
+
+    /**
+     * 获取可用奖品列表
+     */
+    private static function getAvailablePrizes($activity)
+    {
+        $prizes = LotteryPrize::getValidPrizes($activity->id);
+        
+        // 如果开启按人数解锁功能
+        if ($activity->unlock_by_people) {
+            $currentPeopleCount = LotteryUserChance::getActivityParticipants($activity->id);
+            $prizes = $prizes->filter(function($prize) use ($currentPeopleCount) {
+                return $prize->isUnlocked($currentPeopleCount);
+            });
+        }
+
+        return $prizes;
+    }
+
+    /**
+     * 执行抽奖算法(基于概率权重)
+     */
+    private static function executeLotteryAlgorithm($prizes)
+    {
+        // 计算总概率
+        $totalProbability = $prizes->sum('probability');
+        
+        // 生成随机数
+        $randomNumber = mt_rand(1, $totalProbability * 100) / 100;
+        
+        // 按概率选择奖品
+        $currentProbability = 0;
+        foreach ($prizes as $prize) {
+            $currentProbability += $prize->probability;
+            if ($randomNumber <= $currentProbability) {
+                return $prize;
+            }
+        }
+        
+        // 保底返回第一个奖品(通常是未中奖)
+        return $prizes->first();
+    }
+
+    /**
+     * 验证用户资格
+     */
+    private static function validateUserQualification($activity, $userId)
+    {
+        // 检查用户群体限制
+        switch ($activity->user_limit_type) {
+            case LotteryActivity::USER_LIMIT_ALL:
+                return true;
+            case LotteryActivity::USER_LIMIT_LEVEL:
+                return static::checkUserLevel($userId, $activity->user_limit_value);
+            case LotteryActivity::USER_LIMIT_TAG:
+                return static::checkUserTag($userId, $activity->user_limit_value);
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * 检查用户等级
+     */
+    private static function checkUserLevel($userId, $limitValue)
+    {
+        if (empty($limitValue)) {
+            return true;
+        }
+        
+        $user = \app\common\model\User::find($userId);
+        return $user && in_array($user->level, (array)$limitValue);
+    }
+
+    /**
+     * 检查用户标签
+     */
+    private static function checkUserTag($userId, $limitValue)
+    {
+        if (empty($limitValue)) {
+            return true;
+        }
+        
+        // 这里需要根据实际的用户标签系统实现
+        // 暂时返回true
+        return true;
+    }
+
+    /**
+     * 检查用户抽奖次数限制
+     */
+    private static function checkUserDrawLimit($activity, $userId)
+    {
+        if (!$activity->person_limit_num) {
+            return true;
+        }
+        
+        $drawCount = LotteryDrawRecord::getUserDrawCount($activity->id, $userId);
+        return $drawCount < $activity->person_limit_num;
+    }
+
+    /**
+     * 检查订单是否已抽奖
+     */
+    private static function hasDrawnForOrder($activityId, $userId, $orderId)
+    {
+        return LotteryDrawRecord::where('activity_id', $activityId)
+                               ->where('user_id', $userId)
+                               ->where('trigger_order_id', $orderId)
+                               ->count() > 0;
+    }
+
+    /**
+     * 构建中奖信息
+     */
+    private static function buildWinInfo($prize)
+    {
+        return [
+            'prize_id' => $prize->id,
+            'prize_name' => $prize->name,
+            'prize_type' => $prize->type,
+            'prize_image' => $prize->image,
+            'win_prompt' => $prize->win_prompt
+        ];
+    }
+
+    /**
+     * 构建奖品价值信息
+     */
+    private static function buildPrizeValue($prize)
+    {
+        $prizeValue = [
+            'type' => $prize->type,
+            'name' => $prize->name,
+            'image' => $prize->image
+        ];
+
+        switch ($prize->type) {
+            case LotteryPrize::TYPE_COUPON:
+                $prizeValue['coupon_id'] = $prize->coupon_id;
+                break;
+            case LotteryPrize::TYPE_RED_PACKET:
+                $prizeValue['amount'] = $prize->amount;
+                break;
+            case LotteryPrize::TYPE_GOODS:
+                $prizeValue['goods_id'] = $prize->goods_id;
+                $prizeValue['goods_sku_id'] = $prize->goods_sku_id;
+                break;
+            case LotteryPrize::TYPE_EXCHANGE_CODE:
+                $prizeValue['exchange_code'] = $prize->getAvailableExchangeCode();
+                break;
+        }
+
+        return $prizeValue;
+    }
+
+    /**
+     * 自动发放奖品
+     */
+    private static function autoDeliverPrize($winRecord, $prize)
+    {
+        try {
+            switch ($prize->type) {
+                case LotteryPrize::TYPE_COUPON:
+                    static::deliverCoupon($winRecord, $prize);
+                    break;
+                case LotteryPrize::TYPE_RED_PACKET:
+                    static::deliverRedPacket($winRecord, $prize);
+                    break;
+                case LotteryPrize::TYPE_EXCHANGE_CODE:
+                    static::deliverExchangeCode($winRecord, $prize);
+                    break;
+                case LotteryPrize::TYPE_GOODS:
+                    static::deliverGoods($winRecord, $prize);
+                    break;
+            }
+        } catch (Exception $e) {
+            $winRecord->markAsFailed($e->getMessage());
+        }
+    }
+
+    /**
+     * 发放优惠券
+     */
+    private static function deliverCoupon($winRecord, $prize)
+    {
+        // 这里调用优惠券发放接口
+        // 示例代码,需要根据实际优惠券系统实现
+        $winRecord->markAsDelivered(['coupon_id' => $prize->coupon_id]);
+    }
+
+    /**
+     * 发放红包
+     */
+    private static function deliverRedPacket($winRecord, $prize)
+    {
+        // 这里调用红包发放接口
+        // 示例代码,需要根据实际红包系统实现
+        $winRecord->markAsDelivered(['amount' => $prize->amount]);
+    }
+
+    /**
+     * 发放兑换码
+     */
+    private static function deliverExchangeCode($winRecord, $prize)
+    {
+        $code = $prize->getAvailableExchangeCode();
+        if ($code) {
+            $winRecord->setExchangeCode($code);
+            $prize->markExchangeCodeUsed($code);
+            $winRecord->markAsDelivered(['exchange_code' => $code]);
+        } else {
+            throw new Exception('兑换码已用完');
+        }
+    }
+
+    /**
+     * 发放商城商品
+     */
+    private static function deliverGoods($winRecord, $prize)
+    {
+        // 这里可以自动加入购物车或创建订单
+        // 示例代码,需要根据实际商城系统实现
+        $winRecord->markAsDelivered([
+            'goods_id' => $prize->goods_id,
+            'goods_sku_id' => $prize->goods_sku_id
+        ]);
+    }
+
+    /**
+     * 更新活动统计
+     */
+    private static function updateActivityStats($activity, $isWin)
+    {
+        $activity->total_draw_count += 1;
+        if ($isWin) {
+            $activity->total_win_count += 1;
+        }
+        $activity->save();
+    }
+
+    /**
+     * 构建抽奖结果
+     */
+    private static function buildDrawResult($drawRecord, $prize, $winRecord = null)
+    {
+        $result = [
+            'draw_id' => $drawRecord->id,
+            'is_win' => $drawRecord->is_win,
+            'prize' => [
+                'id' => $prize->id,
+                'name' => $prize->name,
+                'type' => $prize->type,
+                'type_text' => $prize->type_text,
+                'image' => $prize->image,
+                'win_prompt' => $prize->win_prompt
+            ],
+            'draw_time' => $drawRecord->draw_time
+        ];
+
+        if ($winRecord) {
+            $result['win_record_id'] = $winRecord->id;
+            $result['deliver_status'] = $winRecord->deliver_status;
+            $result['exchange_code'] = $winRecord->exchange_code;
+        }
+
+        return $result;
+    }
+
+    /**
+     * 获取用户抽奖机会
+     * @param int $activityId 活动ID
+     * @param int $userId 用户ID
+     * @return array
+     */
+    public static function getUserChances($activityId, $userId)
+    {
+        $userChance = LotteryUserChance::getUserChance($activityId, $userId);
+        
+        if (!$userChance) {
+            return [
+                'total_chances' => 0,
+                'used_chances' => 0,
+                'remain_chances' => 0
+            ];
+        }
+
+        return [
+            'total_chances' => $userChance->total_chances,
+            'used_chances' => $userChance->used_chances,
+            'remain_chances' => $userChance->remain_chances,
+            'last_get_time' => $userChance->last_get_time,
+            'last_use_time' => $userChance->last_use_time
+        ];
+    }
+} 

+ 167 - 0
application/common/behavior/LotteryOrderHook.php

@@ -0,0 +1,167 @@
+<?php
+
+namespace app\common\behavior;
+
+use app\common\Service\LotteryChanceService;
+
+/**
+ * 订单相关抽奖钩子
+ * 在订单状态变更时自动检查并分发抽奖机会
+ */
+class LotteryOrderHook
+{
+    /**
+     * 订单支付完成后的钩子
+     * @param array $params 订单信息
+     */
+    public function orderPaid($params)
+    {
+        try {
+            $orderInfo = $params['order'] ?? [];
+            $userId = $orderInfo['user_id'] ?? 0;
+            
+            if (!$userId || empty($orderInfo)) {
+                return;
+            }
+
+            // 构建订单信息用于抽奖条件验证
+            $lotteryOrderInfo = [
+                'id' => $orderInfo['id'] ?? 0,
+                'user_id' => $userId,
+                'total_amount' => $orderInfo['total_amount'] ?? 0,
+                'goods' => $this->getOrderGoods($orderInfo['id'] ?? 0),
+                'type' => 'order'
+            ];
+
+            // 检查并分发抽奖机会
+            $grantedChances = LotteryChanceService::checkAndGrantChanceForOrder($lotteryOrderInfo, $userId);
+            
+            // 可以在这里添加通知逻辑,告知用户获得了抽奖机会
+            if (!empty($grantedChances)) {
+                $this->notifyUserGetChances($userId, $grantedChances);
+            }
+            
+        } catch (\Exception $e) {
+            // 记录错误日志,但不影响订单正常流程
+            trace('抽奖机会分发失败: ' . $e->getMessage(), 'error');
+        }
+    }
+
+    /**
+     * 充值完成后的钩子
+     * @param array $params 充值信息
+     */
+    public function rechargePaid($params)
+    {
+        try {
+            $rechargeInfo = $params['recharge'] ?? [];
+            $userId = $rechargeInfo['user_id'] ?? 0;
+            
+            if (!$userId || empty($rechargeInfo)) {
+                return;
+            }
+
+            // 构建充值信息用于抽奖条件验证
+            $lotteryRechargeInfo = [
+                'id' => $rechargeInfo['id'] ?? 0,
+                'user_id' => $userId,
+                'amount' => $rechargeInfo['amount'] ?? 0,
+                'type' => 'recharge'
+            ];
+
+            // 检查并分发抽奖机会
+            $grantedChances = LotteryChanceService::checkAndGrantChanceForRecharge($lotteryRechargeInfo, $userId);
+            
+            // 可以在这里添加通知逻辑,告知用户获得了抽奖机会
+            if (!empty($grantedChances)) {
+                $this->notifyUserGetChances($userId, $grantedChances);
+            }
+            
+        } catch (\Exception $e) {
+            // 记录错误日志,但不影响充值正常流程
+            trace('抽奖机会分发失败: ' . $e->getMessage(), 'error');
+        }
+    }
+
+    /**
+     * 获取订单商品信息
+     * @param int $orderId 订单ID
+     * @return array
+     */
+    private function getOrderGoods($orderId)
+    {
+        if (!$orderId) {
+            return [];
+        }
+
+        try {
+            $orderGoods = \app\common\model\OrderGoods::where('order_id', $orderId)
+                                                     ->field('goods_id,goods_sku_id,goods_num')
+                                                     ->select();
+            
+            $goods = [];
+            foreach ($orderGoods as $item) {
+                $goods[] = [
+                    'goods_id' => $item->goods_id,
+                    'goods_sku_id' => $item->goods_sku_id,
+                    'nums' => $item->goods_num
+                ];
+            }
+            
+            return $goods;
+        } catch (\Exception $e) {
+            trace('获取订单商品失败: ' . $e->getMessage(), 'error');
+            return [];
+        }
+    }
+
+    /**
+     * 通知用户获得抽奖机会
+     * @param int $userId 用户ID
+     * @param array $grantedChances 获得的抽奖机会
+     */
+    private function notifyUserGetChances($userId, $grantedChances)
+    {
+        try {
+            // 这里可以接入消息推送系统
+            // 例如:站内信、短信、微信消息等
+            
+            foreach ($grantedChances as $chance) {
+                $message = sprintf(
+                    '恭喜您获得「%s」%d次抽奖机会,快去参与抽奖吧!',
+                    $chance['activity_name'],
+                    $chance['chances']
+                );
+                
+                // 发送站内消息示例
+                $this->sendInternalMessage($userId, '抽奖机会获得通知', $message);
+            }
+            
+        } catch (\Exception $e) {
+            trace('抽奖机会通知发送失败: ' . $e->getMessage(), 'error');
+        }
+    }
+
+    /**
+     * 发送站内消息
+     * @param int $userId 用户ID
+     * @param string $title 消息标题
+     * @param string $content 消息内容
+     */
+    private function sendInternalMessage($userId, $title, $content)
+    {
+        // 这里需要根据实际的消息系统实现
+        // 示例代码:
+        /*
+        $messageData = [
+            'user_id' => $userId,
+            'title' => $title,
+            'content' => $content,
+            'type' => 'lottery',
+            'createtime' => time()
+        ];
+        
+        \app\common\model\Message::create($messageData);
+        */
+    }
+} 

+ 190 - 0
application/common/model/lottery/LotteryActivity.php

@@ -0,0 +1,190 @@
+<?php
+
+namespace app\common\model\lottery;
+
+use think\Model;
+use traits\model\SoftDelete;
+
+/**
+ * 抽奖活动模型
+ */
+class LotteryActivity extends Model
+{
+    use SoftDelete;
+
+    // 表名
+    protected $name = 'shop_lottery_activity';
+    
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = 'deletetime';
+
+    // 追加属性
+    protected $append = [
+        'status_text',
+        'lottery_type_text',
+        'user_limit_type_text'
+    ];
+
+    // 状态常量
+    const STATUS_DRAFT = 0;      // 草稿
+    const STATUS_RUNNING = 1;    // 进行中
+    const STATUS_ENDED = 2;      // 已结束
+    const STATUS_PAUSED = 3;     // 已暂停
+
+    // 开奖方式常量
+    const LOTTERY_TYPE_INSTANT = 1;  // 即抽即中
+    const LOTTERY_TYPE_TIME = 2;     // 按时间开奖
+    const LOTTERY_TYPE_PEOPLE = 3;   // 按人数开奖
+
+    // 用户群体类型常量
+    const USER_LIMIT_ALL = 1;        // 全部会员
+    const USER_LIMIT_LEVEL = 2;      // 会员等级
+    const USER_LIMIT_TAG = 3;        // 会员标签
+
+    /**
+     * 关联奖品
+     */
+    public function prizes()
+    {
+        return $this->hasMany('LotteryPrize', 'activity_id');
+    }
+
+    /**
+     * 关联参与条件
+     */
+    public function conditions()
+    {
+        return $this->hasMany('LotteryCondition', 'activity_id');
+    }
+
+    /**
+     * 关联抽奖记录
+     */
+    public function drawRecords()
+    {
+        return $this->hasMany('LotteryDrawRecord', 'activity_id');
+    }
+
+    /**
+     * 关联中奖记录
+     */
+    public function winRecords()
+    {
+        return $this->hasMany('LotteryWinRecord', 'activity_id');
+    }
+
+    /**
+     * 关联用户机会
+     */
+    public function userChances()
+    {
+        return $this->hasMany('LotteryUserChance', 'activity_id');
+    }
+
+    /**
+     * 获取状态文本
+     */
+    public function getStatusTextAttr($value, $data)
+    {
+        $status = [
+            self::STATUS_DRAFT => '草稿',
+            self::STATUS_RUNNING => '进行中',
+            self::STATUS_ENDED => '已结束',
+            self::STATUS_PAUSED => '已暂停'
+        ];
+        return isset($status[$data['status']]) ? $status[$data['status']] : '未知';
+    }
+
+    /**
+     * 获取开奖方式文本
+     */
+    public function getLotteryTypeTextAttr($value, $data)
+    {
+        $types = [
+            self::LOTTERY_TYPE_INSTANT => '即抽即中',
+            self::LOTTERY_TYPE_TIME => '按时间开奖',
+            self::LOTTERY_TYPE_PEOPLE => '按人数开奖'
+        ];
+        return isset($types[$data['lottery_type']]) ? $types[$data['lottery_type']] : '未知';
+    }
+
+    /**
+     * 获取用户群体类型文本
+     */
+    public function getUserLimitTypeTextAttr($value, $data)
+    {
+        $types = [
+            self::USER_LIMIT_ALL => '全部会员',
+            self::USER_LIMIT_LEVEL => '会员等级',
+            self::USER_LIMIT_TAG => '会员标签'
+        ];
+        return isset($types[$data['user_limit_type']]) ? $types[$data['user_limit_type']] : '未知';
+    }
+
+    /**
+     * 获取用户限制值(自动解析JSON)
+     */
+    public function getUserLimitValueAttr($value, $data)
+    {
+        return $value ? json_decode($value, true) : [];
+    }
+
+    /**
+     * 设置用户限制值(自动转换JSON)
+     */
+    public function setUserLimitValueAttr($value)
+    {
+        return is_array($value) ? json_encode($value) : $value;
+    }
+
+    /**
+     * 检查活动是否正在进行
+     */
+    public function isRunning()
+    {
+        $now = time();
+        return $this->status == self::STATUS_RUNNING 
+               && $this->start_time <= $now 
+               && $this->end_time >= $now;
+    }
+
+    /**
+     * 检查活动是否已结束
+     */
+    public function isEnded()
+    {
+        $now = time();
+        return $this->status == self::STATUS_ENDED 
+               || $this->end_time < $now;
+    }
+
+    /**
+     * 检查是否在抽奖时间内
+     */
+    public function isInDrawTime()
+    {
+        if (!$this->draw_time_enable) {
+            return true;
+        }
+        
+        $currentTime = date('H:i');
+        return $currentTime >= $this->draw_time_start 
+               && $currentTime <= $this->draw_time_end;
+    }
+
+    /**
+     * 获取正在进行的活动
+     */
+    public static function getRunningActivities()
+    {
+        $now = time();
+        return static::where('status', self::STATUS_RUNNING)
+                     ->where('start_time', '<=', $now)
+                     ->where('end_time', '>=', $now)
+                     ->select();
+    }
+} 

+ 199 - 0
application/common/model/lottery/LotteryCondition.php

@@ -0,0 +1,199 @@
+<?php
+
+namespace app\common\model\lottery;
+
+use think\Model;
+use traits\model\SoftDelete;
+
+/**
+ * 抽奖参与条件模型
+ */
+class LotteryCondition extends Model
+{
+    use SoftDelete;
+
+    // 表名
+    protected $name = 'shop_lottery_condition';
+    
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = 'deletetime';
+
+    // 追加属性
+    protected $append = [
+        'type_text',
+        'goods_rule_text',
+        'goods_ids_list'
+    ];
+
+    // 条件类型常量
+    const TYPE_GOODS = 1;           // 购买指定商品
+    const TYPE_ORDER_AMOUNT = 2;    // 单笔订单消费满额
+    const TYPE_RECHARGE = 3;        // 单次充值满额
+    const TYPE_ACCUMULATE = 4;      // 活动期间累计消费满额
+
+    // 商品规则常量
+    const GOODS_RULE_INCLUDE = 1;   // 指定商品参与
+    const GOODS_RULE_EXCLUDE = 2;   // 指定商品不可参与
+
+    /**
+     * 关联活动
+     */
+    public function activity()
+    {
+        return $this->belongsTo('LotteryActivity', 'activity_id');
+    }
+
+    /**
+     * 获取条件类型文本
+     */
+    public function getTypeTextAttr($value, $data)
+    {
+        $types = [
+            self::TYPE_GOODS => '购买指定商品',
+            self::TYPE_ORDER_AMOUNT => '单笔订单消费满额',
+            self::TYPE_RECHARGE => '单次充值满额',
+            self::TYPE_ACCUMULATE => '活动期间累计消费满额'
+        ];
+        return isset($types[$data['type']]) ? $types[$data['type']] : '未知';
+    }
+
+    /**
+     * 获取商品规则文本
+     */
+    public function getGoodsRuleTextAttr($value, $data)
+    {
+        $rules = [
+            self::GOODS_RULE_INCLUDE => '指定商品参与',
+            self::GOODS_RULE_EXCLUDE => '指定商品不可参与'
+        ];
+        return isset($rules[$data['goods_rule']]) ? $rules[$data['goods_rule']] : '';
+    }
+
+    /**
+     * 获取商品ID列表
+     */
+    public function getGoodsIdsListAttr($value, $data)
+    {
+        return !empty($data['goods_ids']) ? json_decode($data['goods_ids'], true) : [];
+    }
+
+    /**
+     * 设置商品ID列表
+     */
+    public function setGoodsIdsAttr($value)
+    {
+        return is_array($value) ? json_encode($value) : $value;
+    }
+
+    /**
+     * 验证订单是否满足条件
+     */
+    public function validateOrder($orderInfo)
+    {
+        switch ($this->type) {
+            case self::TYPE_GOODS:
+                return $this->validateGoodsCondition($orderInfo);
+            case self::TYPE_ORDER_AMOUNT:
+                return $this->validateOrderAmountCondition($orderInfo);
+            case self::TYPE_RECHARGE:
+                return $this->validateRechargeCondition($orderInfo);
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * 验证商品条件
+     */
+    private function validateGoodsCondition($orderInfo)
+    {
+        if (empty($orderInfo['goods']) || empty($this->goods_ids_list)) {
+            return false;
+        }
+
+        $orderGoodsIds = array_column($orderInfo['goods'], 'goods_id');
+        $conditionGoodsIds = $this->goods_ids_list;
+        $intersection = array_intersect($orderGoodsIds, $conditionGoodsIds);
+
+        if ($this->goods_rule == self::GOODS_RULE_INCLUDE) {
+            // 指定商品参与:订单中必须包含指定商品
+            return !empty($intersection);
+        } else {
+            // 指定商品不可参与:订单中不能包含指定商品
+            return empty($intersection);
+        }
+    }
+
+    /**
+     * 验证订单金额条件
+     */
+    private function validateOrderAmountCondition($orderInfo)
+    {
+        $orderAmount = $orderInfo['total_amount'] ?? 0;
+        return $orderAmount >= $this->condition_value;
+    }
+
+    /**
+     * 验证充值条件
+     */
+    private function validateRechargeCondition($orderInfo)
+    {
+        if (($orderInfo['type'] ?? '') !== 'recharge') {
+            return false;
+        }
+        
+        $rechargeAmount = $orderInfo['amount'] ?? 0;
+        return $rechargeAmount >= $this->condition_value;
+    }
+
+    /**
+     * 验证累计消费条件
+     */
+    public function validateAccumulateCondition($userId, $activityId)
+    {
+        $activity = LotteryActivity::find($activityId);
+        if (!$activity) {
+            return false;
+        }
+
+        // 计算活动期间用户累计消费
+        $totalAmount = \app\common\model\Order::where('user_id', $userId)
+                                             ->where('status', 'paid')
+                                             ->where('createtime', '>=', $activity->start_time)
+                                             ->where('createtime', '<=', $activity->end_time)
+                                             ->sum('total_amount');
+
+        return $totalAmount >= $this->condition_value;
+    }
+
+    /**
+     * 获取活动的有效条件
+     */
+    public static function getValidConditions($activityId)
+    {
+        return static::where('activity_id', $activityId)
+                     ->where('status', 1)
+                     ->order('id', 'asc')
+                     ->select();
+    }
+
+    /**
+     * 检查条件是否可重复获得奖励
+     */
+    public function canRepeat()
+    {
+        return $this->is_repeatable == 1;
+    }
+
+    /**
+     * 获取奖励次数
+     */
+    public function getRewardTimes()
+    {
+        return $this->reward_times ?: 1;
+    }
+} 

+ 189 - 0
application/common/model/lottery/LotteryDrawRecord.php

@@ -0,0 +1,189 @@
+<?php
+
+namespace app\common\model\lottery;
+
+use think\Model;
+
+/**
+ * 抽奖记录模型
+ */
+class LotteryDrawRecord extends Model
+{
+    // 表名
+    protected $name = 'shop_lottery_draw_record';
+    
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = false;
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'trigger_type_text',
+        'win_info_data'
+    ];
+
+    // 触发类型常量
+    const TRIGGER_GOODS = 1;        // 购买商品
+    const TRIGGER_ORDER = 2;        // 订单消费
+    const TRIGGER_RECHARGE = 3;     // 充值
+    const TRIGGER_ACCUMULATE = 4;   // 累计消费
+
+    /**
+     * 关联活动
+     */
+    public function activity()
+    {
+        return $this->belongsTo('LotteryActivity', 'activity_id');
+    }
+
+    /**
+     * 关联用户
+     */
+    public function user()
+    {
+        return $this->belongsTo('app\common\model\User', 'user_id');
+    }
+
+    /**
+     * 关联奖品
+     */
+    public function prize()
+    {
+        return $this->belongsTo('LotteryPrize', 'prize_id');
+    }
+
+    /**
+     * 关联中奖记录
+     */
+    public function winRecord()
+    {
+        return $this->hasOne('LotteryWinRecord', 'draw_record_id');
+    }
+
+    /**
+     * 关联订单
+     */
+    public function order()
+    {
+        return $this->belongsTo('app\common\model\Order', 'trigger_order_id');
+    }
+
+    /**
+     * 获取触发类型文本
+     */
+    public function getTriggerTypeTextAttr($value, $data)
+    {
+        $types = [
+            self::TRIGGER_GOODS => '购买商品',
+            self::TRIGGER_ORDER => '订单消费',
+            self::TRIGGER_RECHARGE => '充值',
+            self::TRIGGER_ACCUMULATE => '累计消费'
+        ];
+        return isset($types[$data['trigger_type']]) ? $types[$data['trigger_type']] : '未知';
+    }
+
+    /**
+     * 获取中奖信息数据
+     */
+    public function getWinInfoDataAttr($value, $data)
+    {
+        return !empty($data['win_info']) ? json_decode($data['win_info'], true) : [];
+    }
+
+    /**
+     * 设置中奖信息
+     */
+    public function setWinInfoAttr($value)
+    {
+        return is_array($value) ? json_encode($value) : $value;
+    }
+
+    /**
+     * 创建抽奖记录
+     */
+    public static function createRecord($activityId, $userId, $prizeId, $isWin, $triggerType, $triggerOrderId = null, $triggerAmount = null, $winInfo = [])
+    {
+        $data = [
+            'activity_id' => $activityId,
+            'user_id' => $userId,
+            'prize_id' => $prizeId,
+            'is_win' => $isWin ? 1 : 0,
+            'trigger_type' => $triggerType,
+            'trigger_order_id' => $triggerOrderId,
+            'trigger_amount' => $triggerAmount,
+            'win_info' => $winInfo ? json_encode($winInfo) : '',
+            'draw_ip' => request()->ip(),
+            'draw_time' => time(),
+            'device_info' => request()->header('user-agent', '')
+        ];
+        
+        return static::create($data);
+    }
+
+    /**
+     * 获取用户抽奖次数
+     */
+    public static function getUserDrawCount($activityId, $userId, $timeRange = null)
+    {
+        $query = static::where('activity_id', $activityId)
+                       ->where('user_id', $userId);
+        
+        if ($timeRange) {
+            if (isset($timeRange['start'])) {
+                $query->where('draw_time', '>=', $timeRange['start']);
+            }
+            if (isset($timeRange['end'])) {
+                $query->where('draw_time', '<=', $timeRange['end']);
+            }
+        }
+        
+        return $query->count();
+    }
+
+    /**
+     * 获取用户中奖次数
+     */
+    public static function getUserWinCount($activityId, $userId, $timeRange = null)
+    {
+        $query = static::where('activity_id', $activityId)
+                       ->where('user_id', $userId)
+                       ->where('is_win', 1);
+        
+        if ($timeRange) {
+            if (isset($timeRange['start'])) {
+                $query->where('draw_time', '>=', $timeRange['start']);
+            }
+            if (isset($timeRange['end'])) {
+                $query->where('draw_time', '<=', $timeRange['end']);
+            }
+        }
+        
+        return $query->count();
+    }
+
+    /**
+     * 获取活动抽奖统计
+     */
+    public static function getActivityStats($activityId, $timeRange = null)
+    {
+        $query = static::where('activity_id', $activityId);
+        
+        if ($timeRange) {
+            if (isset($timeRange['start'])) {
+                $query->where('draw_time', '>=', $timeRange['start']);
+            }
+            if (isset($timeRange['end'])) {
+                $query->where('draw_time', '<=', $timeRange['end']);
+            }
+        }
+        
+        return [
+            'total_draw' => $query->count(),
+            'total_win' => $query->where('is_win', 1)->count(),
+            'total_people' => $query->distinct('user_id')->count()
+        ];
+    }
+} 

+ 217 - 0
application/common/model/lottery/LotteryPrize.php

@@ -0,0 +1,217 @@
+<?php
+
+namespace app\common\model\lottery;
+
+use think\Model;
+use traits\model\SoftDelete;
+
+/**
+ * 抽奖奖品模型
+ */
+class LotteryPrize extends Model
+{
+    use SoftDelete;
+
+    // 表名
+    protected $name = 'shop_lottery_prize';
+    
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = 'deletetime';
+
+    // 追加属性
+    protected $append = [
+        'type_text',
+        'deliver_type_text',
+        'exchange_codes_list',
+        'used_codes_list'
+    ];
+
+    // 奖品类型常量
+    const TYPE_NO_PRIZE = 1;        // 未中奖
+    const TYPE_PHYSICAL = 2;        // 实物奖品
+    const TYPE_COUPON = 3;          // 优惠券
+    const TYPE_RED_PACKET = 4;      // 红包
+    const TYPE_EXCHANGE_CODE = 5;   // 兑换码
+    const TYPE_GOODS = 6;           // 商城奖品
+
+    // 发放方式常量
+    const DELIVER_AUTO = 1;         // 自动发放
+    const DELIVER_MANUAL = 2;       // 手动发放
+
+    /**
+     * 关联活动
+     */
+    public function activity()
+    {
+        return $this->belongsTo('LotteryActivity', 'activity_id');
+    }
+
+    /**
+     * 关联商品
+     */
+    public function goods()
+    {
+        return $this->belongsTo('app\common\model\Goods', 'goods_id');
+    }
+
+    /**
+     * 关联商品SKU
+     */
+    public function goodsSku()
+    {
+        return $this->belongsTo('app\common\model\Sku', 'goods_sku_id');
+    }
+
+    /**
+     * 关联优惠券
+     */
+    public function coupon()
+    {
+        return $this->belongsTo('app\common\model\Coupon', 'coupon_id');
+    }
+
+    /**
+     * 获取奖品类型文本
+     */
+    public function getTypeTextAttr($value, $data)
+    {
+        $types = [
+            self::TYPE_NO_PRIZE => '未中奖',
+            self::TYPE_PHYSICAL => '实物奖品',
+            self::TYPE_COUPON => '优惠券',
+            self::TYPE_RED_PACKET => '红包',
+            self::TYPE_EXCHANGE_CODE => '兑换码',
+            self::TYPE_GOODS => '商城奖品'
+        ];
+        return isset($types[$data['type']]) ? $types[$data['type']] : '未知';
+    }
+
+    /**
+     * 获取发放方式文本
+     */
+    public function getDeliverTypeTextAttr($value, $data)
+    {
+        $types = [
+            self::DELIVER_AUTO => '自动发放',
+            self::DELIVER_MANUAL => '手动发放'
+        ];
+        return isset($types[$data['deliver_type']]) ? $types[$data['deliver_type']] : '未知';
+    }
+
+    /**
+     * 获取兑换码列表
+     */
+    public function getExchangeCodesListAttr($value, $data)
+    {
+        return !empty($data['exchange_codes']) ? json_decode($data['exchange_codes'], true) : [];
+    }
+
+    /**
+     * 设置兑换码列表
+     */
+    public function setExchangeCodesAttr($value)
+    {
+        return is_array($value) ? json_encode($value) : $value;
+    }
+
+    /**
+     * 获取已使用兑换码列表
+     */
+    public function getUsedCodesListAttr($value, $data)
+    {
+        return !empty($data['used_codes']) ? json_decode($data['used_codes'], true) : [];
+    }
+
+    /**
+     * 设置已使用兑换码列表
+     */
+    public function setUsedCodesAttr($value)
+    {
+        return is_array($value) ? json_encode($value) : $value;
+    }
+
+    /**
+     * 检查奖品库存是否充足
+     */
+    public function hasStock($quantity = 1)
+    {
+        return $this->remain_stock >= $quantity;
+    }
+
+    /**
+     * 减少库存
+     */
+    public function decreaseStock($quantity = 1)
+    {
+        if (!$this->hasStock($quantity)) {
+            return false;
+        }
+        
+        $this->remain_stock -= $quantity;
+        $this->win_count += $quantity;
+        return $this->save();
+    }
+
+    /**
+     * 获取可用的兑换码
+     */
+    public function getAvailableExchangeCode()
+    {
+        if ($this->type != self::TYPE_EXCHANGE_CODE) {
+            return null;
+        }
+        
+        $allCodes = $this->exchange_codes_list;
+        $usedCodes = $this->used_codes_list;
+        
+        $availableCodes = array_diff($allCodes, $usedCodes);
+        
+        if (empty($availableCodes)) {
+            return null;
+        }
+        
+        return array_shift($availableCodes);
+    }
+
+    /**
+     * 标记兑换码为已使用
+     */
+    public function markExchangeCodeUsed($code)
+    {
+        $usedCodes = $this->used_codes_list;
+        if (!in_array($code, $usedCodes)) {
+            $usedCodes[] = $code;
+            $this->used_codes = json_encode($usedCodes);
+            return $this->save();
+        }
+        return true;
+    }
+
+    /**
+     * 获取有效奖品(库存大于0且状态正常)
+     */
+    public static function getValidPrizes($activityId)
+    {
+        return static::where('activity_id', $activityId)
+                     ->where('status', 1)
+                     ->where('remain_stock', '>', 0)
+                     ->order('sort_order', 'asc')
+                     ->select();
+    }
+
+    /**
+     * 检查是否已解锁(按人数解锁功能)
+     */
+    public function isUnlocked($currentPeopleCount)
+    {
+        if (empty($this->unlock_people_num)) {
+            return true;
+        }
+        
+        return $currentPeopleCount >= $this->unlock_people_num;
+    }
+} 

+ 188 - 0
application/common/model/lottery/LotteryUserChance.php

@@ -0,0 +1,188 @@
+<?php
+
+namespace app\common\model\lottery;
+
+use think\Model;
+
+/**
+ * 用户抽奖机会模型
+ */
+class LotteryUserChance extends Model
+{
+    // 表名
+    protected $name = 'shop_lottery_user_chance';
+    
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'get_detail_data'
+    ];
+
+    /**
+     * 关联活动
+     */
+    public function activity()
+    {
+        return $this->belongsTo('LotteryActivity', 'activity_id');
+    }
+
+    /**
+     * 关联用户
+     */
+    public function user()
+    {
+        return $this->belongsTo('app\common\model\User', 'user_id');
+    }
+
+    /**
+     * 获取获得详情数据
+     */
+    public function getGetDetailDataAttr($value, $data)
+    {
+        return !empty($data['get_detail']) ? json_decode($data['get_detail'], true) : [];
+    }
+
+    /**
+     * 设置获得详情
+     */
+    public function setGetDetailAttr($value)
+    {
+        return is_array($value) ? json_encode($value) : $value;
+    }
+
+    /**
+     * 获取用户抽奖机会
+     */
+    public static function getUserChance($activityId, $userId)
+    {
+        return static::where('activity_id', $activityId)
+                     ->where('user_id', $userId)
+                     ->find();
+    }
+
+    /**
+     * 增加抽奖机会
+     */
+    public static function addChance($activityId, $userId, $times = 1, $detail = [])
+    {
+        $chance = static::getUserChance($activityId, $userId);
+        
+        if (!$chance) {
+            // 创建新记录
+            $data = [
+                'activity_id' => $activityId,
+                'user_id' => $userId,
+                'total_chances' => $times,
+                'used_chances' => 0,
+                'remain_chances' => $times,
+                'last_get_time' => time(),
+                'get_detail' => json_encode([$detail])
+            ];
+            return static::create($data);
+        } else {
+            // 更新现有记录
+            $chance->total_chances += $times;
+            $chance->remain_chances += $times;
+            $chance->last_get_time = time();
+            
+            // 更新获得详情
+            $getDetail = $chance->get_detail_data;
+            $getDetail[] = $detail;
+            $chance->get_detail = json_encode($getDetail);
+            
+            return $chance->save() ? $chance : false;
+        }
+    }
+
+    /**
+     * 使用抽奖机会
+     */
+    public function useChance($times = 1)
+    {
+        if ($this->remain_chances < $times) {
+            return false;
+        }
+        
+        $this->used_chances += $times;
+        $this->remain_chances -= $times;
+        $this->last_use_time = time();
+        
+        return $this->save();
+    }
+
+    /**
+     * 检查是否有剩余机会
+     */
+    public function hasChance($times = 1)
+    {
+        return $this->remain_chances >= $times;
+    }
+
+    /**
+     * 重置抽奖机会(用于测试或特殊情况)
+     */
+    public function resetChance()
+    {
+        $this->used_chances = 0;
+        $this->remain_chances = $this->total_chances;
+        return $this->save();
+    }
+
+    /**
+     * 获取活动总参与人数
+     */
+    public static function getActivityParticipants($activityId)
+    {
+        return static::where('activity_id', $activityId)->count();
+    }
+
+    /**
+     * 获取用户在多个活动中的机会统计
+     */
+    public static function getUserChancesStats($userId, $activityIds = [])
+    {
+        $query = static::where('user_id', $userId);
+        
+        if (!empty($activityIds)) {
+            $query->where('activity_id', 'in', $activityIds);
+        }
+        
+        return $query->field([
+                'activity_id',
+                'total_chances',
+                'used_chances', 
+                'remain_chances'
+            ])
+            ->select();
+    }
+
+    /**
+     * 批量创建用户机会(用于活动启动时)
+     */
+    public static function batchCreateChances($activityId, $userIds, $times = 1)
+    {
+        $data = [];
+        $now = time();
+        
+        foreach ($userIds as $userId) {
+            $data[] = [
+                'activity_id' => $activityId,
+                'user_id' => $userId,
+                'total_chances' => $times,
+                'used_chances' => 0,
+                'remain_chances' => $times,
+                'last_get_time' => $now,
+                'createtime' => $now,
+                'updatetime' => $now
+            ];
+        }
+        
+        return static::insertAll($data);
+    }
+} 

+ 235 - 0
application/common/model/lottery/LotteryWinRecord.php

@@ -0,0 +1,235 @@
+<?php
+
+namespace app\common\model\lottery;
+
+use think\Model;
+
+/**
+ * 中奖记录模型
+ */
+class LotteryWinRecord extends Model
+{
+    // 表名
+    protected $name = 'shop_lottery_win_record';
+    
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'deliver_status_text',
+        'prize_value_data',
+        'deliver_info_data'
+    ];
+
+    // 发放状态常量
+    const DELIVER_STATUS_PENDING = 0;   // 待发放
+    const DELIVER_STATUS_SUCCESS = 1;   // 已发放
+    const DELIVER_STATUS_FAILED = 2;    // 发放失败
+    const DELIVER_STATUS_CANCELLED = 3; // 已取消
+
+    /**
+     * 关联抽奖记录
+     */
+    public function drawRecord()
+    {
+        return $this->belongsTo('LotteryDrawRecord', 'draw_record_id');
+    }
+
+    /**
+     * 关联活动
+     */
+    public function activity()
+    {
+        return $this->belongsTo('LotteryActivity', 'activity_id');
+    }
+
+    /**
+     * 关联用户
+     */
+    public function user()
+    {
+        return $this->belongsTo('app\common\model\User', 'user_id');
+    }
+
+    /**
+     * 关联奖品
+     */
+    public function prize()
+    {
+        return $this->belongsTo('LotteryPrize', 'prize_id');
+    }
+
+    /**
+     * 获取发放状态文本
+     */
+    public function getDeliverStatusTextAttr($value, $data)
+    {
+        $status = [
+            self::DELIVER_STATUS_PENDING => '待发放',
+            self::DELIVER_STATUS_SUCCESS => '已发放',
+            self::DELIVER_STATUS_FAILED => '发放失败',
+            self::DELIVER_STATUS_CANCELLED => '已取消'
+        ];
+        return isset($status[$data['deliver_status']]) ? $status[$data['deliver_status']] : '未知';
+    }
+
+    /**
+     * 获取奖品信息数据
+     */
+    public function getPrizeValueDataAttr($value, $data)
+    {
+        return !empty($data['prize_value']) ? json_decode($data['prize_value'], true) : [];
+    }
+
+    /**
+     * 设置奖品信息
+     */
+    public function setPrizeValueAttr($value)
+    {
+        return is_array($value) ? json_encode($value) : $value;
+    }
+
+    /**
+     * 获取发放信息数据
+     */
+    public function getDeliverInfoDataAttr($value, $data)
+    {
+        return !empty($data['deliver_info']) ? json_decode($data['deliver_info'], true) : [];
+    }
+
+    /**
+     * 设置发放信息
+     */
+    public function setDeliverInfoAttr($value)
+    {
+        return is_array($value) ? json_encode($value) : $value;
+    }
+
+    /**
+     * 创建中奖记录
+     */
+    public static function createWinRecord($drawRecordId, $activityId, $userId, $prizeId, $prizeName, $prizeType, $prizeValue = [])
+    {
+        $data = [
+            'draw_record_id' => $drawRecordId,
+            'activity_id' => $activityId,
+            'user_id' => $userId,
+            'prize_id' => $prizeId,
+            'prize_name' => $prizeName,
+            'prize_type' => $prizeType,
+            'prize_value' => is_array($prizeValue) ? json_encode($prizeValue) : $prizeValue,
+            'deliver_status' => self::DELIVER_STATUS_PENDING
+        ];
+        
+        return static::create($data);
+    }
+
+    /**
+     * 标记为发放成功
+     */
+    public function markAsDelivered($deliverInfo = [])
+    {
+        $this->deliver_status = self::DELIVER_STATUS_SUCCESS;
+        $this->deliver_time = time();
+        if ($deliverInfo) {
+            $this->deliver_info = json_encode($deliverInfo);
+        }
+        return $this->save();
+    }
+
+    /**
+     * 标记为发放失败
+     */
+    public function markAsFailed($reason = '')
+    {
+        $this->deliver_status = self::DELIVER_STATUS_FAILED;
+        $this->fail_reason = $reason;
+        return $this->save();
+    }
+
+    /**
+     * 设置收货地址
+     */
+    public function setDeliveryAddress($name, $mobile, $address)
+    {
+        $this->receiver_name = $name;
+        $this->receiver_mobile = $mobile;
+        $this->receiver_address = $address;
+        return $this->save();
+    }
+
+    /**
+     * 设置快递信息
+     */
+    public function setExpressInfo($company, $number)
+    {
+        $this->express_company = $company;
+        $this->express_number = $number;
+        return $this->save();
+    }
+
+    /**
+     * 设置兑换码
+     */
+    public function setExchangeCode($code)
+    {
+        $this->exchange_code = $code;
+        return $this->save();
+    }
+
+    /**
+     * 标记兑换码已使用
+     */
+    public function markCodeUsed()
+    {
+        $this->code_used_time = time();
+        return $this->save();
+    }
+
+    /**
+     * 获取待发放的记录
+     */
+    public static function getPendingRecords($limit = 100)
+    {
+        return static::where('deliver_status', self::DELIVER_STATUS_PENDING)
+                     ->order('createtime', 'asc')
+                     ->limit($limit)
+                     ->select();
+    }
+
+    /**
+     * 获取用户中奖记录
+     */
+    public static function getUserWinRecords($userId, $page = 1, $limit = 20)
+    {
+        return static::where('user_id', $userId)
+                     ->with(['activity', 'prize'])
+                     ->order('createtime', 'desc')
+                     ->page($page, $limit)
+                     ->select();
+    }
+
+    /**
+     * 获取活动中奖统计
+     */
+    public static function getActivityWinStats($activityId)
+    {
+        return [
+            'total_win' => static::where('activity_id', $activityId)->count(),
+            'delivered' => static::where('activity_id', $activityId)
+                                 ->where('deliver_status', self::DELIVER_STATUS_SUCCESS)
+                                 ->count(),
+            'pending' => static::where('activity_id', $activityId)
+                               ->where('deliver_status', self::DELIVER_STATUS_PENDING)
+                               ->count(),
+            'failed' => static::where('activity_id', $activityId)
+                              ->where('deliver_status', self::DELIVER_STATUS_FAILED)
+                              ->count()
+        ];
+    }
+} 

+ 393 - 0
docs/消费抽奖系统接口使用说明.md

@@ -0,0 +1,393 @@
+# 消费抽奖营销活动系统 - 接口使用说明
+
+## 系统概述
+
+本系统是一个基于消费行为的抽奖营销活动系统,支持多种奖品类型、灵活的参与条件和开奖方式。通过用户购买商品、充值等行为自动获得抽奖机会,提高用户参与度和复购率。
+
+## 核心功能
+
+- **多样化奖品**:支持实物奖品、优惠券、红包、兑换码、商城奖品
+- **灵活条件**:支持购买指定商品、消费满额、充值满额等触发条件
+- **三种开奖模式**:即抽即中、按时间开奖、按人数开奖
+- **精准投放**:支持会员等级、会员标签的精准用户群体控制
+- **完善统计**:详细的抽奖记录和数据统计分析
+
+## 数据表结构
+
+系统包含以下核心数据表:
+
+1. **shop_lottery_activity** - 活动主表
+2. **shop_lottery_prize** - 奖品表
+3. **shop_lottery_condition** - 参与条件表
+4. **shop_lottery_draw_record** - 抽奖记录表
+5. **shop_lottery_win_record** - 中奖记录表
+6. **shop_lottery_user_chance** - 用户抽奖机会表
+7. **shop_lottery_statistics** - 活动统计表
+
+请先执行数据库建表脚本创建相关表结构。
+
+## API接口说明
+
+### 基础接口路径
+```
+/api/lottery/
+```
+
+### 1. 获取活动列表
+**接口地址:** `GET /api/lottery/activityList`
+
+**请求参数:**
+```json
+{
+    "page": 1,        // 页码,默认1
+    "limit": 10,      // 每页数量,默认10
+    "status": 1       // 活动状态,默认1(进行中)
+}
+```
+
+**返回示例:**
+```json
+{
+    "code": 1,
+    "msg": "获取成功",
+    "data": {
+        "list": [
+            {
+                "id": 1,
+                "name": "新用户专享抽奖",
+                "description": "新用户购买任意商品即可参与抽奖",
+                "cover_image": "/uploads/lottery/cover1.jpg",
+                "status": 1,
+                "lottery_type": 1,
+                "prizes": [
+                    {
+                        "id": 1,
+                        "name": "iPhone 15",
+                        "type": 2,
+                        "image": "/uploads/prize/iphone15.jpg",
+                        "probability": 0.01
+                    }
+                ],
+                "total_participants": 1580
+            }
+        ],
+        "total": 5
+    }
+}
+```
+
+### 2. 获取活动详情
+**接口地址:** `GET /api/lottery/activityDetail`
+
+**请求参数:**
+```json
+{
+    "activity_id": 1   // 活动ID,必填
+}
+```
+
+**返回示例:**
+```json
+{
+    "code": 1,
+    "msg": "获取成功",
+    "data": {
+        "id": 1,
+        "name": "新用户专享抽奖",
+        "description": "活动描述",
+        "start_time": 1703980800,
+        "end_time": 1704067200,
+        "lottery_type": 1,
+        "person_limit_num": 3,
+        "prizes": [
+            {
+                "id": 1,
+                "name": "iPhone 15",
+                "type": 2,
+                "probability": 0.01,
+                "remain_stock": 10,
+                "is_unlocked": true
+            }
+        ],
+        "user_chances": {
+            "total_chances": 5,
+            "used_chances": 2,
+            "remain_chances": 3
+        },
+        "user_draw_count": 2,
+        "user_win_count": 1
+    }
+}
+```
+
+### 3. 执行抽奖
+**接口地址:** `POST /api/lottery/draw`
+
+**请求参数:**
+```json
+{
+    "activity_id": 1   // 活动ID,必填
+}
+```
+
+**返回示例:**
+```json
+{
+    "code": 1,
+    "msg": "抽奖成功",
+    "data": {
+        "draw_id": 12345,
+        "is_win": 1,
+        "prize": {
+            "id": 2,
+            "name": "10元优惠券",
+            "type": 3,
+            "type_text": "优惠券",
+            "image": "/uploads/coupon.jpg",
+            "win_prompt": "恭喜获得10元优惠券!"
+        },
+        "win_record_id": 6789,
+        "deliver_status": 1,
+        "draw_time": 1703980800
+    }
+}
+```
+
+### 4. 获取用户抽奖机会
+**接口地址:** `GET /api/lottery/getUserChances`
+
+**请求参数:**
+```json
+{
+    "activity_id": 1   // 活动ID,必填
+}
+```
+
+**返回示例:**
+```json
+{
+    "code": 1,
+    "msg": "获取成功",
+    "data": {
+        "total_chances": 5,
+        "used_chances": 2,
+        "remain_chances": 3,
+        "last_get_time": 1703980800,
+        "last_use_time": 1703981000,
+        "get_detail": [
+            {
+                "trigger_type": "order",
+                "order_id": 1001,
+                "order_amount": 299.00,
+                "granted_time": 1703980800
+            }
+        ]
+    }
+}
+```
+
+### 5. 获取抽奖记录
+**接口地址:** `GET /api/lottery/getDrawRecords`
+
+**请求参数:**
+```json
+{
+    "activity_id": 1,  // 活动ID,可选
+    "page": 1,         // 页码,默认1
+    "limit": 20        // 每页数量,默认20
+}
+```
+
+**返回示例:**
+```json
+{
+    "code": 1,
+    "msg": "获取成功",
+    "data": {
+        "list": [
+            {
+                "id": 12345,
+                "activity_id": 1,
+                "activity_name": "新用户专享抽奖",
+                "prize_name": "10元优惠券",
+                "is_win": 1,
+                "draw_time": 1703980800,
+                "win_record": {
+                    "id": 6789,
+                    "deliver_status": 1,
+                    "deliver_status_text": "已发放"
+                }
+            }
+        ],
+        "total": 10
+    }
+}
+```
+
+### 6. 获取中奖记录
+**接口地址:** `GET /api/lottery/getWinRecords`
+
+**请求参数:**
+```json
+{
+    "page": 1,         // 页码,默认1
+    "limit": 20        // 每页数量,默认20
+}
+```
+
+### 7. 设置收货地址
+**接口地址:** `POST /api/lottery/setWinRecordAddress`
+
+**请求参数:**
+```json
+{
+    "win_record_id": 6789,
+    "receiver_name": "张三",
+    "receiver_mobile": "13800138000",
+    "receiver_address": "北京市朝阳区某某小区1号楼101室"
+}
+```
+
+### 8. 获取活动排行榜
+**接口地址:** `GET /api/lottery/getRanking`
+
+**请求参数:**
+```json
+{
+    "activity_id": 1,  // 活动ID,必填
+    "page": 1,         // 页码,默认1
+    "limit": 50        // 每页数量,默认50
+}
+```
+
+## 核心服务使用
+
+### 1. 抽奖服务 (LotteryService)
+
+```php
+use app\common\Service\LotteryService;
+
+// 执行抽奖
+$result = LotteryService::drawLottery($activityId, $userId, $triggerType, $triggerOrderId, $triggerAmount);
+
+// 获取用户抽奖机会
+$chances = LotteryService::getUserChances($activityId, $userId);
+```
+
+### 2. 抽奖机会服务 (LotteryChanceService)
+
+```php
+use app\common\Service\LotteryChanceService;
+
+// 订单完成后分发抽奖机会
+$orderInfo = [
+    'id' => $orderId,
+    'user_id' => $userId,
+    'total_amount' => 299.00,
+    'goods' => [
+        ['goods_id' => 1, 'goods_sku_id' => 1, 'nums' => 2]
+    ]
+];
+$grantedChances = LotteryChanceService::checkAndGrantChanceForOrder($orderInfo, $userId);
+
+// 充值完成后分发抽奖机会
+$rechargeInfo = [
+    'id' => $rechargeId,
+    'user_id' => $userId,
+    'amount' => 100.00
+];
+$grantedChances = LotteryChanceService::checkAndGrantChanceForRecharge($rechargeInfo, $userId);
+
+// 手动给用户增加抽奖机会(管理员操作)
+LotteryChanceService::manualGrantChance($activityId, $userId, 3, '客服补偿', $adminId);
+```
+
+## 订单钩子集成
+
+系统提供了订单完成钩子,可以在订单支付完成或充值完成时自动检查并分发抽奖机会。
+
+### 1. 在 tags.php 中注册钩子
+
+```php
+// application/tags.php 或相应的钩子配置文件
+return [
+    // 订单支付完成钩子
+    'order_paid' => [
+        'app\\common\\behavior\\LotteryOrderHook'
+    ],
+    // 充值完成钩子
+    'recharge_paid' => [
+        'app\\common\\behavior\\LotteryOrderHook'
+    ]
+];
+```
+
+### 2. 在订单支付完成时触发钩子
+
+```php
+// 在订单支付完成的地方调用
+\think\Hook::listen('order_paid', ['order' => $orderInfo]);
+
+// 在充值完成的地方调用
+\think\Hook::listen('recharge_paid', ['recharge' => $rechargeInfo]);
+```
+
+## 抽奖算法说明
+
+系统采用基于概率权重的抽奖算法:
+
+1. **概率计算**:根据奖品设置的概率百分比进行权重计算
+2. **随机选择**:使用 mt_rand() 生成随机数,按权重范围选择中奖奖品
+3. **库存控制**:实时检查奖品库存,库存不足时自动排除
+4. **并发安全**:使用 Redis 锁防止并发抽奖导致的问题
+
+## 常见问题
+
+### 1. 如何设置奖品概率?
+- 概率值为百分比,例如:0.01 表示 0.01%
+- 所有奖品概率之和建议不超过 100%
+- 未中奖奖品的概率可以设置为较大值作为保底
+
+### 2. 如何配置参与条件?
+```php
+// 购买指定商品
+$condition = [
+    'type' => 1,                    // 购买指定商品
+    'goods_ids' => [1, 2, 3],       // 商品ID列表
+    'goods_rule' => 1,              // 1=指定商品参与,2=指定商品不可参与
+    'reward_times' => 1,            // 奖励抽奖次数
+    'is_repeatable' => 0            // 是否可重复获得
+];
+
+// 单笔订单消费满额
+$condition = [
+    'type' => 2,                    // 单笔订单消费满额
+    'condition_value' => 199.00,    // 满额金额
+    'reward_times' => 1,            // 奖励抽奖次数
+    'is_repeatable' => 1            // 可重复获得
+];
+```
+
+### 3. 如何处理奖品发放?
+- **自动发放**:优惠券、红包、兑换码可设置自动发放
+- **手动发放**:实物奖品需要手动处理发货
+- **发放状态**:通过 deliver_status 字段跟踪发放状态
+
+### 4. 性能优化建议
+- 使用 Redis 缓存活动和奖品信息
+- 对于大量用户的活动,考虑使用消息队列异步处理
+- 定期清理过期的抽奖记录数据
+
+## 扩展功能
+
+### 1. 社交分享
+可以扩展分享中奖结果到社交媒体的功能,增加活动传播度。
+
+### 2. 定时开奖
+对于按时间开奖的活动,可以使用定时任务来处理开奖逻辑。
+
+### 3. 数据分析
+基于抽奖数据进行用户行为分析,优化营销策略。
+
+### 4. 多渠道集成
+支持微信小程序、APP、H5 等多个渠道的抽奖功能。 

+ 4 - 2
docs/消费抽奖营销活动_数据库建表脚本.sql

@@ -23,10 +23,12 @@ CREATE TABLE `shop_lottery_activity` (
   `unlock_by_people` tinyint(1) DEFAULT '0' COMMENT '按参与人数依次解锁奖品: 0=否 1=是',
   `user_limit_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '适用人群: 1=全部会员 2=会员等级 3=会员标签',
   `user_limit_value` text COMMENT '适用人群限制值(JSON格式)',
+  `draw_time_enable` tinyint(1) DEFAULT '1' COMMENT '抽奖时间控制: 0=关闭 1=开启',
+  `draw_time_start` varchar(10) DEFAULT NULL COMMENT '每日抽奖开始时间(HH:mm)',
+  `draw_time_end` varchar(10) DEFAULT NULL COMMENT '每日抽奖结束时间(HH:mm)',
   `person_limit_num` int(11) DEFAULT '1' COMMENT '单人参与次数限制',
   `total_people_limit` int(11) DEFAULT NULL COMMENT '参与人数上限',
-  `redeem_expire_type` tinyint(1) DEFAULT 1 COMMENT '兑奖期限类型: 1=永久有效 2=固定时长',
-  `redeem_expire_days` int(11) DEFAULT NULL COMMENT '兑奖有效天数(固定时长)',
+  `draw_deadline` int(11) DEFAULT NULL COMMENT '抽奖截止时间',
   `guide_style` tinyint(1) DEFAULT '1' COMMENT '引导样式: 1=默认样式 2=自定义',
   `guide_image` varchar(500) DEFAULT NULL COMMENT '自定义引导图片',
   `guide_text` varchar(255) DEFAULT NULL COMMENT '引导文案',