Files
order/service/order.go
2025-12-10 13:51:09 +08:00

479 lines
14 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"
"go.mongodb.org/mongo-driver/v2/bson"
"order/consts"
"order/dao"
"order/model/dto"
"order/model/entity"
)
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 {
result = append(result, entity.OrderItem{
ProductID: item.ProductID,
ProductName: item.ProductName,
Price: item.Price,
Quantity: item.Quantity,
TotalPrice: item.Price * int64(item.Quantity),
ImageURL: item.ImageURL,
})
}
return result
}
// convertOrderItems 转换订单商品项
func convertOrderItems(items []entity.OrderItem) []dto.OrderItem {
var result []dto.OrderItem
for _, item := range items {
result = append(result, dto.OrderItem{
ProductID: item.ProductID,
ProductName: item.ProductName,
Price: item.Price,
Quantity: item.Quantity,
TotalPrice: item.TotalPrice,
ImageURL: item.ImageURL,
})
}
return result
}
// convertEntityOrderItemsToDTO 转换entity订单商品项到DTO
func convertEntityOrderItemsToDTO(items []entity.OrderItem) []dto.OrderItem {
var result []dto.OrderItem
for _, item := range items {
result = append(result, dto.OrderItem{
ProductID: item.ProductID,
ProductName: item.ProductName,
Price: item.Price,
Quantity: item.Quantity,
TotalPrice: item.TotalPrice,
ImageURL: item.ImageURL,
})
}
return result
}
// CreateOrder 创建订单
func (s *order) CreateOrder(ctx context.Context, req *dto.CreateOrderReq) (*dto.CreateOrderResp, error) {
// 1. 参数验证
if req.TenantID == "" || req.UserID == "" || req.Subject == "" {
return nil, errors.New("必填参数不能为空")
}
if len(req.OrderItems) == 0 {
return nil, errors.New("订单商品不能为空")
}
// 2. 计算订单金额
totalAmount := int64(0)
for i := range req.OrderItems {
item := &req.OrderItems[i]
if item.Price <= 0 || item.Quantity <= 0 {
return nil, fmt.Errorf("商品价格或数量无效: %s", item.ProductName)
}
totalPrice := item.Price * int64(item.Quantity)
totalAmount += totalPrice
}
if totalAmount <= 0 {
return nil, errors.New("订单总金额必须大于0")
}
// 3. 生成订单号
orderNo := s.generateOrderNo(req.TenantID)
// 4. 设置订单过期时间30分钟后
expiredAt := time.Now().Add(30 * time.Minute)
// 5. 创建待支付订单
order := &entity.OrderPending{
OrderBase: entity.OrderBase{
TenantID: req.TenantID,
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.TenantID == "" || req.OrderNo == "" {
return nil, errors.New("必填参数不能为空")
}
// 2. 查询订单
order, status, err := dao.Order.GetOrderByNo(ctx, req.TenantID, 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: 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 {
result = append(result, dto.OrderItem{
ProductID: item.ProductID,
ProductName: item.ProductName,
Price: item.Price,
Quantity: item.Quantity,
TotalPrice: item.TotalPrice,
ImageURL: item.ImageURL,
})
}
return result
}
// CancelOrder 取消订单
func (s *order) CancelOrder(ctx context.Context, req *dto.CancelOrderReq) (*dto.CancelOrderResp, error) {
// 1. 参数验证
if req.TenantID == "" || req.OrderNo == "" {
return nil, errors.New("必填参数不能为空")
}
// 2. 查询订单
order, status, err := dao.Order.GetOrderByNo(ctx, req.TenantID, 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)
}
// 4. 将订单移动到已取消状态
updateData := bson.M{
"cancel_reason": req.Reason,
}
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPending, consts.OrderStatusCancelled, req.TenantID, 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.TenantID == "" {
return nil, errors.New("租户ID不能为空")
}
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.TenantID, 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, tenantID string) error {
// 获取过期的待支付订单
expiredOrders, err := dao.Order.GetExpiredPendingOrders(ctx, tenantID)
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, tenantID, order.OrderNo, updateData); err != nil {
// 记录错误但继续处理其他订单
fmt.Printf("取消过期订单失败: %s, 错误: %v\n", order.OrderNo, err)
}
}
return nil
}
// UpdatePayInfo 更新支付信息
func (s *order) UpdatePayInfo(ctx context.Context, tenantID, orderNo string, payInfo entity.PayInfo) error {
return dao.Order.UpdatePayInfo(ctx, tenantID, orderNo, payInfo)
}