Files
platforms/backend/api/internal/logic/client/user/cart.go
czl231 3ef33b531d 已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
2026-09-13 00:57:32 +08:00

194 lines
6.6 KiB
Go
Raw 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.
// 功能描述:用户购物车读取及带版本条件的幂等数量设置;版本: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)
}