Files
order/service/order.go
2026-02-24 16:49:31 +08:00

863 lines
25 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
"order/consts"
"order/dao"
"order/model/dto"
"order/model/entity"
"gitea.com/red-future/common/http"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"github.com/robfig/cron/v3"
"go.mongodb.org/mongo-driver/v2/bson"
)
// 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"`
}
type order struct{}
// Order 订单服务
var Order = new(order)
// convertOrderItemsFromDTO 从DTO转换订单商品项
func convertOrderItemsFromDTO(items []dto.OrderItemReq) []entity.OrderItem {
var result []entity.OrderItem
for _, item := range items {
// 转换库存明细,支持明细模式和批次模式
var stocks []entity.OrderItemStock
totalQuantity := 0
totalAmount := int64(0)
for _, stock := range item.Stocks {
stocks = append(stocks, entity.OrderItemStock{
StockID: stock.StockID,
BatchID: stock.BatchID,
BatchNo: stock.BatchNo,
Quantity: stock.Quantity,
Price: stock.Price,
StockMode: stock.StockMode,
StockAttrs: stock.StockAttrs,
})
totalQuantity += stock.Quantity
totalAmount += int64(stock.Quantity) * stock.Price
}
result = append(result, entity.OrderItem{
AssetID: item.AssetID,
AssetName: item.AssetName,
AssetType: item.AssetType,
ImageURL: item.ImageURL,
Quantity: totalQuantity,
TotalPrice: totalAmount,
Stocks: stocks,
})
}
return result
}
// convertOrderItems 转换订单商品项
func convertOrderItems(items []entity.OrderItem) []dto.OrderItem {
var result []dto.OrderItem
for _, item := range items {
// 转换库存明细,支持明细模式和批次模式
var stocks []dto.OrderItemStock
for _, stock := range item.Stocks {
stocks = append(stocks, dto.OrderItemStock{
StockID: stock.StockID,
BatchID: stock.BatchID,
BatchNo: stock.BatchNo,
Quantity: stock.Quantity,
Price: stock.Price,
StockMode: stock.StockMode,
StockAttrs: stock.StockAttrs,
})
}
result = append(result, dto.OrderItem{
AssetID: item.AssetID,
AssetName: item.AssetName,
AssetType: item.AssetType,
ImageURL: item.ImageURL,
Quantity: item.Quantity,
TotalPrice: item.TotalPrice,
Stocks: stocks,
})
}
return result
}
// convertEntityOrderItemsToDTO 转换entity订单商品项到DTO
func convertEntityOrderItemsToDTO(items []entity.OrderItem) []dto.OrderItem {
var result []dto.OrderItem
for _, item := range items {
// 转换库存明细,支持明细模式和批次模式
var stocks []dto.OrderItemStock
for _, stock := range item.Stocks {
stocks = append(stocks, dto.OrderItemStock{
StockID: stock.StockID,
BatchID: stock.BatchID,
BatchNo: stock.BatchNo,
Quantity: stock.Quantity,
Price: stock.Price,
StockMode: stock.StockMode,
StockAttrs: stock.StockAttrs,
})
}
result = append(result, dto.OrderItem{
AssetID: item.AssetID,
AssetName: item.AssetName,
AssetType: item.AssetType,
ImageURL: item.ImageURL,
Quantity: item.Quantity,
TotalPrice: item.TotalPrice,
Stocks: stocks,
})
}
return result
}
// CreateOrder 创建订单
func (s *order) CreateOrder(ctx context.Context, req *dto.CreateOrderReq) (*dto.CreateOrderResp, error) {
// 1. 参数验证
if req.UserID == 0 || req.Subject == "" {
return nil, errors.New("必填参数不能为空")
}
if len(req.OrderItems) == 0 {
return nil, errors.New("订单商品不能为空")
}
// 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. 计算订单金额
totalAmount := int64(0)
for i := range req.OrderItems {
item := &req.OrderItems[i]
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
}
totalAmount += itemTotal
}
if totalAmount <= 0 {
return nil, errors.New("订单总金额必须大于0")
}
// 4. 生成订单号
orderNo := s.generateOrderNo(gconv.String(req.TenantID))
// 5. 设置订单过期时间30分钟后
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,
},
OrderItems: convertOrderItemsFromDTO(req.OrderItems),
ShippingInfo: (*entity.ShippingInfo)(&req.ShippingInfo),
PayInfo: entity.PayInfo{},
}
// 6. 保存订单
if err := dao.Order.CreatePendingOrder(ctx, order); err != nil {
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 生成订单号
func (s *order) generateOrderNo(tenantID string) string {
timestamp := time.Now().Format("20060102150405")
random := rand.Intn(10000)
return fmt.Sprintf("%s%s%04d", tenantID, timestamp, random)
}
// QueryOrder 查询订单详情
func (s *order) QueryOrder(ctx context.Context, req *dto.QueryOrderReq) (*dto.QueryOrderResp, error) {
// 1. 参数验证
if req.OrderNo == "" {
return nil, errors.New("必填参数不能为空")
}
// 2. 查询订单
order, status, err := dao.Order.GetOrderByNo(ctx, req.OrderNo)
if err != nil {
return nil, fmt.Errorf("获取订单失败: %w", err)
}
if order == nil {
return nil, errors.New("订单不存在")
}
// 3. 构建响应
var resp dto.QueryOrderResp
switch status {
case consts.OrderStatusPending:
if pendingOrder, ok := order.(*entity.OrderPending); ok {
resp.Order = s.convertPendingOrderToDetail(pendingOrder)
}
case consts.OrderStatusPaid:
if paidOrder, ok := order.(*entity.OrderPaid); ok {
resp.Order = s.convertPaidOrderToDetail(paidOrder)
}
case consts.OrderStatusShipped:
if shippedOrder, ok := order.(*entity.OrderShipped); ok {
resp.Order = s.convertShippedOrderToDetail(shippedOrder)
}
case consts.OrderStatusCompleted:
if completedOrder, ok := order.(*entity.OrderCompleted); ok {
resp.Order = s.convertCompletedOrderToDetail(completedOrder)
}
default:
return nil, fmt.Errorf("不支持的订单状态: %s", status)
}
return &resp, nil
}
// convertPendingOrderToDetail 转换待支付订单为详情
func (s *order) convertPendingOrderToDetail(order *entity.OrderPending) dto.OrderDetail {
return dto.OrderDetail{
ID: order.Id.Hex(),
TenantID: gconv.String(order.TenantId),
OrderNo: order.OrderNo,
UserID: order.UserID,
TotalAmount: order.TotalAmount,
PayAmount: order.PayAmount,
Status: string(consts.OrderStatusPending),
PayMethod: order.PayMethod,
PayStatus: "unpaid",
OrderType: order.OrderType,
Subject: order.Subject,
Description: order.Description,
OrderItems: convertOrderItems(order.OrderItems),
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 转换已支付订单为详情
func (s *order) convertPaidOrderToDetail(order *entity.OrderPaid) dto.OrderDetail {
return dto.OrderDetail{
ID: order.Id.Hex(),
TenantID: order.TenantId,
OrderNo: order.OrderNo,
UserID: order.UserID,
TotalAmount: order.TotalAmount,
PayAmount: order.PayAmount,
Status: string(consts.OrderStatusPaid),
PayMethod: order.PayMethod,
PayStatus: "paid",
OrderType: order.OrderType,
Subject: order.Subject,
Description: order.Description,
OrderItems: convertOrderItems(order.OrderItems),
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 转换已发货订单为详情
func (s *order) convertShippedOrderToDetail(order *entity.OrderShipped) dto.OrderDetail {
return dto.OrderDetail{
ID: order.Id.Hex(),
TenantID: order.TenantId,
OrderNo: order.OrderNo,
UserID: order.UserID,
TotalAmount: order.TotalAmount,
PayAmount: order.PayAmount,
Status: string(consts.OrderStatusShipped),
PayMethod: order.PayMethod,
PayStatus: "paid",
OrderType: order.OrderType,
Subject: order.Subject,
Description: order.Description,
OrderItems: convertOrderItems(order.OrderItems),
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 转换已完成订单为详情
func (s *order) convertCompletedOrderToDetail(order *entity.OrderCompleted) dto.OrderDetail {
return dto.OrderDetail{
ID: order.Id.Hex(),
TenantID: order.TenantId,
OrderNo: order.OrderNo,
UserID: order.UserID,
TotalAmount: order.TotalAmount,
PayAmount: order.PayAmount,
Status: string(consts.OrderStatusCompleted),
PayMethod: order.PayMethod,
PayStatus: "paid",
OrderType: order.OrderType,
Subject: order.Subject,
Description: order.Description,
OrderItems: convertOrderItems(order.OrderItems),
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 转换订单商品项
func (s *order) convertOrderItems(items []*entity.OrderItem) []dto.OrderItem {
var result []dto.OrderItem
for _, item := range items {
// 转换库存明细,支持明细模式和批次模式
var stocks []dto.OrderItemStock
for _, stock := range item.Stocks {
stocks = append(stocks, dto.OrderItemStock{
StockID: stock.StockID,
BatchID: stock.BatchID,
BatchNo: stock.BatchNo,
Quantity: stock.Quantity,
Price: stock.Price,
StockMode: stock.StockMode,
StockAttrs: stock.StockAttrs,
})
}
result = append(result, dto.OrderItem{
AssetID: item.AssetID,
AssetName: item.AssetName,
AssetType: item.AssetType,
ImageURL: item.ImageURL,
Quantity: item.Quantity,
TotalPrice: item.TotalPrice,
Stocks: stocks,
})
}
return result
}
// CancelOrder 取消订单
func (s *order) CancelOrder(ctx context.Context, req *dto.CancelOrderReq) (*dto.CancelOrderResp, error) {
// 1. 参数验证
if req.OrderNo == "" {
return nil, errors.New("必填参数不能为空")
}
// 2. 查询订单
order, status, err := dao.Order.GetOrderByNo(ctx, req.OrderNo)
if err != nil {
return nil, fmt.Errorf("获取订单失败: %w", err)
}
if order == nil {
return nil, errors.New("订单不存在")
}
// 3. 检查订单状态(只有待支付订单可以取消)
if status != consts.OrderStatusPending {
return nil, fmt.Errorf("订单状态不正确,当前状态: %s", status)
}
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. 将订单移动到已取消状态
updateData := bson.M{
"cancel_reason": req.Reason,
}
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPending, consts.OrderStatusCancelled, req.OrderNo, updateData); err != nil {
return nil, fmt.Errorf("取消订单失败: %w", err)
}
// 5. 返回结果
resp := &dto.CancelOrderResp{
OrderNo: req.OrderNo,
Status: string(consts.OrderStatusCancelled),
}
return resp, nil
}
// ListOrders 查询订单列表
func (s *order) ListOrders(ctx context.Context, req *dto.ListOrdersReq) (*dto.ListOrdersResp, error) {
// 1. 参数验证
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 || req.PageSize > 100 {
req.PageSize = 20
}
// 2. 根据状态查询订单列表
var status consts.OrderStatus
if req.Status != "" {
status = consts.OrderStatus(req.Status)
} else {
// 默认查询所有状态
status = consts.OrderStatusPending
}
orders, total, err := dao.Order.ListOrdersByStatus(ctx, status, req.UserID, req.Page, req.PageSize)
if err != nil {
return nil, fmt.Errorf("查询订单列表失败: %w", err)
}
// 3. 转换订单列表
var orderSummaries []dto.OrderSummary
switch status {
case consts.OrderStatusPending:
if pendingOrders, ok := orders.([]entity.OrderPending); ok {
for _, order := range pendingOrders {
orderSummaries = append(orderSummaries, dto.OrderSummary{
ID: order.Id.Hex(),
OrderNo: order.OrderNo,
TotalAmount: order.TotalAmount,
PayAmount: order.PayAmount,
Status: string(consts.OrderStatusPending),
Subject: order.Subject,
CreatedAt: order.CreatedAt,
})
}
}
case consts.OrderStatusPaid:
if paidOrders, ok := orders.([]entity.OrderPaid); ok {
for _, order := range paidOrders {
orderSummaries = append(orderSummaries, dto.OrderSummary{
ID: order.Id.Hex(),
OrderNo: order.OrderNo,
TotalAmount: order.TotalAmount,
PayAmount: order.PayAmount,
Status: string(consts.OrderStatusPaid),
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 处理过期订单
func (s *order) ProcessExpiredOrders(ctx context.Context) error {
// 获取过期的待支付订单
expiredOrders, err := dao.Order.GetExpiredPendingOrders(ctx)
if err != nil {
return fmt.Errorf("获取过期订单失败: %w", err)
}
// 批量取消过期订单
for _, order := range expiredOrders {
updateData := bson.M{
"cancel_reason": "订单超时自动取消",
}
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPending, consts.OrderStatusCancelled, order.OrderNo, updateData); err != nil {
// 记录错误但继续处理其他订单
fmt.Printf("取消过期订单失败: %s, 错误: %v\n", order.OrderNo, err)
}
}
return nil
}
// UpdatePayInfo 更新支付信息
func (s *order) UpdatePayInfo(ctx context.Context, orderNo string, payInfo entity.PayInfo) error {
return dao.Order.UpdatePayInfo(ctx, orderNo, payInfo)
}
// 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), nil, &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", nil, &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", nil, &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
}