手续费: - 新增 fee_type 字段(rate/fixed),百分比与固定金额二选一,不再叠加 - calculateServiceFee 按 fee_type 分支计算 - 商户创建/更新校验 fee_type,订单落库快照 fee_type - 前端表单改为下拉选择手续费类型,动态显示对应输入框 - 测试拆为 TestFeeRate + TestFeeFixed 清理旧链路: - 删除旧 Skin/Order/ShipLog 模型及 OrderService/SkinService - Dashboard 迁移到 FulfillmentService - 上游 /api/open/v1 改为基于 FulfillmentOrder 实现,接口契约不变 - 推送留痕改用 AuditLog,不再建 ShipLog 表 - 删除前端 Skins/Orders/ShipLogs/Distributors 页面及路由、菜单、API、类型 - 新增迁移 003: 添加 fee_type 列并 DROP 旧表
120 lines
4.1 KiB
Go
120 lines
4.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"affiliate_dash/internal/pkg/openlog"
|
|
"affiliate_dash/internal/pkg/response"
|
|
"affiliate_dash/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// OpenHandler 皮肤源头开放接口(上游 SourceOpen),基于 FulfillmentOrder 履约模型。
|
|
type OpenHandler struct {
|
|
fulfillmentSvc *service.FulfillmentService
|
|
}
|
|
|
|
func NewOpenHandler(fulfillmentSvc *service.FulfillmentService) *OpenHandler {
|
|
return &OpenHandler{fulfillmentSvc: fulfillmentSvc}
|
|
}
|
|
|
|
// QueryOrder GET /api/open/v1/orders/:order_no
|
|
// 上游输入店铺订单号,查询商品与是否可发货
|
|
func (h *OpenHandler) QueryOrder(c *gin.Context) {
|
|
orderNo := c.Param("order_no")
|
|
if orderNo == "" {
|
|
orderNo = c.Query("order_no")
|
|
}
|
|
openlog.Info(c, "query_order start order_no=%s", orderNo)
|
|
|
|
data, err := h.fulfillmentSvc.QueryOpenOrder(orderNo)
|
|
if err != nil {
|
|
openlog.Warn(c, "query_order fail order_no=%s err=%s", orderNo, err.Error())
|
|
if err.Error() == "订单不存在" {
|
|
response.NotFound(c, err.Error())
|
|
return
|
|
}
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
sku, name, game := "", "", ""
|
|
if data.Product != nil {
|
|
sku, name, game = data.Product.SKU, data.Product.Name, data.Product.Game
|
|
}
|
|
openlog.Info(c, "query_order ok order_no=%s status=%s can_ship=%v reason=%q sku=%s name=%s game=%s buyer=%s amount=%.2f provider_no=%s shipped_at=%v fail_reason=%q game_channel=%s game_uid=%s role_name=%s pay_score=%d",
|
|
data.OrderNo, data.Status, data.CanShip, data.CannotShipReason,
|
|
sku, name, game, data.BuyerName, data.Amount, data.ProviderOrderNo, data.ShippedAt, data.ShipFailReason,
|
|
data.GameChannel, data.GameUID, data.RoleName, data.PayScore,
|
|
)
|
|
response.OK(c, data)
|
|
}
|
|
|
|
type shipNotifyReq struct {
|
|
OrderNo string `json:"order_no" binding:"required"`
|
|
ShipStatus string `json:"ship_status" binding:"required"` // success/failed/processing
|
|
ProviderOrderNo string `json:"provider_order_no"`
|
|
ShippedAt string `json:"shipped_at"` // RFC3339 可选
|
|
FailReason string `json:"fail_reason"`
|
|
GameChannel *string `json:"game_channel"` // 账号区服(安卓/IOS-微信/QQ)
|
|
GameUID *string `json:"game_uid"` // 游戏角色UUID
|
|
RoleName *string `json:"role_name"` // 角色名
|
|
PayScore *int `json:"pay_score"` // 消耗积分
|
|
}
|
|
|
|
// ShipNotify POST /api/open/v1/orders/ship-notify
|
|
// 上游发货后推送结果,同步订单状态
|
|
func (h *OpenHandler) ShipNotify(c *gin.Context) {
|
|
var req shipNotifyReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
openlog.Warn(c, "ship_notify bind_fail err=%v", err)
|
|
response.BadRequest(c, "参数错误:order_no 与 ship_status 必填")
|
|
return
|
|
}
|
|
|
|
raw, _ := json.Marshal(req)
|
|
openlog.Info(c, "ship_notify start order_no=%s ship_status=%s provider_no=%s shipped_at=%s fail_reason=%q payload=%s",
|
|
req.OrderNo, req.ShipStatus, req.ProviderOrderNo, req.ShippedAt, req.FailReason, openlog.Truncate(string(raw), 800),
|
|
)
|
|
|
|
var shippedAt *time.Time
|
|
if req.ShippedAt != "" {
|
|
t, err := time.Parse(time.RFC3339, req.ShippedAt)
|
|
if err != nil {
|
|
openlog.Warn(c, "ship_notify bad_shipped_at raw=%s err=%v", req.ShippedAt, err)
|
|
response.BadRequest(c, "shipped_at 格式错误,请使用 RFC3339,如 2026-07-20T16:00:00+08:00")
|
|
return
|
|
}
|
|
shippedAt = &t
|
|
}
|
|
|
|
result, err := h.fulfillmentSvc.HandleShipNotify(service.ShipNotifyInput{
|
|
OrderNo: req.OrderNo,
|
|
ShipStatus: req.ShipStatus,
|
|
ProviderOrderNo: req.ProviderOrderNo,
|
|
ShippedAt: shippedAt,
|
|
FailReason: req.FailReason,
|
|
RawPayload: string(raw),
|
|
GameChannel: req.GameChannel,
|
|
GameUID: req.GameUID,
|
|
RoleName: req.RoleName,
|
|
PayScore: req.PayScore,
|
|
})
|
|
if err != nil {
|
|
openlog.Warn(c, "ship_notify fail order_no=%s ship_status=%s err=%s",
|
|
req.OrderNo, req.ShipStatus, err.Error())
|
|
if err.Error() == "订单不存在" {
|
|
response.NotFound(c, err.Error())
|
|
return
|
|
}
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
openlog.Info(c, "ship_notify ok order_no=%s result_status=%s message=%s",
|
|
result.OrderNo, result.Status, result.Message)
|
|
response.OK(c, result)
|
|
}
|