package handler import ( "errors" "fmt" "math/rand" "strconv" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "nl-game-api-gin/internal/database" "nl-game-api-gin/internal/middleware" "nl-game-api-gin/internal/model" "nl-game-api-gin/internal/service" "nl-game-api-gin/pkg/resp" ) // PropList 道具商城列表(上架中) func PropList(c *gin.Context) { props := make([]model.Prop, 0) database.DB.Where("status = 1").Order("sort").Find(&props) resp.OK(c, props) } // cartItem 购物车条目(附商品名称与价格) type cartItem struct { ID int `json:"id"` // 购物车ID ItemType int `json:"item_type"` // 1游戏 2道具 ItemID int `json:"item_id"` // 商品ID Quantity int `json:"quantity"` // 数量 Name string `json:"name"` // 商品名称 Icon string `json:"icon"` // 商品图标 Price int `json:"price"` // 单价(积分) } // loadCartItems 加载用户购物车明细(游戏与道具分别联表取信息) func loadCartItems(uid int) []cartItem { list := make([]cartItem, 0) // 游戏类条目 database.DB.Table("carts ct"). Select("ct.id, ct.item_type, ct.item_id, ct.quantity, g.name, g.icon, g.price"). Joins("JOIN games g ON g.id = ct.item_id"). Where("ct.user_id = ? AND ct.item_type = ?", uid, model.ItemTypeGame).Scan(&list) // 道具类条目 var propItems []cartItem database.DB.Table("carts ct"). Select("ct.id, ct.item_type, ct.item_id, ct.quantity, p.name, p.icon, p.price"). Joins("JOIN props p ON p.id = ct.item_id"). Where("ct.user_id = ? AND ct.item_type = ?", uid, model.ItemTypeProp).Scan(&propItems) return append(list, propItems...) } // CartList 我的购物车明细 func CartList(c *gin.Context) { list := loadCartItems(middleware.UID(c)) // 计算合计积分 total := 0 for _, it := range list { total += it.Price * it.Quantity } resp.OK(c, gin.H{"list": list, "total": total}) } // addCartReq 加入购物车请求体 type addCartReq struct { ItemType int `json:"item_type" binding:"required,oneof=1 2"` // 1游戏 2道具 ItemID int `json:"item_id" binding:"required"` // 商品ID Quantity int `json:"quantity"` // 数量(默认为1) } // AddCart 加入购物车:游戏查重不可重复购买,道具可累加数量 func AddCart(c *gin.Context) { var req addCartReq if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, "参数有误") return } if req.Quantity < 1 { req.Quantity = 1 } uid := middleware.UID(c) if req.ItemType == model.ItemTypeGame { // 游戏:校验存在、非免费、未拥有 var game model.Game if err := database.DB.Where("id = ? AND status = 1", req.ItemID).First(&game).Error; err != nil { resp.Fail(c, "游戏不存在") return } if game.Price == 0 { resp.Fail(c, "免费游戏无需购买") return } var owned int64 database.DB.Model(&model.UserGame{}).Where("user_id = ? AND game_id = ?", uid, game.ID).Count(&owned) if owned > 0 { resp.Fail(c, "已拥有该游戏") return } req.Quantity = 1 // 游戏固定数量为1 } else { var prop model.Prop if err := database.DB.Where("id = ? AND status = 1", req.ItemID).First(&prop).Error; err != nil { resp.Fail(c, "道具不存在") return } } // 已在购物车:道具累加数量,游戏保持1份不动 var exist model.Cart err := database.DB.Where("user_id = ? AND item_type = ? AND item_id = ?", uid, req.ItemType, req.ItemID).First(&exist).Error if err == nil { if req.ItemType == model.ItemTypeProp { database.DB.Model(&exist).Update("quantity", exist.Quantity+req.Quantity) } resp.OK(c, nil) return } database.DB.Create(&model.Cart{UserID: uid, ItemType: req.ItemType, ItemID: req.ItemID, Quantity: req.Quantity}) resp.OK(c, nil) } // RemoveCart 移除购物车条目(只能删自己的) func RemoveCart(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) database.DB.Where("id = ? AND user_id = ?", id, middleware.UID(c)).Delete(&model.Cart{}) resp.OK(c, nil) } // genOrderNo 生成订单号(时间戳 + 4位随机) func genOrderNo() string { return fmt.Sprintf("PO%s%04d", time.Now().Format("20060102150405"), rand.Intn(10000)) } // purchaseItems 核心购买逻辑:事务内扣积分、写订单、发放游戏/道具 // items 由调用方保证已校验(存在且可购买) func purchaseItems(uid int, items []cartItem) (*model.Order, error) { if len(items) == 0 { return nil, errors.New("没有可购买的商品") } total := 0 names := make([]string, 0, len(items)) for _, it := range items { total += it.Price * it.Quantity names = append(names, it.Name) } var order model.Order err := database.DB.Transaction(func(tx *gorm.DB) error { // 创建订单主体 order = model.Order{ OrderNo: genOrderNo(), UserID: uid, TotalPoints: total, Status: model.OrderStatusDone, Remark: strings.Join(names, "、"), } if err := tx.Create(&order).Error; err != nil { return err } // 扣积分(余额不足时事务整体回滚) if _, err := service.ChangePoints(tx, uid, -total, model.PointTypeShop, order.ID, fmt.Sprintf("订单 %s 消费", order.OrderNo)); err != nil { return err } for _, it := range items { // 写订单明细(价格快照) if err := tx.Create(&model.OrderItem{ OrderID: order.ID, ItemType: it.ItemType, ItemID: it.ItemID, ItemName: it.Name, Price: it.Price, Quantity: it.Quantity, }).Error; err != nil { return err } if it.ItemType == model.ItemTypeGame { // 发放游戏(唯一索引防重复购买) if err := tx.Create(&model.UserGame{UserID: uid, GameID: it.ItemID, OrderID: order.ID}).Error; err != nil { return errors.New("已拥有该游戏,请勿重复购买") } } else { // 发放道具:已有记录累加,否则新建 res := tx.Exec(`UPDATE user_props SET quantity = quantity + ? WHERE user_id = ? AND prop_id = ?`, it.Quantity, uid, it.ItemID) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { if err := tx.Create(&model.UserProp{UserID: uid, PropID: it.ItemID, Quantity: it.Quantity}).Error; err != nil { return err } } } } return nil }) if err != nil { return nil, err } return &order, nil } // Checkout 购物车结算下单 func Checkout(c *gin.Context) { uid := middleware.UID(c) items := loadCartItems(uid) order, err := purchaseItems(uid, items) if err != nil { resp.Fail(c, err.Error()) return } // 下单成功后清空购物车 database.DB.Where("user_id = ?", uid).Delete(&model.Cart{}) resp.OK(c, order) } // buyReq 立即购买请求体(跳过购物车直接下单) type buyReq struct { ItemType int `json:"item_type" binding:"required,oneof=1 2"` // 1游戏 2道具 ItemID int `json:"item_id" binding:"required"` // 商品ID Quantity int `json:"quantity"` // 数量(仅道具) } // BuyNow 立即购买单件商品 func BuyNow(c *gin.Context) { var req buyReq if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, "参数有误") return } if req.Quantity < 1 { req.Quantity = 1 } uid := middleware.UID(c) var item cartItem if req.ItemType == model.ItemTypeGame { var game model.Game if err := database.DB.Where("id = ? AND status = 1", req.ItemID).First(&game).Error; err != nil { resp.Fail(c, "游戏不存在") return } if game.Price == 0 { resp.Fail(c, "免费游戏无需购买") return } var owned int64 database.DB.Model(&model.UserGame{}).Where("user_id = ? AND game_id = ?", uid, game.ID).Count(&owned) if owned > 0 { resp.Fail(c, "已拥有该游戏") return } item = cartItem{ItemType: 1, ItemID: game.ID, Quantity: 1, Name: game.Name, Price: game.Price} } else { var prop model.Prop if err := database.DB.Where("id = ? AND status = 1", req.ItemID).First(&prop).Error; err != nil { resp.Fail(c, "道具不存在") return } item = cartItem{ItemType: 2, ItemID: prop.ID, Quantity: req.Quantity, Name: prop.Name, Price: prop.Price} } order, err := purchaseItems(uid, []cartItem{item}) if err != nil { resp.Fail(c, err.Error()) return } resp.OK(c, order) } // OrderList 我的订单列表(含明细,分页) func OrderList(c *gin.Context) { page, size, offset := pageParams(c) uid := middleware.UID(c) var total int64 orders := make([]model.Order, 0) database.DB.Model(&model.Order{}).Where("user_id = ?", uid).Count(&total) database.DB.Preload("Items").Where("user_id = ?", uid). Order("id DESC").Limit(size).Offset(offset).Find(&orders) resp.OK(c, gin.H{"list": orders, "total": total, "page": page, "size": size}) }