diff --git a/controller/order.go b/controller/order.go new file mode 100644 index 0000000..f01ac1b --- /dev/null +++ b/controller/order.go @@ -0,0 +1,381 @@ +package controller + +import ( + "context" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "order/model/dto" + "order/service" +) + +// Order 订单控制器 + +type Order struct{} + +var OrderController = &Order{} + +// Create 创建订单 +func (c *Order) Create(r *ghttp.Request) { + ctx := r.Context() + + var req dto.CreateOrderReq + if err := r.Parse(&req); err != nil { + g.Log().Error(ctx, "解析请求参数失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 400, + "message": "参数错误", + "data": nil, + }) + return + } + + // 获取订单服务实例 + orderService := service.GetOrderService() + if orderService == nil { + g.Log().Error(ctx, "订单服务未初始化") + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": "服务内部错误", + "data": nil, + }) + return + } + + // 创建订单 + resp, err := orderService.CreateOrder(ctx, &req) + if err != nil { + g.Log().Error(ctx, "创建订单失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + r.Response.WriteJson(g.Map{ + "code": 200, + "message": "success", + "data": resp, + }) +} + +// Pay 支付订单 +func (c *Order) Pay(r *ghttp.Request) { + ctx := r.Context() + + var req dto.PayOrderReq + if err := r.Parse(&req); err != nil { + g.Log().Error(ctx, "解析请求参数失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 400, + "message": "参数错误", + "data": nil, + }) + return + } + + // 获取支付服务实例 + paymentService := service.GetPaymentService() + if paymentService == nil { + g.Log().Error(ctx, "支付服务未初始化") + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": "服务内部错误", + "data": nil, + }) + return + } + + // 支付订单 + resp, err := paymentService.PayOrder(ctx, &req) + if err != nil { + g.Log().Error(ctx, "支付订单失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + r.Response.WriteJson(g.Map{ + "code": 200, + "message": "success", + "data": resp, + }) +} + +// Query 查询订单详情 +func (c *Order) Query(r *ghttp.Request) { + ctx := r.Context() + + var req dto.QueryOrderReq + if err := r.Parse(&req); err != nil { + g.Log().Error(ctx, "解析请求参数失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 400, + "message": "参数错误", + "data": nil, + }) + return + } + + // 获取订单服务实例 + orderService := service.GetOrderService() + if orderService == nil { + g.Log().Error(ctx, "订单服务未初始化") + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": "服务内部错误", + "data": nil, + }) + return + } + + // 查询订单 + resp, err := orderService.QueryOrder(ctx, &req) + if err != nil { + g.Log().Error(ctx, "查询订单失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + r.Response.WriteJson(g.Map{ + "code": 200, + "message": "success", + "data": resp, + }) +} + +// Cancel 取消订单 +func (c *Order) Cancel(r *ghttp.Request) { + ctx := r.Context() + + var req dto.CancelOrderReq + if err := r.Parse(&req); err != nil { + g.Log().Error(ctx, "解析请求参数失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 400, + "message": "参数错误", + "data": nil, + }) + return + } + + // 获取订单服务实例 + orderService := service.GetOrderService() + if orderService == nil { + g.Log().Error(ctx, "订单服务未初始化") + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": "服务内部错误", + "data": nil, + }) + return + } + + // 取消订单 + resp, err := orderService.CancelOrder(ctx, &req) + if err != nil { + g.Log().Error(ctx, "取消订单失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + r.Response.WriteJson(g.Map{ + "code": 200, + "message": "success", + "data": resp, + }) +} + +// Refund 退款 +func (c *Order) Refund(r *ghttp.Request) { + ctx := r.Context() + + var req dto.RefundOrderReq + if err := r.Parse(&req); err != nil { + g.Log().Error(ctx, "解析请求参数失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 400, + "message": "参数错误", + "data": nil, + }) + return + } + + // 获取支付服务实例 + paymentService := service.GetPaymentService() + if paymentService == nil { + g.Log().Error(ctx, "支付服务未初始化") + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": "服务内部错误", + "data": nil, + }) + return + } + + // 退款 + resp, err := paymentService.RefundOrder(ctx, &req) + if err != nil { + g.Log().Error(ctx, "退款失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + r.Response.WriteJson(g.Map{ + "code": 200, + "message": "success", + "data": resp, + }) +} + +// List 查询订单列表 +func (c *Order) List(r *ghttp.Request) { + ctx := r.Context() + + var req dto.ListOrdersReq + if err := r.Parse(&req); err != nil { + g.Log().Error(ctx, "解析请求参数失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 400, + "message": "参数错误", + "data": nil, + }) + return + } + + // 获取订单服务实例 + orderService := service.GetOrderService() + if orderService == nil { + g.Log().Error(ctx, "订单服务未初始化") + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": "服务内部错误", + "data": nil, + }) + return + } + + // 查询订单列表 + resp, err := orderService.ListOrders(ctx, &req) + if err != nil { + g.Log().Error(ctx, "查询订单列表失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + r.Response.WriteJson(g.Map{ + "code": 200, + "message": "success", + "data": resp, + }) +} + +// PaymentNotify 支付回调 +func (c *Order) PaymentNotify(r *ghttp.Request) { + ctx := r.Context() + + var req service.PaymentNotifyReq + if err := r.Parse(&req); err != nil { + g.Log().Error(ctx, "解析支付回调参数失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 400, + "message": "参数错误", + "data": nil, + }) + return + } + + // 获取支付服务实例 + paymentService := service.GetPaymentService() + if paymentService == nil { + g.Log().Error(ctx, "支付服务未初始化") + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": "服务内部错误", + "data": nil, + }) + return + } + + // 处理支付回调 + if err := paymentService.HandlePaymentNotify(ctx, &req); err != nil { + g.Log().Error(ctx, "处理支付回调失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + r.Response.WriteJson(g.Map{ + "code": 200, + "message": "success", + "data": nil, + }) +} + +// RefundNotify 退款回调 +func (c *Order) RefundNotify(r *ghttp.Request) { + ctx := r.Context() + + var req service.RefundNotifyReq + if err := r.Parse(&req); err != nil { + g.Log().Error(ctx, "解析退款回调参数失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 400, + "message": "参数错误", + "data": nil, + }) + return + } + + // 获取支付服务实例 + paymentService := service.GetPaymentService() + if paymentService == nil { + g.Log().Error(ctx, "支付服务未初始化") + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": "服务内部错误", + "data": nil, + }) + return + } + + // 处理退款回调 + if err := paymentService.HandleRefundNotify(ctx, &req); err != nil { + g.Log().Error(ctx, "处理退款回调失败:", err) + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + r.Response.WriteJson(g.Map{ + "code": 200, + "message": "success", + "data": nil, + }) +} diff --git a/controller/payment_config.go b/controller/payment_config.go new file mode 100644 index 0000000..f86d91e --- /dev/null +++ b/controller/payment_config.go @@ -0,0 +1,316 @@ +package controller + +import ( + "context" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "go.mongodb.org/mongo-driver/bson" + "order/model/dto" + "order/service" +) + +type PaymentConfigController struct{} + +var PaymentConfig = &PaymentConfigController{} + +// CreatePaymentConfig 创建支付配置 +func (c *PaymentConfigController) CreatePaymentConfig(r *ghttp.Request) { + var req dto.CreatePaymentConfigReq + if err := r.Parse(&req); err != nil { + r.Response.WriteJsonExit(g.Map{ + "code": 400, + "message": err.Error(), + "data": nil, + }) + return + } + + ctx := context.Background() + + // 构建支付配置实体 + config := &service.PaymentConfig{ + TenantID: req.TenantID, + PayMethod: req.PayMethod, + ConfigType: req.ConfigType, + Environment: req.Environment, + IsEnabled: req.IsEnabled, + } + + if req.WechatConfig != nil { + config.WechatConfig = &service.WechatConfig{ + AppID: req.WechatConfig.AppID, + MchID: req.WechatConfig.MchID, + APIKey: req.WechatConfig.APIKey, + APIClientCert: req.WechatConfig.APIClientCert, + APIClientKey: req.WechatConfig.APIClientKey, + NotifyURL: req.WechatConfig.NotifyURL, + RefundNotifyURL: req.WechatConfig.RefundNotifyURL, + SubAppID: req.WechatConfig.SubAppID, + SubMchID: req.WechatConfig.SubMchID, + } + } + + if req.AlipayConfig != nil { + config.AlipayConfig = &service.AlipayConfig{ + AppID: req.AlipayConfig.AppID, + PrivateKey: req.AlipayConfig.PrivateKey, + PublicKey: req.AlipayConfig.PublicKey, + AlipayPublicKey: req.AlipayConfig.AlipayPublicKey, + NotifyURL: req.AlipayConfig.NotifyURL, + RefundNotifyURL: req.AlipayConfig.RefundNotifyURL, + Format: req.AlipayConfig.Format, + Charset: req.AlipayConfig.Charset, + SignType: req.AlipayConfig.SignType, + IsSandbox: req.AlipayConfig.IsSandbox, + } + } + + if err := service.PaymentService.CreatePaymentConfig(ctx, config); err != nil { + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + r.Response.WriteJsonExit(g.Map{ + "code": 200, + "message": "success", + "data": nil, + }) +} + +// UpdatePaymentConfig 更新支付配置 +func (c *PaymentConfigController) UpdatePaymentConfig(r *ghttp.Request) { + var req dto.UpdatePaymentConfigReq + if err := r.Parse(&req); err != nil { + r.Response.WriteJsonExit(g.Map{ + "code": 400, + "message": err.Error(), + "data": nil, + }) + return + } + + ctx := context.Background() + + update := bson.M{} + if req.ConfigType != "" { + update["config_type"] = req.ConfigType + } + if req.Environment != "" { + update["environment"] = req.Environment + } + if req.IsEnabled != nil { + update["is_enabled"] = *req.IsEnabled + } + + if req.WechatConfig != nil { + wechatConfig := bson.M{} + if req.WechatConfig.AppID != "" { + wechatConfig["app_id"] = req.WechatConfig.AppID + } + if req.WechatConfig.MchID != "" { + wechatConfig["mch_id"] = req.WechatConfig.MchID + } + if req.WechatConfig.APIKey != "" { + wechatConfig["api_key"] = req.WechatConfig.APIKey + } + if req.WechatConfig.APIClientCert != "" { + wechatConfig["api_client_cert"] = req.WechatConfig.APIClientCert + } + if req.WechatConfig.APIClientKey != "" { + wechatConfig["api_client_key"] = req.WechatConfig.APIClientKey + } + if req.WechatConfig.NotifyURL != "" { + wechatConfig["notify_url"] = req.WechatConfig.NotifyURL + } + if req.WechatConfig.RefundNotifyURL != "" { + wechatConfig["refund_notify_url"] = req.WechatConfig.RefundNotifyURL + } + if req.WechatConfig.SubAppID != "" { + wechatConfig["sub_app_id"] = req.WechatConfig.SubAppID + } + if req.WechatConfig.SubMchID != "" { + wechatConfig["sub_mch_id"] = req.WechatConfig.SubMchID + } + update["wechat_config"] = wechatConfig + } + + if req.AlipayConfig != nil { + alipayConfig := bson.M{} + if req.AlipayConfig.AppID != "" { + alipayConfig["app_id"] = req.AlipayConfig.AppID + } + if req.AlipayConfig.PrivateKey != "" { + alipayConfig["private_key"] = req.AlipayConfig.PrivateKey + } + if req.AlipayConfig.PublicKey != "" { + alipayConfig["public_key"] = req.AlipayConfig.PublicKey + } + if req.AlipayConfig.AlipayPublicKey != "" { + alipayConfig["alipay_public_key"] = req.AlipayConfig.AlipayPublicKey + } + if req.AlipayConfig.NotifyURL != "" { + alipayConfig["notify_url"] = req.AlipayConfig.NotifyURL + } + if req.AlipayConfig.RefundNotifyURL != "" { + alipayConfig["refund_notify_url"] = req.AlipayConfig.RefundNotifyURL + } + if req.AlipayConfig.Format != "" { + alipayConfig["format"] = req.AlipayConfig.Format + } + if req.AlipayConfig.Charset != "" { + alipayConfig["charset"] = req.AlipayConfig.Charset + } + if req.AlipayConfig.SignType != "" { + alipayConfig["sign_type"] = req.AlipayConfig.SignType + } + if req.AlipayConfig.IsSandbox != false { + alipayConfig["is_sandbox"] = req.AlipayConfig.IsSandbox + } + update["alipay_config"] = alipayConfig + } + + if err := service.PaymentService.UpdatePaymentConfig(ctx, req.ID, update); err != nil { + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + r.Response.WriteJsonExit(g.Map{ + "code": 200, + "message": "success", + "data": nil, + }) +} + +// QueryPaymentConfig 查询支付配置 +func (c *PaymentConfigController) QueryPaymentConfig(r *ghttp.Request) { + tentID := r.Get("tenant_id").String() + payMethod := r.Get("pay_method").String() + configType := r.Get("config_type").String() + environment := r.Get("environment").String() + + ctx := context.Background() + + config, err := service.PaymentService.QueryPaymentConfig(ctx, tentID, payMethod, configType, environment) + if err != nil { + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + r.Response.WriteJsonExit(g.Map{ + "code": 200, + "message": "success", + "data": config, + }) +} + +// ListPaymentConfigs 查询支付配置列表 +func (c *PaymentConfigController) ListPaymentConfigs(r *ghttp.Request) { + var req dto.QueryPaymentConfigReq + if err := r.Parse(&req); err != nil { + r.Response.WriteJsonExit(g.Map{ + "code": 400, + "message": err.Error(), + "data": nil, + }) + return + } + + ctx := context.Background() + + filter := bson.M{"tenant_id": req.TenantID} + if req.PayMethod != "" { + filter["pay_method"] = req.PayMethod + } + if req.ConfigType != "" { + filter["config_type"] = req.ConfigType + } + if req.Environment != "" { + filter["environment"] = req.Environment + } + if req.IsEnabled != nil { + filter["is_enabled"] = *req.IsEnabled + } + + if req.Page <= 0 { + req.Page = 1 + } + if req.PageSize <= 0 { + req.PageSize = 10 + } + + configs, total, err := service.PaymentService.ListPaymentConfigs(ctx, filter, req.Page, req.PageSize) + if err != nil { + r.Response.WriteJsonExit(g.Map{ + "code": 500, + "message": err.Error(), + "data": nil, + }) + return + } + + resp := dto.PaymentConfigListResp{ + List: make([]dto.PaymentConfigResp, 0, len(configs)), + Total: total, + Page: req.Page, + PageSize: req.PageSize, + TotalPage: int((total + int64(req.PageSize) - 1) / int64(req.PageSize)), + } + + for _, config := range configs { + configResp := dto.PaymentConfigResp{ + ID: config.ID.Hex(), + TenantID: config.TenantID, + PayMethod: config.PayMethod, + ConfigType: config.ConfigType, + Environment: config.Environment, + IsEnabled: config.IsEnabled, + CreatedAt: config.CreatedAt, + UpdatedAt: config.UpdatedAt, + CreatedBy: config.CreatedBy, + UpdatedBy: config.UpdatedBy, + } + + if config.WechatConfig != nil { + configResp.WechatConfig = &dto.WechatConfigResp{ + AppID: config.WechatConfig.AppID, + MchID: config.WechatConfig.MchID, + NotifyURL: config.WechatConfig.NotifyURL, + RefundNotifyURL: config.WechatConfig.RefundNotifyURL, + SubAppID: config.WechatConfig.SubAppID, + SubMchID: config.WechatConfig.SubMchID, + } + } + + if config.AlipayConfig != nil { + configResp.AlipayConfig = &dto.AlipayConfigResp{ + AppID: config.AlipayConfig.AppID, + NotifyURL: config.AlipayConfig.NotifyURL, + RefundNotifyURL: config.AlipayConfig.RefundNotifyURL, + Format: config.AlipayConfig.Format, + Charset: config.AlipayConfig.Charset, + SignType: config.AlipayConfig.SignType, + IsSandbox: config.AlipayConfig.IsSandbox, + } + } + + resp.List = append(resp.List, configResp) + } + + r.Response.WriteJsonExit(g.Map{ + "code": 200, + "message": "success", + "data": resp, + }) +} diff --git a/dao/order_dao.go b/dao/order_dao.go new file mode 100644 index 0000000..c7abee9 --- /dev/null +++ b/dao/order_dao.go @@ -0,0 +1,276 @@ +package dao + +import ( + "context" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "order/model/entity" +) + +// OrderDao 订单数据访问对象 +// 支持按状态拆分的订单表操作 + +type OrderDao struct { + collections map[entity.OrderStatus]*mongo.Collection +} + +// NewOrderDao 创建订单DAO实例 +func NewOrderDao(collections map[entity.OrderStatus]*mongo.Collection) *OrderDao { + return &OrderDao{ + collections: collections, + } +} + +// getCollection 根据订单状态获取对应的集合 +func (d *OrderDao) getCollection(status entity.OrderStatus) (*mongo.Collection, error) { + collection, exists := d.collections[status] + if !exists { + return nil, fmt.Errorf("collection for status %s not found", status) + } + return collection, nil +} + +// CreatePendingOrder 创建待支付订单 +func (d *OrderDao) CreatePendingOrder(ctx context.Context, order *entity.OrderPending) error { + collection, err := d.getCollection(entity.OrderStatusPending) + if err != nil { + return err + } + + order.ID = primitive.NewObjectID() + order.CreatedAt = time.Now() + order.UpdatedAt = time.Now() + + _, err = collection.InsertOne(ctx, order) + return err +} + +// GetOrderByNo 根据订单号查询订单(自动识别状态) +func (d *OrderDao) GetOrderByNo(ctx context.Context, tenantID, orderNo string) (interface{}, entity.OrderStatus, error) { + // 按状态优先级搜索(从最新状态开始) + statuses := []entity.OrderStatus{ + entity.OrderStatusCompleted, + entity.OrderStatusShipped, + entity.OrderStatusPaid, + entity.OrderStatusPending, + } + + for _, status := range statuses { + collection, err := d.getCollection(status) + if err != nil { + continue + } + + switch status { + case entity.OrderStatusPending: + var order entity.OrderPending + if err := d.findOrderByNo(collection, ctx, tenantID, orderNo, &order); err == nil { + return &order, status, nil + } + case entity.OrderStatusPaid: + var order entity.OrderPaid + if err := d.findOrderByNo(collection, ctx, tenantID, orderNo, &order); err == nil { + return &order, status, nil + } + case entity.OrderStatusShipped: + var order entity.OrderShipped + if err := d.findOrderByNo(collection, ctx, tenantID, orderNo, &order); err == nil { + return &order, status, nil + } + case entity.OrderStatusCompleted: + var order entity.OrderCompleted + if err := d.findOrderByNo(collection, ctx, tenantID, orderNo, &order); err == nil { + return &order, status, nil + } + } + } + + return nil, "", errors.New("order not found") +} + +// findOrderByNo 通用订单查询方法 +func (d *OrderDao) findOrderByNo(collection *mongo.Collection, ctx context.Context, tenantID, orderNo string, result interface{}) error { + filter := bson.M{ + "tenant_id": tenantID, + "order_no": orderNo, + } + + err := collection.FindOne(ctx, filter).Decode(result) + if err == mongo.ErrNoDocuments { + return errors.New("order not found") + } + return err +} + +// MoveOrderToStatus 将订单从一个状态移动到另一个状态 +func (d *OrderDao) MoveOrderToStatus(ctx context.Context, fromStatus, toStatus entity.OrderStatus, tenantID, orderNo string, updateData bson.M) error { + // 获取源集合 + srcCollection, err := d.getCollection(fromStatus) + if err != nil { + return err + } + + // 获取目标集合 + destCollection, err := d.getCollection(toStatus) + if err != nil { + return err + } + + // 查找源订单 + var orderData bson.M + filter := bson.M{ + "tenant_id": tenantID, + "order_no": orderNo, + } + + err = srcCollection.FindOne(ctx, filter).Decode(&orderData) + if err != nil { + return err + } + + // 更新数据 + orderData["updated_at"] = time.Now() + for key, value := range updateData { + orderData[key] = value + } + + // 插入到目标集合 + _, err = destCollection.InsertOne(ctx, orderData) + if err != nil { + return err + } + + // 从源集合删除 + _, err = srcCollection.DeleteOne(ctx, filter) + return err +} + +// UpdatePendingOrder 更新待支付订单 +func (d *OrderDao) UpdatePendingOrder(ctx context.Context, tenantID, orderNo string, update bson.M) error { + collection, err := d.getCollection(entity.OrderStatusPending) + if err != nil { + return err + } + + update["updated_at"] = time.Now() + + _, err = collection.UpdateOne(ctx, bson.M{ + "tenant_id": tenantID, + "order_no": orderNo, + }, bson.M{"$set": update}) + + return err +} + +// ListOrdersByStatus 根据状态查询订单列表 +func (d *OrderDao) ListOrdersByStatus(ctx context.Context, status entity.OrderStatus, tenantID, userID string, page, pageSize int) (interface{}, int64, error) { + collection, err := d.getCollection(status) + if err != nil { + return nil, 0, err + } + + // 构建查询条件 + filter := bson.M{"tenant_id": tenantID} + if userID != "" { + filter["user_id"] = userID + } + + // 计算总数 + total, err := collection.CountDocuments(ctx, filter) + if err != nil { + return nil, 0, err + } + + // 分页查询 + skip := int64((page - 1) * pageSize) + opt := options.Find(). + SetSort(bson.D{{Key: "created_at", Value: -1}}). + SetSkip(skip). + SetLimit(int64(pageSize)) + + cursor, err := collection.Find(ctx, filter, opt) + if err != nil { + return nil, 0, err + } + defer cursor.Close(ctx) + + // 根据状态返回对应的订单类型 + switch status { + case entity.OrderStatusPending: + var orders []entity.OrderPending + if err := cursor.All(ctx, &orders); err != nil { + return nil, 0, err + } + return orders, total, nil + case entity.OrderStatusPaid: + var orders []entity.OrderPaid + if err := cursor.All(ctx, &orders); err != nil { + return nil, 0, err + } + return orders, total, nil + case entity.OrderStatusShipped: + var orders []entity.OrderShipped + if err := cursor.All(ctx, &orders); err != nil { + return nil, 0, err + } + return orders, total, nil + case entity.OrderStatusCompleted: + var orders []entity.OrderCompleted + if err := cursor.All(ctx, &orders); err != nil { + return nil, 0, err + } + return orders, total, nil + default: + return nil, 0, errors.New("unsupported order status") + } +} + +// GetExpiredPendingOrders 获取过期的待支付订单 +func (d *OrderDao) GetExpiredPendingOrders(ctx context.Context, tenantID string) ([]entity.OrderPending, error) { + collection, err := d.getCollection(entity.OrderStatusPending) + if err != nil { + return nil, err + } + + filter := bson.M{ + "tenant_id": tenantID, + "expired_at": bson.M{"$lte": time.Now()}, + } + + cursor, err := collection.Find(ctx, filter) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + + var orders []entity.OrderPending + if err := cursor.All(ctx, &orders); err != nil { + return nil, err + } + + return orders, nil +} + +// UpdatePayInfo 更新支付信息(待支付订单) +func (d *OrderDao) UpdatePayInfo(ctx context.Context, tenantID, orderNo string, payInfo entity.PayInfo) error { + collection, err := d.getCollection(entity.OrderStatusPending) + if err != nil { + return err + } + + _, err = collection.UpdateOne(ctx, bson.M{ + "tenant_id": tenantID, + "order_no": orderNo, + }, bson.M{"$set": bson.M{ + "pay_info": payInfo, + "updated_at": time.Now(), + }}) + + return err +} diff --git a/dao/payment_dao.go b/dao/payment_dao.go new file mode 100644 index 0000000..97369e0 --- /dev/null +++ b/dao/payment_dao.go @@ -0,0 +1,229 @@ +package dao + +import ( + "context" + "errors" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "order/model/entity" +) + +// PaymentConfigDao 支付配置数据访问对象 + +type PaymentConfigDao struct { + collection *mongo.Collection +} + +// NewPaymentConfigDao 创建支付配置DAO实例 +func NewPaymentConfigDao(collection *mongo.Collection) *PaymentConfigDao { + return &PaymentConfigDao{ + collection: collection, + } +} + +// Create 创建支付配置 +func (d *PaymentConfigDao) Create(ctx context.Context, config *entity.PaymentConfig) error { + if d.collection == nil { + return errors.New("collection not initialized") + } + + config.ID = primitive.NewObjectID() + + _, err := d.collection.InsertOne(ctx, config) + return err +} + +// GetByTenantAndMethod 根据租户和支付方式获取配置 +func (d *PaymentConfigDao) GetByTenantAndMethod(ctx context.Context, tenantID, payMethod string) (*entity.PaymentConfig, error) { + if d.collection == nil { + return nil, errors.New("collection not initialized") + } + + var config entity.PaymentConfig + err := d.collection.FindOne(ctx, bson.M{ + "tenant_id": tenantID, + "pay_method": payMethod, + "enabled": true, + }).Decode(&config) + + if err == mongo.ErrNoDocuments { + return nil, nil + } + + return &config, err +} + +// Update 更新支付配置 +func (d *PaymentConfigDao) Update(ctx context.Context, id string, update bson.M) error { + if d.collection == nil { + return errors.New("collection not initialized") + } + + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return err + } + + _, err = d.collection.UpdateOne(ctx, bson.M{"_id": objectID}, bson.M{"$set": update}) + return err +} + +// ListByTenant 查询租户的支付配置列表 +func (d *PaymentConfigDao) ListByTenant(ctx context.Context, tenantID string) ([]entity.PaymentConfig, error) { + if d.collection == nil { + return nil, errors.New("collection not initialized") + } + + filter := bson.M{"tenant_id": tenantID} + cursor, err := d.collection.Find(ctx, filter) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + + var configs []entity.PaymentConfig + if err := cursor.All(ctx, &configs); err != nil { + return nil, err + } + + return configs, nil +} + +// PaymentRecordDao 支付记录数据访问对象 + +type PaymentRecordDao struct { + collection *mongo.Collection +} + +// NewPaymentRecordDao 创建支付记录DAO实例 +func NewPaymentRecordDao(collection *mongo.Collection) *PaymentRecordDao { + return &PaymentRecordDao{ + collection: collection, + } +} + +// Create 创建支付记录 +func (d *PaymentRecordDao) Create(ctx context.Context, record *entity.PaymentRecord) error { + if d.collection == nil { + return errors.New("collection not initialized") + } + + record.ID = primitive.NewObjectID() + record.CreatedAt = time.Now().Unix() + record.UpdatedAt = time.Now().Unix() + + _, err := d.collection.InsertOne(ctx, record) + return err +} + +// GetByOrderNo 根据订单号获取支付记录 +func (d *PaymentRecordDao) GetByOrderNo(ctx context.Context, tenantID, orderNo string) (*entity.PaymentRecord, error) { + if d.collection == nil { + return nil, errors.New("collection not initialized") + } + + var record entity.PaymentRecord + err := d.collection.FindOne(ctx, bson.M{ + "tenant_id": tenantID, + "order_no": orderNo, + }).Decode(&record) + + if err == mongo.ErrNoDocuments { + return nil, nil + } + + return &record, err +} + +// UpdateStatus 更新支付记录状态 +func (d *PaymentRecordDao) UpdateStatus(ctx context.Context, id string, status, transactionID, tradeNo string) error { + if d.collection == nil { + return errors.New("collection not initialized") + } + + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return err + } + + update := bson.M{ + "status": status, + "transaction_id": transactionID, + "trade_no": tradeNo, + "updated_at": time.Now().Unix(), + } + + _, err = d.collection.UpdateOne(ctx, bson.M{"_id": objectID}, bson.M{"$set": update}) + return err +} + +// RefundRecordDao 退款记录数据访问对象 + +type RefundRecordDao struct { + collection *mongo.Collection +} + +// NewRefundRecordDao 创建退款记录DAO实例 +func NewRefundRecordDao(collection *mongo.Collection) *RefundRecordDao { + return &RefundRecordDao{ + collection: collection, + } +} + +// Create 创建退款记录 +func (d *RefundRecordDao) Create(ctx context.Context, record *entity.RefundRecord) error { + if d.collection == nil { + return errors.New("collection not initialized") + } + + record.ID = primitive.NewObjectID() + record.CreatedAt = time.Now().Unix() + record.UpdatedAt = time.Now().Unix() + + _, err := d.collection.InsertOne(ctx, record) + return err +} + +// GetByRefundNo 根据退款单号获取退款记录 +func (d *RefundRecordDao) GetByRefundNo(ctx context.Context, tenantID, refundNo string) (*entity.RefundRecord, error) { + if d.collection == nil { + return nil, errors.New("collection not initialized") + } + + var record entity.RefundRecord + err := d.collection.FindOne(ctx, bson.M{ + "tenant_id": tenantID, + "refund_no": refundNo, + }).Decode(&record) + + if err == mongo.ErrNoDocuments { + return nil, nil + } + + return &record, err +} + +// UpdateRefundStatus 更新退款记录状态 +func (d *RefundRecordDao) UpdateRefundStatus(ctx context.Context, id, status, refundID string) error { + if d.collection == nil { + return errors.New("collection not initialized") + } + + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return err + } + + update := bson.M{ + "status": status, + "refund_id": refundID, + "updated_at": time.Now().Unix(), + } + + _, err = d.collection.UpdateOne(ctx, bson.M{"_id": objectID}, bson.M{"$set": update}) + return err +} diff --git a/go.mod b/go.mod index 7fb421e..8edbcf2 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,9 @@ require ( gitee.com/red-future---jilin-g/common v0.1.9 github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.6 github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.6 - golang.org/x/net v0.48.0 + github.com/gogf/gf/v2 v2.9.6 + go.mongodb.org/mongo-driver v1.17.6 + go.mongodb.org/mongo-driver/v2 v2.4.0 ) require ( @@ -27,7 +29,6 @@ require ( github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 // indirect github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 // indirect - github.com/gogf/gf/v2 v2.9.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/glog v1.2.5 // indirect @@ -55,6 +56,7 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/ll v0.0.9 // indirect github.com/olekukonko/tablewriter v1.1.0 // indirect @@ -66,7 +68,6 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - go.mongodb.org/mongo-driver/v2 v2.4.0 // indirect go.opencensus.io v0.22.5 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect @@ -78,6 +79,7 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect + golang.org/x/net v0.48.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect diff --git a/go.sum b/go.sum index cf13752..95ad987 100644 --- a/go.sum +++ b/go.sum @@ -207,6 +207,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= @@ -273,6 +275,8 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo= go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= diff --git a/init.go b/init.go new file mode 100644 index 0000000..d516a26 --- /dev/null +++ b/init.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/gogf/gf/v2/frame/g" + "order/service" +) + +// initServices 初始化服务 +func initServices() error { + return service.InitServices() +} + +// initRoutes 初始化路由 +func initRoutes() { + s := g.Server() + + // 订单相关路由 + orderGroup := s.Group("/api/order") + orderGroup.POST("/create", "order.controller.Order.Create") + orderGroup.POST("/pay", "order.controller.Order.Pay") + orderGroup.GET("/query", "order.controller.Order.Query") + orderGroup.POST("/cancel", "order.controller.Order.Cancel") + orderGroup.POST("/refund", "order.controller.Order.Refund") + orderGroup.GET("/list", "order.controller.Order.List") + + // 支付回调路由 + paymentGroup := s.Group("/api/payment") + paymentGroup.POST("/notify", "order.controller.Order.PaymentNotify") + paymentGroup.POST("/refund-notify", "order.controller.Order.RefundNotify") + + g.Log().Info(g.Request().GetCtx(), "路由初始化完成") +} diff --git a/main.go b/main.go index b59b279..81ab997 100644 --- a/main.go +++ b/main.go @@ -1,21 +1,33 @@ package main import ( - // "order/controller" - + "context" "gitee.com/red-future---jilin-g/common/http" "gitee.com/red-future---jilin-g/common/jaeger" _ "gitee.com/red-future---jilin-g/common/mongo" _ "gitee.com/red-future---jilin-g/common/ragflow" // RAGFlow 客户端自动初始化 _ "github.com/gogf/gf/contrib/drivers/mysql/v2" _ "github.com/gogf/gf/contrib/nosql/redis/v2" - "golang.org/x/net/context" + "github.com/gogf/gf/v2/frame/g" + "order/controller" ) func main() { defer jaeger.ShutDown(context.Background()) - //http.RouteRegister([]interface{}{ - // controller.Order, - //}) - select {} + + // 初始化服务 + if err := initServices(); err != nil { + g.Log().Fatal(context.Background(), "初始化服务失败:", err) + } + + // 初始化路由 + initRoutes() + + // 注册路由 + http.RouteRegister([]interface{}{ + controller.OrderController, + }) + + // 启动服务 + g.Server().Run() } diff --git a/model/dto/order.go b/model/dto/order.go new file mode 100644 index 0000000..307d5e3 --- /dev/null +++ b/model/dto/order.go @@ -0,0 +1,206 @@ +package dto + +import ( + "time" +) + +// CreateOrderReq 创建订单请求 + +type CreateOrderReq struct { + TenantID string `json:"tenant_id" binding:"required"` // 租户ID + UserID string `json:"user_id" binding:"required"` // 用户ID + OrderType string `json:"order_type" binding:"required"` // 订单类型 + Subject string `json:"subject" binding:"required"` // 订单标题 + Description string `json:"description"` // 订单描述 + OrderItems []OrderItemReq `json:"order_items" binding:"required"` // 订单商品 + ShippingInfo ShippingInfoReq `json:"shipping_info"` // 收货信息 +} + +// OrderItemReq 创建订单商品项请求 + +type OrderItemReq struct { + ProductID string `json:"product_id" binding:"required"` // 商品ID + ProductName string `json:"product_name" binding:"required"` // 商品名称 + Price int64 `json:"price" binding:"required,min=1"` // 单价(分) + Quantity int `json:"quantity" binding:"required,min=1"` // 数量 + ImageURL string `json:"image_url"` // 商品图片 +} + +// ShippingInfoReq 收货信息请求 + +type ShippingInfoReq struct { + Consignee string `json:"consignee" binding:"required"` // 收货人 + Phone string `json:"phone" binding:"required"` // 手机号 + Province string `json:"province" binding:"required"` // 省份 + City string `json:"city" binding:"required"` // 城市 + District string `json:"district" binding:"required"` // 区县 + Address string `json:"address" binding:"required"` // 详细地址 + PostalCode string `json:"postal_code"` // 邮编 +} + +// CreateOrderResp 创建订单响应 + +type CreateOrderResp struct { + OrderNo string `json:"order_no"` // 订单号 + TotalAmount int64 `json:"total_amount"` // 订单总金额 + PayAmount int64 `json:"pay_amount"` // 实付金额 + ExpiredAt string `json:"expired_at"` // 过期时间 +} + +// PayOrderReq 支付订单请求 + +type PayOrderReq struct { + TenantID string `json:"tenant_id" binding:"required"` // 租户ID + OrderNo string `json:"order_no" binding:"required"` // 订单号 + PayMethod string `json:"pay_method" binding:"required"` // 支付方式:wechat/alipay + PayType string `json:"pay_type" binding:"required"` // 支付类型:native/jsapi/app/h5 + ClientIP string `json:"client_ip"` // 客户端IP + OpenID string `json:"openid"` // 用户OpenID(JSAPI支付) + AuthCode string `json:"auth_code"` // 授权码(付款码支付) + ReturnURL string `json:"return_url"` // 支付完成跳转URL +} + +// PayOrderResp 支付订单响应 + +type PayOrderResp struct { + OrderNo string `json:"order_no"` // 订单号 + QRCode string `json:"qrcode"` // 支付二维码(Native支付) + PayURL string `json:"pay_url"` // 支付链接(H5支付) + PrepayID string `json:"prepay_id"` // 预支付ID(微信) + JSAPIParams string `json:"jsapi_params"` // JSAPI参数(微信) + APPParams string `json:"app_params"` // APP参数 +} + +// QueryOrderReq 查询订单请求 + +type QueryOrderReq struct { + TenantID string `json:"tenant_id" binding:"required"` // 租户ID + OrderNo string `json:"order_no" binding:"required"` // 订单号 +} + +// QueryOrderResp 查询订单响应 + +type QueryOrderResp struct { + Order OrderDetail `json:"order"` // 订单详情 +} + +// OrderDetail 订单详情 + +type OrderDetail struct { + ID string `json:"id"` // 订单ID + TenantID string `json:"tenant_id"` // 租户ID + OrderNo string `json:"order_no"` // 订单号 + UserID string `json:"user_id"` // 用户ID + TotalAmount int64 `json:"total_amount"` // 订单总金额 + PayAmount int64 `json:"pay_amount"` // 实付金额 + Status string `json:"status"` // 订单状态 + PayMethod string `json:"pay_method"` // 支付方式 + PayStatus string `json:"pay_status"` // 支付状态 + OrderType string `json:"order_type"` // 订单类型 + Subject string `json:"subject"` // 订单标题 + Description string `json:"description"` // 订单描述 + OrderItems []OrderItem `json:"order_items"` // 订单商品 + ShippingInfo ShippingInfo `json:"shipping_info"` // 收货信息 + PayInfo PayInfo `json:"pay_info"` // 支付信息 + CreatedAt time.Time `json:"created_at"` // 创建时间 + UpdatedAt time.Time `json:"updated_at"` // 更新时间 + PaidAt *time.Time `json:"paid_at"` // 支付时间 + ExpiredAt *time.Time `json:"expired_at"` // 过期时间 +} + +// OrderItem 订单商品项(响应) + +type OrderItem struct { + ProductID string `json:"product_id"` // 商品ID + ProductName string `json:"product_name"` // 商品名称 + Price int64 `json:"price"` // 单价(分) + Quantity int `json:"quantity"` // 数量 + TotalPrice int64 `json:"total_price"` // 小计(分) + ImageURL string `json:"image_url"` // 商品图片 +} + +// ShippingInfo 收货信息(响应) + +type ShippingInfo struct { + Consignee string `json:"consignee"` // 收货人 + Phone string `json:"phone"` // 手机号 + Province string `json:"province"` // 省份 + City string `json:"city"` // 城市 + District string `json:"district"` // 区县 + Address string `json:"address"` // 详细地址 + PostalCode string `json:"postal_code"` // 邮编 +} + +// PayInfo 支付信息(响应) + +type PayInfo struct { + TransactionID string `json:"transaction_id"` // 支付平台交易号 + OutTradeNo string `json:"out_trade_no"` // 商户订单号 + PrepayID string `json:"prepay_id"` // 预支付ID + QRCode string `json:"qrcode"` // 支付二维码 + PayURL string `json:"pay_url"` // 支付链接 +} + +// CancelOrderReq 取消订单请求 + +type CancelOrderReq struct { + TenantID string `json:"tenant_id" binding:"required"` // 租户ID + OrderNo string `json:"order_no" binding:"required"` // 订单号 + Reason string `json:"reason"` // 取消原因 +} + +// CancelOrderResp 取消订单响应 + +type CancelOrderResp struct { + OrderNo string `json:"order_no"` // 订单号 + Status string `json:"status"` // 新状态 +} + +// RefundOrderReq 退款请求 + +type RefundOrderReq struct { + TenantID string `json:"tenant_id" binding:"required"` // 租户ID + OrderNo string `json:"order_no" binding:"required"` // 订单号 + RefundAmount int64 `json:"refund_amount" binding:"required"` // 退款金额(分) + Reason string `json:"reason"` // 退款原因 +} + +// RefundOrderResp 退款响应 + +type RefundOrderResp struct { + RefundNo string `json:"refund_no"` // 退款单号 + RefundID string `json:"refund_id"` // 退款ID + RefundAmount int64 `json:"refund_amount"` // 退款金额 +} + +// ListOrdersReq 订单列表请求 + +type ListOrdersReq struct { + TenantID string `json:"tenant_id" binding:"required"` // 租户ID + UserID string `json:"user_id"` // 用户ID(可选) + Status string `json:"status"` // 订单状态(可选) + Page int `json:"page"` // 页码 + PageSize int `json:"page_size"` // 每页大小 +} + +// ListOrdersResp 订单列表响应 + +type ListOrdersResp struct { + Orders []OrderSummary `json:"orders"` // 订单列表 + Total int64 `json:"total"` // 总记录数 + Page int `json:"page"` // 当前页码 + PageSize int `json:"page_size"` // 每页大小 +} + +// OrderSummary 订单摘要(用于列表) + +type OrderSummary struct { + ID string `json:"id"` // 订单ID + OrderNo string `json:"order_no"` // 订单号 + TotalAmount int64 `json:"total_amount"` // 订单总金额 + PayAmount int64 `json:"pay_amount"` // 实付金额 + Status string `json:"status"` // 订单状态 + Subject string `json:"subject"` // 订单标题 + CreatedAt time.Time `json:"created_at"` // 创建时间 + PaidAt *time.Time `json:"paid_at"` // 支付时间 +} diff --git a/model/entity/order_base.go b/model/entity/order_base.go new file mode 100644 index 0000000..a372961 --- /dev/null +++ b/model/entity/order_base.go @@ -0,0 +1,54 @@ +package entity + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +// OrderBase 订单基础信息 +// 所有订单状态共有的字段 +// 按状态拆分的订单表会继承这个基础结构 +// 每个状态对应一个独立的MongoDB集合 +// 例如:orders_pending, orders_paid, orders_shipped, orders_completed, orders_cancelled + +type OrderBase struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + TenantID string `bson:"tenant_id" json:"tenant_id"` // 租户ID + OrderNo string `bson:"order_no" json:"order_no"` // 订单号 + UserID string `bson:"user_id" json:"user_id"` // 用户ID + TotalAmount int64 `bson:"total_amount" json:"total_amount"` // 订单总金额(分) + PayAmount int64 `bson:"pay_amount" json:"pay_amount"` // 实付金额(分) + OrderType string `bson:"order_type" json:"order_type"` // 订单类型:normal-普通订单 + Subject string `bson:"subject" json:"subject"` // 订单标题 + Description string `bson:"description" json:"description"` // 订单描述 + CreatedAt time.Time `bson:"created_at" json:"created_at"` // 创建时间 + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` // 更新时间 + ExpiredAt *time.Time `bson:"expired_at,omitempty" json:"expired_at"` // 过期时间 +} + +// OrderItem 订单商品项 +// 所有订单状态共有的商品信息 +// 按状态拆分的订单表会包含这个结构 + +type OrderItem struct { + ProductID string `bson:"product_id" json:"product_id"` // 商品ID + ProductName string `bson:"product_name" json:"product_name"` // 商品名称 + Price int64 `bson:"price" json:"price"` // 单价(分) + Quantity int `bson:"quantity" json:"quantity"` // 数量 + TotalPrice int64 `bson:"total_price" json:"total_price"` // 小计(分) + ImageURL string `bson:"image_url,omitempty" json:"image_url"` // 商品图片 +} + +// ShippingInfo 收货信息 +// 所有订单状态共有的收货信息 +// 按状态拆分的订单表会包含这个结构 + +type ShippingInfo struct { + Consignee string `bson:"consignee" json:"consignee"` // 收货人 + Phone string `bson:"phone" json:"phone"` // 手机号 + Province string `bson:"province" json:"province"` // 省份 + City string `bson:"city" json:"city"` // 城市 + District string `bson:"district" json:"district"` // 区县 + Address string `bson:"address" json:"address"` // 详细地址 + PostalCode string `bson:"postal_code,omitempty" json:"postal_code"` // 邮编 +} diff --git a/model/entity/order_completed.go b/model/entity/order_completed.go new file mode 100644 index 0000000..7a33e1d --- /dev/null +++ b/model/entity/order_completed.go @@ -0,0 +1,36 @@ +package entity + +import ( + "time" +) + +// OrderCompleted 已完成订单 +// 对应MongoDB集合:orders_completed +// 用户确认收货后订单进入此状态 + +type OrderCompleted struct { + OrderBase // 基础订单信息 + PayMethod string `bson:"pay_method" json:"pay_method"` // 支付方式:wechat/alipay + OrderItems []OrderItem `bson:"order_items" json:"order_items"` // 订单商品列表 + ShippingInfo *ShippingInfo `bson:"shipping_info,omitempty" json:"shipping_info"` // 收货信息 + + // 支付成功信息 + PaidAt time.Time `bson:"paid_at" json:"paid_at"` // 支付时间 + TransactionID string `bson:"transaction_id" json:"transaction_id"` // 支付平台交易号 + TradeNo string `bson:"trade_no" json:"trade_no"` // 交易号 + PaymentChannel string `bson:"payment_channel" json:"payment_channel"` // 支付渠道:wechat/alipay + + // 发货信息 + ShippedAt time.Time `bson:"shipped_at" json:"shipped_at"` // 发货时间 + ExpressCompany string `bson:"express_company" json:"express_company"` // 快递公司 + ExpressNo string `bson:"express_no" json:"express_no"` // 快递单号 + ShippingCost int64 `bson:"shipping_cost" json:"shipping_cost"` // 运费(分) + + // 完成特有字段 + CompletedAt time.Time `bson:"completed_at" json:"completed_at"` // 完成时间 + ReceivedAt time.Time `bson:"received_at" json:"received_at"` // 收货时间 + + // 评价相关字段 + Rating int `bson:"rating,omitempty" json:"rating"` // 评分(1-5) + Comment string `bson:"comment,omitempty" json:"comment"` // 评价内容 +} diff --git a/model/entity/order_paid.go b/model/entity/order_paid.go new file mode 100644 index 0000000..72a281c --- /dev/null +++ b/model/entity/order_paid.go @@ -0,0 +1,26 @@ +package entity + +import ( + "time" +) + +// OrderPaid 已支付订单 +// 对应MongoDB集合:orders_paid +// 支付成功后订单进入此状态 + +type OrderPaid struct { + OrderBase // 基础订单信息 + PayMethod string `bson:"pay_method" json:"pay_method"` // 支付方式:wechat/alipay + PayType string `bson:"pay_type" json:"pay_type"` // 支付类型:native/jsapi/app/h5 + OrderItems []OrderItem `bson:"order_items" json:"order_items"` // 订单商品列表 + ShippingInfo *ShippingInfo `bson:"shipping_info,omitempty" json:"shipping_info"` // 收货信息 + + // 支付成功特有字段 + PaidAt time.Time `bson:"paid_at" json:"paid_at"` // 支付时间 + TransactionID string `bson:"transaction_id" json:"transaction_id"` // 支付平台交易号 + TradeNo string `bson:"trade_no" json:"trade_no"` // 交易号 + PaymentChannel string `bson:"payment_channel" json:"payment_channel"` // 支付渠道:wechat/alipay + + // 发货准备相关字段 + ReadyToShip bool `bson:"ready_to_ship" json:"ready_to_ship"` // 是否准备发货 +} diff --git a/model/entity/order_pending.go b/model/entity/order_pending.go new file mode 100644 index 0000000..9d61fc6 --- /dev/null +++ b/model/entity/order_pending.go @@ -0,0 +1,32 @@ +package entity + +import ( + "time" +) + +// OrderPending 待支付订单 +// 对应MongoDB集合:orders_pending +// 订单创建后进入此状态 + +type OrderPending struct { + OrderBase // 基础订单信息 + PayMethod string `bson:"pay_method" json:"pay_method"` // 支付方式:wechat/alipay + PayType string `bson:"pay_type" json:"pay_type"` // 支付类型:native/jsapi/app/h5 + OrderItems []OrderItem `bson:"order_items" json:"order_items"` // 订单商品列表 + ShippingInfo *ShippingInfo `bson:"shipping_info,omitempty" json:"shipping_info"` // 收货信息 + + // 支付相关字段 + PayInfo PayInfo `bson:"pay_info" json:"pay_info"` // 支付信息 +} + +// PayInfo 支付信息(待支付订单特有) +// 包含支付二维码、支付链接等 + +type PayInfo struct { + OutTradeNo string `bson:"out_trade_no" json:"out_trade_no"` // 商户订单号 + QRCode string `bson:"qrcode,omitempty" json:"qrcode"` // 支付二维码 + PayURL string `bson:"pay_url,omitempty" json:"pay_url"` // 支付链接 + PrepayID string `bson:"prepay_id,omitempty" json:"prepay_id"` // 预支付ID(微信) + JSAPIParams string `bson:"jsapi_params,omitempty" json:"jsapi_params"` // JSAPI参数 + APPParams string `bson:"app_params,omitempty" json:"app_params"` // APP参数 +} diff --git a/model/entity/order_shipped.go b/model/entity/order_shipped.go new file mode 100644 index 0000000..0e9672b --- /dev/null +++ b/model/entity/order_shipped.go @@ -0,0 +1,31 @@ +package entity + +import ( + "time" +) + +// OrderShipped 已发货订单 +// 对应MongoDB集合:orders_shipped +// 发货后订单进入此状态 + +type OrderShipped struct { + OrderBase // 基础订单信息 + PayMethod string `bson:"pay_method" json:"pay_method"` // 支付方式:wechat/alipay + OrderItems []OrderItem `bson:"order_items" json:"order_items"` // 订单商品列表 + ShippingInfo *ShippingInfo `bson:"shipping_info,omitempty" json:"shipping_info"` // 收货信息 + + // 支付成功信息 + PaidAt time.Time `bson:"paid_at" json:"paid_at"` // 支付时间 + TransactionID string `bson:"transaction_id" json:"transaction_id"` // 支付平台交易号 + TradeNo string `bson:"trade_no" json:"trade_no"` // 交易号 + PaymentChannel string `bson:"payment_channel" json:"payment_channel"` // 支付渠道:wechat/alipay + + // 发货特有字段 + ShippedAt time.Time `bson:"shipped_at" json:"shipped_at"` // 发货时间 + ExpressCompany string `bson:"express_company" json:"express_company"` // 快递公司 + ExpressNo string `bson:"express_no" json:"express_no"` // 快递单号 + ShippingCost int64 `bson:"shipping_cost" json:"shipping_cost"` // 运费(分) + + // 收货相关字段 + ExpectedArrival time.Time `bson:"expected_arrival,omitempty" json:"expected_arrival"` // 预计到达时间 +} diff --git a/model/entity/order_status.go b/model/entity/order_status.go new file mode 100644 index 0000000..45f4be7 --- /dev/null +++ b/model/entity/order_status.go @@ -0,0 +1,48 @@ +package entity + +// OrderStatus 订单状态枚举 +// 用于标识订单当前所处的状态 +// 每个状态对应一个独立的MongoDB集合 + +type OrderStatus string + +const ( + OrderStatusPending OrderStatus = "pending" // 待支付 - orders_pending + OrderStatusPaid OrderStatus = "paid" // 已支付 - orders_paid + OrderStatusShipped OrderStatus = "shipped" // 已发货 - orders_shipped + OrderStatusCompleted OrderStatus = "completed" // 已完成 - orders_completed + OrderStatusCancelled OrderStatus = "cancelled" // 已取消 - orders_cancelled + OrderStatusRefunded OrderStatus = "refunded" // 已退款 - orders_refunded +) + +// PayStatus 支付状态枚举 +// 用于标识订单的支付状态 + +type PayStatus string + +const ( + PayStatusUnpaid PayStatus = "unpaid" // 未支付 + PayStatusPaid PayStatus = "paid" // 已支付 + PayStatusFailed PayStatus = "failed" // 支付失败 + PayStatusRefunded PayStatus = "refunded" // 已退款 +) + +// PayMethod 支付方式枚举 + +type PayMethod string + +const ( + PayMethodWechat PayMethod = "wechat" // 微信支付 + PayMethodAlipay PayMethod = "alipay" // 支付宝支付 +) + +// PayType 支付类型枚举 + +type PayType string + +const ( + PayTypeNative PayType = "native" // 扫码支付 + PayTypeJSAPI PayType = "jsapi" // JSAPI支付 + PayTypeAPP PayType = "app" // APP支付 + PayTypeH5 PayType = "h5" // H5支付 +) diff --git a/model/entity/payment_config.go b/model/entity/payment_config.go new file mode 100644 index 0000000..803d391 --- /dev/null +++ b/model/entity/payment_config.go @@ -0,0 +1,82 @@ +package entity + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// PaymentConfig 支付配置 +// 每个租户有独立的支付配置 +// 支持微信支付和支付宝支付 + +type PaymentConfig struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + TenantID string `bson:"tenant_id" json:"tenant_id"` // 租户ID + PayMethod string `bson:"pay_method" json:"pay_method"` // 支付方式:wechat/alipay + ConfigName string `bson:"config_name" json:"config_name"` // 配置名称 + Description string `bson:"description" json:"description"` // 配置描述 + Enabled bool `bson:"enabled" json:"enabled"` // 是否启用 + + // 通用配置 + AppID string `bson:"app_id" json:"app_id"` // 应用ID + MchID string `bson:"mch_id" json:"mch_id"` // 商户号 + APIKey string `bson:"api_key" json:"api_key"` // API密钥 + NotifyURL string `bson:"notify_url" json:"notify_url"` // 回调地址 + ReturnURL string `bson:"return_url" json:"return_url"` // 返回地址 + + // 证书配置(微信支付) + CertPath string `bson:"cert_path,omitempty" json:"cert_path"` // 证书路径 + KeyPath string `bson:"key_path,omitempty" json:"key_path"` // 密钥路径 + + // 支付宝特有配置 + AppPrivateKey string `bson:"app_private_key,omitempty" json:"app_private_key"` // 应用私钥 + AlipayPublicKey string `bson:"alipay_public_key,omitempty" json:"alipay_public_key"` // 支付宝公钥 + + // 环境配置 + Sandbox bool `bson:"sandbox" json:"sandbox"` // 是否沙箱环境 + GatewayURL string `bson:"gateway_url" json:"gateway_url"` // 网关地址 + + // 支持的支付类型 + SupportNative bool `bson:"support_native" json:"support_native"` // 支持扫码支付 + SupportJSAPI bool `bson:"support_jsapi" json:"support_jsapi"` // 支持JSAPI支付 + SupportAPP bool `bson:"support_app" json:"support_app"` // 支持APP支付 + SupportH5 bool `bson:"support_h5" json:"support_h5"` // 支持H5支付 +} + +// PaymentRecord 支付记录 +// 记录每次支付操作的结果 + +type PaymentRecord struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + TenantID string `bson:"tenant_id" json:"tenant_id"` // 租户ID + OrderID primitive.ObjectID `bson:"order_id" json:"order_id"` // 订单ID + OrderNo string `bson:"order_no" json:"order_no"` // 订单号 + PayMethod string `bson:"pay_method" json:"pay_method"` // 支付方式 + PayType string `bson:"pay_type" json:"pay_type"` // 支付类型 + Amount int64 `bson:"amount" json:"amount"` // 支付金额(分) + TransactionID string `bson:"transaction_id" json:"transaction_id"` // 支付平台交易号 + OutTradeNo string `bson:"out_trade_no" json:"out_trade_no"` // 商户订单号 + TradeNo string `bson:"trade_no" json:"trade_no"` // 交易号 + PrepayID string `bson:"prepay_id,omitempty" json:"prepay_id"` // 预支付ID + Status string `bson:"status" json:"status"` // 支付状态:success/failed + ErrorMsg string `bson:"error_msg,omitempty" json:"error_msg"` // 错误信息 + CreatedAt int64 `bson:"created_at" json:"created_at"` // 创建时间 + UpdatedAt int64 `bson:"updated_at" json:"updated_at"` // 更新时间 +} + +// RefundRecord 退款记录 +// 记录每次退款操作的结果 + +type RefundRecord struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + TenantID string `bson:"tenant_id" json:"tenant_id"` // 租户ID + OrderID primitive.ObjectID `bson:"order_id" json:"order_id"` // 订单ID + OrderNo string `bson:"order_no" json:"order_no"` // 订单号 + RefundNo string `bson:"refund_no" json:"refund_no"` // 退款单号 + RefundID string `bson:"refund_id" json:"refund_id"` // 退款ID + RefundAmount int64 `bson:"refund_amount" json:"refund_amount"` // 退款金额(分) + Reason string `bson:"reason" json:"reason"` // 退款原因 + Status string `bson:"status" json:"status"` // 退款状态:success/failed + ErrorMsg string `bson:"error_msg,omitempty" json:"error_msg"` // 错误信息 + CreatedAt int64 `bson:"created_at" json:"created_at"` // 创建时间 + UpdatedAt int64 `bson:"updated_at" json:"updated_at"` // 更新时间 +} diff --git a/service/order.go b/service/order.go new file mode 100644 index 0000000..7b4053e --- /dev/null +++ b/service/order.go @@ -0,0 +1,439 @@ +package service + +import ( + "context" + "errors" + "fmt" + "math/rand" + "strconv" + "time" + + "github.com/gogf/gf/v2/util/gconv" + "go.mongodb.org/mongo-driver/bson" + "order/dao" + "order/model/dto" + "order/model/entity" +) + +// OrderService 订单服务 + +type OrderService struct { + orderDao *dao.OrderDao +} + +// NewOrderService 创建订单服务实例 +func NewOrderService(orderDao *dao.OrderDao) *OrderService { + return &OrderService{ + orderDao: orderDao, + } +} + +// CreateOrder 创建订单 +func (s *OrderService) 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) + } + item.TotalPrice = item.Price * int64(item.Quantity) + totalAmount += item.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: gconv.Slice[*entity.OrderItem](req.OrderItems), + ShippingInfo: (*entity.ShippingInfo)(&req.ShippingInfo), + PayInfo: entity.PayInfo{}, + } + + // 6. 保存订单 + if err := s.orderDao.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 *OrderService) 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 *OrderService) QueryOrder(ctx context.Context, req *dto.QueryOrderReq) (*dto.QueryOrderResp, error) { + // 1. 参数验证 + if req.TenantID == "" || req.OrderNo == "" { + return nil, errors.New("必填参数不能为空") + } + + // 2. 查询订单 + order, status, err := s.orderDao.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 entity.OrderStatusPending: + if pendingOrder, ok := order.(*entity.OrderPending); ok { + resp.Order = s.convertPendingOrderToDetail(pendingOrder) + } + case entity.OrderStatusPaid: + if paidOrder, ok := order.(*entity.OrderPaid); ok { + resp.Order = s.convertPaidOrderToDetail(paidOrder) + } + case entity.OrderStatusShipped: + if shippedOrder, ok := order.(*entity.OrderShipped); ok { + resp.Order = s.convertShippedOrderToDetail(shippedOrder) + } + case entity.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 *OrderService) 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(entity.OrderStatusPending), + PayMethod: order.PayMethod, + PayStatus: "unpaid", + OrderType: order.OrderType, + Subject: order.Subject, + Description: order.Description, + OrderItems: s.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 *OrderService) 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(entity.OrderStatusPaid), + PayMethod: order.PayMethod, + PayStatus: "paid", + OrderType: order.OrderType, + Subject: order.Subject, + Description: order.Description, + OrderItems: s.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 *OrderService) 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(entity.OrderStatusShipped), + PayMethod: order.PayMethod, + PayStatus: "paid", + OrderType: order.OrderType, + Subject: order.Subject, + Description: order.Description, + OrderItems: s.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 *OrderService) 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(entity.OrderStatusCompleted), + PayMethod: order.PayMethod, + PayStatus: "paid", + OrderType: order.OrderType, + Subject: order.Subject, + Description: order.Description, + OrderItems: s.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 *OrderService) 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 *OrderService) CancelOrder(ctx context.Context, req *dto.CancelOrderReq) (*dto.CancelOrderResp, error) { + // 1. 参数验证 + if req.TenantID == "" || req.OrderNo == "" { + return nil, errors.New("必填参数不能为空") + } + + // 2. 查询订单 + order, status, err := s.orderDao.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 != entity.OrderStatusPending { + return nil, fmt.Errorf("订单状态不正确,当前状态: %s", status) + } + + // 4. 将订单移动到已取消状态 + updateData := bson.M{ + "cancel_reason": req.Reason, + } + + if err := s.orderDao.MoveOrderToStatus(ctx, entity.OrderStatusPending, entity.OrderStatusCancelled, req.TenantID, req.OrderNo, updateData); err != nil { + return nil, fmt.Errorf("取消订单失败: %w", err) + } + + // 5. 返回结果 + resp := &dto.CancelOrderResp{ + OrderNo: req.OrderNo, + Status: string(entity.OrderStatusCancelled), + } + + return resp, nil +} + +// ListOrders 查询订单列表 +func (s *OrderService) 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 entity.OrderStatus + if req.Status != "" { + status = entity.OrderStatus(req.Status) + } else { + // 默认查询所有状态 + status = entity.OrderStatusPending + } + + orders, total, err := s.orderDao.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 entity.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(entity.OrderStatusPending), + Subject: order.Subject, + CreatedAt: order.CreatedAt, + }) + } + } + case entity.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(entity.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 *OrderService) ProcessExpiredOrders(ctx context.Context, tenantID string) error { + // 获取过期的待支付订单 + expiredOrders, err := s.orderDao.GetExpiredPendingOrders(ctx, tenantID) + if err != nil { + return fmt.Errorf("获取过期订单失败: %w", err) + } + + // 批量取消过期订单 + for _, order := range expiredOrders { + updateData := bson.M{ + "cancel_reason": "订单超时自动取消", + } + + if err := s.orderDao.MoveOrderToStatus(ctx, entity.OrderStatusPending, entity.OrderStatusCancelled, tenantID, order.OrderNo, updateData); err != nil { + // 记录错误但继续处理其他订单 + fmt.Printf("取消过期订单失败: %s, 错误: %v\n", order.OrderNo, err) + } + } + + return nil +} + +// UpdatePayInfo 更新支付信息 +func (s *OrderService) UpdatePayInfo(ctx context.Context, tenantID, orderNo string, payInfo entity.PayInfo) error { + return s.orderDao.UpdatePayInfo(ctx, tenantID, orderNo, payInfo) +} diff --git a/service/payment.go b/service/payment.go new file mode 100644 index 0000000..303f81b --- /dev/null +++ b/service/payment.go @@ -0,0 +1,387 @@ +package service + +import ( + "context" + "errors" + "fmt" + "math/rand" + "time" + + "go.mongodb.org/mongo-driver/bson" + "order/dao" + "order/model/dto" + "order/model/entity" +) + +// PaymentService 支付服务 + +type PaymentService struct { + orderDao *dao.OrderDao + paymentConfigDao *dao.PaymentConfigDao + paymentRecordDao *dao.PaymentRecordDao + refundRecordDao *dao.RefundRecordDao +} + +// NewPaymentService 创建支付服务实例 +func NewPaymentService( + orderDao *dao.OrderDao, + paymentConfigDao *dao.PaymentConfigDao, + paymentRecordDao *dao.PaymentRecordDao, + refundRecordDao *dao.RefundRecordDao, +) *PaymentService { + return &PaymentService{ + orderDao: orderDao, + paymentConfigDao: paymentConfigDao, + paymentRecordDao: paymentRecordDao, + refundRecordDao: refundRecordDao, + } +} + +// PayOrder 支付订单 +func (s *PaymentService) PayOrder(ctx context.Context, req *dto.PayOrderReq) (*dto.PayOrderResp, error) { + // 1. 参数验证 + if req.TenantID == "" || req.OrderNo == "" || req.PayMethod == "" || req.PayType == "" { + return nil, errors.New("必填参数不能为空") + } + + // 2. 查询订单 + order, status, err := s.orderDao.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 != entity.OrderStatusPending { + return nil, fmt.Errorf("订单状态不正确,当前状态: %s", status) + } + + pendingOrder, ok := order.(*entity.OrderPending) + if !ok { + return nil, errors.New("订单类型错误") + } + + // 4. 获取支付配置 + paymentConfig, err := s.paymentConfigDao.GetByTenantAndMethod(ctx, req.TenantID, req.PayMethod) + if err != nil { + return nil, fmt.Errorf("获取支付配置失败: %w", err) + } + + if paymentConfig == nil { + return nil, errors.New("支付配置不存在") + } + + // 5. 调用第三方支付接口 + payResp, err := s.callThirdPartyPayment(ctx, paymentConfig, req, pendingOrder) + if err != nil { + return nil, fmt.Errorf("调用支付接口失败: %w", err) + } + + // 6. 创建支付记录 + paymentRecord := &entity.PaymentRecord{ + TenantID: req.TenantID, + OrderID: pendingOrder.ID, + OrderNo: req.OrderNo, + PayMethod: req.PayMethod, + PayType: req.PayType, + Amount: pendingOrder.PayAmount, + OutTradeNo: payResp.OutTradeNo, + Status: "pending", + } + + if err := s.paymentRecordDao.Create(ctx, paymentRecord); err != nil { + return nil, fmt.Errorf("创建支付记录失败: %w", err) + } + + // 7. 更新订单支付信息 + payInfo := entity.PayInfo{ + OutTradeNo: payResp.OutTradeNo, + QRCode: payResp.QRCode, + PayURL: payResp.PayURL, + PrepayID: payResp.PrepayID, + JSAPIParams: payResp.JSAPIParams, + APPParams: payResp.APPParams, + } + + if err := s.orderDao.UpdatePayInfo(ctx, req.TenantID, req.OrderNo, payInfo); err != nil { + return nil, fmt.Errorf("更新订单支付信息失败: %w", err) + } + + // 8. 返回支付信息 + resp := &dto.PayOrderResp{ + OrderNo: req.OrderNo, + QRCode: payResp.QRCode, + PayURL: payResp.PayURL, + PrepayID: payResp.PrepayID, + JSAPIParams: payResp.JSAPIParams, + APPParams: payResp.APPParams, + } + + return resp, nil +} + +// callThirdPartyPayment 调用第三方支付接口 +func (s *PaymentService) callThirdPartyPayment(ctx context.Context, config *entity.PaymentConfig, req *dto.PayOrderReq, order *entity.OrderPending) (*PaymentResponse, error) { + // 这里应该是实际的第三方支付接口调用 + // 为了演示,我们返回模拟数据 + + outTradeNo := s.generateOutTradeNo(req.TenantID) + + payResp := &PaymentResponse{ + OutTradeNo: outTradeNo, + } + + // 根据支付类型返回不同的支付参数 + switch req.PayType { + case "native": + // 扫码支付 + payResp.QRCode = fmt.Sprintf("https://api.example.com/qrcode/%s", outTradeNo) + case "jsapi": + // JSAPI支付 + payResp.PrepayID = fmt.Sprintf("wxprepay_%s", outTradeNo) + payResp.JSAPIParams = fmt.Sprintf(`{"appId":"%s","timeStamp":"%d","nonceStr":"%s","package":"prepay_id=%s","signType":"MD5","paySign":"signature"}`, + config.AppID, time.Now().Unix(), s.generateNonceStr(), payResp.PrepayID) + case "app": + // APP支付 + payResp.APPParams = fmt.Sprintf(`{"appid":"%s","partnerid":"%s","prepayid":"%s","package":"Sign=WXPay","noncestr":"%s","timestamp":"%d","sign":"signature"}`, + config.AppID, config.MchID, payResp.PrepayID, s.generateNonceStr(), time.Now().Unix()) + case "h5": + // H5支付 + payResp.PayURL = fmt.Sprintf("https://api.example.com/h5pay/%s", outTradeNo) + } + + return payResp, nil +} + +// PaymentResponse 支付响应 + +type PaymentResponse struct { + OutTradeNo string `json:"out_trade_no"` // 商户订单号 + QRCode string `json:"qrcode"` // 支付二维码 + PayURL string `json:"pay_url"` // 支付链接 + PrepayID string `json:"prepay_id"` // 预支付ID + JSAPIParams string `json:"jsapi_params"` // JSAPI参数 + APPParams string `json:"app_params"` // APP参数 +} + +// generateOutTradeNo 生成商户订单号 +func (s *PaymentService) generateOutTradeNo(tenantID string) string { + timestamp := time.Now().Format("20060102150405") + random := rand.Intn(10000) + return fmt.Sprintf("%s%s%04d", tenantID, timestamp, random) +} + +// generateNonceStr 生成随机字符串 +func (s *PaymentService) generateNonceStr() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, 16) + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) +} + +// HandlePaymentNotify 处理支付回调 +func (s *PaymentService) HandlePaymentNotify(ctx context.Context, req *PaymentNotifyReq) error { + // 1. 验证回调签名 + if !s.verifyNotifySignature(req) { + return errors.New("签名验证失败") + } + + // 2. 查询支付记录 + paymentRecord, err := s.paymentRecordDao.GetByOrderNo(ctx, req.TenantID, req.OrderNo) + if err != nil { + return fmt.Errorf("查询支付记录失败: %w", err) + } + + if paymentRecord == nil { + return errors.New("支付记录不存在") + } + + // 3. 更新支付记录状态 + if err := s.paymentRecordDao.UpdateStatus(ctx, paymentRecord.ID.Hex(), req.Status, req.TransactionID, req.TradeNo); err != nil { + return fmt.Errorf("更新支付记录失败: %w", err) + } + + // 4. 如果支付成功,更新订单状态 + if req.Status == "success" { + updateData := bson.M{ + "paid_at": time.Now(), + "transaction_id": req.TransactionID, + "trade_no": req.TradeNo, + "payment_channel": req.PayMethod, + } + + if err := s.orderDao.MoveOrderToStatus(ctx, entity.OrderStatusPending, entity.OrderStatusPaid, req.TenantID, req.OrderNo, updateData); err != nil { + return fmt.Errorf("更新订单状态失败: %w", err) + } + } + + return nil +} + +// PaymentNotifyReq 支付回调请求 + +type PaymentNotifyReq struct { + TenantID string `json:"tenant_id"` // 租户ID + OrderNo string `json:"order_no"` // 订单号 + PayMethod string `json:"pay_method"` // 支付方式 + Status string `json:"status"` // 支付状态 + TransactionID string `json:"transaction_id"` // 交易号 + TradeNo string `json:"trade_no"` // 交易号 + Sign string `json:"sign"` // 签名 +} + +// verifyNotifySignature 验证回调签名 +func (s *PaymentService) verifyNotifySignature(req *PaymentNotifyReq) bool { + // 这里应该是实际的签名验证逻辑 + // 为了演示,我们总是返回true + return true +} + +// RefundOrder 退款 +func (s *PaymentService) RefundOrder(ctx context.Context, req *dto.RefundOrderReq) (*dto.RefundOrderResp, error) { + // 1. 参数验证 + if req.TenantID == "" || req.OrderNo == "" || req.RefundAmount <= 0 { + return nil, errors.New("必填参数不能为空") + } + + // 2. 查询订单 + order, status, err := s.orderDao.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 != entity.OrderStatusPaid { + return nil, fmt.Errorf("订单状态不正确,当前状态: %s", status) + } + + paidOrder, ok := order.(*entity.OrderPaid) + if !ok { + return nil, errors.New("订单类型错误") + } + + // 4. 验证退款金额 + if req.RefundAmount > paidOrder.PayAmount { + return nil, errors.New("退款金额不能超过支付金额") + } + + // 5. 调用第三方退款接口 + refundResp, err := s.callThirdPartyRefund(ctx, req, paidOrder) + if err != nil { + return nil, fmt.Errorf("调用退款接口失败: %w", err) + } + + // 6. 创建退款记录 + refundRecord := &entity.RefundRecord{ + TenantID: req.TenantID, + OrderID: paidOrder.ID, + OrderNo: req.OrderNo, + RefundNo: refundResp.RefundNo, + RefundAmount: req.RefundAmount, + Reason: req.Reason, + Status: "pending", + } + + if err := s.refundRecordDao.Create(ctx, refundRecord); err != nil { + return nil, fmt.Errorf("创建退款记录失败: %w", err) + } + + // 7. 如果是全额退款,更新订单状态 + if req.RefundAmount == paidOrder.PayAmount { + updateData := bson.M{ + "refund_reason": req.Reason, + } + + if err := s.orderDao.MoveOrderToStatus(ctx, entity.OrderStatusPaid, entity.OrderStatusRefunded, req.TenantID, req.OrderNo, updateData); err != nil { + return nil, fmt.Errorf("更新订单状态失败: %w", err) + } + } + + // 8. 返回退款结果 + resp := &dto.RefundOrderResp{ + RefundNo: refundResp.RefundNo, + RefundID: refundResp.RefundID, + RefundAmount: req.RefundAmount, + } + + return resp, nil +} + +// callThirdPartyRefund 调用第三方退款接口 +func (s *PaymentService) callThirdPartyRefund(ctx context.Context, req *dto.RefundOrderReq, order *entity.OrderPaid) (*RefundResponse, error) { + // 这里应该是实际的第三方退款接口调用 + // 为了演示,我们返回模拟数据 + + refundNo := s.generateRefundNo(req.TenantID) + refundID := fmt.Sprintf("refund_%s", refundNo) + + return &RefundResponse{ + RefundNo: refundNo, + RefundID: refundID, + }, nil +} + +// RefundResponse 退款响应 + +type RefundResponse struct { + RefundNo string `json:"refund_no"` // 退款单号 + RefundID string `json:"refund_id"` // 退款ID +} + +// generateRefundNo 生成退款单号 +func (s *PaymentService) generateRefundNo(tenantID string) string { + timestamp := time.Now().Format("20060102150405") + random := rand.Intn(10000) + return fmt.Sprintf("R%s%s%04d", tenantID, timestamp, random) +} + +// HandleRefundNotify 处理退款回调 +func (s *PaymentService) HandleRefundNotify(ctx context.Context, req *RefundNotifyReq) error { + // 1. 验证回调签名 + if !s.verifyRefundNotifySignature(req) { + return errors.New("签名验证失败") + } + + // 2. 查询退款记录 + refundRecord, err := s.refundRecordDao.GetByRefundNo(ctx, req.TenantID, req.RefundNo) + if err != nil { + return fmt.Errorf("查询退款记录失败: %w", err) + } + + if refundRecord == nil { + return errors.New("退款记录不存在") + } + + // 3. 更新退款记录状态 + if err := s.refundRecordDao.UpdateRefundStatus(ctx, refundRecord.ID.Hex(), req.Status, req.RefundID); err != nil { + return fmt.Errorf("更新退款记录失败: %w", err) + } + + return nil +} + +// RefundNotifyReq 退款回调请求 + +type RefundNotifyReq struct { + TenantID string `json:"tenant_id"` // 租户ID + RefundNo string `json:"refund_no"` // 退款单号 + Status string `json:"status"` // 退款状态 + RefundID string `json:"refund_id"` // 退款ID + Sign string `json:"sign"` // 签名 +} + +// verifyRefundNotifySignature 验证退款回调签名 +func (s *PaymentService) verifyRefundNotifySignature(req *RefundNotifyReq) bool { + // 这里应该是实际的签名验证逻辑 + // 为了演示,我们总是返回true + return true +} diff --git a/service/service_manager.go b/service/service_manager.go new file mode 100644 index 0000000..a67121a --- /dev/null +++ b/service/service_manager.go @@ -0,0 +1,50 @@ +package service + +import ( + "context" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/mongo" + "order/dao" + "order/model/entity" +) + +var ( + orderService *OrderService + paymentService *PaymentService +) + +// InitServices 初始化服务 +func InitServices() error { + ctx := context.Background() + + // 创建订单集合映射(模拟数据) + orderCollections := make(map[entity.OrderStatus]*mongo.Collection) + + // 创建DAO实例 + orderDao := dao.NewOrderDao(orderCollections) + paymentConfigDao := dao.NewPaymentConfigDao(nil) + paymentRecordDao := dao.NewPaymentRecordDao(nil) + refundRecordDao := dao.NewRefundRecordDao(nil) + + // 创建服务实例 + orderService = NewOrderService(orderDao) + paymentService = NewPaymentService( + orderDao, + paymentConfigDao, + paymentRecordDao, + refundRecordDao, + ) + + g.Log().Info(ctx, "服务初始化完成") + return nil +} + +// GetOrderService 获取订单服务实例 +func GetOrderService() *OrderService { + return orderService +} + +// GetPaymentService 获取支付服务实例 +func GetPaymentService() *PaymentService { + return paymentService +}