From 8509413f9fc5c88544570ca9f1e2c25280d76043 Mon Sep 17 00:00:00 2001 From: chenlin Date: Wed, 6 Aug 2025 18:07:11 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9E=E7=A7=AF=E5=88=86?= =?UTF-8?q?=E6=8A=BD=E5=A5=96=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/game_card.go | 4 + controller/lottery.go | 673 ++++++++++++++++++++++++++++++++++++++++ model/config.go | 48 +++ model/game_card.go | 4 + model/lottery.go | 237 ++++++++++++++ model/user_vm.go | 48 +++ router/router_app.go | 16 + 7 files changed, 1030 insertions(+) create mode 100644 controller/lottery.go create mode 100644 model/lottery.go diff --git a/controller/game_card.go b/controller/game_card.go index e8d80d3..ad185c8 100644 --- a/controller/game_card.go +++ b/controller/game_card.go @@ -228,6 +228,10 @@ func GameCardSearch(c *gin.Context) { } } + if cardList == nil { + cardList = []model.GameCard{} + } + ret := map[string]interface{}{ "card_list": cardList, "cur_page": req.Page, diff --git a/controller/lottery.go b/controller/lottery.go new file mode 100644 index 0000000..6e8e088 --- /dev/null +++ b/controller/lottery.go @@ -0,0 +1,673 @@ +package controller + +import ( + "errors" + "fmt" + "github.com/codinl/go-logger" + "github.com/gin-gonic/gin" + "github.com/jinzhu/gorm" + "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 + } + + lotteryConfig, _ := model.GetLotteryConfig() + if !lotteryConfig.LotteryEnabled { + RespJson(c, 500, "抽奖系统维护中...") + return + } + if lotteryConfig.CostPerDraw == 0 { // 每次抽奖消耗的积分,可配置 + lotteryConfig.CostPerDraw = 99 + } + + // 1. 判断用户当前积分是否足够 + var userVm model.UserVm + if err := model.DB.Where("uid = ?", uc.Uid).First(&userVm).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) || userVm.Vm < uint32(lotteryConfig.CostPerDraw) { + RespJson(c, 400, "积分不足,无法抽奖") + } else { + RespJson(c, 500, "查询积分失败") + } + return + } + + // 2. 获取可用奖品 + var prizes []model.LotteryPrize + if err := model.DB.Where("status = ? AND stock > 0", model.LotteryPrizeStatusEnabled).Find(&prizes).Error; err != nil { + 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 { + RespJson(c, 400, "积分扣除失败") + return + } + + // 4. 开启事务处理:扣积分 + 抽奖 + 减库存 + 记录 + tx := model.DB.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + RespJson(c, 500, "系统异常") + } + }() + + // 5. 查询抽奖次数 + drawNumber, totalDrawNumber, err := model.GetLotteryDrawStats(uc.Uid) + if err != nil { + drawNumber = 0 + totalDrawNumber = 0 + } + + // 更新总抽奖数 + if err = model.IncrementTotalDraw(); err != nil { + model.UserVmUpdateTx(model.DB, uc.Uid, int(lotteryConfig.CostPerDraw), "lottery", "抽奖失败积分补偿") + logger.Error("更新总抽奖次数失败:", err) + RespJson(c, 500, "抽奖失败,系统异常") + return + } + + // 6. 白名单判断 + var prize model.LotteryPrize + var whiteEntry model.LotteryWhiteList + err = model.DB. + Where("uid = ?", uc.Uid).First(&whiteEntry).Error + + if err == nil { + // 命中白名单,直接返回配置奖品 + err = model.DB.First(&prize, whiteEntry.PrizeID).Error + if err != nil { + RespOK(c, model.LotteryDrawResponse{ + PrizeID: 0, + PrizeName: "谢谢参与", + PrizeLevel: 0, + Message: "很遗憾未中奖,祝你下次好运!", + }) + } + } else { + // 普通抽奖逻辑 + prize, err = model.DrawFromDatabasePrizes(prizes, drawNumber, totalDrawNumber) + if err != nil { + RespOK(c, model.LotteryDrawResponse{ + PrizeID: 0, + PrizeName: "谢谢参与", + PrizeLevel: 0, + Message: "很遗憾未中奖,祝你下次好运!", + }) + return + } + } + + // 7. 减少奖品库存(乐观锁) + 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() + model.UserVmUpdateTx(model.DB, uc.Uid, int(lotteryConfig.CostPerDraw), "lottery", "抽奖失败积分补偿") + RespJson(c, 500, "奖品库存不足") + return + } + + // 8. 记录中奖信息 + 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() + model.UserVmUpdateTx(model.DB, uc.Uid, int(lotteryConfig.CostPerDraw), "lottery", "抽奖失败积分补偿") + RespJson(c, 500, "记录中奖信息失败") + return + } + + // 如果是积分奖品,则直接发放积分 + if prize.PrizeType == model.LotteryPrizeTypePoint { + err = model.UserVmUpdateTx(tx, uc.Uid, prize.PrizeValue, "lottery_add", "抽奖赢取积分") + if err != nil { + model.UserVmUpdateTx(model.DB, uc.Uid, int(lotteryConfig.CostPerDraw), "lottery", "抽奖失败积分补偿") + tx.Rollback() + RespJson(c, 400, "积分发放失败") + return + } + } + + // 如果是优惠券奖品,则直接发放优惠券 + if prize.PrizeType == model.LotteryPrizeTypeCoupon { + var coupon model.Coupon + err = model.NewCouponQuerySet(model.DB).ActivityIdEq(uint32(prize.PrizeValue)).One(&coupon) + if err != nil { + logger.Error("get coupon err:", err) + } + + if coupon.ID != 0 { + couponCode, err := utils.GenerateRandomNumber19() + if err != nil { + logger.Error("GenerateRandomNumber19err:", err) + } + 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 { + logger.Error("user coupon err:", err) + model.UserVmUpdateTx(model.DB, uc.Uid, int(lotteryConfig.CostPerDraw), "lottery", "抽奖失败积分补偿") + tx.Rollback() + RespJson(c, 400, "优惠券发放失败") + return + } + } + } + + // 9. 提交事务 + if err := tx.Commit().Error; err != nil { + model.UserVmUpdateTx(model.DB, uc.Uid, int(lotteryConfig.CostPerDraw), "lottery", "抽奖失败积分补偿") + RespJson(c, 500, "抽奖失败,请稍后重试") + return + } + + // 10. 返回中奖结果 + 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, + }) + +} + +// 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) + } + + // 获取总数 + 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) +} diff --git a/model/config.go b/model/config.go index ac77911..ba476ab 100644 --- a/model/config.go +++ b/model/config.go @@ -41,6 +41,8 @@ const ( ConfigPaymentGenre = "payment_genre_config" // 支付方式 ConfigNamePrivilegeMember = "privilege_member_config" // 尊享会员配置 ConfigNameOverdueNotice = "overdue_notice" // 超期提示语 + ConfigNameLotteryLimit = "lottery_config" // 积分抽奖配置 + ConfigNameLotteryTitle = "lottery_title_config" // 积分抽奖标题 ) func PayConfigInfo() (*PayConfig, error) { @@ -432,6 +434,52 @@ func GetPaymentGenre() (uint32, error) { } } +type LotteryConfig struct { + CostPerDraw uint `json:"cost_per_draw"` // 单次抽奖积分 + DailyLimit uint `json:"daily_limit"` // 每日抽奖上限 + LotteryEnabled bool `json:"lottery_enabled"` // 是否开启抽奖功能 +} + +type LotteryTitleConfig struct { + LotteryTitle string `json:"lottery_title"` // 积分抽奖标题 +} + +func GetLotteryConfig() (*LotteryConfig, error) { + var config Config + err := NewConfigQuerySet(DB).NameEq(ConfigNameLotteryLimit).One(&config) + if err != nil { + logger.Error("读取配置失败:", err) + return nil, err + } + + var lotteryCfg LotteryConfig + err = json.Unmarshal([]byte(config.Value), &lotteryCfg) + if err != nil { + logger.Error("配置解析失败:", err) + return nil, err + } + + return &lotteryCfg, nil +} + +func GetLotteryTitle() (*LotteryTitleConfig, error) { + var config Config + err := NewConfigQuerySet(DB).NameEq(ConfigNameLotteryTitle).One(&config) + if err != nil { + logger.Error("读取配置失败:", err) + return nil, err + } + + var lotteryCfg LotteryTitleConfig + err = json.Unmarshal([]byte(config.Value), &lotteryCfg) + if err != nil { + logger.Error("配置解析失败:", err) + return nil, err + } + + return &lotteryCfg, nil +} + //type ConfigInterface interface { // Encode() string //} diff --git a/model/game_card.go b/model/game_card.go index fb23df2..51e627e 100644 --- a/model/game_card.go +++ b/model/game_card.go @@ -630,6 +630,10 @@ func GetGameCardSearch(name string, page, pageSize int, storeId uint32) ([]GameC } } + if cards == nil { + cards = make([]GameCard, 0) + } + return cards, totalPage, nil } diff --git a/model/lottery.go b/model/lottery.go new file mode 100644 index 0000000..6ad87b8 --- /dev/null +++ b/model/lottery.go @@ -0,0 +1,237 @@ +package model + +import ( + "errors" + "math/rand" + "time" +) + +const ( + LotteryPrizeTypePoint = 1 // 积分奖品 + LotteryPrizeTypeCoupon = 2 // 优惠券奖品 + LotteryPrizeTypePhysical = 3 // 实物奖品 + + LotteryRecordStatusPending = 0 // 待处理 + LotteryRecordStatusDelivered = 1 // 已发放 + LotteryRecordStatusInProcess = 2 // 处理中 + LotteryRecordStatusFailed = 3 // 失败 + + LotteryPrizeStatusEnabled = 1 // 启用奖品 + LotteryPrizeStatusDisabled = 2 // 禁用奖品 + + LotteryPrizeTypeNone = 0 // 谢谢参与 +) + +// LotteryPrize 奖品信息表(抽奖) +type LotteryPrize struct { + Model + + Name string `json:"name"` // 奖品名称 + PrizeType int `json:"prize_type"` // 奖品类型:1-积分 2-优惠券 3-实物 0-谢谢参与 + PrizeValue int `json:"prize_value"` // 奖品值(积分数或券ID等) + Level int `json:"level"` // 奖品等级,如1等奖、2等奖等 + Weight int `json:"weight"` // 抽奖权重 + Stock int `json:"stock"` // 剩余库存 + Status int `json:"status"` // 奖品状态:1-启用 2-禁用 + UnlockUserCount int `json:"unlock_user_count"` // 解锁条件:用户个人抽奖次数 + UnlockTotalCount int `json:"unlock_total_count"` // 解锁条件:所有用户总抽奖次数 + Images string `json:"images"` // 奖品图片 +} + +// LotteryRecord 抽奖记录 +type LotteryRecord struct { + Model + + Uid uint `json:"uid"` // 用户ID + PrizeId uint `json:"prize_id"` // 奖品ID + PrizeName string `json:"prize_name"` // 奖品名称 + PrizeType int `json:"prize_type"` // 奖品类型 + PrizeLevel int `json:"prize_level"` // 奖品等级 + PrizeValue int `json:"prize_value"` // 奖品值(积分数或券ID等) + Status int `json:"status"` // 状态:0-待处理 1-已发放 2-处理中 3-失败 + IsWin bool `json:"is_win"` // 是否中奖(false 表示“谢谢参与”等无奖项) + Images string `json:"images" gorm:"-"` // 奖品图片 +} + +// LotteryPrizeOrder 抽奖奖品订单(包含用户收件信息、物流信息、发货状态) +type LotteryPrizeOrder struct { + Model + + RecordId uint `json:"record_id"` // 抽奖记录ID + Uid uint `json:"uid"` // 用户ID + Tel string `json:"tel"` // 用户手机号 + PrizeId uint `json:"prize_id"` // 奖品ID + PrizeName string `json:"prize_name"` // 奖品名称 + + // 用户提交的收货信息 + ReceiverName string `json:"receiver_name"` // 收件人 + ReceiverPhone string `json:"receiver_phone"` // 收件人手机号 + ReceiverAddr string `json:"receiver_addr"` // 收件地址 + + // 发货信息 + LogisticsCompany string `json:"logistics_company"` // 快递公司 + LogisticsNumber string `json:"logistics_number"` // 快递单号 + ShippedAt time.Time `json:"shipped_at"` // 发货时间 + ReceivedAt time.Time `json:"received_at"` // 收货时间 + + Status int `json:"status"` // 发货状态:0-待发货 1-已发货 2-已收货 3-取消 +} + +// LotteryStats 抽奖统计表 +type LotteryStats struct { + Model + + TotalDraw uint `json:"total_draw"` // 总抽奖数 +} + +// LotteryWhiteList 抽奖白名单表 +type LotteryWhiteList struct { + Model + + Uid uint `json:"uid"` // 用户ID + DrawNumber uint `json:"draw_number"` // 第几次抽奖命中(如第3次) + PrizeID uint `json:"prize_id"` // 奖品ID(外键指向 LotteryPrize) +} + +type LotteryDrawResponse struct { + PrizeID int `json:"prize_id"` // 奖品id + PrizeName string `json:"prize_name"` // 奖品名称 + PrizeLevel int `json:"prize_level"` // 奖品等级 + Message string `json:"message"` // 中奖提示信息 +} + +type LotteryPrizeQuery struct { + PageNum int `json:"page_num"` + PageSize int `json:"page_size"` +} + +type LotteryPrizePageResponse struct { + Count uint32 `json:"count"` // 奖品总数 + PageNum int `json:"page_num"` // 当前页码 + List []LotteryPrize `json:"list"` // 奖品列表 +} + +type LotteryRecordQuery struct { + PageNum int `json:"page_num"` + PageSize int `json:"page_size"` + WinStatus int `json:"win_status"` // 新增字段:0=全部,1=已中奖,2=未中奖 +} + +type LotteryRecordPageResponse struct { + Count uint32 `json:"count"` // 总记录数 + PageNum int `json:"page_num"` // 当前页码 + List []LotteryRecord `json:"list"` // 抽奖记录列表 +} + +func DrawFromDatabasePrizes(prizes []LotteryPrize, userDrawCount, globalDrawCount int) (LotteryPrize, error) { + var available []LotteryPrize + totalWeight := 0 + + for _, p := range prizes { + // 同时满足两个解锁条件才可参与抽奖 + if userDrawCount >= p.UnlockUserCount && globalDrawCount >= p.UnlockTotalCount { + available = append(available, p) + totalWeight += p.Weight + } + } + + if totalWeight == 0 { + return LotteryPrize{}, errors.New("无可抽奖奖品") + } + + r := rand.Intn(totalWeight) + acc := 0 + for _, p := range available { + acc += p.Weight + if r < acc { + return p, nil + } + } + + return LotteryPrize{}, errors.New("抽奖失败") +} + +func getUnlockDrawCount(level int) int { + switch level { + case 1: + return 5000 + case 2: + return 2000 + case 3: + return 1000 + case 4: + return 500 + default: + return 0 + } +} + +// IncrementTotalDraw 更新总抽奖数 +func IncrementTotalDraw() error { + return DB.Exec("UPDATE lottery_stats SET total_draw = total_draw + 1 WHERE id = 1").Error +} + +// GetLotteryDrawStats 查询某个用户的抽奖次数和全站总抽奖次数 +func GetLotteryDrawStats(uid uint32) (userDrawCount int, totalDrawCount int, err error) { + // 查询用户抽奖次数 + err = DB.Model(&LotteryRecord{}). + Where("uid = ?", uid). + Count(&userDrawCount).Error + if err != nil { + return + } + + // 查询总抽奖次数(从 lottery_stats 表) + var stats LotteryStats + err = DB.First(&stats).Error + if err != nil { + return + } + + totalDrawCount = int(stats.TotalDraw) + return +} + +type RecentWinnersQuery struct { + Limit int `json:"limit"` // 期望获取的记录数 +} + +type RecentWinnersResponse struct { + Count uint32 `json:"count"` // 实际中奖总数 + List []LotteryRecord `json:"list"` // 最近中奖记录列表 +} + +type SubmitDeliveryRequest struct { + RecordId uint `json:"record_id" binding:"required"` // 抽奖记录ID,必填 + ReceiverName string `json:"receiver_name" binding:"required"` // 收件人姓名,必填 + ReceiverPhone string `json:"receiver_phone" binding:"required"` // 收件人手机号,必填 + ReceiverAddr string `json:"receiver_addr" binding:"required"` // 收件人地址,必填 +} + +type ConfirmReceiptRequest struct { + RecordId uint `json:"record_id" binding:"required"` // 抽奖记录ID,必填 +} + +type TodayDrawCountResponse struct { + DrawCount int `json:"draw_count"` // 今日抽奖次数 +} + +type GetLotteryPrizeOrderDetailRequest struct { + OrderID uint `json:"order_id" binding:"required"` // 奖品订单ID +} + +type LotteryPrizeOrderDetailResponse struct { + LotteryPrizeOrder + + // 用户信息 + Nickname string `json:"nickname"` // 用户昵称 + Tel string `json:"tel"` // 用户手机号 + MemberLevel uint32 `json:"member_level"` // 当前会员等级 + StoreName string `json:"store_name"` // 所属门店 + + // 奖品信息(扩展字段) + PrizeType int `json:"prize_type"` // 奖品类型 + PrizeLevel int `json:"prize_level"` // 奖品等级 + PrizeValue int `json:"prize_value"` // 奖品值 + Images string `json:"images"` // 奖品图片 +} diff --git a/model/user_vm.go b/model/user_vm.go index 8aa2d3f..7855964 100644 --- a/model/user_vm.go +++ b/model/user_vm.go @@ -145,6 +145,54 @@ func UserVmUpdate(uid uint32, amount int, event, describe string) error { return nil } +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func UserVmUpdateTx(tx *gorm.DB, uid uint32, amount int, event, describe string) error { + var userVm UserVm + err := NewUserVmQuerySet(tx).UidEq(uid).One(&userVm) + if err != nil && err != RecordNotFound { + logger.Error("积分查询失败:", err) + return err + } + if err == RecordNotFound { + if amount < 0 { + return errors.New("积分不足") + } + userVm = UserVm{Uid: uid, Vm: uint32(amount)} + if err := tx.Create(&userVm).Error; err != nil { + return err + } + } else { + newVm := int(userVm.Vm) + amount + if newVm < 0 { + newVm = 0 + } + err = tx.Exec("UPDATE user_vm SET vm = ? WHERE uid = ?", newVm, uid).Error + if err != nil { + return err + } + } + + record := UserVmRecord{ + Uid: uid, + BeforeVm: userVm.Vm, + AfterVm: uint32(max(0, int(userVm.Vm)+amount)), + Alter: amount, + Event: event, + Describe: describe, + } + if amount > 0 { + expireTime := time.Now().AddDate(1, 0, 0) + record.ExpiryDate = &expireTime + } + return tx.Create(&record).Error +} + // GetUserAvailablePointsRecords 查找用户的所有可用积分记录,按时间排序 func GetUserAvailablePointsRecords(uid uint32) []UserVmRecord { var userVmRecord []UserVmRecord diff --git a/router/router_app.go b/router/router_app.go index b1da49a..6c73f1c 100644 --- a/router/router_app.go +++ b/router/router_app.go @@ -323,4 +323,20 @@ func ConfigAppRouter(r gin.IRouter) { retail.POST("commodity_list", controller.ErpCommodityList) } + lottery := api.Group("lottery") // 抽奖相关接口 + { + lottery.Use(auth.UserAccessAuth) + + lottery.POST("/draw", controller.LotteryDraw) // 抽奖接口 + lottery.POST("/records", controller.LotteryRecords) // 抽奖记录接口 + lottery.POST("/prizes", controller.LotteryPrizes) // 奖品列表接口 + lottery.POST("/recent_winners", controller.RecentWinners) // 查询最近中奖用户的抽奖记录 + lottery.POST("/config/public", controller.GetPublicLotteryConfigHandler) // 公开查询抽奖配置 + lottery.POST("/config/title", controller.GetPublicLotteryTitleConfigHandler) // 公开查询抽奖标题 + lottery.POST("/submit_delivery", controller.SubmitDeliveryInfo) // 用户填写地址 + lottery.POST("/confirm_receipt", controller.ConfirmLotteryReceipt) // 用户确认收货 + lottery.POST("/today_draw_count", controller.GetTodayDrawCount) // 查询用户当天抽奖次数 + lottery.POST("/prize_order/detail", controller.GetLotteryPrizeOrderDetail) // 查询抽奖订单详情 + } + }