对接皮肤源头开放接口:查询、发货推送与 HMAC 签名鉴权
- 新增订单查询与发货结果推送开放接口,支持 can_ship 与幂等 - 鉴权采用 X-Api-Key + Timestamp + Nonce + HMAC-SHA256 签名 - 订单扩展发货字段与 ship_logs,管理端增加发货记录与开放文档页
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
@@ -103,10 +104,12 @@ func (s *OrderService) Create(in CreateOrderInput) (*model.Order, error) {
|
||||
|
||||
func (s *OrderService) UpdateStatus(id uint, status string) error {
|
||||
allowed := map[string]bool{
|
||||
model.OrderStatusPending: true,
|
||||
model.OrderStatusPaid: true,
|
||||
model.OrderStatusDelivered: true,
|
||||
model.OrderStatusCancelled: true,
|
||||
model.OrderStatusPending: true,
|
||||
model.OrderStatusPaid: true,
|
||||
model.OrderStatusDelivering: true,
|
||||
model.OrderStatusDelivered: true,
|
||||
model.OrderStatusShipFailed: true,
|
||||
model.OrderStatusCancelled: true,
|
||||
}
|
||||
if !allowed[status] {
|
||||
return errors.New("无效的订单状态")
|
||||
@@ -121,6 +124,261 @@ func (s *OrderService) UpdateStatus(id uint, status string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----- 开放接口:皮肤源头对接 -----
|
||||
|
||||
// OpenOrderQuery 开放接口订单查询结果
|
||||
type OpenOrderQuery struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Status string `json:"status"`
|
||||
CanShip bool `json:"can_ship"`
|
||||
CannotShipReason string `json:"cannot_ship_reason,omitempty"`
|
||||
Product *OpenOrderProduct `json:"product,omitempty"`
|
||||
BuyerName string `json:"buyer_name"`
|
||||
Amount float64 `json:"amount"`
|
||||
ProviderOrderNo string `json:"provider_order_no,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ShippedAt *time.Time `json:"shipped_at"`
|
||||
ShipFailReason string `json:"ship_fail_reason,omitempty"`
|
||||
}
|
||||
|
||||
type OpenOrderProduct struct {
|
||||
Name string `json:"name"`
|
||||
SKU string `json:"sku"`
|
||||
Game string `json:"game"`
|
||||
}
|
||||
|
||||
// ShipNotifyInput 上游发货结果推送
|
||||
type ShipNotifyInput struct {
|
||||
OrderNo string
|
||||
ShipStatus string // success / failed / processing
|
||||
ProviderOrderNo string
|
||||
ShippedAt *time.Time
|
||||
FailReason string
|
||||
RawPayload string
|
||||
}
|
||||
|
||||
type ShipNotifyResult struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (s *OrderService) GetByOrderNo(orderNo string) (*model.Order, error) {
|
||||
var order model.Order
|
||||
err := s.db.Preload("Skin").Where("order_no = ?", orderNo).First(&order).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("订单不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
// QueryOpenOrder 供上游查询:商品信息 + 是否可发货
|
||||
func (s *OrderService) QueryOpenOrder(orderNo string) (*OpenOrderQuery, error) {
|
||||
if orderNo == "" {
|
||||
return nil, errors.New("订单号不能为空")
|
||||
}
|
||||
order, err := s.GetByOrderNo(orderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canShip, reason := evaluateCanShip(order)
|
||||
out := &OpenOrderQuery{
|
||||
OrderNo: order.OrderNo,
|
||||
Status: order.Status,
|
||||
CanShip: canShip,
|
||||
CannotShipReason: reason,
|
||||
BuyerName: order.BuyerName,
|
||||
Amount: order.Amount,
|
||||
ProviderOrderNo: order.ProviderOrderNo,
|
||||
CreatedAt: order.CreatedAt,
|
||||
ShippedAt: order.ShippedAt,
|
||||
ShipFailReason: order.ShipFailReason,
|
||||
}
|
||||
if order.Skin != nil {
|
||||
out.Product = &OpenOrderProduct{
|
||||
Name: order.Skin.Name,
|
||||
SKU: order.Skin.SKU,
|
||||
Game: order.Skin.Game,
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func evaluateCanShip(order *model.Order) (bool, string) {
|
||||
switch order.Status {
|
||||
case model.OrderStatusPaid, model.OrderStatusShipFailed:
|
||||
return true, ""
|
||||
case model.OrderStatusPending:
|
||||
return false, "订单未支付"
|
||||
case model.OrderStatusDelivering:
|
||||
return false, "订单发货中"
|
||||
case model.OrderStatusDelivered:
|
||||
return false, "订单已发货完成"
|
||||
case model.OrderStatusCancelled:
|
||||
return false, "订单已取消"
|
||||
default:
|
||||
return false, "当前状态不可发货: " + order.Status
|
||||
}
|
||||
}
|
||||
|
||||
// HandleShipNotify 处理上游发货结果推送(幂等)
|
||||
func (s *OrderService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyResult, error) {
|
||||
if in.OrderNo == "" {
|
||||
return nil, errors.New("订单号不能为空")
|
||||
}
|
||||
switch in.ShipStatus {
|
||||
case model.ShipNotifySuccess, model.ShipNotifyFailed, model.ShipNotifyProcessing:
|
||||
default:
|
||||
return nil, errors.New("无效的 ship_status,仅支持 success/failed/processing")
|
||||
}
|
||||
|
||||
order, err := s.GetByOrderNo(in.OrderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 已交付:success 推送幂等成功
|
||||
if order.Status == model.OrderStatusDelivered && in.ShipStatus == model.ShipNotifySuccess {
|
||||
_ = s.appendShipLog(order, in, order.Status, "订单已是已交付状态,幂等忽略")
|
||||
return &ShipNotifyResult{
|
||||
OrderNo: order.OrderNo,
|
||||
Status: order.Status,
|
||||
Message: "订单已交付,幂等成功",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 已取消不允许再推成功
|
||||
if order.Status == model.OrderStatusCancelled {
|
||||
_ = s.appendShipLog(order, in, order.Status, "订单已取消,拒绝更新")
|
||||
return nil, errors.New("订单已取消,无法更新发货状态")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
shippedAt := in.ShippedAt
|
||||
if shippedAt == nil && in.ShipStatus == model.ShipNotifySuccess {
|
||||
shippedAt = &now
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
var nextStatus string
|
||||
var msg string
|
||||
|
||||
switch in.ShipStatus {
|
||||
case model.ShipNotifySuccess:
|
||||
// 仅 paid / ship_failed / delivering 可转为 delivered
|
||||
if order.Status != model.OrderStatusPaid &&
|
||||
order.Status != model.OrderStatusShipFailed &&
|
||||
order.Status != model.OrderStatusDelivering {
|
||||
_ = s.appendShipLog(order, in, order.Status, "当前状态不允许标记发货成功")
|
||||
return nil, fmt.Errorf("当前状态 %s 不允许标记发货成功", order.Status)
|
||||
}
|
||||
nextStatus = model.OrderStatusDelivered
|
||||
updates["status"] = nextStatus
|
||||
updates["shipped_at"] = shippedAt
|
||||
updates["ship_fail_reason"] = ""
|
||||
if in.ProviderOrderNo != "" {
|
||||
updates["provider_order_no"] = in.ProviderOrderNo
|
||||
}
|
||||
msg = "发货成功,订单已交付"
|
||||
case model.ShipNotifyFailed:
|
||||
if order.Status == model.OrderStatusDelivered {
|
||||
_ = s.appendShipLog(order, in, order.Status, "订单已交付,忽略失败推送")
|
||||
return nil, errors.New("订单已交付,不能标记发货失败")
|
||||
}
|
||||
nextStatus = model.OrderStatusShipFailed
|
||||
updates["status"] = nextStatus
|
||||
updates["ship_fail_reason"] = in.FailReason
|
||||
if in.ProviderOrderNo != "" {
|
||||
updates["provider_order_no"] = in.ProviderOrderNo
|
||||
}
|
||||
msg = "已记录发货失败"
|
||||
case model.ShipNotifyProcessing:
|
||||
if order.Status == model.OrderStatusDelivered {
|
||||
_ = s.appendShipLog(order, in, order.Status, "订单已交付,忽略发货中推送")
|
||||
return &ShipNotifyResult{
|
||||
OrderNo: order.OrderNo,
|
||||
Status: order.Status,
|
||||
Message: "订单已交付,忽略 processing",
|
||||
}, nil
|
||||
}
|
||||
if order.Status != model.OrderStatusPaid &&
|
||||
order.Status != model.OrderStatusShipFailed &&
|
||||
order.Status != model.OrderStatusDelivering {
|
||||
_ = s.appendShipLog(order, in, order.Status, "当前状态不允许进入发货中")
|
||||
return nil, fmt.Errorf("当前状态 %s 不允许进入发货中", order.Status)
|
||||
}
|
||||
nextStatus = model.OrderStatusDelivering
|
||||
updates["status"] = nextStatus
|
||||
if in.ProviderOrderNo != "" {
|
||||
updates["provider_order_no"] = in.ProviderOrderNo
|
||||
}
|
||||
msg = "订单已标记为发货中"
|
||||
}
|
||||
|
||||
if err := s.db.Model(&model.Order{}).Where("id = ?", order.ID).Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.appendShipLog(order, in, nextStatus, msg)
|
||||
|
||||
return &ShipNotifyResult{
|
||||
OrderNo: order.OrderNo,
|
||||
Status: nextStatus,
|
||||
Message: msg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OrderService) appendShipLog(order *model.Order, in ShipNotifyInput, resultStatus, message string) error {
|
||||
payload := in.RawPayload
|
||||
if payload == "" {
|
||||
b, _ := json.Marshal(in)
|
||||
payload = string(b)
|
||||
}
|
||||
log := &model.ShipLog{
|
||||
OrderNo: order.OrderNo,
|
||||
OrderID: order.ID,
|
||||
ShipStatus: in.ShipStatus,
|
||||
ProviderOrderNo: in.ProviderOrderNo,
|
||||
FailReason: in.FailReason,
|
||||
Payload: payload,
|
||||
ResultStatus: resultStatus,
|
||||
Message: message,
|
||||
}
|
||||
return s.db.Create(log).Error
|
||||
}
|
||||
|
||||
type ShipLogListQuery struct {
|
||||
Page int
|
||||
Size int
|
||||
OrderNo string
|
||||
ShipStatus string
|
||||
}
|
||||
|
||||
func (s *OrderService) ListShipLogs(q ShipLogListQuery) ([]model.ShipLog, int64, error) {
|
||||
if q.Page < 1 {
|
||||
q.Page = 1
|
||||
}
|
||||
if q.Size < 1 || q.Size > 100 {
|
||||
q.Size = 20
|
||||
}
|
||||
tx := s.db.Model(&model.ShipLog{})
|
||||
if q.OrderNo != "" {
|
||||
tx = tx.Where("order_no LIKE ?", "%"+q.OrderNo+"%")
|
||||
}
|
||||
if q.ShipStatus != "" {
|
||||
tx = tx.Where("ship_status = ?", q.ShipStatus)
|
||||
}
|
||||
var total int64
|
||||
if err := tx.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var list []model.ShipLog
|
||||
err := tx.Order("id DESC").Offset((q.Page - 1) * q.Size).Limit(q.Size).Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
type DashboardStats struct {
|
||||
SkinCount int64 `json:"skin_count"`
|
||||
DistributorCount int64 `json:"distributor_count"`
|
||||
|
||||
Reference in New Issue
Block a user