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