Files
affiliate_dash/backend/internal/handler/open_v1.go
T
yml2213 bafe41a3ea 重构: 手续费改为百分比/固定二选一,清理旧 Skin/Order/ShipLog 演示链路
手续费:
- 新增 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 旧表
2026-07-30 14:12:27 +08:00

225 lines
6.5 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"strconv"
"affiliate_dash/internal/middleware"
"affiliate_dash/internal/model"
"affiliate_dash/internal/pkg/response"
"affiliate_dash/internal/service"
"github.com/gin-gonic/gin"
)
// OpenV1Handler 提供面向商户系统和履约器的通用开放接口。
type OpenV1Handler struct {
merchantSvc *service.MerchantService
fulfillmentSvc *service.FulfillmentService
}
func NewOpenV1Handler(merchantSvc *service.MerchantService, fulfillmentSvc *service.FulfillmentService) *OpenV1Handler {
return &OpenV1Handler{merchantSvc: merchantSvc, fulfillmentSvc: fulfillmentSvc}
}
func (h *OpenV1Handler) ListProducts(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
list, total, err := h.merchantSvc.ListMerchantProducts(middleware.GetMerchantID(c), page, size, true)
if err != nil {
response.ServerError(c, err.Error())
return
}
response.Page(c, list, total, page, size)
}
type openCreateOrderReq struct {
ClientOrderNo string `json:"client_order_no" binding:"required"`
SKU string `json:"sku" binding:"required"`
Quantity int64 `json:"quantity"`
BuyerReference string `json:"buyer_reference"`
Data json.RawMessage `json:"data"`
}
func (h *OpenV1Handler) CreateOrder(c *gin.Context) {
var req openCreateOrderReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:client_order_no 与 sku 必填")
return
}
if key := c.GetHeader("Idempotency-Key"); key != "" && key != req.ClientOrderNo {
response.BadRequest(c, "Idempotency-Key 必须与 client_order_no 一致")
return
}
var data interface{}
if len(req.Data) > 0 {
var decoded interface{}
if err := json.Unmarshal(req.Data, &decoded); err != nil {
response.BadRequest(c, "data 必须是有效 JSON")
return
}
data = decoded
}
client := middleware.GetAPIClient(c)
result, err := h.fulfillmentSvc.CreateOrder(service.CreateFulfillmentOrderInput{
MerchantID: middleware.GetMerchantID(c),
APIClientID: client.ID,
ClientOrderNo: req.ClientOrderNo,
SKU: req.SKU,
Quantity: req.Quantity,
BuyerReference: req.BuyerReference,
RequestData: data,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
status := http.StatusOK
if !result.Idempotent {
status = http.StatusCreated
}
c.JSON(status, response.Body{
Code: 0,
Message: "ok",
Data: gin.H{
"order": buildOpenOrderResponse(result.Order),
"idempotent": result.Idempotent,
},
})
}
func (h *OpenV1Handler) QueryOrder(c *gin.Context) {
order, err := h.fulfillmentSvc.GetOrder(middleware.GetMerchantID(c), c.Param("order_no"))
if err != nil {
if err.Error() == "订单不存在" {
response.NotFound(c, err.Error())
return
}
response.ServerError(c, err.Error())
return
}
response.OK(c, buildOpenOrderResponse(order))
}
type openCancelOrderReq struct {
Reason string `json:"reason"`
}
func (h *OpenV1Handler) CancelOrder(c *gin.Context) {
var req openCancelOrderReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误")
return
}
client := middleware.GetAPIClient(c)
order, err := h.fulfillmentSvc.CancelOrder(middleware.GetMerchantID(c), client.ID, c.Param("order_no"), req.Reason)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, buildOpenOrderResponse(order))
}
type openShipNotifyReq struct {
OrderNo string `json:"order_no"`
ShipStatus string `json:"ship_status" binding:"required"`
ProviderOrderNo string `json:"provider_order_no"`
FailReason string `json:"fail_reason"`
Result json.RawMessage `json:"result"`
}
func (h *OpenV1Handler) ShipNotify(c *gin.Context) {
var req openShipNotifyReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:ship_status 必填")
return
}
status := ""
switch req.ShipStatus {
case "processing":
status = model.FulfillmentStatusProcessing
case "success":
status = model.FulfillmentStatusSucceeded
case "failed":
status = model.FulfillmentStatusFailed
default:
response.BadRequest(c, "ship_status 仅支持 processing、success、failed")
return
}
var result interface{}
if len(req.Result) > 0 {
if err := json.Unmarshal(req.Result, &result); err != nil {
response.BadRequest(c, "result 必须是有效 JSON")
return
}
}
client := middleware.GetAPIClient(c)
orderNo := c.Param("order_no")
if orderNo == "" {
orderNo = req.OrderNo
}
if orderNo == "" {
response.BadRequest(c, "order_no 必填")
return
}
order, err := h.fulfillmentSvc.UpdateFulfillment(service.FulfillmentUpdateInput{
MerchantID: middleware.GetMerchantID(c),
APIClientID: client.ID,
OrderNo: orderNo,
Status: status,
ProviderOrderNo: req.ProviderOrderNo,
FailureReason: req.FailReason,
ResultData: result,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, buildOpenOrderResponse(order))
}
func (h *OpenV1Handler) GetWallet(c *gin.Context) {
wallet, err := h.fulfillmentSvc.GetWallet(middleware.GetMerchantID(c))
if err != nil {
response.ServerError(c, err.Error())
return
}
response.OK(c, wallet)
}
func buildOpenOrderResponse(order *model.FulfillmentOrder) gin.H {
canFulfill, reason := service.CanFulfill(order)
data := gin.H{
"order_no": order.OrderNo,
"client_order_no": order.ClientOrderNo,
"payment_status": order.PaymentStatus,
"fulfillment_status": order.FulfillmentStatus,
"can_fulfill": canFulfill,
"cannot_fulfill_reason": reason,
"product": gin.H{
"sku": order.ProductSKU,
"name": order.ProductName,
},
"quantity": order.Quantity,
"base_amount": order.BaseAmount,
"fee_type": order.FeeType,
"service_fee_amount": order.ServiceFeeAmount,
"amount": order.Amount,
"currency": order.Currency,
"buyer_reference": order.BuyerReference,
"provider_order_no": order.ProviderOrderNo,
"failure_reason": order.FailureReason,
"created_at": order.CreatedAt,
"delivered_at": order.DeliveredAt,
"cancelled_at": order.CancelledAt,
}
if json.Valid([]byte(order.RequestData)) {
data["data"] = json.RawMessage(order.RequestData)
}
if json.Valid([]byte(order.ResultData)) {
data["result"] = json.RawMessage(order.ResultData)
}
return data
}