package order import ( "backend/internal/dto" "backend/internal/model" customersvc "backend/internal/service/customer" "backend/internal/service/payment" "backend/utils" "errors" "fmt" "math" "strings" "time" "gorm.io/gorm" ) // OrderService 订单业务逻辑层 // Logan.Li type OrderService struct { db *gorm.DB } // NewOrderService 创建 service 实例 // Logan.Li func NewOrderService(db *gorm.DB) *OrderService { return &OrderService{db: db} } // List 分页查询订单 // Logan.Li func (s *OrderService) List(page, pageSize int, status string, productID uint) ([]dto.OrderResponse, int64, error) { var orders []model.Order var total int64 query := s.db.Model(&model.Order{}).Preload("Customer").Preload("Product").Preload("Plan") if status != "" { query = query.Where("status = ?", status) } if productID > 0 { query = query.Where("product_id = ?", productID) } query.Count(&total) if err := query.Order("created_at DESC"). Offset((page - 1) * pageSize).Limit(pageSize). Find(&orders).Error; err != nil { return nil, 0, err } results := make([]dto.OrderResponse, 0, len(orders)) for _, o := range orders { results = append(results, toOrderResponse(o)) } return results, total, nil } // GetByID 获取订单详情 // Logan.Li func (s *OrderService) GetByID(id uint) (*dto.OrderResponse, error) { var order model.Order if err := s.db.Preload("Customer").Preload("Product").Preload("Plan").First(&order, id).Error; err != nil { return nil, errors.New("订单不存在") } resp := toOrderResponse(order) return &resp, nil } // CreateOrder 客户端下单(含防重复购买检查) // Logan.Li func (s *OrderService) CreateOrder(req dto.CreateOrderRequest, clientIP, userAgent string) (*dto.CreateOrderResponse, error) { // 1. 查找或创建客户 // Logan.Li var customer model.Customer if err := s.db.Where("email = ?", req.CustomerEmail).First(&customer).Error; err != nil { name := req.CustomerName if name == "" { name = req.CustomerEmail } customer = model.Customer{Name: name, Email: req.CustomerEmail, Status: 1} if err := s.db.Create(&customer).Error; err != nil { return nil, errors.New("创建客户失败") } } // 2. 解析产品与计划:优先 product_code / plan_type,否则要求 ProductID+PlanID // Logan.Li var product model.Product var plan model.Plan productCode := strings.TrimSpace(req.ProductCode) planType := strings.TrimSpace(req.PlanType) if productCode != "" { if err := s.db.Where("code = ?", productCode).First(&product).Error; err != nil { return nil, errors.New("产品不存在") } if planType == "" { planType = "perpetual" } if err := s.db.Where("product_id = ? AND plan_type = ? AND status = ?", product.ID, planType, 1). Order("sort_order ASC").First(&plan).Error; err != nil { return nil, fmt.Errorf("未找到计划: product=%s plan_type=%s", productCode, planType) } } else { if req.ProductID == 0 || req.PlanID == 0 { return nil, errors.New("请提供 product_code/plan_type 或 product_id/plan_id") } if err := s.db.First(&product, req.ProductID).Error; err != nil { return nil, errors.New("产品不存在") } if err := s.db.First(&plan, req.PlanID).Error; err != nil { return nil, errors.New("定价计划不存在") } if plan.ProductID != product.ID { return nil, errors.New("定价计划与产品不匹配") } } // 3. 防重复购买检查 // Logan.Li checkResult := s.CheckDuplicatePurchase(customer.ID, product.ID, plan.PlanType) if !checkResult.AllowPurchase { return nil, fmt.Errorf("重复购买拦截: %s", checkResult.Reason) } // 4. 选取可用支付渠道(先选网关,写入订单) // Logan.Li var gateway model.PaymentGatewayConfig if err := s.db.Where("enabled = ?", true).Order("priority ASC").First(&gateway).Error; err != nil { return nil, errors.New("暂无可用的支付渠道") } notes := "" if deviceID := strings.TrimSpace(req.DeviceID); deviceID != "" { notes = fmt.Sprintf(`{"device_id":"%s"}`, deviceID) } // 5. 创建订单 // Logan.Li order := model.Order{ OrderNo: utils.GenerateOrderNo(), CustomerID: customer.ID, ProductID: product.ID, PlanID: plan.ID, Amount: plan.Price, Currency: plan.Currency, Status: "pending", PaymentGateway: gateway.Gateway, ClientIP: clientIP, UserAgent: userAgent, Notes: notes, } if err := s.db.Create(&order).Error; err != nil { return nil, errors.New("创建订单失败") } _ = customersvc.AttachProduct(s.db, customer.ID, product.ID) // 6. 生成收银台地址 // Logan.Li var checkoutURL string if gateway.Gateway == "waffo" { url, err := payment.BuildCheckoutURL( payment.LoadWaffoConfig(s.db), order.OrderNo, strings.TrimSpace(req.DeviceID), req.CustomerEmail, req.ReturnURL, plan.PlanType, ) if err != nil { return nil, fmt.Errorf("Waffo 收银台配置错误: %v", err) } checkoutURL = url } else { checkoutURL = fmt.Sprintf("https://checkout.%s.com/?order_no=%s", gateway.Gateway, order.OrderNo) } return &dto.CreateOrderResponse{ OrderNo: order.OrderNo, CheckoutURL: checkoutURL, Amount: plan.Price, Currency: plan.Currency, Status: "pending", Gateway: gateway.Gateway, ExpiresAt: time.Now().Add(30 * time.Minute).Format("2006-01-02 15:04:05"), }, nil } // Checkout desktop-friendly entry: defaults product_code=opsxshell, plan_type=perpetual. // Logan.Li func (s *OrderService) Checkout(req dto.CheckoutRequest, clientIP, userAgent string) (*dto.CreateOrderResponse, error) { productCode := strings.TrimSpace(req.ProductCode) if productCode == "" { productCode = "opsxshell" } planType := strings.TrimSpace(req.PlanType) if planType == "" { planType = "perpetual" } return s.CreateOrder(dto.CreateOrderRequest{ CustomerEmail: req.CustomerEmail, CustomerName: req.CustomerName, ProductCode: productCode, PlanType: planType, DeviceID: req.DeviceID, ReturnURL: req.ReturnURL, CancelURL: req.CancelURL, }, clientIP, userAgent) } // ClaimPurchase 领取授权:必须同时提供 order_no + customer_email,防止仅凭邮箱取证 // Logan.Li func (s *OrderService) ClaimPurchase(req dto.ClaimPurchaseRequest) (*dto.ClaimPurchaseResponse, error) { orderNo := strings.TrimSpace(req.OrderNo) email := strings.TrimSpace(req.CustomerEmail) if orderNo == "" || email == "" { return nil, errors.New("请同时提供 order_no 与 customer_email") } var order model.Order if err := s.db.Preload("Customer").Where("order_no = ?", orderNo).First(&order).Error; err != nil { return nil, errors.New("订单不存在") } if order.Customer.ID == 0 { if err := s.db.First(&order.Customer, order.CustomerID).Error; err != nil { return nil, errors.New("订单客户不存在") } } if !strings.EqualFold(strings.TrimSpace(order.Customer.Email), email) { return nil, errors.New("订单与邮箱不匹配") } switch order.Status { case "pending": return &dto.ClaimPurchaseResponse{ OrderNo: order.OrderNo, Status: "pending", Message: "订单尚未支付完成,请稍后再试", }, nil case "paid": licenseKey := "" if order.LicenseID > 0 { var lic model.License if err := s.db.First(&lic, order.LicenseID).Error; err == nil { licenseKey = lic.LicenseKey } } if licenseKey == "" { var ent model.CustomerEntitlement if err := s.db.Where("customer_id = ? AND product_id = ? AND status = ?", order.CustomerID, order.ProductID, "active"). First(&ent).Error; err == nil { licenseKey = ent.LicenseKey } } if licenseKey == "" { return &dto.ClaimPurchaseResponse{ OrderNo: order.OrderNo, Status: "paid", Message: "订单已支付,但 License 尚未就绪", }, nil } return &dto.ClaimPurchaseResponse{ LicenseKey: licenseKey, OrderNo: order.OrderNo, Status: "paid", Message: "ok", }, nil default: return &dto.ClaimPurchaseResponse{ OrderNo: order.OrderNo, Status: order.Status, Message: fmt.Sprintf("订单状态为 %s,无法领取授权", order.Status), }, nil } } // resolveWebhookOrder 仅按 order_no / gateway_order_no 匹配,禁止邮箱兜底劫持 // Logan.Li func (s *OrderService) resolveWebhookOrder(req dto.PaymentWebhookRequest) (model.Order, error) { var order model.Order if no := strings.TrimSpace(req.OrderNo); no != "" { if err := s.db.Where("order_no = ?", no).First(&order).Error; err == nil { return order, nil } // Hosted-store mistaken: Waffo ORD_ put in OrderNo — try gateway column. // Logan.Li if err := s.db.Where("gateway_order_no = ?", no).First(&order).Error; err == nil { return order, nil } } if g := strings.TrimSpace(req.GatewayOrderNo); g != "" { if err := s.db.Where("gateway_order_no = ?", g).First(&order).Error; err == nil { return order, nil } } return order, errors.New("订单不存在") } // PaymentWebhook 支付回调:原子状态流转 + 金额校验 + 退款可达 // Logan.Li func (s *OrderService) PaymentWebhook(req dto.PaymentWebhookRequest, clientIP string) error { status := strings.ToLower(strings.TrimSpace(req.Status)) if status == "completed" { status = "paid" } switch status { case "paid", "failed", "cancelled", "refunded": default: return nil // 未识别状态直接忽略 // Logan.Li } order, err := s.resolveWebhookOrder(req) if err != nil { return err } // 金额/货币校验(有金额时) // Logan.Li if req.Amount > 0 && (math.Abs(req.Amount-order.Amount) > 0.005 || (req.Currency != "" && !strings.EqualFold(req.Currency, order.Currency))) { utils.LogSystemAction(s.db, "payment_amount_mismatch", "order", order.ID, order.OrderNo, fmt.Sprintf("webhook %.2f%s 与订单 %.2f%s 不符", req.Amount, req.Currency, order.Amount, order.Currency), clientIP) return errors.New("支付金额与订单不符") } now := time.Now() updates := map[string]interface{}{ "payment_gateway": req.Gateway, "payment_method": req.Gateway, } if g := strings.TrimSpace(req.GatewayOrderNo); g != "" { updates["gateway_order_no"] = g } switch status { case "paid": updates["status"] = "paid" updates["paid_at"] = now case "refunded": updates["status"] = "refunded" updates["refund_at"] = now default: updates["status"] = status } // 原子状态流转:refund 只允许 paid→refunded;paid/failed/cancelled 只允许非终态 // Logan.Li res := s.db.Model(&model.Order{}).Where("id = ?", order.ID). Where("(status NOT IN ('paid','refunded') AND ? IN ('paid','failed','cancelled')) OR (status = 'paid' AND ? = 'refunded')", status, status). Updates(updates) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return nil // 幂等:已处理或非法流转 // Logan.Li } return s.db.Transaction(func(tx *gorm.DB) error { switch status { case "paid": var plan model.Plan if err := tx.First(&plan, order.PlanID).Error; err != nil { return fmt.Errorf("plan 不存在: %w", err) } var validUntil *time.Time var endDate time.Time if plan.Duration == 0 { endDate = model.PermanentEndDate } else { endDate = now.AddDate(0, 0, plan.Duration) validUntil = &endDate } lic := model.License{ LicenseKey: utils.GenerateLicenseKey(), CustomerID: order.CustomerID, ProductID: order.ProductID, Status: model.LicenseStatusActive, StartDate: now, EndDate: endDate, Duration: plan.Duration, MaxActivations: plan.MaxDevices, } if err := tx.Create(&lic).Error; err != nil { return err } _ = customersvc.AttachProduct(tx, order.CustomerID, order.ProductID) if err := tx.Model(&model.Order{}).Where("id = ?", order.ID). Update("license_id", lic.ID).Error; err != nil { return err } renewable := plan.PlanType != "perpetual" && plan.PlanType != "lifetime" var ent model.CustomerEntitlement entResult := tx.Where("customer_id = ? AND product_id = ?", order.CustomerID, order.ProductID).First(&ent) if entResult.Error != nil { ent = model.CustomerEntitlement{ CustomerID: order.CustomerID, ProductID: order.ProductID, PlanID: order.PlanID, LicenseID: lic.ID, LicenseKey: lic.LicenseKey, PlanType: plan.PlanType, Status: "active", ValidFrom: now, ValidUntil: validUntil, Renewable: renewable, } if err := tx.Create(&ent).Error; err != nil { return err } } else { ent.PlanID = order.PlanID ent.LicenseID = lic.ID ent.LicenseKey = lic.LicenseKey ent.PlanType = plan.PlanType ent.Status = "active" ent.ValidFrom = now ent.ValidUntil = validUntil ent.Renewable = renewable if err := tx.Save(&ent).Error; err != nil { return err } } utils.LogSystemAction(tx, "payment_completed", "order", order.ID, order.OrderNo, fmt.Sprintf("订单支付成功,已发放 License: %s", lic.LicenseKey), clientIP) case "refunded": if order.LicenseID > 0 { if err := tx.Model(&model.License{}).Where("id = ?", order.LicenseID). Updates(map[string]interface{}{ "status": model.LicenseStatusRevoked, "revoke_reason": "订单退款", }).Error; err != nil { return err } if err := tx.Model(&model.CustomerEntitlement{}).Where("license_id = ?", order.LicenseID). Update("status", "revoked").Error; err != nil { return err } } utils.LogSystemAction(tx, "payment_refunded", "order", order.ID, order.OrderNo, "订单退款,已吊销关联授权", clientIP) } return nil }) } // ManualGrant 手动发放 License(管理后台) // Logan.Li func (s *OrderService) ManualGrant(req dto.ManualGrantRequest, operatorID uint, operatorName, clientIP string) (map[string]interface{}, error) { // 查找或创建客户 // Logan.Li var customer model.Customer if err := s.db.Where("email = ?", req.CustomerEmail).First(&customer).Error; err != nil { name := req.CustomerName if name == "" { name = req.CustomerEmail } customer = model.Customer{Name: name, Email: req.CustomerEmail, Status: 1} if err := s.db.Create(&customer).Error; err != nil { return nil, errors.New("创建客户失败") } } var plan model.Plan if err := s.db.First(&plan, req.PlanID).Error; err != nil { return nil, errors.New("定价计划不存在") } now := time.Now() var endDate time.Time var validUntil *time.Time if plan.Duration == 0 { endDate = model.PermanentEndDate } else { endDate = now.AddDate(0, 0, plan.Duration) validUntil = &endDate } lic := model.License{ LicenseKey: utils.GenerateLicenseKey(), CustomerID: customer.ID, ProductID: req.ProductID, Status: model.LicenseStatusActive, StartDate: now, EndDate: endDate, Duration: plan.Duration, MaxActivations: plan.MaxDevices, Notes: req.Notes, } if err := s.db.Create(&lic).Error; err != nil { return nil, errors.New("创建 License 失败") } _ = customersvc.AttachProduct(s.db, customer.ID, req.ProductID) // 创建/更新权益记录 // Logan.Li renewable := plan.PlanType != "perpetual" && plan.PlanType != "lifetime" var ent model.CustomerEntitlement if err := s.db.Where("customer_id = ? AND product_id = ?", customer.ID, req.ProductID).First(&ent).Error; err != nil { ent = model.CustomerEntitlement{ CustomerID: customer.ID, ProductID: req.ProductID, PlanID: req.PlanID, LicenseID: lic.ID, LicenseKey: lic.LicenseKey, PlanType: plan.PlanType, Status: "active", ValidFrom: now, ValidUntil: validUntil, Renewable: renewable, } s.db.Create(&ent) } else { ent.PlanID = req.PlanID ent.LicenseID = lic.ID ent.LicenseKey = lic.LicenseKey ent.PlanType = plan.PlanType ent.Status = "active" ent.ValidFrom = now ent.ValidUntil = validUntil ent.Renewable = renewable s.db.Save(&ent) } utils.LogSystemAction(s.db, "manual_grant", "license", lic.ID, lic.LicenseKey, fmt.Sprintf("手动发放 License: %s 给客户 %s", lic.LicenseKey, req.CustomerEmail), clientIP) return map[string]interface{}{ "license_key": lic.LicenseKey, "license_id": lic.ID, "customer_id": customer.ID, }, nil } // CheckDuplicatePurchase 防重复购买检测 // Logan.Li func (s *OrderService) CheckDuplicatePurchase(customerID, productID uint, newPlanType string) dto.DuplicatePurchaseCheckResponse { var entitlements []model.CustomerEntitlement s.db.Where("customer_id = ? AND product_id = ?", customerID, productID).Find(&entitlements) if len(entitlements) == 0 { return dto.DuplicatePurchaseCheckResponse{AllowPurchase: true} } // 已有有效的永久/买断授权:禁止重复购买,但可升级 // Logan.Li for _, ent := range entitlements { if ent.Status != "active" { continue } switch ent.PlanType { case "perpetual", "lifetime": return dto.DuplicatePurchaseCheckResponse{ AllowPurchase: false, Reason: "您已拥有该产品的永久授权,无需重复购买", ExistingEntitlementID: ent.ID, ExistingLicenseKey: ent.LicenseKey, ExistingPlanType: ent.PlanType, CanUpgrade: true, } case "monthly", "yearly": // 订阅可续费 // Logan.Li if ent.Renewable && (newPlanType == "monthly" || newPlanType == "yearly") { return dto.DuplicatePurchaseCheckResponse{ AllowPurchase: true, Reason: "您有有效的订阅,将为您续费", ExistingEntitlementID: ent.ID, ExistingLicenseKey: ent.LicenseKey, ExistingPlanType: ent.PlanType, CanUpgrade: true, } } } } // 已过期但可续费 // Logan.Li for _, ent := range entitlements { if ent.Status == "expired" && ent.Renewable { return dto.DuplicatePurchaseCheckResponse{ AllowPurchase: true, Reason: "您的订阅已过期,可以续费恢复", ExistingEntitlementID: ent.ID, ExistingLicenseKey: ent.LicenseKey, ExistingPlanType: ent.PlanType, CanUpgrade: true, } } } return dto.DuplicatePurchaseCheckResponse{AllowPurchase: true} } // CheckPurchaseStatus 查询购买状态 // Logan.Li func (s *OrderService) CheckPurchaseStatus(customerEmail string, productID uint) (*dto.CheckPurchaseStatusResponse, error) { var customer model.Customer if err := s.db.Where("email = ?", customerEmail).First(&customer).Error; err != nil { return &dto.CheckPurchaseStatusResponse{ Purchased: false, HasActiveLicense: false, Message: "未找到购买记录", }, nil } var ent model.CustomerEntitlement if err := s.db.Where("customer_id = ? AND product_id = ?", customer.ID, productID). Preload("Plan").First(&ent).Error; err != nil { return &dto.CheckPurchaseStatusResponse{ Purchased: false, HasActiveLicense: false, Message: "未购买该产品", }, nil } daysLeft := 0 if ent.ValidUntil != nil { daysLeft = utils.CalculateDaysLeft(*ent.ValidUntil) } // 查询 License 设备数 // Logan.Li var lic model.License var currentDevices int64 licenseStatus := "" maxDevices := 0 if ent.LicenseID > 0 { s.db.First(&lic, ent.LicenseID) licenseStatus = lic.Status maxDevices = lic.MaxActivations s.db.Model(&model.Activation{}).Where("license_id = ?", lic.ID).Count(¤tDevices) } isActive := ent.Status == "active" && (ent.ValidUntil == nil || daysLeft > 0) var validUntilStr *string if ent.ValidUntil != nil { s := ent.ValidUntil.Format("2006-01-02") validUntilStr = &s } return &dto.CheckPurchaseStatusResponse{ Purchased: true, HasActiveLicense: isActive, LicenseKey: "", // 公开接口不回传完整密钥,请走 claim(order+email) // Logan.Li LicenseStatus: licenseStatus, PlanType: ent.PlanType, ValidUntil: validUntilStr, DaysLeft: daysLeft, Renewable: ent.Renewable, MaxDevices: maxDevices, CurrentDevices: int(currentDevices), EntitlementID: ent.ID, }, nil } // toOrderResponse 模型转响应 DTO // Logan.Li func toOrderResponse(o model.Order) dto.OrderResponse { resp := dto.OrderResponse{ ID: o.ID, OrderNo: o.OrderNo, CustomerID: o.CustomerID, ProductID: o.ProductID, PlanID: o.PlanID, LicenseID: o.LicenseID, Amount: o.Amount, Currency: o.Currency, Status: o.Status, PaymentGateway: o.PaymentGateway, CreatedAt: o.CreatedAt.Format("2006-01-02 15:04:05"), } if o.Customer.ID > 0 { resp.CustomerName = o.Customer.Name } if o.Product.ID > 0 { resp.ProductName = o.Product.Name } if o.Plan.ID > 0 { resp.PlanName = o.Plan.Name } if o.PaidAt != nil { resp.PaidAt = o.PaidAt.Format("2006-01-02 15:04:05") } return resp }