194 lines
6.6 KiB
Go
194 lines
6.6 KiB
Go
|
|
// 功能描述:用户购物车读取及带版本条件的幂等数量设置;版本:1.0.0。
|
|||
|
|
package user
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"errors"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"git.apinb.com/bsm-sdk/core/errcode"
|
|||
|
|
"git.apinb.com/bsm-sdk/core/infra"
|
|||
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
|||
|
|
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
|||
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
"gorm.io/gorm/clause"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var errCartChanged = errcode.NewError(2403, "购物车已变化,请刷新后重新确认")
|
|||
|
|
|
|||
|
|
// productCoverURL 返回商品当前封面;订单创建时会把它固化到成交快照。
|
|||
|
|
func productCoverURL(db *gorm.DB, productID uint64) (string, error) {
|
|||
|
|
var picture models.EcProductImage
|
|||
|
|
if err := db.Where("ec_product_id = ? AND status = ?", productID, common.StatusEnable).
|
|||
|
|
Order("is_cover desc, sort_no, identity").Find(&picture).Error; err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
return picture.ImageURI, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cartRevision 返回公开并发版本,不向客户端暴露数据库主键。
|
|||
|
|
func cartRevision(item models.EcCart) string {
|
|||
|
|
if item.ID == 0 {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
// PostgreSQL 时间戳精度为微秒,响应必须与重读后的版本一致。
|
|||
|
|
return item.Identity + ":" + item.UpdatedAt.UTC().Truncate(time.Microsecond).Format(time.RFC3339Nano)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cartValue 仅展示关联商品和本人的购物状态;下架商品仍保留可删除条目。
|
|||
|
|
func cartValue(db *gorm.DB, item models.EcCart, product models.EcProduct) (gin.H, error) {
|
|||
|
|
quantity := item.Quantity
|
|||
|
|
if item.ID == 0 || item.Status == common.StatusArchived {
|
|||
|
|
quantity = 0
|
|||
|
|
}
|
|||
|
|
imageURL, err := productCoverURL(db, product.ID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return gin.H{
|
|||
|
|
"product_identity": product.Identity, "name": product.Name, "image_url": imageURL,
|
|||
|
|
"price_amount": product.PriceAmount, "stock_quantity": product.StockQuantity,
|
|||
|
|
"available": product.Status == common.StatusEnable && !product.DeletedAt.Valid && product.StockQuantity > 0,
|
|||
|
|
"quantity": quantity, "selected": quantity > 0 && item.Selected, "revision": cartRevision(item),
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ListCart 返回本人未归档条目,失效商品不会被静默移除。
|
|||
|
|
func ListCart(ctx *gin.Context) {
|
|||
|
|
account, ok := common.UserAccount(ctx)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var items []models.EcCart
|
|||
|
|
if err := impl.DBService.Where("user_account_id = ? AND status <> ?", account.ID, common.StatusArchived).
|
|||
|
|
Order("created_at, identity").Find(&items).Error; err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
result := make([]gin.H, 0, len(items))
|
|||
|
|
for _, item := range items {
|
|||
|
|
var product models.EcProduct
|
|||
|
|
if err := impl.DBService.Unscoped().Where("id = ?", item.EcProductID).Take(&product).Error; err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
value, err := cartValue(impl.DBService, item, product)
|
|||
|
|
if err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
result = append(result, value)
|
|||
|
|
}
|
|||
|
|
infra.Response.Success(ctx, result)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetCartItem 返回本商品当前数量和归档版本,重新加入时不会覆盖另一端的操作。
|
|||
|
|
func GetCartItem(ctx *gin.Context) {
|
|||
|
|
account, ok := common.UserAccount(ctx)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var product models.EcProduct
|
|||
|
|
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).Take(&product).Error; err != nil {
|
|||
|
|
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var item models.EcCart
|
|||
|
|
if err := impl.DBService.Where("user_account_id = ? AND ec_product_id = ?", account.ID, product.ID).Find(&item).Error; err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
value, err := cartValue(impl.DBService, item, product)
|
|||
|
|
if err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
infra.Response.Success(ctx, value)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetCartItem 以绝对数量写入;同目标重试无副作用,过期版本拒绝覆盖,数量零表示归档。
|
|||
|
|
func SetCartItem(ctx *gin.Context) {
|
|||
|
|
account, ok := common.UserAccount(ctx)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var request struct {
|
|||
|
|
Quantity *int `json:"quantity" binding:"required,gte=0,lte=999"`
|
|||
|
|
Selected *bool `json:"selected" binding:"required"`
|
|||
|
|
Revision string `json:"revision" binding:"max=128"`
|
|||
|
|
}
|
|||
|
|
if ctx.ShouldBindJSON(&request) != nil {
|
|||
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var result gin.H
|
|||
|
|
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
|||
|
|
// 所有用户端购物车写入共用账户行锁,无需改动现有远程表结构。
|
|||
|
|
var owner models.UserAccount
|
|||
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", account.ID).Take(&owner).Error; err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
var product models.EcProduct
|
|||
|
|
if err := tx.Unscoped().Where("identity = ?", ctx.Param("identity")).Take(&product).Error; err != nil {
|
|||
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|||
|
|
return errcode.ErrRecordNotFound
|
|||
|
|
}
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
var item models.EcCart
|
|||
|
|
if err := tx.Where("user_account_id = ? AND ec_product_id = ?", account.ID, product.ID).Find(&item).Error; err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
current := item.Quantity
|
|||
|
|
if item.ID == 0 || item.Status == common.StatusArchived {
|
|||
|
|
current = 0
|
|||
|
|
}
|
|||
|
|
same := current == *request.Quantity && (current == 0 || item.Selected == *request.Selected)
|
|||
|
|
if !same {
|
|||
|
|
if cartRevision(item) != request.Revision {
|
|||
|
|
return errCartChanged
|
|||
|
|
}
|
|||
|
|
if *request.Quantity > 0 && (*request.Quantity > current || current == 0) &&
|
|||
|
|
(product.Status != common.StatusEnable || product.DeletedAt.Valid || product.StockQuantity < *request.Quantity) {
|
|||
|
|
return errShopStockChanged
|
|||
|
|
}
|
|||
|
|
status := common.StatusEnable
|
|||
|
|
if *request.Quantity == 0 {
|
|||
|
|
status = common.StatusArchived
|
|||
|
|
}
|
|||
|
|
if current == 0 {
|
|||
|
|
var count int64
|
|||
|
|
if err := tx.Model(&models.EcCart{}).Where("user_account_id = ? AND status <> ?", account.ID, common.StatusArchived).Count(&count).Error; err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if count >= 100 {
|
|||
|
|
return errcode.ErrOutOfRange
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if item.ID == 0 {
|
|||
|
|
item = models.EcCart{Entity: common.NewEntity(status), UserAccountID: account.ID, EcProductID: product.ID, Quantity: *request.Quantity, Selected: true}
|
|||
|
|
if err := tx.Create(&item).Error; err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 保留正数量以兼容现有 CHECK;归档状态对外显示零。
|
|||
|
|
quantity := *request.Quantity
|
|||
|
|
if quantity == 0 {
|
|||
|
|
quantity = item.Quantity
|
|||
|
|
}
|
|||
|
|
if err := tx.Model(&item).Updates(map[string]any{"quantity": quantity, "selected": *request.Selected, "status": status}).Error; err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
var err error
|
|||
|
|
result, err = cartValue(tx, item, product)
|
|||
|
|
return err
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
infra.Response.Success(ctx, result)
|
|||
|
|
}
|