Files
license-manager/backend/internal/api/handlers/public_renewal_handler.go
T
leeexin eb1f68b423
Deploy Backend via Git / deploy (push) Failing after 1m0s
Deploy Frontend via Git / build-and-deploy (push) Failing after 1m0s
feat: add public purchase portal and admin tools
2026-03-28 17:55:03 +08:00

583 lines
17 KiB
Go

package handlers
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"license-manager/internal/models"
"license-manager/internal/repository"
"license-manager/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type PublicRenewalHandler struct {
licenseRepo repository.LicenseRepository
customerRepo repository.CustomerRepository
cuUserRepo repository.CuUserRepository
packageService service.PackageService
cuOrderService service.CuOrderService
paymentService service.PaymentService
}
func NewPublicRenewalHandler(
licenseRepo repository.LicenseRepository,
customerRepo repository.CustomerRepository,
cuUserRepo repository.CuUserRepository,
packageService service.PackageService,
cuOrderService service.CuOrderService,
paymentService service.PaymentService,
) *PublicRenewalHandler {
return &PublicRenewalHandler{
licenseRepo: licenseRepo,
customerRepo: customerRepo,
cuUserRepo: cuUserRepo,
packageService: packageService,
cuOrderService: cuOrderService,
paymentService: paymentService,
}
}
type publicAuthCodeQueryRequest struct {
AuthorizationCode string `json:"authorization_code" binding:"required"`
}
type publicRenewalOrderRequest struct {
AuthorizationCode string `json:"authorization_code" binding:"required"`
PackageID string `json:"package_id" binding:"required"`
LicenseCount int `json:"license_count" binding:"required,min=1,max=1000"`
RenewDays int `json:"renew_days" binding:"required,min=1,max=3650"`
PaymentMethod string `json:"payment_method"`
ContactName string `json:"contact_name" binding:"required,max=100"`
ContactPhone string `json:"contact_phone" binding:"required,min=7,max=20"`
ContactEmail string `json:"contact_email" binding:"omitempty,email,max=255"`
CompanyName string `json:"company_name" binding:"omitempty,max=200"`
}
type publicPurchaseOrderRequest struct {
PackageID string `json:"package_id" binding:"required"`
LicenseCount int `json:"license_count" binding:"required,min=1,max=1000"`
PaymentMethod string `json:"payment_method"`
ContactName string `json:"contact_name" binding:"required,max=100"`
ContactPhone string `json:"contact_phone" binding:"required,min=7,max=20"`
ContactEmail string `json:"contact_email" binding:"omitempty,email,max=255"`
CompanyName string `json:"company_name" binding:"omitempty,max=200"`
CustomerType string `json:"customer_type" binding:"omitempty,oneof=individual enterprise government education"`
Description string `json:"description" binding:"omitempty,max=500"`
}
// QueryAuthorizationCode 公开查询授权码信息
func (h *PublicRenewalHandler) QueryAuthorizationCode(c *gin.Context) {
var req publicAuthCodeQueryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: "900001",
Message: "请求参数错误",
Timestamp: getCurrentTimestamp(),
})
return
}
code := strings.TrimSpace(req.AuthorizationCode)
authCode, err := h.licenseRepo.GetAuthorizationCodeByCode(c.Request.Context(), code)
if err != nil {
c.JSON(http.StatusNotFound, models.ErrorResponse{
Code: "300001",
Message: "授权码不存在",
Timestamp: getCurrentTimestamp(),
})
return
}
customerName := ""
contactPerson := ""
contactPhone := ""
contactEmail := ""
customer, err := h.customerRepo.GetCustomerByID(c.Request.Context(), authCode.CustomerID)
if err == nil {
customerName = customer.CustomerName
contactPerson = customer.ContactPerson
if customer.Phone != nil {
contactPhone = *customer.Phone
}
if customer.Email != nil {
contactEmail = *customer.Email
}
}
activeCount, _ := h.licenseRepo.GetActiveLicenseCount(c.Request.Context(), authCode.ID)
status := "normal"
now := time.Now()
if authCode.IsLocked {
status = "locked"
} else if now.After(authCode.EndDate) {
status = "expired"
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: "success",
Data: gin.H{
"authorization_code_id": authCode.ID,
"authorization_code": authCode.Code,
"customer_id": authCode.CustomerID,
"customer_name": customerName,
"contact_person": contactPerson,
"contact_phone": contactPhone,
"contact_email": contactEmail,
"status": status,
"is_locked": authCode.IsLocked,
"start_date": authCode.StartDate.Format(time.RFC3339),
"end_date": authCode.EndDate.Format(time.RFC3339),
"max_activations": authCode.MaxActivations,
"active_licenses": activeCount,
},
Timestamp: getCurrentTimestamp(),
})
}
// CreateRenewalOrder 公开创建续期支付单
func (h *PublicRenewalHandler) CreateRenewalOrder(c *gin.Context) {
var req publicRenewalOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: "900001",
Message: "请求参数错误",
Timestamp: getCurrentTimestamp(),
})
return
}
authCode, err := h.licenseRepo.GetAuthorizationCodeByCode(c.Request.Context(), strings.TrimSpace(req.AuthorizationCode))
if err != nil {
c.JSON(http.StatusNotFound, models.ErrorResponse{
Code: "300001",
Message: "授权码不存在",
Timestamp: getCurrentTimestamp(),
})
return
}
if authCode.IsLocked {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: "300003",
Message: "授权码已锁定,无法续期",
Timestamp: getCurrentTimestamp(),
})
return
}
_, err = h.packageService.GetPackageDetail(c.Request.Context(), req.PackageID)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: "800002",
Message: "套餐不存在",
Timestamp: getCurrentTimestamp(),
})
return
}
priceResult, err := h.cuOrderService.CalculatePrice(c.Request.Context(), req.PackageID, req.LicenseCount)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: "601004",
Message: "价格计算失败",
Timestamp: getCurrentTimestamp(),
})
return
}
customer, err := h.customerRepo.GetCustomerByID(c.Request.Context(), authCode.CustomerID)
if err == nil {
if strings.TrimSpace(req.CompanyName) != "" {
customer.CustomerName = strings.TrimSpace(req.CompanyName)
}
customer.ContactPerson = strings.TrimSpace(req.ContactName)
phone := strings.TrimSpace(req.ContactPhone)
if phone != "" {
customer.Phone = &phone
}
email := strings.TrimSpace(req.ContactEmail)
if email != "" {
customer.Email = &email
}
_ = h.customerRepo.UpdateCustomer(c.Request.Context(), customer)
}
cuUserID, err := h.ensureCuUser(c.Request.Context(), authCode.CustomerID, req.ContactPhone, req.ContactName, req.ContactEmail)
if err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Code: "900004",
Message: "创建客户用户失败",
Timestamp: getCurrentTimestamp(),
})
return
}
renewalExtra := map[string]interface{}{
"authorization_code_id": authCode.ID,
"renew_days": req.RenewDays,
}
extraDataBytes, _ := json.Marshal(renewalExtra)
extraData := string(extraDataBytes)
paymentMethod := strings.TrimSpace(req.PaymentMethod)
if paymentMethod == "" {
paymentMethod = models.PaymentMethodAlipay
}
businessID := authCode.ID
payment, err := h.paymentService.CreatePayment(c.Request.Context(), &models.PaymentCreateRequest{
BusinessType: models.BusinessTypeLicenseExtend,
BusinessID: &businessID,
CustomerID: authCode.CustomerID,
CuUserID: cuUserID,
Amount: priceResult.TotalAmount,
Currency: "CNY",
PaymentMethod: paymentMethod,
ExtraData: &extraData,
})
if err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Code: "602001",
Message: "创建支付单失败",
Timestamp: getCurrentTimestamp(),
})
return
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: "success",
Data: gin.H{
"payment_no": payment.PaymentNo,
"payment_url": payment.PaymentURL,
"total_amount": payment.Amount,
"expire_time": payment.ExpireTime.Format(time.RFC3339),
"renew_days": req.RenewDays,
},
Timestamp: getCurrentTimestamp(),
})
}
// CreatePurchaseOrder 公开创建新购支付单
func (h *PublicRenewalHandler) CreatePurchaseOrder(c *gin.Context) {
var req publicPurchaseOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: "900001",
Message: "请求参数错误",
Timestamp: getCurrentTimestamp(),
})
return
}
pkg, err := h.packageService.GetPackageDetail(c.Request.Context(), req.PackageID)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: "800002",
Message: "套餐不存在",
Timestamp: getCurrentTimestamp(),
})
return
}
priceResult, err := h.cuOrderService.CalculatePrice(c.Request.Context(), req.PackageID, req.LicenseCount)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: "601004",
Message: "价格计算失败",
Timestamp: getCurrentTimestamp(),
})
return
}
customerID, cuUserID, err := h.ensureCustomerAndUser(
c.Request.Context(),
strings.TrimSpace(req.CompanyName),
strings.TrimSpace(req.ContactName),
strings.TrimSpace(req.ContactPhone),
strings.TrimSpace(req.ContactEmail),
strings.TrimSpace(req.CustomerType),
strings.TrimSpace(req.Description),
)
if err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Code: "900004",
Message: "客户信息写入失败",
Timestamp: getCurrentTimestamp(),
})
return
}
orderReq := &models.CuOrderCreateRequest{
PackageID: req.PackageID,
LicenseCount: req.LicenseCount,
PaymentMethod: strings.TrimSpace(req.PaymentMethod),
}
if orderReq.PaymentMethod == "" {
orderReq.PaymentMethod = models.PaymentMethodAlipay
}
if priceResult.TotalAmount == 0 {
order, createErr := h.cuOrderService.CreateOrder(c.Request.Context(), cuUserID, customerID, orderReq)
if createErr != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: "601001",
Message: "订单创建失败",
Timestamp: getCurrentTimestamp(),
})
return
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: "success",
Data: gin.H{
"order_id": order.ID,
"order_no": order.OrderNo,
"package_name": pkg.Name,
"total_amount": 0,
"license_count": req.LicenseCount,
"authorization_code": order.AuthorizationCode,
},
Timestamp: getCurrentTimestamp(),
})
return
}
order, err := h.cuOrderService.CreatePendingOrder(c.Request.Context(), cuUserID, customerID, orderReq)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: "601001",
Message: "订单创建失败",
Timestamp: getCurrentTimestamp(),
})
return
}
payment, err := h.paymentService.CreatePayment(c.Request.Context(), &models.PaymentCreateRequest{
BusinessType: models.BusinessTypePackageOrder,
BusinessID: &order.ID,
CustomerID: customerID,
CuUserID: cuUserID,
Amount: priceResult.TotalAmount,
Currency: "CNY",
PaymentMethod: orderReq.PaymentMethod,
})
if err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Code: "602001",
Message: "支付单创建失败",
Timestamp: getCurrentTimestamp(),
})
return
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: "success",
Data: gin.H{
"order_id": order.ID,
"order_no": order.OrderNo,
"package_name": pkg.Name,
"payment_no": payment.PaymentNo,
"payment_url": payment.PaymentURL,
"total_amount": payment.Amount,
"expire_time": payment.ExpireTime.Format(time.RFC3339),
"license_count": req.LicenseCount,
},
Timestamp: getCurrentTimestamp(),
})
}
// GetPublicPaymentStatus 公开支付状态查询
func (h *PublicRenewalHandler) GetPublicPaymentStatus(c *gin.Context) {
paymentNo := strings.TrimSpace(c.Param("payment_no"))
if paymentNo == "" {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: "900001",
Message: "支付单号不能为空",
Timestamp: getCurrentTimestamp(),
})
return
}
statusResp, err := h.paymentService.GetPaymentStatus(c.Request.Context(), paymentNo)
if err != nil {
c.JSON(http.StatusNotFound, models.ErrorResponse{
Code: "602002",
Message: "支付单不存在",
Timestamp: getCurrentTimestamp(),
})
return
}
payment, err := h.paymentService.GetPayment(c.Request.Context(), paymentNo)
if err != nil {
c.JSON(http.StatusNotFound, models.ErrorResponse{
Code: "602002",
Message: "支付单不存在",
Timestamp: getCurrentTimestamp(),
})
return
}
authCodeID := ""
endDate := ""
if payment.ExtraData != nil && strings.TrimSpace(*payment.ExtraData) != "" {
var renewalData struct {
AuthorizationCodeID string `json:"authorization_code_id"`
}
if err := json.Unmarshal([]byte(*payment.ExtraData), &renewalData); err == nil {
authCodeID = renewalData.AuthorizationCodeID
if strings.TrimSpace(authCodeID) != "" {
authCode, err := h.licenseRepo.GetAuthorizationCodeByID(c.Request.Context(), authCodeID)
if err == nil {
endDate = authCode.EndDate.Format(time.RFC3339)
}
}
}
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: "success",
Data: gin.H{
"payment_no": statusResp.PaymentNo,
"status": statusResp.Status,
"amount": statusResp.Amount,
"payment_time": statusResp.PaymentTime,
"trade_no": statusResp.TradeNo,
"authorization_code_id": authCodeID,
"authorization_end_date": endDate,
"business_order": statusResp.BusinessOrder,
},
Timestamp: getCurrentTimestamp(),
})
}
func (h *PublicRenewalHandler) ensureCuUser(ctx context.Context, customerID, phone, contactName, contactEmail string) (string, error) {
countryCode := "+86"
trimmedPhone := strings.TrimSpace(phone)
user, err := h.cuUserRepo.GetByPhone(trimmedPhone, countryCode)
if err == nil {
return user.ID, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return "", err
}
var realName *string
name := strings.TrimSpace(contactName)
if name != "" {
realName = &name
}
var email *string
trimmedEmail := strings.TrimSpace(contactEmail)
if trimmedEmail != "" {
email = &trimmedEmail
}
newUser := &models.CuUser{
CustomerID: customerID,
Phone: trimmedPhone,
PhoneCountryCode: countryCode,
UserRole: "member",
RealName: realName,
Email: email,
Status: "active",
PhoneVerified: true,
EmailVerified: false,
Language: "zh-CN",
Timezone: "Asia/Shanghai",
}
if err := h.cuUserRepo.Create(newUser); err != nil {
return "", err
}
return newUser.ID, nil
}
func (h *PublicRenewalHandler) ensureCustomerAndUser(
ctx context.Context,
companyName, contactName, phone, email, customerType, description string,
) (string, string, error) {
countryCode := "+86"
trimmedPhone := strings.TrimSpace(phone)
existingUser, err := h.cuUserRepo.GetByPhone(trimmedPhone, countryCode)
if err == nil {
customer, customerErr := h.customerRepo.GetCustomerByID(ctx, existingUser.CustomerID)
if customerErr == nil {
if companyName != "" {
customer.CustomerName = companyName
}
if contactName != "" {
customer.ContactPerson = contactName
}
if trimmedPhone != "" {
customer.Phone = &trimmedPhone
}
if email != "" {
customer.Email = &email
}
if description != "" {
customer.Description = &description
}
_ = h.customerRepo.UpdateCustomer(ctx, customer)
}
return existingUser.CustomerID, existingUser.ID, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return "", "", err
}
ct := customerType
if ct == "" {
ct = "enterprise"
}
customerName := companyName
if customerName == "" {
customerName = contactName
}
if customerName == "" {
customerName = trimmedPhone
}
contactPerson := contactName
if contactPerson == "" {
contactPerson = trimmedPhone
}
customer := &models.Customer{
CustomerName: customerName,
CustomerType: ct,
ContactPerson: contactPerson,
CustomerLevel: "basic",
Status: "active",
CreatedBy: "public-order",
}
if trimmedPhone != "" {
customer.Phone = &trimmedPhone
}
if email != "" {
customer.Email = &email
}
if description != "" {
customer.Description = &description
}
if err := h.customerRepo.CreateCustomer(ctx, customer); err != nil {
return "", "", err
}
cuUserID, err := h.ensureCuUser(ctx, customer.ID, trimmedPhone, contactName, email)
if err != nil {
return "", "", err
}
return customer.ID, cuUserID, nil
}