481 lines
14 KiB
Go
481 lines
14 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"affiliate_dash/internal/model"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type OrderService struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewOrderService(db *gorm.DB) *OrderService {
|
|
return &OrderService{db: db}
|
|
}
|
|
|
|
type OrderListQuery struct {
|
|
MerchantID uint
|
|
Page int
|
|
Size int
|
|
Status string
|
|
DistributorID *uint
|
|
}
|
|
|
|
type CreateOrderInput struct {
|
|
MerchantID uint
|
|
SkinID uint
|
|
DistributorID uint
|
|
BuyerName string
|
|
Remark string
|
|
// Status 可选:pending(默认)/ paid(联调测试可直接创建可发货订单)
|
|
Status string
|
|
}
|
|
|
|
func (s *OrderService) List(q OrderListQuery) ([]model.Order, int64, error) {
|
|
if q.Page < 1 {
|
|
q.Page = 1
|
|
}
|
|
if q.Size < 1 || q.Size > 100 {
|
|
q.Size = 20
|
|
}
|
|
tx := s.db.Model(&model.Order{})
|
|
if q.MerchantID != 0 {
|
|
tx = tx.Where("merchant_id = ?", q.MerchantID)
|
|
}
|
|
if q.Status != "" {
|
|
tx = tx.Where("status = ?", q.Status)
|
|
}
|
|
if q.DistributorID != nil {
|
|
tx = tx.Where("distributor_id = ?", *q.DistributorID)
|
|
}
|
|
var total int64
|
|
if err := tx.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
var list []model.Order
|
|
err := tx.Preload("Skin").Preload("Distributor").
|
|
Order("id DESC").
|
|
Offset((q.Page - 1) * q.Size).Limit(q.Size).
|
|
Find(&list).Error
|
|
return list, total, err
|
|
}
|
|
|
|
func (s *OrderService) Create(in CreateOrderInput) (*model.Order, error) {
|
|
var skin model.Skin
|
|
if in.MerchantID == 0 {
|
|
return nil, errors.New("商户不能为空")
|
|
}
|
|
if err := s.db.Where("id = ? AND merchant_id = ?", in.SkinID, in.MerchantID).First(&skin).Error; err != nil {
|
|
return nil, errors.New("皮肤不存在")
|
|
}
|
|
if skin.Status != 1 {
|
|
return nil, errors.New("皮肤已下架")
|
|
}
|
|
if skin.Stock == 0 {
|
|
return nil, errors.New("库存不足")
|
|
}
|
|
var memberCount int64
|
|
if err := s.db.Model(&model.MerchantMember{}).
|
|
Where("merchant_id = ? AND user_id = ? AND status = ?", in.MerchantID, in.DistributorID, 1).
|
|
Count(&memberCount).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
if memberCount == 0 {
|
|
return nil, errors.New("分销商不属于当前商户")
|
|
}
|
|
|
|
status := model.OrderStatusPending
|
|
switch in.Status {
|
|
case model.OrderStatusPending,
|
|
model.OrderStatusPaid,
|
|
model.OrderStatusDelivering,
|
|
model.OrderStatusDelivered,
|
|
model.OrderStatusShipFailed,
|
|
model.OrderStatusCancelled:
|
|
status = in.Status
|
|
case "":
|
|
// default pending
|
|
default:
|
|
return nil, errors.New("无效的订单状态")
|
|
}
|
|
|
|
order := &model.Order{
|
|
MerchantID: in.MerchantID,
|
|
OrderNo: generateOrderNo(),
|
|
SkinID: in.SkinID,
|
|
DistributorID: in.DistributorID,
|
|
BuyerName: in.BuyerName,
|
|
Amount: skin.Price,
|
|
CommissionAmt: skin.Price * skin.Commission,
|
|
Status: status,
|
|
Remark: in.Remark,
|
|
}
|
|
|
|
err := s.db.Transaction(func(tx *gorm.DB) error {
|
|
if skin.Stock > 0 {
|
|
res := tx.Model(&model.Skin{}).
|
|
Where("id = ? AND stock > 0", skin.ID).
|
|
Update("stock", gorm.Expr("stock - 1"))
|
|
if res.Error != nil {
|
|
return res.Error
|
|
}
|
|
if res.RowsAffected == 0 {
|
|
return errors.New("库存不足")
|
|
}
|
|
}
|
|
return tx.Create(order).Error
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// 带回商品信息,方便前端展示 sku / 订单号联调
|
|
_ = s.db.Preload("Skin").First(order, order.ID).Error
|
|
return order, nil
|
|
}
|
|
|
|
func (s *OrderService) UpdateStatus(merchantID, id uint, status string) error {
|
|
allowed := map[string]bool{
|
|
model.OrderStatusPending: true,
|
|
model.OrderStatusPaid: true,
|
|
model.OrderStatusDelivering: true,
|
|
model.OrderStatusDelivered: true,
|
|
model.OrderStatusShipFailed: true,
|
|
model.OrderStatusCancelled: true,
|
|
}
|
|
if !allowed[status] {
|
|
return errors.New("无效的订单状态")
|
|
}
|
|
res := s.db.Model(&model.Order{}).Where("id = ? AND merchant_id = ?", id, merchantID).Update("status", status)
|
|
if res.Error != nil {
|
|
return res.Error
|
|
}
|
|
if res.RowsAffected == 0 {
|
|
return errors.New("订单不存在")
|
|
}
|
|
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"`
|
|
GameChannel string `json:"game_channel,omitempty"`
|
|
GameUID string `json:"game_uid,omitempty"`
|
|
RoleName string `json:"role_name,omitempty"`
|
|
PayScore int `json:"pay_score,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
|
|
GameChannel *string
|
|
GameUID *string
|
|
RoleName *string
|
|
PayScore *int
|
|
}
|
|
|
|
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,
|
|
GameChannel: order.GameChannel,
|
|
GameUID: order.GameUID,
|
|
RoleName: order.RoleName,
|
|
PayScore: order.PayScore,
|
|
}
|
|
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 in.GameChannel != nil {
|
|
updates["game_channel"] = *in.GameChannel
|
|
}
|
|
if in.GameUID != nil {
|
|
updates["game_uid"] = *in.GameUID
|
|
}
|
|
if in.RoleName != nil {
|
|
updates["role_name"] = *in.RoleName
|
|
}
|
|
if in.PayScore != nil {
|
|
updates["pay_score"] = *in.PayScore
|
|
}
|
|
|
|
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{
|
|
MerchantID: order.MerchantID,
|
|
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 {
|
|
MerchantID uint
|
|
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.MerchantID != 0 {
|
|
tx = tx.Where("merchant_id = ?", q.MerchantID)
|
|
}
|
|
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"`
|
|
OrderCount int64 `json:"order_count"`
|
|
TotalSales float64 `json:"total_sales"`
|
|
TotalCommission float64 `json:"total_commission"`
|
|
PendingOrderCount int64 `json:"pending_order_count"`
|
|
}
|
|
|
|
func (s *OrderService) Dashboard(merchantID uint) (*DashboardStats, error) {
|
|
stats := &DashboardStats{}
|
|
s.db.Model(&model.Skin{}).Where("merchant_id = ?", merchantID).Count(&stats.SkinCount)
|
|
s.db.Model(&model.User{}).
|
|
Joins("JOIN merchant_members ON merchant_members.user_id = users.id").
|
|
Where("merchant_members.merchant_id = ? AND users.role = ?", merchantID, model.RoleDistributor).
|
|
Count(&stats.DistributorCount)
|
|
s.db.Model(&model.Order{}).Where("merchant_id = ?", merchantID).Count(&stats.OrderCount)
|
|
s.db.Model(&model.Order{}).Where("merchant_id = ? AND status = ?", merchantID, model.OrderStatusPending).Count(&stats.PendingOrderCount)
|
|
s.db.Model(&model.Order{}).
|
|
Where("merchant_id = ?", merchantID).
|
|
Where("status IN ?", []string{model.OrderStatusPaid, model.OrderStatusDelivered}).
|
|
Select("COALESCE(SUM(amount),0)").Scan(&stats.TotalSales)
|
|
s.db.Model(&model.Order{}).
|
|
Where("merchant_id = ?", merchantID).
|
|
Where("status IN ?", []string{model.OrderStatusPaid, model.OrderStatusDelivered}).
|
|
Select("COALESCE(SUM(commission_amt),0)").Scan(&stats.TotalCommission)
|
|
return stats, nil
|
|
}
|
|
|
|
func generateOrderNo() string {
|
|
return fmt.Sprintf("O%s%04d", time.Now().Format("20060102150405"), time.Now().Nanosecond()%10000)
|
|
}
|