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

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

91 lines
3.1 KiB
Go
Executable File
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 (
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"net/http"
)
type requestShippingBatchRequest struct {
InventoryIDs []int64 `json:"inventory_ids"`
AddressID *int64 `json:"address_id"`
}
type requestShippingBatchResponse struct {
AddressID int64 `json:"address_id"`
BatchNo string `json:"batch_no"`
SuccessIDs []int64 `json:"success_ids"`
Skipped []map[string]any `json:"skipped"`
Failed []map[string]any `json:"failed"`
}
// RequestShippingBatch 批量申请发货(使用默认地址或指定地址)
// @Summary 批量申请发货
// @Description 为多个资产申请发货,校验所有权与状态;不满 5 件需先支付运费;幂等:已申请的资产跳过
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param user_id path integer true "用户ID"
// @Param RequestBody body requestShippingBatchRequest true "请求参数资产ID列表与可选地址ID"
// @Success 200 {object} requestShippingBatchResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/users/{user_id}/inventory/request-shipping-batch [post]
func (h *handler) RequestShippingBatch() core.HandlerFunc {
return func(ctx core.Context) {
req := new(requestShippingBatchRequest)
rsp := new(requestShippingBatchResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if len(req.InventoryIDs) == 0 || len(req.InventoryIDs) > 100 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "invalid 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, 150004, err.Error()))
return
}
if needFee {
paid, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).
Where(
h.readDB.Orders.UserID.Eq(userID),
h.readDB.Orders.SourceType.Eq(shippingFeeSourceType),
h.readDB.Orders.Status.Eq(2),
).Count()
if paid == 0 {
msg := "需先支付运费"
if reason == shippingFeeReasonContainsNonFreeShipping {
msg = "所选商品包含不包邮商品,需先支付运费"
} else if reason == shippingFeeReasonBelowThreshold {
msg = "不满5件需先支付运费"
}
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150003, msg))
return
}
}
addrID, batchNo, success, skipped, failed, err := h.user.RequestShippings(ctx.RequestContext(), userID, req.InventoryIDs, req.AddressID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10023, err.Error()))
return
}
rsp.AddressID = addrID
rsp.BatchNo = batchNo
rsp.SuccessIDs = success
for _, s := range skipped {
rsp.Skipped = append(rsp.Skipped, map[string]any{"id": s.ID, "reason": s.Reason})
}
for _, f := range failed {
rsp.Failed = append(rsp.Failed, map[string]any{"id": f.ID, "reason": f.Reason})
}
ctx.Payload(rsp)
}
}