bindbox-game/internal/api/user/shipping_fee_preorder_app.go
Zuncle 0a397adf41 feat: 支持批量合成并收口不包邮运费规则
本次提交同时完成碎片批量合成与盒柜发货运费规则改造,减少用户重复操作,并确保不包邮商品在任何件数下都必须支付运费。

- 合成功能:新增批量合成接口与前端一键合成入口,按配方可合成上限批量消耗碎片并生成对应资产,同时补充批量合成测试
- 运费规则:后端新增统一运费判定逻辑,命中 category_id 14/15 的商品时整单强制收取运费,否则继续沿用少于 5 件收运费的旧规则
- 发货流程:新增运费检查接口,前端发货前先向后端确认是否需要支付运费,并根据“件数不足”或“包含不包邮商品”展示不同提示文案
- 接口校验:运费预下单与批量申请发货统一复用后端判定逻辑,避免前端规则被绕过
2026-04-21 02:06:56 +08:00

128 lines
4.3 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.

package app
import (
"encoding/json"
"fmt"
"net/http"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
const (
shippingFeeThreshold = 5
shippingFeeCents = 1000 // 运费金额10 元
shippingFeeSourceType = int32(5) // orders.source_type: 5 = 运费订单
shippingFeeReasonBelowThreshold = "below_threshold"
shippingFeeReasonContainsNonFreeShipping = "contains_non_free_shipping_item"
)
type shippingFeePreorderRequest struct {
InventoryIDs []int64 `json:"inventory_ids"`
}
type shippingFeePreorderResponse struct {
OrderNo string `json:"order_no"`
}
type shippingFeeCheckResponse struct {
NeedFee bool `json:"need_fee"`
Reason string `json:"reason,omitempty"`
FeeCents int64 `json:"fee_cents"`
}
func (h *handler) ShippingFeeCheck() core.HandlerFunc {
return func(ctx core.Context) {
req := new(shippingFeePreorderRequest)
rsp := &shippingFeeCheckResponse{FeeCents: shippingFeeCents}
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if len(req.InventoryIDs) == 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "inventory_ids 不能为空"))
return
}
userID := int64(ctx.SessionUserInfo().Id)
needFee, reason, err := h.user.CheckShippingFeeRequirement(ctx.RequestContext(), userID, req.InventoryIDs)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150002, err.Error()))
return
}
rsp.NeedFee = needFee
rsp.Reason = reason
ctx.Payload(rsp)
}
}
// ShippingFeePreorder 创建运费订单
// @Summary 创建运费订单
// @Description 选中商品命中运费规则时,创建 10 元运费订单并返回 order_no前端再调用 /pay/wechat/jsapi/preorder 发起支付;无需运费时不应调用
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param user_id path integer true "用户ID"
// @Param RequestBody body shippingFeePreorderRequest true "请求参数资产ID列表"
// @Success 200 {object} shippingFeePreorderResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/users/{user_id}/inventory/shipping-fee/preorder [post]
func (h *handler) ShippingFeePreorder() core.HandlerFunc {
return func(ctx core.Context) {
req := new(shippingFeePreorderRequest)
rsp := new(shippingFeePreorderResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if len(req.InventoryIDs) == 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "inventory_ids 不能为空"))
return
}
userID := int64(ctx.SessionUserInfo().Id)
needFee, _, err := h.user.CheckShippingFeeRequirement(ctx.RequestContext(), userID, req.InventoryIDs)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150002, err.Error()))
return
}
if !needFee {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150001, fmt.Sprintf("件数满 %d 件且均非不包邮分类商品,无需支付运费", shippingFeeThreshold)))
return
}
remarkBytes, _ := json.Marshal(req.InventoryIDs)
// 生成运费订单号SF + 时间戳 + 用户ID后6位保证唯一
now := time.Now()
orderNo := fmt.Sprintf("SF%s%06d", now.Format("20060102150405"), userID%1000000)
// 创建运费订单source_type=5 区分普通商品订单status=1 待支付)
order := &model.Orders{
UserID: userID,
OrderNo: orderNo,
SourceType: shippingFeeSourceType,
TotalAmount: shippingFeeCents,
DiscountAmount: 0,
PointsAmount: 0,
ActualAmount: shippingFeeCents,
Status: 1,
IsConsumed: 0,
Remark: string(remarkBytes),
}
if err := h.writeDB.Orders.WithContext(ctx.RequestContext()).
Omit(h.writeDB.Orders.PaidAt, h.writeDB.Orders.CancelledAt).
Create(order); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150002, "创建运费订单失败: "+err.Error()))
return
}
rsp.OrderNo = orderNo
ctx.Payload(rsp)
}
}