完善购买和套餐功能
This commit is contained in:
@@ -325,7 +325,8 @@ func (c *BackendProductPurchaseController) GetMyOrders() {
|
||||
// GetPackageList GET /backend/package/list
|
||||
// 租户端可购买套餐列表(仅启用的套餐)
|
||||
func (c *BackendProductPurchaseController) GetPackageList() {
|
||||
if _, err := c.claims(); err != nil {
|
||||
claims, err := c.claims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -345,6 +346,22 @@ func (c *BackendProductPurchaseController) GetPackageList() {
|
||||
}
|
||||
moduleMap := loadPackageModules(ids)
|
||||
|
||||
// 该租户已购买过(已支付/已开通)的套餐:用于前端对「仅可购买一次」的体验套餐禁用购买按钮
|
||||
purchased := map[uint64]bool{}
|
||||
if tid := uint64(claims.TenantId); tid > 0 && len(ids) > 0 {
|
||||
var orders []models.PlatformProductOrder
|
||||
if _, err := models.Orm.QueryTable(new(models.PlatformProductOrder)).
|
||||
Filter("tid", tid).
|
||||
Filter("package_id__in", ids).
|
||||
Filter("status__in", models.ProductOrderStatusPaid, models.ProductOrderStatusActivated).
|
||||
Filter("delete_time__isnull", true).
|
||||
All(&orders, "ID", "PackageID"); err == nil {
|
||||
for _, o := range orders {
|
||||
purchased[o.PackageID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
modules := moduleMap[r.ID]
|
||||
@@ -361,6 +378,8 @@ func (c *BackendProductPurchaseController) GetPackageList() {
|
||||
"user_quota": r.UserQuota,
|
||||
"extra_user_price": r.ExtraUserPrice,
|
||||
"is_default": r.IsDefault,
|
||||
"allow_repurchase": r.AllowRepurchase,
|
||||
"purchased": purchased[r.ID],
|
||||
"sort": r.Sort,
|
||||
"status": r.Status,
|
||||
"remark": r.Remark,
|
||||
@@ -466,6 +485,19 @@ func (c *BackendProductPurchaseController) CreatePackageOrder() {
|
||||
c.jsonErr(400, 400, "该套餐未配置价格,请联系平台")
|
||||
return
|
||||
}
|
||||
// 体验套餐(allow_repurchase=0):每个租户仅可购买一次
|
||||
if pkg.AllowRepurchase == 0 {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.PlatformProductOrder)).
|
||||
Filter("tid", tid).
|
||||
Filter("package_id", pkg.ID).
|
||||
Filter("status__in", models.ProductOrderStatusPaid, models.ProductOrderStatusActivated).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if cnt > 0 {
|
||||
c.jsonErr(400, 400, "该套餐为体验套餐,每个租户仅可购买一次")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
order := &models.PlatformProductOrder{
|
||||
|
||||
@@ -403,8 +403,8 @@ func (c *PlatformProductController) ActivateProductOrder() {
|
||||
c.jsonErr(404, 404, "订单不存在")
|
||||
return
|
||||
}
|
||||
if order.Status == models.ProductOrderStatusActivated {
|
||||
c.jsonErr(400, 400, "订单已开通")
|
||||
if order.Status == models.ProductOrderStatusActivated && !services.ProductOrderExpired(&order, time.Now()) {
|
||||
c.jsonErr(400, 400, "订单已开通且在有效期内")
|
||||
return
|
||||
}
|
||||
if err := activateProductOrder(&order, "平台手动开通"); err != nil {
|
||||
@@ -414,18 +414,73 @@ func (c *PlatformProductController) ActivateProductOrder() {
|
||||
c.jsonOK(nil, "开通成功")
|
||||
}
|
||||
|
||||
// activateProductOrder 订单开通:绑定套餐(产品配置了套餐时)并置为已开通
|
||||
// CloseProductOrder POST /platform/product/order/close/:id
|
||||
// 关闭已开通的单品功能:订单置为已关闭,租户端对应模块权益即时回收(菜单/卡片恢复锁定)。
|
||||
// 仅允许关闭单品订单(package_id=0);误操作可在购买订单列表重新「手动开通」。
|
||||
func (c *PlatformProductController) CloseProductOrder() {
|
||||
id := parseUint64Param(c.Ctx.Input.Param(":id"))
|
||||
if id == 0 {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var order models.PlatformProductOrder
|
||||
if err := models.Orm.QueryTable(new(models.PlatformProductOrder)).
|
||||
Filter("id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&order); err != nil {
|
||||
c.jsonErr(404, 404, "订单不存在")
|
||||
return
|
||||
}
|
||||
if order.PackageID > 0 {
|
||||
c.jsonErr(400, 400, "套餐订单请通过「套餐与用户数」调整套餐")
|
||||
return
|
||||
}
|
||||
if order.Status != models.ProductOrderStatusActivated {
|
||||
c.jsonErr(400, 400, "仅已开通的订单可关闭功能")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
order.Status = models.ProductOrderStatusClosed
|
||||
order.UpdateTime = &now
|
||||
order.Remark = strings.TrimSpace(order.Remark + " 平台关闭功能")
|
||||
if _, err := models.Orm.Update(&order, "Status", "Remark", "UpdateTime"); err != nil {
|
||||
c.jsonErr(500, 500, "关闭失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
c.jsonOK(nil, "功能已关闭")
|
||||
}
|
||||
|
||||
// activateProductOrder 订单开通:绑定套餐(产品配置了套餐时)并置为已开通。
|
||||
// 单品功能默认 1 年有效期(ProductOrderDurationDays),到期后租户端权益自动失效,需续费。
|
||||
func activateProductOrder(order *models.PlatformProductOrder, operator string) error {
|
||||
now := time.Now()
|
||||
if order.PackageID > 0 && order.Tid > 0 {
|
||||
// 套餐订单:绑定/续费套餐,有效期由套餐自身到期时间管理
|
||||
if _, err := services.ApplyPackageToTenant(order.Tid, order.PackageID, true); err != nil {
|
||||
return fmt.Errorf("开通失败:%v", err)
|
||||
}
|
||||
} else if order.ProductID > 0 {
|
||||
// 单品功能:有效期精确到日(自然日 0 点),到期日当天 0 点起即失效;
|
||||
// 未到期续费从原到期日顺延 1 年,已过期重新开通从开通当日 0 点起算 1 年
|
||||
base := services.DayStart(now)
|
||||
if cur := services.ProductOrderExpireAt(order); cur != nil && cur.After(base) {
|
||||
base = *cur
|
||||
}
|
||||
expire := base.AddDate(0, 0, models.ProductOrderDurationDays)
|
||||
order.ExpireTime = &expire
|
||||
}
|
||||
now := time.Now()
|
||||
order.Status = models.ProductOrderStatusActivated
|
||||
order.ActivateTime = &now
|
||||
if order.ActivateTime == nil {
|
||||
// 开通时间精确到日(自然日 0 点),与有效期的计算粒度保持一致
|
||||
day := services.DayStart(now)
|
||||
order.ActivateTime = &day
|
||||
}
|
||||
order.UpdateTime = &now
|
||||
order.Remark = strings.TrimSpace(order.Remark + " " + operator)
|
||||
_, err := models.Orm.Update(order, "Status", "ActivateTime", "Remark", "UpdateTime")
|
||||
fields := []string{"Status", "ActivateTime", "Remark", "UpdateTime"}
|
||||
if order.ExpireTime != nil {
|
||||
fields = append(fields, "ExpireTime")
|
||||
}
|
||||
_, err := models.Orm.Update(order, fields...)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -103,10 +103,12 @@ type tenantPackagePayload struct {
|
||||
UserQuota *int `json:"user_quota"`
|
||||
ExtraUserPrice *float64 `json:"extra_user_price"`
|
||||
IsDefault *int8 `json:"is_default"`
|
||||
Sort *int `json:"sort"`
|
||||
Status *int8 `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
Modules []string `json:"modules"`
|
||||
// AllowRepurchase 是否允许重复购买:1允许(默认) 0仅可购买一次(体验套餐)
|
||||
AllowRepurchase *int8 `json:"allow_repurchase"`
|
||||
Sort *int `json:"sort"`
|
||||
Status *int8 `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
Modules []string `json:"modules"`
|
||||
}
|
||||
|
||||
// normalizePackageDuration 套餐有效时长(天)规范化:<=0 时按 365(年付)
|
||||
@@ -193,6 +195,7 @@ func (c *PlatformTenantPackageController) GetPackageList() {
|
||||
"user_quota": r.UserQuota,
|
||||
"extra_user_price": r.ExtraUserPrice,
|
||||
"is_default": r.IsDefault,
|
||||
"allow_repurchase": r.AllowRepurchase,
|
||||
"sort": r.Sort,
|
||||
"status": r.Status,
|
||||
"remark": r.Remark,
|
||||
@@ -249,6 +252,7 @@ func (c *PlatformTenantPackageController) GetPackageSelectList() {
|
||||
"user_quota": r.UserQuota,
|
||||
"extra_user_price": r.ExtraUserPrice,
|
||||
"is_default": r.IsDefault,
|
||||
"allow_repurchase": r.AllowRepurchase,
|
||||
"modules": modules,
|
||||
"module_codes": moduleCodes(modules),
|
||||
})
|
||||
@@ -393,22 +397,27 @@ func (c *PlatformTenantPackageController) CreatePackage() {
|
||||
|
||||
now := time.Now()
|
||||
row := &models.SystemTenantPackage{
|
||||
Name: p.Name,
|
||||
Code: p.Code,
|
||||
Description: strings.TrimSpace(p.Description),
|
||||
Price: valueFloat(p.Price, 0),
|
||||
DurationDays: normalizePackageDuration(p.DurationDays),
|
||||
UserQuota: valueInt(p.UserQuota, services.DefaultTenantUserQuota),
|
||||
ExtraUserPrice: valueFloat(p.ExtraUserPrice, services.DefaultTenantExtraUserPrice),
|
||||
IsDefault: valueInt8(p.IsDefault, 0),
|
||||
Sort: valueInt(p.Sort, 0),
|
||||
Status: valueInt8(p.Status, 1),
|
||||
Remark: strings.TrimSpace(p.Remark),
|
||||
UpdateTime: &now,
|
||||
Name: p.Name,
|
||||
Code: p.Code,
|
||||
Description: strings.TrimSpace(p.Description),
|
||||
Price: valueFloat(p.Price, 0),
|
||||
DurationDays: normalizePackageDuration(p.DurationDays),
|
||||
UserQuota: valueInt(p.UserQuota, services.DefaultTenantUserQuota),
|
||||
ExtraUserPrice: valueFloat(p.ExtraUserPrice, services.DefaultTenantExtraUserPrice),
|
||||
IsDefault: valueInt8(p.IsDefault, 0),
|
||||
AllowRepurchase: valueInt8(p.AllowRepurchase, 1),
|
||||
Sort: valueInt(p.Sort, 0),
|
||||
Status: valueInt8(p.Status, 1),
|
||||
Remark: strings.TrimSpace(p.Remark),
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if row.UserQuota <= 0 {
|
||||
row.UserQuota = services.DefaultTenantUserQuota
|
||||
}
|
||||
// 体验套餐(不允许重复购买)不涉及用户数增购,增购价恒为 0
|
||||
if row.AllowRepurchase == 0 {
|
||||
row.ExtraUserPrice = 0
|
||||
}
|
||||
id, err := models.Orm.Insert(row)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "创建失败:"+err.Error())
|
||||
@@ -483,6 +492,26 @@ func (c *PlatformTenantPackageController) EditPackage() {
|
||||
update["is_default"] = *p.IsDefault
|
||||
update["status"] = 1
|
||||
}
|
||||
if p.AllowRepurchase != nil {
|
||||
update["allow_repurchase"] = *p.AllowRepurchase
|
||||
}
|
||||
|
||||
// 体验套餐(不允许重复购买)不涉及用户数增购,增购价恒为 0
|
||||
allowRepurchase := int8(1)
|
||||
if p.AllowRepurchase != nil {
|
||||
allowRepurchase = *p.AllowRepurchase
|
||||
} else {
|
||||
var cur models.SystemTenantPackage
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||
Filter("id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&cur); err == nil {
|
||||
allowRepurchase = cur.AllowRepurchase
|
||||
}
|
||||
}
|
||||
if allowRepurchase == 0 {
|
||||
update["extra_user_price"] = 0
|
||||
}
|
||||
|
||||
if _, err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||
Filter("id", id).
|
||||
@@ -512,12 +541,21 @@ func clearOtherDefaultPackages(keepID uint64) {
|
||||
}
|
||||
|
||||
// DeletePackage DELETE /platform/tenantPackage/delete/:id(软删)
|
||||
// 注意:基础套餐(code=basic)为系统内置兜底套餐(套餐到期自动降级目标),任何情况下不允许删除。
|
||||
func (c *PlatformTenantPackageController) DeletePackage() {
|
||||
id := parseUint64Param(c.Ctx.Input.Param(":id"))
|
||||
if id == 0 {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var pkg models.SystemTenantPackage
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||
Filter("id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&pkg); err == nil && strings.EqualFold(strings.TrimSpace(pkg.Code), "basic") {
|
||||
c.jsonErr(400, 400, "基础套餐为系统内置套餐,不允许删除")
|
||||
return
|
||||
}
|
||||
// 有租户正在使用该套餐时不允许删除
|
||||
used, _ := models.Orm.QueryTable(new(models.SystemTenant)).Filter("package_id", id).Count()
|
||||
if used > 0 {
|
||||
@@ -744,23 +782,128 @@ func (c *PlatformTenantPackageController) GetTenantQuotaInfo() {
|
||||
moduleList = append(moduleList, tenantPackageModuleItem{ModuleCode: m.ModuleCode, ModuleName: m.ModuleName})
|
||||
}
|
||||
|
||||
// 已开通单品功能:单品订单不改变租户套餐,需与套餐模块并集展示,避免“买了功能看不到”
|
||||
productOrders, productModules := loadTenantProductEntitlements(tid)
|
||||
|
||||
c.jsonOK(map[string]interface{}{
|
||||
"tid": info.Tid,
|
||||
"package_id": info.PackageID,
|
||||
"tid": info.Tid,
|
||||
"package_id": info.PackageID,
|
||||
"effective_package_id": info.EffectivePackageID,
|
||||
"package_name": info.PackageName,
|
||||
"modules": moduleList,
|
||||
"quota": info.Quota,
|
||||
"used": info.Used,
|
||||
"remaining": info.Remaining,
|
||||
"extra_user_price": info.ExtraUserPrice,
|
||||
"duration_days": info.DurationDays,
|
||||
"package_expire_time": info.PackageExpireTime,
|
||||
"days_remaining": info.DaysRemaining,
|
||||
"quota_packages": quotaPackages,
|
||||
"package_name": info.PackageName,
|
||||
"modules": moduleList,
|
||||
"product_modules": productModules,
|
||||
"product_orders": productOrders,
|
||||
"quota": info.Quota,
|
||||
"used": info.Used,
|
||||
"remaining": info.Remaining,
|
||||
"extra_user_price": info.ExtraUserPrice,
|
||||
"duration_days": info.DurationDays,
|
||||
"package_expire_time": info.PackageExpireTime,
|
||||
"days_remaining": info.DaysRemaining,
|
||||
"quota_packages": quotaPackages,
|
||||
}, "获取成功")
|
||||
}
|
||||
|
||||
// productEntitlement 已开通单品功能明细(租户详情「套餐与用户数」展示 / 关闭用)
|
||||
type productEntitlement struct {
|
||||
OrderID uint64 `json:"order_id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
ProductID uint64 `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
ModuleCode string `json:"module_code"`
|
||||
ModuleName string `json:"module_name"`
|
||||
Amount float64 `json:"amount"`
|
||||
ActivateTime *time.Time `json:"activate_time"`
|
||||
ExpireTime *time.Time `json:"expire_time"`
|
||||
// Expired 是否已过有效期:到期后租户端权益自动失效,需续费
|
||||
Expired bool `json:"expired"`
|
||||
}
|
||||
|
||||
// loadTenantProductEntitlements 加载租户已开通的单品功能:
|
||||
// - orders 订单明细(按开通时间倒序),供面板展示与「关闭功能」操作;
|
||||
// - modules 去重后的模块列表,与套餐模块并集构成租户完整功能权益。
|
||||
func loadTenantProductEntitlements(tid uint64) ([]productEntitlement, []tenantPackageModuleItem) {
|
||||
orders := make([]productEntitlement, 0)
|
||||
modules := make([]tenantPackageModuleItem, 0)
|
||||
if tid == 0 {
|
||||
return orders, modules
|
||||
}
|
||||
|
||||
var rows []models.PlatformProductOrder
|
||||
if _, err := models.Orm.QueryTable(new(models.PlatformProductOrder)).
|
||||
Filter("tid", tid).
|
||||
Filter("status", models.ProductOrderStatusActivated).
|
||||
Filter("product_id__gt", 0).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").
|
||||
All(&rows); err != nil || len(rows) == 0 {
|
||||
return orders, modules
|
||||
}
|
||||
|
||||
productIDs := make([]uint64, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
productIDs = append(productIDs, r.ProductID)
|
||||
}
|
||||
var products []models.PlatformProduct
|
||||
_, _ = models.Orm.QueryTable(new(models.PlatformProduct)).
|
||||
Filter("id__in", productIDs).
|
||||
All(&products)
|
||||
productMap := make(map[uint64]models.PlatformProduct, len(products))
|
||||
for _, p := range products {
|
||||
productMap[p.ID] = p
|
||||
}
|
||||
|
||||
// 模块 code -> 名称(大小写/空格不敏感,与租户端权益判定保持一致)
|
||||
var moduleRows []models.SystemModules
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemModules)).
|
||||
Filter("delete_time__isnull", true).
|
||||
All(&moduleRows)
|
||||
moduleNameMap := make(map[string]string, len(moduleRows))
|
||||
for _, m := range moduleRows {
|
||||
moduleNameMap[services.NormalizeModuleCode(m.Code)] = m.Name
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
seen := map[string]bool{}
|
||||
for _, r := range rows {
|
||||
code, name := "", ""
|
||||
if p, ok := productMap[r.ProductID]; ok {
|
||||
code = strings.TrimSpace(p.ModuleCode)
|
||||
if code == "" {
|
||||
// 产品未显式关联模块时按产品编码兜底(与 GetTenantModuleCodes 一致)
|
||||
code = strings.TrimSpace(p.Code)
|
||||
}
|
||||
name = p.Name
|
||||
}
|
||||
normCode := services.NormalizeModuleCode(code)
|
||||
expire := services.ProductOrderExpireAt(&r)
|
||||
expired := services.ProductOrderExpired(&r, now)
|
||||
// 权益模块只统计仍在有效期内的订单,与租户端 GetTenantModuleCodes 判定保持一致
|
||||
if !expired && normCode != "" && !seen[normCode] {
|
||||
seen[normCode] = true
|
||||
moduleName := moduleNameMap[normCode]
|
||||
if moduleName == "" {
|
||||
moduleName = name
|
||||
}
|
||||
modules = append(modules, tenantPackageModuleItem{ModuleCode: code, ModuleName: moduleName})
|
||||
}
|
||||
// 明细保留全部已开通订单(含已过期):便于平台看到「已过期,待续费并关闭」
|
||||
orders = append(orders, productEntitlement{
|
||||
OrderID: r.ID,
|
||||
OrderNo: r.OrderNo,
|
||||
ProductID: r.ProductID,
|
||||
ProductName: name,
|
||||
ModuleCode: code,
|
||||
ModuleName: moduleNameMap[normCode],
|
||||
Amount: r.Amount,
|
||||
ActivateTime: r.ActivateTime,
|
||||
ExpireTime: expire,
|
||||
Expired: expired,
|
||||
})
|
||||
}
|
||||
return orders, modules
|
||||
}
|
||||
|
||||
type rechargeQuotaPayload struct {
|
||||
Tid uint64 `json:"tid"`
|
||||
Type int8 `json:"type"` // 1 单个增购 2 套餐增购
|
||||
|
||||
Reference in New Issue
Block a user