Files
nl-game-api/internal/handler/shop.go
2026-08-14 13:17:03 +08:00

275 lines
8.6 KiB
Go
Raw Permalink 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 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})
}