优化支付功能

This commit is contained in:
2026-09-16 14:28:07 +08:00
parent ac14452cae
commit 605c2a569f
19 changed files with 1020 additions and 479 deletions
-4
View File
@@ -7,7 +7,6 @@ import (
"server/models"
"server/pkg/jwtutil"
"server/services"
"server/services"
beego "github.com/beego/beego/v2/server/web"
)
@@ -59,8 +58,6 @@ type moduleWithEnabled struct {
// 返回当前 backend 账号可见的模块(status=1 且 is_show=1),并根据租户绑定的套餐标记每个模块是否已开通。
// 前端可据此排序(已开通在前)、置灰未开通卡片、阻止跳转。
func (c *BackendModulesController) GetTenantList() {
claims, err := c.backendModulesClaims()
if err != nil {
claims, err := c.backendModulesClaims()
if err != nil {
c.jsonErr(401, 401, err.Error())
@@ -68,7 +65,6 @@ func (c *BackendModulesController) GetTenantList() {
}
var rows []models.SystemModules
_, err = models.Orm.QueryTable(new(models.SystemModules)).
_, err = models.Orm.QueryTable(new(models.SystemModules)).
Filter("delete_time__isnull", true).
Filter("status", 1).
+215
View File
@@ -310,3 +310,218 @@ func (c *BackendProductPurchaseController) GetMyOrders() {
}
c.jsonOK(map[string]interface{}{"list": rows, "total": len(rows)}, "获取成功")
}
// ============================ 套餐购买 ============================
// GetPackageList GET /backend/package/list
// 租户端可购买套餐列表(仅启用的套餐)
func (c *BackendProductPurchaseController) GetPackageList() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var rows []models.SystemTenantPackage
if _, err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
Filter("delete_time__isnull", true).
Filter("status", 1).
OrderBy("sort", "id").
All(&rows); err != nil {
c.jsonErr(500, 500, "获取失败:"+err.Error())
return
}
ids := make([]uint64, 0, len(rows))
for _, r := range rows {
ids = append(ids, r.ID)
}
moduleMap := loadPackageModules(ids)
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
modules := moduleMap[r.ID]
if modules == nil {
modules = []tenantPackageModuleItem{}
}
list = append(list, map[string]interface{}{
"id": r.ID,
"name": r.Name,
"code": r.Code,
"description": r.Description,
"price": r.Price,
"duration_days": r.DurationDays,
"user_quota": r.UserQuota,
"extra_user_price": r.ExtraUserPrice,
"is_default": r.IsDefault,
"sort": r.Sort,
"status": r.Status,
"remark": r.Remark,
"modules": modules,
"module_codes": moduleCodes(modules),
"create_time": r.CreateTime,
"update_time": r.UpdateTime,
})
}
c.jsonOK(map[string]interface{}{"list": list, "total": len(list)}, "获取成功")
}
// GetPackageDetailByCode GET /backend/package/detail/:code
// 按套餐编码获取详情(租户端购买页使用)
func (c *BackendProductPurchaseController) GetPackageDetailByCode() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
code := strings.TrimSpace(c.Ctx.Input.Param(":code"))
if code == "" {
c.jsonErr(400, 400, "套餐编码不能为空")
return
}
var row models.SystemTenantPackage
if err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
Filter("code", code).
Filter("delete_time__isnull", true).
Filter("status", 1).
One(&row); err != nil {
c.jsonErr(404, 404, "套餐不存在或已下架")
return
}
modules := loadPackageModules([]uint64{row.ID})[row.ID]
if modules == nil {
modules = []tenantPackageModuleItem{}
}
c.jsonOK(map[string]interface{}{
"id": row.ID,
"name": row.Name,
"code": row.Code,
"description": row.Description,
"price": row.Price,
"duration_days": row.DurationDays,
"user_quota": row.UserQuota,
"extra_user_price": row.ExtraUserPrice,
"is_default": row.IsDefault,
"sort": row.Sort,
"status": row.Status,
"remark": row.Remark,
"modules": modules,
"module_codes": moduleCodes(modules),
"create_time": row.CreateTime,
"update_time": row.UpdateTime,
}, "获取成功")
}
type createPackageOrderPayload struct {
PackageCode string `json:"package_code"`
Channel string `json:"channel"`
PayType string `json:"pay_type"`
ReturnURL string `json:"return_url"`
}
// CreatePackageOrder POST /backend/package/order/create
// 创建套餐购买订单;channel 非空时同时创建支付单并返回拉起支付参数。
// 订单绑定 PackageID,支付成功后自动为租户开通对应套餐。
func (c *BackendProductPurchaseController) CreatePackageOrder() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
tid := uint64(claims.TenantId)
if tid == 0 {
c.jsonErr(401, 401, "未获取到租户信息")
return
}
body, _ := io.ReadAll(c.Ctx.Request.Body)
var p createPackageOrderPayload
if err := json.Unmarshal(body, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
p.PackageCode = strings.TrimSpace(p.PackageCode)
p.Channel = strings.TrimSpace(p.Channel)
if p.PackageCode == "" {
c.jsonErr(400, 400, "套餐不能为空")
return
}
var pkg models.SystemTenantPackage
if err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
Filter("code", p.PackageCode).
Filter("delete_time__isnull", true).
Filter("status", 1).
One(&pkg); err != nil {
c.jsonErr(404, 404, "套餐不存在或已下架")
return
}
if pkg.Price <= 0 {
c.jsonErr(400, 400, "该套餐未配置价格,请联系平台")
return
}
now := time.Now()
order := &models.PlatformProductOrder{
OrderNo: "PO" + strconv.FormatInt(now.UnixNano(), 10),
Tid: tid,
TenantName: c.tenantName(tid),
ProductID: 0, // 套餐购买不关联产品
ProductCode: pkg.Code,
ProductName: pkg.Name,
Amount: pkg.Price,
PackageID: pkg.ID,
Status: models.ProductOrderStatusPending,
UpdateTime: &now,
}
// 未选择支付渠道:仅落单,等待平台确认(线下转账/试用审批场景)
if p.Channel == "" {
if _, err := models.Orm.Insert(order); err != nil {
c.jsonErr(500, 500, "下单失败:"+err.Error())
return
}
c.jsonOK(map[string]interface{}{
"order_no": order.OrderNo,
"pay_no": "",
"amount": order.Amount,
"offline": true,
}, "下单成功,请等待平台开通")
return
}
amountFen := int64(order.Amount*100 + 0.5)
subject := pkg.Name
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
payOrder, params, err := payment.CreatePayment(ctx, payment.CreateInput{
OutTradeNo: order.OrderNo,
OrderType: models.CommissionOrderTypeModuleShop,
TenantID: strconv.FormatUint(tid, 10),
TenantName: order.TenantName,
Amount: amountFen,
Channel: p.Channel,
Subject: subject,
ReturnURL: strings.TrimSpace(p.ReturnURL),
ClientIP: c.Ctx.Input.IP(),
PayType: p.PayType,
})
if err != nil {
c.jsonErr(400, 400, "创建支付单失败:"+err.Error())
return
}
order.PayNo = payOrder.PayNo
order.Channel = p.Channel
if _, err := models.Orm.Insert(order); err != nil {
c.jsonErr(500, 500, "下单失败:"+err.Error())
return
}
c.jsonOK(map[string]interface{}{
"order_no": order.OrderNo,
"pay_no": payOrder.PayNo,
"amount": order.Amount,
"channel": p.Channel,
"pay_params": params,
"offline": false,
}, "下单成功")
}