本次提交同时完成碎片批量合成与盒柜发货运费规则改造,减少用户重复操作,并确保不包邮商品在任何件数下都必须支付运费。 - 合成功能:新增批量合成接口与前端一键合成入口,按配方可合成上限批量消耗碎片并生成对应资产,同时补充批量合成测试 - 运费规则:后端新增统一运费判定逻辑,命中 category_id 14/15 的商品时整单强制收取运费,否则继续沿用少于 5 件收运费的旧规则 - 发货流程:新增运费检查接口,前端发货前先向后端确认是否需要支付运费,并根据“件数不足”或“包含不包邮商品”展示不同提示文案 - 接口校验:运费预下单与批量申请发货统一复用后端判定逻辑,避免前端规则被绕过
128 lines
4.3 KiB
Go
128 lines
4.3 KiB
Go
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)
|
||
}
|
||
}
|