Files

275 lines
8.6 KiB
Go
Raw Permalink Normal View History

2026-08-14 13:17:03 +08:00
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})
}