mh_server/controller/lottery.go

748 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}