package controller import ( "fmt" "github.com/codinl/go-logger" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" "github.com/pkg/errors" "mh-server/lib/auth" "mh-server/lib/status" "mh-server/lib/utils" "mh-server/model" "net/http" "time" ) // LotteryDraw 抽奖接口 // @Summary 抽奖接口 // @Tags 积分抽奖 // @Accept json // @Produce json // @Success 200 {object} model.LotteryDrawResponse // @Router /api/v1/lottery/draw [post] func LotteryDraw(c *gin.Context) { uc := auth.GetCurrentUser(c) if uc == nil { RespJson(c, status.Unauthorized, nil) return } logger.Info("参与抽奖用户uid:", uc.Uid) lotteryConfig, _ := model.GetLotteryConfig() if !lotteryConfig.LotteryEnabled { RespJson(c, 500, "抽奖系统维护中...") return } if lotteryConfig.CostPerDraw == 0 { // 每次抽奖消耗的积分,可配置 lotteryConfig.CostPerDraw = 99 } // 1. 判断用户当前积分是否足够 var userVm model.UserVm err := model.DB.Where("uid = ?", uc.Uid).First(&userVm).Error if err != nil { logger.Errorf("查询用户积分失败 uid=%d err=%v", uc.Uid, err) RespJson(c, 400, "积分不足,无法抽奖") return } if userVm.Vm < uint32(lotteryConfig.CostPerDraw) { logger.Warnf("积分不足 uid=%d 积分=%d", uc.Uid, userVm.Vm) RespJson(c, 400, "积分不足,无法抽奖") return } // 2. 获取可用奖品 var prizes []model.LotteryPrize if err := model.DB.Where("status = ? AND stock > 0", model.LotteryPrizeStatusEnabled).Find(&prizes).Error; err != nil { logger.Error("读取奖品失败:", err) RespJson(c, 500, "奖品更新中,请稍后再试...") return } if len(prizes) == 0 { RespOK(c, model.LotteryDrawResponse{ PrizeID: 0, PrizeName: "谢谢参与", PrizeLevel: 0, Message: "奖品暂不可用,稍后再试~", }) return } // 3. 扣除积分 err = model.UserVmUpdateTx(model.DB, uc.Uid, -int(lotteryConfig.CostPerDraw), "lottery", "抽奖消耗积分") if err != nil { logger.Errorf("积分扣除失败 uid=%d err=%v", uc.Uid, err) RespJson(c, 400, "积分扣除失败") return } // 4. 开启事务处理:扣积分 + 抽奖 + 减库存 + 记录 tx := model.DB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() RespJson(c, 500, "系统异常") return } }() // 5. 查询抽奖次数 & 更新总抽奖数 drawNumber, totalDrawNumber, err := model.GetLotteryDrawStats(uc.Uid) if err != nil { drawNumber = 0 totalDrawNumber = 0 } logger.Infof("用户抽奖次数为:%d,总抽奖次数为:%d", drawNumber, totalDrawNumber) if err = model.IncrementTotalDraw(tx); err != nil { tx.Rollback() logger.Errorf("更新总抽奖次数失败 uid=%d err=%v", uc.Uid, err) respNoPrize(c) return } // 6. 获取用户信息 userInfo := model.GetUserByUidTx(tx, uc.Uid) if userInfo == nil { tx.Rollback() logger.Error("查询用户信息异常,uid:", uc.Uid) respNoPrize(c) return } // 7. 白名单判断 var prize model.LotteryPrize var whiteEntry model.LotteryWhiteList err = tx.Where("uid = ?", uc.Uid).First(&whiteEntry).Error if errors.Is(err, gorm.ErrRecordNotFound) || (err == nil && drawNumber < int(whiteEntry.DrawNumber)) { // 普通抽奖 if err == nil && drawNumber < int(whiteEntry.DrawNumber) { logger.Errorf("用户%d在白名单,但抽奖次数 %d 小于设定次数 %d", uc.Uid, drawNumber, whiteEntry.DrawNumber) } // 查询用户当天消费金额 userTotalAmount := model.GetUserDailyTotalAmount(uc.Uid) isRentalMember := false // 判断用户是否为租卡会员 if userInfo.IsMemberNew() { isRentalMember = true } // 当用户抽奖次数超过40,则判断是否中过1-4等奖;收集用户已中奖的奖项 hasWon4OrAbove, userPrizeIds, _ := model.PrepareUserLotteryInfo(uc.Uid) // 普通抽奖逻辑 prize, err = model.DrawFromDatabasePrizes(prizes, drawNumber, totalDrawNumber, userTotalAmount, userPrizeIds, hasWon4OrAbove, isRentalMember) if err != nil { tx.Rollback() logger.Errorf("DrawFromDatabasePrizes err uid=%d err=%v", uc.Uid, err) respNoPrize(c) return } } else if err != nil { logger.Errorf("查询白名单失败 uid=%d err=%v", uc.Uid, err) RespJson(c, 500, "系统异常,请稍后再试...") return } else { logger.Infof("用户%d在白名单中,%d等奖,第%d次中奖", uc.Uid, whiteEntry.PrizeLevel, whiteEntry.DrawNumber) // 命中白名单,直接返回配置奖品 err = tx.Where("level = ? and stock > 1", whiteEntry.PrizeLevel).First(&prize).Error if err != nil { tx.Rollback() logger.Errorf("白名单抽奖失败 uid=%d err=%v", uc.Uid, err) respNoPrize(c) return } } // 8. 减少奖品库存(乐观锁) result := tx.Model(&model.LotteryPrize{}). Where("id = ? AND stock > 0", prize.ID). UpdateColumn("stock", gorm.Expr("stock - 1")) if result.Error != nil || result.RowsAffected == 0 { tx.Rollback() logger.Errorf("减少奖品库存失败 uid=%d err=%v", uc.Uid, result.Error) respNoPrize(c) return } // 9. 记录中奖信息 isWin := prize.PrizeType != model.LotteryPrizeTypeNone lotteryRecord := model.LotteryRecord{ Uid: uint(uc.Uid), PrizeId: uint(prize.ID), PrizeName: prize.Name, PrizeType: prize.PrizeType, PrizeLevel: prize.Level, Status: model.LotteryRecordStatusDelivered, IsWin: isWin, } if prize.PrizeType == model.LotteryPrizeTypePhysical { // 判断是不是实物奖品 lotteryRecord.Status = model.LotteryRecordStatusPending // 待处理 } err = tx.Create(&lotteryRecord).Error if err != nil { tx.Rollback() logger.Errorf("记录中奖信息失败 uid=%d err=%v", uc.Uid, err) respNoPrize(c) return } // 10. 发放奖品 如果是租卡会员兑换码,则直接发放兑换券 if prize.PrizeType == model.LotteryPrizeTypeRentCard30 { var redeemCode model.RedeemCode err = tx.Table("redeem_code").Where("status=?", model.RedeemCodeStatusStock). Where("code_type=?", model.CodeTypeMemberCard30).Order("id ASC").Limit(1).Find(&redeemCode).Error if err != nil { tx.Rollback() logger.Error("get redeem_code err:", err) respNoPrize(c) return } userRedeemCode := &model.UserRedeemCode{ Uid: uc.Uid, Status: model.UserRedeemCodeStatusHold, SerialCode: redeemCode.SerialCode, CodeType: redeemCode.CodeType, ActivityType: model.RedeemCodeActivityTypeLottery, StoreId: uint32(userInfo.StoreId), } err = tx.Create(userRedeemCode).Error if err != nil { tx.Rollback() logger.Error("create userRedeemCode err:", err) respNoPrize(c) return } } else if prize.PrizeType == model.LotteryPrizeTypePoint { // 如果是积分奖品,则直接发放积分 err = model.UserVmUpdateTx(tx, uc.Uid, prize.PrizeValue, "lottery_add", "抽奖赢取积分") if err != nil { tx.Rollback() logger.Error("积分发放失败:", err) respNoPrize(c) return } } else if prize.PrizeType == model.LotteryPrizeTypeCoupon { // 如果是优惠券奖品,则直接发放优惠券 var coupon model.Coupon err = model.NewCouponQuerySet(tx).ActivityIdEq(uint32(prize.PrizeValue)).One(&coupon) if err != nil { tx.Rollback() logger.Error("get coupon err:", err) respNoPrize(c) return } if coupon.ID != 0 { couponCode, err := utils.GenerateRandomNumber19() if err != nil { tx.Rollback() logger.Error("GenerateRandomNumber19err:", err) respNoPrize(c) return } userCoupon := &model.UserCoupon{ Uid: uc.Uid, CouponId: coupon.ID, CouponType: coupon.CouponType, ActivityType: coupon.ActivityType, ActivityId: coupon.ActivityId, Value: coupon.Value, State: 1, ActiveStart: time.Now(), ActiveEnd: time.Now().AddDate(0, 1, 0), UseTime: time.Time{}, MemberLevel: coupon.MemberLevel, Approach: 0, PromotionalSales: 0, RedeemCode: "", CategoryNumber: coupon.CategoryNumber, CommodityNumber: coupon.CommodityNumber, Code: couponCode, } err = tx.Create(userCoupon).Error if err != nil { tx.Rollback() logger.Error("优惠券发放失败:", err) respNoPrize(c) return } } } // 11. 提交事务 if err = tx.Commit().Error; err != nil { tx.Rollback() logger.Errorf("抽奖提交事务失败 uid=%d err=%v", uc.Uid, err) respNoPrize(c) return } // 12. 返回中奖结果 var message string if prize.Name == "谢谢参与" || prize.PrizeType == model.LotteryPrizeTypeNone { message = "很遗憾未中奖,祝您下次好运~" } else { message = fmt.Sprintf("🎉 恭喜您获得【%s】", prize.Name) } RespOK(c, model.LotteryDrawResponse{ PrizeID: int(prize.ID), PrizeName: prize.Name, PrizeLevel: prize.Level, Message: message, }) } func respNoPrize(c *gin.Context) { RespOK(c, model.LotteryDrawResponse{ PrizeID: 8, PrizeName: "谢谢参与", PrizeLevel: 0, Message: "很遗憾未中奖,祝您下次好运!", }) } // LotteryPrizes 奖品列表接口 // @Summary 奖品列表 // @Tags 积分抽奖 // @Produce json // @Param data body model.LotteryPrizeQuery true "分页参数" // @Success 200 {object} model.LotteryPrizePageResponse // @Router /api/v1/lottery/prizes [post] func LotteryPrizes(c *gin.Context) { var req model.LotteryPrizeQuery if err := c.ShouldBindJSON(&req); err != nil { RespJson(c, 400, "参数错误") return } if req.PageNum <= 0 { req.PageNum = 1 } if req.PageSize <= 0 || req.PageSize > 100 { req.PageSize = 20 } offset := (req.PageNum - 1) * req.PageSize var total int64 if err := model.DB.Model(&model.LotteryPrize{}). Where("status = ?", model.LotteryPrizeStatusEnabled). // 仅启用奖品 Count(&total).Error; err != nil { RespJson(c, 500, "获取奖品总数失败") return } var prizes []model.LotteryPrize err := model.DB. Where("status = ?", model.LotteryPrizeStatusEnabled). Order("level ASC"). Limit(req.PageSize). Offset(offset). Find(&prizes).Error if err != nil { RespJson(c, 500, "获取奖品列表失败") return } resp := model.LotteryPrizePageResponse{ Count: uint32(total), PageNum: req.PageNum, List: prizes, } RespOK(c, resp) } // LotteryRecords 抽奖记录接口(分页) // @Summary 用户抽奖记录(分页) // @Tags 积分抽奖 // @Accept json // @Produce json // @Param data body model.LotteryRecordQuery true "分页参数" // @Success 200 {array} model.LotteryRecordPageResponse // @Router /api/v1/lottery/records [post] func LotteryRecords(c *gin.Context) { uc := auth.GetCurrentUser(c) if uc == nil { RespJson(c, status.Unauthorized, nil) return } var req model.LotteryRecordQuery if err := c.ShouldBindJSON(&req); err != nil { RespJson(c, 400, "参数错误") return } // 默认分页参数 if req.PageNum <= 0 { req.PageNum = 1 } if req.PageSize <= 0 || req.PageSize > 100 { req.PageSize = 20 } offset := (req.PageNum - 1) * req.PageSize // 构建基础查询 db := model.DB.Table("lottery_record AS r"). Joins("LEFT JOIN lottery_prize AS p ON r.prize_id = p.id"). Where("r.uid = ?", uc.Uid) // 根据 win_status 筛选记录:0=全部,1=已中奖,2=未中奖 switch req.WinStatus { case 1: db = db.Where("r.is_win = ?", true) case 2: db = db.Where("r.is_win = ?", false) } if req.PrizeLevel != 0 { db = db.Where("r.prize_level = ?", req.PrizeLevel) } if req.PrizeType != 0 { db = db.Where("r.prize_type = ?", req.PrizeType) } // 获取总数 var total int64 if err := db.Count(&total).Error; err != nil { RespJson(c, 500, "获取记录总数失败") return } // 查询数据 type LotteryRecordWithImage struct { model.LotteryRecord PrizeImages string `json:"prize_images"` } var joinedRecords []LotteryRecordWithImage if err := db.Select("r.*, p.images AS prize_images"). Order("r.created_at DESC"). Limit(req.PageSize). Offset(offset). Scan(&joinedRecords).Error; err != nil { RespJson(c, 500, "获取抽奖记录失败") return } // 映射结果 var records []model.LotteryRecord for _, jr := range joinedRecords { record := jr.LotteryRecord record.Images = jr.PrizeImages records = append(records, record) } resp := model.LotteryRecordPageResponse{ Count: uint32(total), PageNum: req.PageNum, List: records, } RespOK(c, resp) } // RecentWinners 查询最近中奖用户的抽奖记录 // @Summary 查询最近中奖用户 // @Tags 积分抽奖 // @Accept json // @Produce json // @Param data body model.RecentWinnersQuery true "查询参数(记录条数)" // @Success 200 {object} model.RecentWinnersResponse // @Router /api/v1/lottery/recent_winners [post] func RecentWinners(c *gin.Context) { var req model.RecentWinnersQuery if err := c.ShouldBindJSON(&req); err != nil || req.Limit <= 0 { req.Limit = 10 // 默认值 } var total int64 if err := model.DB.Model(&model.LotteryRecord{}). Where("is_win = ?", true). Count(&total).Error; err != nil { RespJson(c, 500, "获取中奖总数失败") return } var records []model.LotteryRecord if err := model.DB. Where("is_win = ? AND prize_type != ?", true, model.LotteryPrizeTypeNone). Order("created_at DESC"). Limit(req.Limit). Find(&records).Error; err != nil { RespJson(c, 500, "获取中奖记录失败") return } resp := model.RecentWinnersResponse{ Count: uint32(total), List: records, } RespOK(c, resp) } // GetPublicLotteryConfigHandler 查询抽奖配置(公开接口) // @Summary 查询抽奖模块配置(公开) // @Tags 积分抽奖 // @Produce json // @Success 200 {object} model.LotteryConfig // @Router /api/v1/lottery/config/public [post] func GetPublicLotteryConfigHandler(c *gin.Context) { cfg, err := model.GetLotteryConfig() if err != nil { RespJson(c, 500, "获取抽奖配置失败") return } RespOK(c, cfg) } // GetPublicLotteryTitleConfigHandler 查询积分抽奖标题(公开) // @Summary 查询积分抽奖标题(公开) // @Tags 积分抽奖 // @Produce json // @Success 200 {object} model.LotteryTitleConfig // @Router /api/v1/lottery/config/title [post] func GetPublicLotteryTitleConfigHandler(c *gin.Context) { cfg, err := model.GetLotteryTitle() if err != nil { RespJson(c, 500, "获取抽奖配置失败") return } RespOK(c, cfg) } // SubmitDeliveryInfo 用户填写奖品收货地址 // @Summary 用户填写实物奖品收货地址 // @Tags 积分抽奖 // @Accept json // @Produce json // @Param data body model.SubmitDeliveryRequest true "收货信息" // @Success 200 {string} string "提交成功" // @Router /api/v1/lottery/submit_delivery [post] func SubmitDeliveryInfo(c *gin.Context) { uc := auth.GetCurrentUser(c) if uc == nil { RespJson(c, status.Unauthorized, nil) return } var req model.SubmitDeliveryRequest if err := c.ShouldBindJSON(&req); err != nil || req.RecordId == 0 { RespJson(c, 400, "参数错误") return } // 查询中奖记录 var record model.LotteryRecord if err := model.DB.Where("id = ? AND uid = ?", req.RecordId, uc.Uid).First(&record).Error; err != nil { RespJson(c, 404, "未找到对应的抽奖记录") return } if record.PrizeType != model.LotteryPrizeTypePhysical { RespJson(c, 400, "该奖品非实物奖品,无需填写地址") return } // 检查是否已提交过订单 var exist model.LotteryPrizeOrder if err := model.DB.Where("record_id = ?", record.ID).First(&exist).Error; err == nil { RespJson(c, 400, "您已提交过收货信息") return } // 查询用户手机号 userInfo := model.GetUserByUid(uc.Uid) // 开启事务 tx := model.DB.Begin() if tx.Error != nil { RespJson(c, 500, "事务开启失败") return } // 创建订单 order := model.LotteryPrizeOrder{ RecordId: uint(record.ID), Uid: record.Uid, Tel: userInfo.Tel, PrizeId: record.PrizeId, PrizeName: record.PrizeName, ReceiverName: req.ReceiverName, ReceiverPhone: req.ReceiverPhone, ReceiverAddr: req.ReceiverAddr, Status: 0, // 待发货 } if err := tx.Create(&order).Error; err != nil { tx.Rollback() RespJson(c, 500, "保存收货信息失败") return } // 更新抽奖记录状态为处理中(2) if err := tx.Model(&model.LotteryRecord{}). Where("id = ?", record.ID). Update("status", model.LotteryRecordStatusInProcess).Error; err != nil { tx.Rollback() RespJson(c, 500, "更新抽奖记录状态失败") return } if err := tx.Commit().Error; err != nil { RespJson(c, 500, "保存收货信息失败:事务提交失败") return } RespOK(c, "收货信息提交成功") } // ConfirmLotteryReceipt 用户确认收货 // @Summary 用户确认收货 // @Tags 积分抽奖 // @Accept json // @Produce json // @Param data body model.ConfirmReceiptRequest true "收货确认请求" // @Success 200 {string} string "确认成功" // @Router /api/v1/lottery/confirm_receipt [post] func ConfirmLotteryReceipt(c *gin.Context) { uc := auth.GetCurrentUser(c) if uc == nil { RespJson(c, status.Unauthorized, nil) return } var req model.ConfirmReceiptRequest if err := c.ShouldBindJSON(&req); err != nil || req.RecordId == 0 { RespJson(c, 400, "参数错误") return } // 查询奖品订单 var order model.LotteryPrizeOrder if err := model.DB.Where("record_id = ? AND uid = ?", req.RecordId, uc.Uid).First(&order).Error; err != nil { RespJson(c, 404, "未找到对应的奖品订单") return } // 状态只能是已发货(1)时才能确认收货 if order.Status != 1 { RespJson(c, 400, "当前状态不可确认收货") return } now := time.Now() // 开启事务 tx := model.DB.Begin() // 1. 更新订单状态为 已收货(2) if err := tx.Model(&order). Updates(map[string]interface{}{ "status": 2, "received_at": now, }).Error; err != nil { tx.Rollback() RespJson(c, 500, "确认收货失败") return } // 2. 更新抽奖记录状态为 已发放(1) if err := tx.Model(&model.LotteryRecord{}). Where("id = ? AND uid = ?", req.RecordId, uc.Uid). Update("status", model.LotteryRecordStatusDelivered).Error; err != nil { tx.Rollback() RespJson(c, 500, "同步更新抽奖记录失败") return } if err := tx.Commit().Error; err != nil { RespJson(c, 500, "确认失败,请稍后重试") return } RespOK(c, "确认收货成功") } // GetTodayDrawCount 查询用户当天抽奖次数 // @Summary 查询用户当天抽奖次数 // @Tags 积分抽奖 // @Accept json // @Produce json // @Success 200 {object} model.TodayDrawCountResponse // @Router /api/v1/lottery/today_draw_count [post] func GetTodayDrawCount(c *gin.Context) { uc := auth.GetCurrentUser(c) if uc == nil { RespJson(c, status.Unauthorized, nil) return } // 获取当天零点时间 startOfDay := time.Now().Truncate(24 * time.Hour) var count int64 if err := model.DB.Model(&model.LotteryRecord{}). Where("uid = ? AND created_at >= ?", uc.Uid, startOfDay). Count(&count).Error; err != nil { RespJson(c, 500, "查询抽奖次数失败") return } resp := model.TodayDrawCountResponse{ DrawCount: int(count), } RespOK(c, resp) } // GetLotteryPrizeOrderDetail 查询抽奖订单详情 // @Summary 查询抽奖订单详情 // @Tags 积分抽奖 // @Accept json // @Produce json // @Param data body model.GetLotteryPrizeOrderDetailRequest true "查询参数" // @Success 200 {object} model.LotteryPrizeOrderDetailResponse // @Router /api/v1/lottery/prize_order/detail [post] func GetLotteryPrizeOrderDetail(c *gin.Context) { uc := auth.GetCurrentUser(c) if uc == nil { RespJson(c, status.Unauthorized, nil) return } var req model.GetLotteryPrizeOrderDetailRequest if err := c.ShouldBindJSON(&req); err != nil || req.OrderID == 0 { RespJson(c, http.StatusBadRequest, "参数错误:order_id 必传") return } var detail model.LotteryPrizeOrderDetailResponse db := model.DB.Table("lottery_prize_order"). Select(`lottery_prize_order.*, user.wx_name as nickname, user.tel, user.member_level, store.name as store_name, lottery_prize.prize_type, lottery_prize.level as prize_level, lottery_prize.images, lottery_prize.prize_value`). Joins("LEFT JOIN user ON user.uid = lottery_prize_order.uid"). Joins("LEFT JOIN store ON store.id = user.store_id"). Joins("LEFT JOIN lottery_prize ON lottery_prize.id = lottery_prize_order.prize_id"). Where("lottery_prize_order.record_id = ?", req.OrderID) if err := db.First(&detail).Error; err != nil { RespJson(c, http.StatusNotFound, "未找到对应的订单记录") return } RespOK(c, detail) }