2025-12-10 09:02:41 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"math/rand"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
"order/consts"
|
2025-12-10 09:02:41 +08:00
|
|
|
|
"order/dao"
|
|
|
|
|
|
"order/model/dto"
|
|
|
|
|
|
"order/model/entity"
|
2025-12-12 18:16:28 +08:00
|
|
|
|
|
2025-12-15 09:02:30 +08:00
|
|
|
|
"gitee.com/red-future---jilin-g/common/http"
|
|
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
2025-12-12 18:16:28 +08:00
|
|
|
|
"github.com/gogf/gf/v2/util/gconv"
|
2025-12-15 09:02:30 +08:00
|
|
|
|
"github.com/robfig/cron/v3"
|
2025-12-12 18:16:28 +08:00
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
2025-12-10 09:02:41 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-15 09:02:30 +08:00
|
|
|
|
// AssetServiceResponse 资产服务响应结构体
|
|
|
|
|
|
type AssetServiceResponse struct {
|
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
UnlimitedStock bool `json:"unlimitedStock"`
|
|
|
|
|
|
Price *struct {
|
|
|
|
|
|
TotalStock int `json:"totalStock"`
|
|
|
|
|
|
} `json:"price"`
|
|
|
|
|
|
SaleMode string `json:"saleMode"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
type order struct{}
|
2025-12-10 09:02:41 +08:00
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
// Order 订单服务
|
|
|
|
|
|
var Order = new(order)
|
|
|
|
|
|
|
|
|
|
|
|
// convertOrderItemsFromDTO 从DTO转换订单商品项
|
|
|
|
|
|
func convertOrderItemsFromDTO(items []dto.OrderItemReq) []entity.OrderItem {
|
|
|
|
|
|
var result []entity.OrderItem
|
|
|
|
|
|
for _, item := range items {
|
2025-12-12 11:03:05 +08:00
|
|
|
|
// 转换库存明细,每个库存项只会有一个实例
|
|
|
|
|
|
var stocks []entity.OrderItemStock
|
|
|
|
|
|
totalQuantity := len(item.Stocks) // 每个库存项数量为1
|
|
|
|
|
|
totalAmount := int64(0)
|
|
|
|
|
|
|
|
|
|
|
|
for _, stock := range item.Stocks {
|
|
|
|
|
|
stocks = append(stocks, entity.OrderItemStock{
|
|
|
|
|
|
StockID: stock.StockID,
|
|
|
|
|
|
Price: stock.Price,
|
|
|
|
|
|
StockAttrs: stock.StockAttrs,
|
|
|
|
|
|
})
|
|
|
|
|
|
totalAmount += stock.Price // 每个库存项数量为1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
result = append(result, entity.OrderItem{
|
2025-12-12 11:03:05 +08:00
|
|
|
|
AssetID: item.AssetID,
|
|
|
|
|
|
AssetName: item.AssetName,
|
|
|
|
|
|
AssetType: item.AssetType,
|
|
|
|
|
|
ImageURL: item.ImageURL,
|
|
|
|
|
|
Quantity: totalQuantity,
|
|
|
|
|
|
TotalPrice: totalAmount,
|
|
|
|
|
|
Stocks: stocks,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// convertOrderItems 转换订单商品项
|
|
|
|
|
|
func convertOrderItems(items []entity.OrderItem) []dto.OrderItem {
|
|
|
|
|
|
var result []dto.OrderItem
|
|
|
|
|
|
for _, item := range items {
|
2025-12-12 11:03:05 +08:00
|
|
|
|
// 转换库存明细
|
|
|
|
|
|
var stocks []dto.OrderItemStock
|
|
|
|
|
|
for _, stock := range item.Stocks {
|
|
|
|
|
|
stocks = append(stocks, dto.OrderItemStock{
|
|
|
|
|
|
StockID: stock.StockID,
|
|
|
|
|
|
Price: stock.Price,
|
|
|
|
|
|
StockAttrs: stock.StockAttrs,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
result = append(result, dto.OrderItem{
|
2025-12-12 11:03:05 +08:00
|
|
|
|
AssetID: item.AssetID,
|
|
|
|
|
|
AssetName: item.AssetName,
|
|
|
|
|
|
AssetType: item.AssetType,
|
|
|
|
|
|
ImageURL: item.ImageURL,
|
|
|
|
|
|
Quantity: item.Quantity,
|
|
|
|
|
|
TotalPrice: item.TotalPrice,
|
|
|
|
|
|
Stocks: stocks,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
2025-12-10 09:02:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
// convertEntityOrderItemsToDTO 转换entity订单商品项到DTO
|
|
|
|
|
|
func convertEntityOrderItemsToDTO(items []entity.OrderItem) []dto.OrderItem {
|
|
|
|
|
|
var result []dto.OrderItem
|
|
|
|
|
|
for _, item := range items {
|
2025-12-12 11:03:05 +08:00
|
|
|
|
// 转换库存明细
|
|
|
|
|
|
var stocks []dto.OrderItemStock
|
|
|
|
|
|
for _, stock := range item.Stocks {
|
|
|
|
|
|
stocks = append(stocks, dto.OrderItemStock{
|
|
|
|
|
|
StockID: stock.StockID,
|
|
|
|
|
|
Price: stock.Price,
|
|
|
|
|
|
StockAttrs: stock.StockAttrs,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
result = append(result, dto.OrderItem{
|
2025-12-12 11:03:05 +08:00
|
|
|
|
AssetID: item.AssetID,
|
|
|
|
|
|
AssetName: item.AssetName,
|
|
|
|
|
|
AssetType: item.AssetType,
|
|
|
|
|
|
ImageURL: item.ImageURL,
|
|
|
|
|
|
Quantity: item.Quantity,
|
|
|
|
|
|
TotalPrice: item.TotalPrice,
|
|
|
|
|
|
Stocks: stocks,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
})
|
2025-12-10 09:02:41 +08:00
|
|
|
|
}
|
2025-12-10 13:51:09 +08:00
|
|
|
|
return result
|
2025-12-10 09:02:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CreateOrder 创建订单
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *order) CreateOrder(ctx context.Context, req *dto.CreateOrderReq) (*dto.CreateOrderResp, error) {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 1. 参数验证
|
2025-12-12 18:16:28 +08:00
|
|
|
|
if req.UserID == 0 || req.Subject == "" {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, errors.New("必填参数不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(req.OrderItems) == 0 {
|
|
|
|
|
|
return nil, errors.New("订单商品不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 09:02:30 +08:00
|
|
|
|
// 2. 检查库存扣减策略并处理库存
|
|
|
|
|
|
for i := range req.OrderItems {
|
|
|
|
|
|
item := &req.OrderItems[i]
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否需要扣减库存
|
|
|
|
|
|
shouldDeduct, saleMode, err := s.shouldDeductStock(ctx, gconv.String(req.TenantID), item.AssetID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("检查库存策略失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果需要扣减库存,判断扣减时机
|
|
|
|
|
|
if shouldDeduct {
|
|
|
|
|
|
timing, err := s.getDeductStockTiming(ctx, gconv.String(req.TenantID), item.AssetID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("获取库存扣减时机失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 秒杀场景:下单时立即扣减库存
|
|
|
|
|
|
if timing == "order_create" {
|
|
|
|
|
|
quantity := len(item.Stocks) // 每个库存项数量为1
|
|
|
|
|
|
|
|
|
|
|
|
// 先生成订单号用于库存操作
|
|
|
|
|
|
orderNo := s.generateOrderNo(gconv.String(req.TenantID))
|
|
|
|
|
|
|
|
|
|
|
|
if err := s.deductStock(ctx, gconv.String(req.TenantID), orderNo, item.AssetID, quantity, fmt.Sprintf("秒杀订单预占库存,售卖方式:%s", saleMode)); err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("秒杀订单预占库存失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 记录库存已预占
|
|
|
|
|
|
g.Log().Infof(ctx, "资产 %s 秒杀订单 %s 预占库存成功,数量:%d", item.AssetID, orderNo, quantity)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 计算订单金额
|
2025-12-10 09:02:41 +08:00
|
|
|
|
totalAmount := int64(0)
|
|
|
|
|
|
for i := range req.OrderItems {
|
|
|
|
|
|
item := &req.OrderItems[i]
|
2025-12-12 11:03:05 +08:00
|
|
|
|
itemTotal := int64(0)
|
|
|
|
|
|
for j := range item.Stocks {
|
|
|
|
|
|
stock := &item.Stocks[j]
|
|
|
|
|
|
if stock.Price <= 0 {
|
|
|
|
|
|
return nil, fmt.Errorf("资产价格无效: %s", item.AssetName)
|
|
|
|
|
|
}
|
|
|
|
|
|
itemTotal += stock.Price // 每个库存项数量为1
|
2025-12-10 09:02:41 +08:00
|
|
|
|
}
|
2025-12-12 11:03:05 +08:00
|
|
|
|
totalAmount += itemTotal
|
2025-12-10 09:02:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if totalAmount <= 0 {
|
|
|
|
|
|
return nil, errors.New("订单总金额必须大于0")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 09:02:30 +08:00
|
|
|
|
// 4. 生成订单号
|
2025-12-12 18:16:28 +08:00
|
|
|
|
orderNo := s.generateOrderNo(gconv.String(req.TenantID))
|
2025-12-10 09:02:41 +08:00
|
|
|
|
|
2025-12-15 09:02:30 +08:00
|
|
|
|
// 5. 设置订单过期时间(30分钟后)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
expiredAt := time.Now().Add(30 * time.Minute)
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 创建待支付订单
|
|
|
|
|
|
order := &entity.OrderPending{
|
|
|
|
|
|
OrderBase: entity.OrderBase{
|
|
|
|
|
|
OrderNo: orderNo,
|
|
|
|
|
|
UserID: req.UserID,
|
|
|
|
|
|
TotalAmount: totalAmount,
|
|
|
|
|
|
PayAmount: totalAmount, // 暂时没有优惠,实付金额等于总金额
|
|
|
|
|
|
OrderType: req.OrderType,
|
|
|
|
|
|
Subject: req.Subject,
|
|
|
|
|
|
Description: req.Description,
|
|
|
|
|
|
ExpiredAt: &expiredAt,
|
|
|
|
|
|
},
|
2025-12-10 13:51:09 +08:00
|
|
|
|
OrderItems: convertOrderItemsFromDTO(req.OrderItems),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
ShippingInfo: (*entity.ShippingInfo)(&req.ShippingInfo),
|
|
|
|
|
|
PayInfo: entity.PayInfo{},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 保存订单
|
2025-12-10 13:51:09 +08:00
|
|
|
|
if err := dao.Order.CreatePendingOrder(ctx, order); err != nil {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, fmt.Errorf("创建订单失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 7. 返回结果
|
|
|
|
|
|
resp := &dto.CreateOrderResp{
|
|
|
|
|
|
OrderNo: orderNo,
|
|
|
|
|
|
TotalAmount: totalAmount,
|
|
|
|
|
|
PayAmount: totalAmount,
|
|
|
|
|
|
ExpiredAt: expiredAt.Format(time.RFC3339),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// generateOrderNo 生成订单号
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *order) generateOrderNo(tenantID string) string {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
timestamp := time.Now().Format("20060102150405")
|
|
|
|
|
|
random := rand.Intn(10000)
|
|
|
|
|
|
return fmt.Sprintf("%s%s%04d", tenantID, timestamp, random)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// QueryOrder 查询订单详情
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *order) QueryOrder(ctx context.Context, req *dto.QueryOrderReq) (*dto.QueryOrderResp, error) {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 1. 参数验证
|
2025-12-12 18:16:28 +08:00
|
|
|
|
if req.OrderNo == "" {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, errors.New("必填参数不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 查询订单
|
2025-12-12 18:16:28 +08:00
|
|
|
|
order, status, err := dao.Order.GetOrderByNo(ctx, req.OrderNo)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("获取订单失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if order == nil {
|
|
|
|
|
|
return nil, errors.New("订单不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 构建响应
|
|
|
|
|
|
var resp dto.QueryOrderResp
|
|
|
|
|
|
|
|
|
|
|
|
switch status {
|
2025-12-10 13:51:09 +08:00
|
|
|
|
case consts.OrderStatusPending:
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if pendingOrder, ok := order.(*entity.OrderPending); ok {
|
|
|
|
|
|
resp.Order = s.convertPendingOrderToDetail(pendingOrder)
|
|
|
|
|
|
}
|
2025-12-10 13:51:09 +08:00
|
|
|
|
case consts.OrderStatusPaid:
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if paidOrder, ok := order.(*entity.OrderPaid); ok {
|
|
|
|
|
|
resp.Order = s.convertPaidOrderToDetail(paidOrder)
|
|
|
|
|
|
}
|
2025-12-10 13:51:09 +08:00
|
|
|
|
case consts.OrderStatusShipped:
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if shippedOrder, ok := order.(*entity.OrderShipped); ok {
|
|
|
|
|
|
resp.Order = s.convertShippedOrderToDetail(shippedOrder)
|
|
|
|
|
|
}
|
2025-12-10 13:51:09 +08:00
|
|
|
|
case consts.OrderStatusCompleted:
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if completedOrder, ok := order.(*entity.OrderCompleted); ok {
|
|
|
|
|
|
resp.Order = s.convertCompletedOrderToDetail(completedOrder)
|
|
|
|
|
|
}
|
|
|
|
|
|
default:
|
|
|
|
|
|
return nil, fmt.Errorf("不支持的订单状态: %s", status)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &resp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// convertPendingOrderToDetail 转换待支付订单为详情
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *order) convertPendingOrderToDetail(order *entity.OrderPending) dto.OrderDetail {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return dto.OrderDetail{
|
2025-12-12 16:20:47 +08:00
|
|
|
|
ID: order.Id.Hex(),
|
|
|
|
|
|
TenantID: gconv.String(order.TenantId),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
OrderNo: order.OrderNo,
|
|
|
|
|
|
UserID: order.UserID,
|
|
|
|
|
|
TotalAmount: order.TotalAmount,
|
|
|
|
|
|
PayAmount: order.PayAmount,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
Status: string(consts.OrderStatusPending),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
PayMethod: order.PayMethod,
|
|
|
|
|
|
PayStatus: "unpaid",
|
|
|
|
|
|
OrderType: order.OrderType,
|
|
|
|
|
|
Subject: order.Subject,
|
|
|
|
|
|
Description: order.Description,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
OrderItems: convertOrderItems(order.OrderItems),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
ShippingInfo: dto.ShippingInfo{
|
|
|
|
|
|
Consignee: order.ShippingInfo.Consignee,
|
|
|
|
|
|
Phone: order.ShippingInfo.Phone,
|
|
|
|
|
|
Province: order.ShippingInfo.Province,
|
|
|
|
|
|
City: order.ShippingInfo.City,
|
|
|
|
|
|
District: order.ShippingInfo.District,
|
|
|
|
|
|
Address: order.ShippingInfo.Address,
|
|
|
|
|
|
PostalCode: order.ShippingInfo.PostalCode,
|
|
|
|
|
|
},
|
|
|
|
|
|
PayInfo: dto.PayInfo{
|
|
|
|
|
|
OutTradeNo: order.PayInfo.OutTradeNo,
|
|
|
|
|
|
PrepayID: order.PayInfo.PrepayID,
|
|
|
|
|
|
QRCode: order.PayInfo.QRCode,
|
|
|
|
|
|
PayURL: order.PayInfo.PayURL,
|
|
|
|
|
|
},
|
|
|
|
|
|
CreatedAt: order.CreatedAt,
|
|
|
|
|
|
UpdatedAt: order.UpdatedAt,
|
|
|
|
|
|
ExpiredAt: order.ExpiredAt,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// convertPaidOrderToDetail 转换已支付订单为详情
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *order) convertPaidOrderToDetail(order *entity.OrderPaid) dto.OrderDetail {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return dto.OrderDetail{
|
2025-12-12 18:16:28 +08:00
|
|
|
|
ID: order.Id.Hex(),
|
2025-12-12 16:20:47 +08:00
|
|
|
|
TenantID: order.TenantId,
|
2025-12-10 09:02:41 +08:00
|
|
|
|
OrderNo: order.OrderNo,
|
|
|
|
|
|
UserID: order.UserID,
|
|
|
|
|
|
TotalAmount: order.TotalAmount,
|
|
|
|
|
|
PayAmount: order.PayAmount,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
Status: string(consts.OrderStatusPaid),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
PayMethod: order.PayMethod,
|
|
|
|
|
|
PayStatus: "paid",
|
|
|
|
|
|
OrderType: order.OrderType,
|
|
|
|
|
|
Subject: order.Subject,
|
|
|
|
|
|
Description: order.Description,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
OrderItems: convertOrderItems(order.OrderItems),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
ShippingInfo: dto.ShippingInfo{
|
|
|
|
|
|
Consignee: order.ShippingInfo.Consignee,
|
|
|
|
|
|
Phone: order.ShippingInfo.Phone,
|
|
|
|
|
|
Province: order.ShippingInfo.Province,
|
|
|
|
|
|
City: order.ShippingInfo.City,
|
|
|
|
|
|
District: order.ShippingInfo.District,
|
|
|
|
|
|
Address: order.ShippingInfo.Address,
|
|
|
|
|
|
PostalCode: order.ShippingInfo.PostalCode,
|
|
|
|
|
|
},
|
|
|
|
|
|
PayInfo: dto.PayInfo{
|
|
|
|
|
|
TransactionID: order.TransactionID,
|
|
|
|
|
|
OutTradeNo: order.OrderNo,
|
|
|
|
|
|
},
|
|
|
|
|
|
CreatedAt: order.CreatedAt,
|
|
|
|
|
|
UpdatedAt: order.UpdatedAt,
|
|
|
|
|
|
PaidAt: &order.PaidAt,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// convertShippedOrderToDetail 转换已发货订单为详情
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *order) convertShippedOrderToDetail(order *entity.OrderShipped) dto.OrderDetail {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return dto.OrderDetail{
|
2025-12-12 18:16:28 +08:00
|
|
|
|
ID: order.Id.Hex(),
|
|
|
|
|
|
TenantID: order.TenantId,
|
2025-12-10 09:02:41 +08:00
|
|
|
|
OrderNo: order.OrderNo,
|
|
|
|
|
|
UserID: order.UserID,
|
|
|
|
|
|
TotalAmount: order.TotalAmount,
|
|
|
|
|
|
PayAmount: order.PayAmount,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
Status: string(consts.OrderStatusShipped),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
PayMethod: order.PayMethod,
|
|
|
|
|
|
PayStatus: "paid",
|
|
|
|
|
|
OrderType: order.OrderType,
|
|
|
|
|
|
Subject: order.Subject,
|
|
|
|
|
|
Description: order.Description,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
OrderItems: convertOrderItems(order.OrderItems),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
ShippingInfo: dto.ShippingInfo{
|
|
|
|
|
|
Consignee: order.ShippingInfo.Consignee,
|
|
|
|
|
|
Phone: order.ShippingInfo.Phone,
|
|
|
|
|
|
Province: order.ShippingInfo.Province,
|
|
|
|
|
|
City: order.ShippingInfo.City,
|
|
|
|
|
|
District: order.ShippingInfo.District,
|
|
|
|
|
|
Address: order.ShippingInfo.Address,
|
|
|
|
|
|
PostalCode: order.ShippingInfo.PostalCode,
|
|
|
|
|
|
},
|
|
|
|
|
|
CreatedAt: order.CreatedAt,
|
|
|
|
|
|
UpdatedAt: order.UpdatedAt,
|
|
|
|
|
|
PaidAt: &order.PaidAt,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// convertCompletedOrderToDetail 转换已完成订单为详情
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *order) convertCompletedOrderToDetail(order *entity.OrderCompleted) dto.OrderDetail {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return dto.OrderDetail{
|
2025-12-12 18:16:28 +08:00
|
|
|
|
ID: order.Id.Hex(),
|
|
|
|
|
|
TenantID: order.TenantId,
|
2025-12-10 09:02:41 +08:00
|
|
|
|
OrderNo: order.OrderNo,
|
|
|
|
|
|
UserID: order.UserID,
|
|
|
|
|
|
TotalAmount: order.TotalAmount,
|
|
|
|
|
|
PayAmount: order.PayAmount,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
Status: string(consts.OrderStatusCompleted),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
PayMethod: order.PayMethod,
|
|
|
|
|
|
PayStatus: "paid",
|
|
|
|
|
|
OrderType: order.OrderType,
|
|
|
|
|
|
Subject: order.Subject,
|
|
|
|
|
|
Description: order.Description,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
OrderItems: convertOrderItems(order.OrderItems),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
ShippingInfo: dto.ShippingInfo{
|
|
|
|
|
|
Consignee: order.ShippingInfo.Consignee,
|
|
|
|
|
|
Phone: order.ShippingInfo.Phone,
|
|
|
|
|
|
Province: order.ShippingInfo.Province,
|
|
|
|
|
|
City: order.ShippingInfo.City,
|
|
|
|
|
|
District: order.ShippingInfo.District,
|
|
|
|
|
|
Address: order.ShippingInfo.Address,
|
|
|
|
|
|
PostalCode: order.ShippingInfo.PostalCode,
|
|
|
|
|
|
},
|
|
|
|
|
|
CreatedAt: order.CreatedAt,
|
|
|
|
|
|
UpdatedAt: order.UpdatedAt,
|
|
|
|
|
|
PaidAt: &order.PaidAt,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// convertOrderItems 转换订单商品项
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *order) convertOrderItems(items []*entity.OrderItem) []dto.OrderItem {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
var result []dto.OrderItem
|
|
|
|
|
|
for _, item := range items {
|
2025-12-12 11:03:05 +08:00
|
|
|
|
// 转换库存明细
|
|
|
|
|
|
var stocks []dto.OrderItemStock
|
|
|
|
|
|
for _, stock := range item.Stocks {
|
|
|
|
|
|
stocks = append(stocks, dto.OrderItemStock{
|
|
|
|
|
|
StockID: stock.StockID,
|
|
|
|
|
|
Price: stock.Price,
|
|
|
|
|
|
StockAttrs: stock.StockAttrs,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 09:02:41 +08:00
|
|
|
|
result = append(result, dto.OrderItem{
|
2025-12-12 11:03:05 +08:00
|
|
|
|
AssetID: item.AssetID,
|
|
|
|
|
|
AssetName: item.AssetName,
|
|
|
|
|
|
AssetType: item.AssetType,
|
|
|
|
|
|
ImageURL: item.ImageURL,
|
|
|
|
|
|
Quantity: item.Quantity,
|
|
|
|
|
|
TotalPrice: item.TotalPrice,
|
|
|
|
|
|
Stocks: stocks,
|
2025-12-10 09:02:41 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CancelOrder 取消订单
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *order) CancelOrder(ctx context.Context, req *dto.CancelOrderReq) (*dto.CancelOrderResp, error) {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 1. 参数验证
|
2025-12-12 18:16:28 +08:00
|
|
|
|
if req.OrderNo == "" {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, errors.New("必填参数不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 查询订单
|
2025-12-12 18:16:28 +08:00
|
|
|
|
order, status, err := dao.Order.GetOrderByNo(ctx, req.OrderNo)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("获取订单失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if order == nil {
|
|
|
|
|
|
return nil, errors.New("订单不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 检查订单状态(只有待支付订单可以取消)
|
2025-12-10 13:51:09 +08:00
|
|
|
|
if status != consts.OrderStatusPending {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, fmt.Errorf("订单状态不正确,当前状态: %s", status)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 09:02:30 +08:00
|
|
|
|
pendingOrder, ok := order.(*entity.OrderPending)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil, errors.New("订单类型错误")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 处理库存回充(针对秒杀场景预占的库存)
|
|
|
|
|
|
for _, item := range pendingOrder.OrderItems {
|
|
|
|
|
|
// 判断是否需要回充库存
|
|
|
|
|
|
shouldDeduct, saleMode, err := s.shouldDeductStock(ctx, req.TenantID, item.AssetID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
g.Log().Errorf(ctx, "检查库存策略失败,订单:%s,资产:%s,错误:%v", req.OrderNo, item.AssetID, err)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果该资产需要扣减库存且是秒杀场景,则需要回充预占库存
|
|
|
|
|
|
if shouldDeduct && saleMode == "flash" {
|
|
|
|
|
|
timing, err := s.getDeductStockTiming(ctx, req.TenantID, item.AssetID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
g.Log().Errorf(ctx, "获取库存扣减时机失败,订单:%s,资产:%s,错误:%v", req.OrderNo, item.AssetID, err)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 秒杀场景:下单时已扣减库存,取消时需要回充
|
|
|
|
|
|
if timing == "order_create" {
|
|
|
|
|
|
quantity := len(item.Stocks) // 每个库存项数量为1
|
|
|
|
|
|
|
|
|
|
|
|
if err := s.refundStock(ctx, req.TenantID, req.OrderNo, item.AssetID, quantity, fmt.Sprintf("秒杀订单取消回充库存,售卖方式:%s,原因:%s", saleMode, req.Reason)); err != nil {
|
|
|
|
|
|
g.Log().Errorf(ctx, "订单取消回充库存失败,订单:%s,资产:%s,错误:%v", req.OrderNo, item.AssetID, err)
|
|
|
|
|
|
// 库存回充失败不影响订单状态更新,但需要记录错误
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
g.Log().Infof(ctx, "资产 %s 订单取消回充库存成功,订单:%s,数量:%d", item.AssetID, req.OrderNo, quantity)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 将订单移动到已取消状态
|
2025-12-10 09:02:41 +08:00
|
|
|
|
updateData := bson.M{
|
|
|
|
|
|
"cancel_reason": req.Reason,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 18:16:28 +08:00
|
|
|
|
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPending, consts.OrderStatusCancelled, req.OrderNo, updateData); err != nil {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, fmt.Errorf("取消订单失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 返回结果
|
|
|
|
|
|
resp := &dto.CancelOrderResp{
|
|
|
|
|
|
OrderNo: req.OrderNo,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
Status: string(consts.OrderStatusCancelled),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ListOrders 查询订单列表
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *order) ListOrders(ctx context.Context, req *dto.ListOrdersReq) (*dto.ListOrdersResp, error) {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 1. 参数验证
|
|
|
|
|
|
if req.Page <= 0 {
|
|
|
|
|
|
req.Page = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if req.PageSize <= 0 || req.PageSize > 100 {
|
|
|
|
|
|
req.PageSize = 20
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 根据状态查询订单列表
|
2025-12-10 13:51:09 +08:00
|
|
|
|
var status consts.OrderStatus
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if req.Status != "" {
|
2025-12-10 13:51:09 +08:00
|
|
|
|
status = consts.OrderStatus(req.Status)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 默认查询所有状态
|
2025-12-10 13:51:09 +08:00
|
|
|
|
status = consts.OrderStatusPending
|
2025-12-10 09:02:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 18:16:28 +08:00
|
|
|
|
orders, total, err := dao.Order.ListOrdersByStatus(ctx, status, req.UserID, req.Page, req.PageSize)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("查询订单列表失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 转换订单列表
|
|
|
|
|
|
var orderSummaries []dto.OrderSummary
|
|
|
|
|
|
|
|
|
|
|
|
switch status {
|
2025-12-10 13:51:09 +08:00
|
|
|
|
case consts.OrderStatusPending:
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if pendingOrders, ok := orders.([]entity.OrderPending); ok {
|
|
|
|
|
|
for _, order := range pendingOrders {
|
|
|
|
|
|
orderSummaries = append(orderSummaries, dto.OrderSummary{
|
2025-12-12 18:16:28 +08:00
|
|
|
|
ID: order.Id.Hex(),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
OrderNo: order.OrderNo,
|
|
|
|
|
|
TotalAmount: order.TotalAmount,
|
|
|
|
|
|
PayAmount: order.PayAmount,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
Status: string(consts.OrderStatusPending),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
Subject: order.Subject,
|
|
|
|
|
|
CreatedAt: order.CreatedAt,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-10 13:51:09 +08:00
|
|
|
|
case consts.OrderStatusPaid:
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if paidOrders, ok := orders.([]entity.OrderPaid); ok {
|
|
|
|
|
|
for _, order := range paidOrders {
|
|
|
|
|
|
orderSummaries = append(orderSummaries, dto.OrderSummary{
|
2025-12-12 18:16:28 +08:00
|
|
|
|
ID: order.Id.Hex(),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
OrderNo: order.OrderNo,
|
|
|
|
|
|
TotalAmount: order.TotalAmount,
|
|
|
|
|
|
PayAmount: order.PayAmount,
|
2025-12-10 13:51:09 +08:00
|
|
|
|
Status: string(consts.OrderStatusPaid),
|
2025-12-10 09:02:41 +08:00
|
|
|
|
Subject: order.Subject,
|
|
|
|
|
|
CreatedAt: order.CreatedAt,
|
|
|
|
|
|
PaidAt: &order.PaidAt,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 其他状态类似处理...
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 返回结果
|
|
|
|
|
|
resp := &dto.ListOrdersResp{
|
|
|
|
|
|
Orders: orderSummaries,
|
|
|
|
|
|
Total: total,
|
|
|
|
|
|
Page: req.Page,
|
|
|
|
|
|
PageSize: req.PageSize,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ProcessExpiredOrders 处理过期订单
|
2025-12-12 18:16:28 +08:00
|
|
|
|
func (s *order) ProcessExpiredOrders(ctx context.Context) error {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 获取过期的待支付订单
|
2025-12-12 18:16:28 +08:00
|
|
|
|
expiredOrders, err := dao.Order.GetExpiredPendingOrders(ctx)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("获取过期订单失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 批量取消过期订单
|
|
|
|
|
|
for _, order := range expiredOrders {
|
|
|
|
|
|
updateData := bson.M{
|
|
|
|
|
|
"cancel_reason": "订单超时自动取消",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 18:16:28 +08:00
|
|
|
|
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPending, consts.OrderStatusCancelled, order.OrderNo, updateData); err != nil {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 记录错误但继续处理其他订单
|
|
|
|
|
|
fmt.Printf("取消过期订单失败: %s, 错误: %v\n", order.OrderNo, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UpdatePayInfo 更新支付信息
|
2025-12-12 18:16:28 +08:00
|
|
|
|
func (s *order) UpdatePayInfo(ctx context.Context, orderNo string, payInfo entity.PayInfo) error {
|
|
|
|
|
|
return dao.Order.UpdatePayInfo(ctx, orderNo, payInfo)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
}
|
2025-12-15 09:02:30 +08:00
|
|
|
|
|
|
|
|
|
|
// shouldDeductStock 判断是否需要扣减库存
|
|
|
|
|
|
func (s *order) shouldDeductStock(ctx context.Context, tenantID, assetID string) (bool, string, error) {
|
|
|
|
|
|
// 调用assets服务获取资产信息
|
|
|
|
|
|
assetResp, err := s.getAssetFromAssetService(ctx, tenantID, assetID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false, "", fmt.Errorf("获取资产信息失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if assetResp == nil {
|
|
|
|
|
|
return false, "", errors.New("资产不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 无库存限制的资产不需要扣减库存
|
|
|
|
|
|
if assetResp.UnlimitedStock {
|
|
|
|
|
|
return false, "unlimited_stock", nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 预售场景不扣减库存
|
|
|
|
|
|
if assetResp.SaleMode == "presale" {
|
|
|
|
|
|
return false, "presale_no_deduct", nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 有库存限制的常规售卖和秒杀需要扣减库存
|
|
|
|
|
|
return true, assetResp.SaleMode, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getDeductStockTiming 获取库存扣减时机
|
|
|
|
|
|
func (s *order) getDeductStockTiming(ctx context.Context, tenantID, assetID string) (string, error) {
|
|
|
|
|
|
assetResp, err := s.getAssetFromAssetService(ctx, tenantID, assetID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("获取资产信息失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if assetResp == nil {
|
|
|
|
|
|
return "", errors.New("资产不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据售卖方式确定扣减时机
|
|
|
|
|
|
switch assetResp.SaleMode {
|
|
|
|
|
|
case "flash":
|
|
|
|
|
|
return "order_create", nil // 秒杀:下单时扣减库存
|
|
|
|
|
|
case "regular":
|
|
|
|
|
|
return "payment_success", nil // 常规售卖:支付成功时扣减库存
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "", errors.New("未知售卖方式")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// deductStock 扣减库存
|
|
|
|
|
|
func (s *order) deductStock(ctx context.Context, tenantID, orderNo, assetID string, quantity int, reason string) error {
|
|
|
|
|
|
// 调用assets服务扣减库存
|
|
|
|
|
|
err := s.deductStockFromAssetService(ctx, tenantID, assetID, quantity)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("扣减库存失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
g.Log().Infof(ctx, "库存扣减成功,订单:%s,资产:%s,数量:%d,原因:%s", orderNo, assetID, quantity, reason)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// refundStock 回充库存
|
|
|
|
|
|
func (s *order) refundStock(ctx context.Context, tenantID, orderNo, assetID string, quantity int, reason string) error {
|
|
|
|
|
|
// 调用资产服务回充库存
|
|
|
|
|
|
err := s.refundStockFromAssetService(ctx, tenantID, assetID, quantity)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("回充库存失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
g.Log().Infof(ctx, "库存回充成功,订单:%s,资产:%s,数量:%d,原因:%s", orderNo, assetID, quantity, reason)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getAssetFromAssetService 从资产服务获取资产信息
|
|
|
|
|
|
func (s *order) getAssetFromAssetService(ctx context.Context, tenantID, assetID string) (*AssetServiceResponse, error) {
|
|
|
|
|
|
// 使用common/http中的封装方法调用资产服务
|
|
|
|
|
|
// 这里应该调用assets服务的getAsset接口
|
|
|
|
|
|
// 暂时返回模拟数据
|
|
|
|
|
|
var assetResp AssetServiceResponse
|
|
|
|
|
|
err := http.Get(ctx, fmt.Sprintf("http://assets-service/internal/asset/%s", assetID), &assetResp)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &assetResp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// deductStockFromAssetService 从资产服务扣减库存
|
|
|
|
|
|
func (s *order) deductStockFromAssetService(ctx context.Context, tenantID, assetID string, quantity int) error {
|
|
|
|
|
|
// 调用assets服务的扣减库存接口
|
|
|
|
|
|
req := map[string]interface{}{
|
|
|
|
|
|
"asset_id": assetID,
|
|
|
|
|
|
"quantity": quantity,
|
|
|
|
|
|
"reason": "订单扣减",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var result struct {
|
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := http.Post(ctx, "http://assets-service/internal/stock/deduct", &result, req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !result.Success {
|
|
|
|
|
|
return errors.New(result.Message)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// refundStockFromAssetService 从资产服务回充库存
|
|
|
|
|
|
func (s *order) refundStockFromAssetService(ctx context.Context, tenantID, assetID string, quantity int) error {
|
|
|
|
|
|
// 调用资产服务的回充库存接口
|
|
|
|
|
|
req := map[string]interface{}{
|
|
|
|
|
|
"asset_id": assetID,
|
|
|
|
|
|
"quantity": quantity,
|
|
|
|
|
|
"reason": "订单取消回充",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var result struct {
|
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := http.Post(ctx, "http://assets-service/internal/stock/refund", &result, req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !result.Success {
|
|
|
|
|
|
return errors.New(result.Message)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// OrderTimeoutScheduler 订单超时处理定时任务
|
|
|
|
|
|
type OrderTimeoutScheduler struct {
|
|
|
|
|
|
cron *cron.Cron
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var orderTimeoutSchedulerInstance = &OrderTimeoutScheduler{}
|
|
|
|
|
|
|
|
|
|
|
|
// StartOrderTimeoutScheduler 启动订单超时处理定时任务
|
|
|
|
|
|
func StartOrderTimeoutScheduler(ctx context.Context) error {
|
|
|
|
|
|
return orderTimeoutSchedulerInstance.StartScheduler(ctx)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// StopOrderTimeoutScheduler 停止订单超时处理定时任务
|
|
|
|
|
|
func StopOrderTimeoutScheduler() {
|
|
|
|
|
|
orderTimeoutSchedulerInstance.StopScheduler()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// StartScheduler 启动订单超时处理定时任务
|
|
|
|
|
|
func (s *OrderTimeoutScheduler) StartScheduler(ctx context.Context) error {
|
|
|
|
|
|
if s.cron != nil {
|
|
|
|
|
|
s.cron.Stop()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s.cron = cron.New(cron.WithSeconds())
|
|
|
|
|
|
|
|
|
|
|
|
// 每分钟检查一次超时订单
|
|
|
|
|
|
_, err := s.cron.AddFunc("0 */1 * * * *", func() {
|
|
|
|
|
|
s.processTimeoutOrders(ctx)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("添加订单超时检查定时任务失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s.cron.Start()
|
|
|
|
|
|
g.Log().Info(ctx, "订单超时处理定时任务已启动")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// StopScheduler 停止定时任务
|
|
|
|
|
|
func (s *OrderTimeoutScheduler) StopScheduler() {
|
|
|
|
|
|
if s.cron != nil {
|
|
|
|
|
|
s.cron.Stop()
|
|
|
|
|
|
s.cron = nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// processTimeoutOrders 处理超时订单
|
|
|
|
|
|
func (s *OrderTimeoutScheduler) processTimeoutOrders(ctx context.Context) {
|
|
|
|
|
|
g.Log().Info(ctx, "开始处理超时订单")
|
|
|
|
|
|
|
|
|
|
|
|
// 查询所有超时的待支付订单
|
|
|
|
|
|
expiredOrders, err := dao.Order.GetExpiredPendingOrders(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
g.Log().Errorf(ctx, "查询超时订单失败: %v", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(expiredOrders) == 0 {
|
|
|
|
|
|
g.Log().Debug(ctx, "没有需要处理的超时订单")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
g.Log().Infof(ctx, "发现 %d 个超时订单", len(expiredOrders))
|
|
|
|
|
|
|
|
|
|
|
|
// 逐个处理超时订单
|
|
|
|
|
|
for _, order := range expiredOrders {
|
|
|
|
|
|
if err := s.processTimeoutOrder(ctx, &order); err != nil {
|
|
|
|
|
|
g.Log().Errorf(ctx, "处理超时订单失败,订单号:%s,错误:%v", order.OrderNo, err)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
g.Log().Infof(ctx, "超时订单处理成功,订单号:%s", order.OrderNo)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// processTimeoutOrder 处理单个超时订单
|
|
|
|
|
|
func (s *OrderTimeoutScheduler) processTimeoutOrder(ctx context.Context, order *entity.OrderPending) error {
|
|
|
|
|
|
// 获取租户ID(从订单中获取)
|
|
|
|
|
|
tenantID := order.TenantId
|
|
|
|
|
|
if tenantID == "" {
|
|
|
|
|
|
return fmt.Errorf("订单缺少租户ID")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将订单移动到已取消状态
|
|
|
|
|
|
updateData := map[string]interface{}{
|
|
|
|
|
|
"cancel_reason": "订单超时自动取消",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPending, consts.OrderStatusCancelled, order.OrderNo, updateData); err != nil {
|
|
|
|
|
|
return fmt.Errorf("更新订单状态失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|