完善购买和套餐功能

This commit is contained in:
2026-09-16 17:26:02 +08:00
parent 898dedd75a
commit b48688cbab
19 changed files with 554 additions and 58 deletions
+33 -1
View File
@@ -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{
+61 -6
View File
@@ -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
}
+171 -28
View File
@@ -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 套餐增购
@@ -60,6 +60,7 @@ CREATE TABLE IF NOT EXISTS `yz_platform_product_order` (
`channel` varchar(32) NOT NULL DEFAULT '' COMMENT '支付渠道:wechat/alipay/... 空=线下',
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending待支付 paid已支付 activated已开通 closed已关闭',
`activate_time` datetime DEFAULT NULL COMMENT '开通时间',
`expire_time` datetime DEFAULT NULL COMMENT '有效期截止时间(单品功能默认开通后 1 年,到期权益自动失效,需续费)',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL,
@@ -82,6 +83,12 @@ SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_platform_product_feature` ADD COLUMN `pid` bigint(20) NOT NULL DEFAULT 0 COMMENT ''上级节点ID,0=顶级节点'' AFTER `product_id`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 购买订单有效期字段(历史库补齐;单品功能默认开通后 1 年有效)
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_product_order');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_product_order' AND COLUMN_NAME = 'expire_time');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_platform_product_order` ADD COLUMN `expire_time` datetime DEFAULT NULL COMMENT ''有效期截止时间(单品功能默认开通后 1 年,到期权益自动失效,需续费)'' AFTER `activate_time`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 5) 平台端菜单:基础设置 → 产品定价(仅平台端可见)
-- pid=17 为「基础设置」菜单;若环境中 17 不是基础设置,请在平台端「菜单管理」里手工挂载本页面:
-- 组件路径 /basicSettings/pricing/index.vue,路径 /basicSettings/pricing
+10
View File
@@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS `yz_system_tenant_package` (
`user_quota` int(11) NOT NULL DEFAULT 20 COMMENT '套餐包含的用户数(新租户绑定套餐后初始化的用户数上限)',
`extra_user_price` decimal(10,2) NOT NULL DEFAULT 200.00 COMMENT '单个用户增购价格(元/人)',
`is_default` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否默认套餐:1是 0否(新建租户未指定套餐时使用)',
`allow_repurchase` tinyint(4) NOT NULL DEFAULT 1 COMMENT '是否允许重复购买:1允许(默认) 0仅可购买一次(体验套餐)',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序号,越小越靠前',
`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态:1启用 0禁用',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
@@ -83,6 +84,15 @@ CREATE TABLE IF NOT EXISTS `yz_system_tenant_quota_order` (
KEY `idx_tid` (`tid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户用户数增购记录表';
-- 4.5) 历史库补列:套餐是否允许重复购买(0=仅可购买一次,适用于体验套餐)
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_package');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_package' AND COLUMN_NAME = 'allow_repurchase');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_tenant_package` ADD COLUMN `allow_repurchase` tinyint(4) NOT NULL DEFAULT 1 COMMENT ''是否允许重复购买:1允许(默认) 0仅可购买一次(体验套餐)'' AFTER `is_default`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 体验套餐(不允许重复购买)不涉及用户数增购:清理历史增购价
UPDATE `yz_system_tenant_package` SET `extra_user_price` = 0 WHERE `allow_repurchase` = 0;
-- 5) 租户表补列:套餐ID + 用户数上限
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant' AND COLUMN_NAME = 'package_id');
+5
View File
@@ -45,6 +45,10 @@ const (
ProductOrderStatusClosed = "closed" // 已关闭
)
// ProductOrderDurationDays 单品功能默认有效期(天):1 年。
// 到期后租户端权益自动失效(GetTenantModuleCodes 按有效期过滤),需重新购买续费。
const ProductOrderDurationDays = 365
// PlatformProductOrder 产品购买订单 yz_platform_product_order
type PlatformProductOrder struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
@@ -60,6 +64,7 @@ type PlatformProductOrder struct {
Channel string `orm:"column(channel);size(32)" json:"channel"`
Status string `orm:"column(status);size(20);default(pending)" json:"status"`
ActivateTime *time.Time `orm:"column(activate_time);type(datetime);null" json:"activate_time"`
ExpireTime *time.Time `orm:"column(expire_time);type(datetime);null" json:"expire_time"`
Remark string `orm:"column(remark);size(255)" json:"remark"`
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
+5 -3
View File
@@ -14,9 +14,11 @@ type SystemTenantPackage struct {
UserQuota int `orm:"column(user_quota);default(20)" json:"user_quota"`
ExtraUserPrice float64 `orm:"column(extra_user_price);digits(10);decimals(2)" json:"extra_user_price"`
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"`
Remark string `orm:"column(remark);size(255)" json:"remark"`
// AllowRepurchase 是否允许重复购买:1允许(默认) 0仅可购买一次(体验套餐场景)
AllowRepurchase int8 `orm:"column(allow_repurchase);default(1)" json:"allow_repurchase"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"`
Remark string `orm:"column(remark);size(255)" json:"remark"`
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
+1
View File
@@ -80,6 +80,7 @@ func Register() {
beego.Router("/platform/product/delete/:id", &controllers.PlatformProductController{}, "delete:DeleteProduct")
beego.Router("/platform/product/orders", &controllers.PlatformProductController{}, "get:GetProductOrders")
beego.Router("/platform/product/order/activate/:id", &controllers.PlatformProductController{}, "post:ActivateProductOrder")
beego.Router("/platform/product/order/close/:id", &controllers.PlatformProductController{}, "post:CloseProductOrder")
// 用户数加购规格(yz_system_tenant_user_quota_package)
beego.Router("/platform/tenantQuotaPackage/list", &controllers.PlatformTenantPackageController{}, "get:GetQuotaPackageList")
+45 -1
View File
@@ -151,6 +151,37 @@ func NormalizeModuleCode(code string) string {
return strings.ToLower(strings.TrimSpace(code))
}
// DayStart 取时间所在自然日的 0 点(有效期按「日」计算,不精确到秒)
func DayStart(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
// ProductOrderExpireAt 单品订单有效期截止时间(精确到日):
// - 优先取落库的 expire_time(按日规范化到当天 0 点);
// - 历史订单未落库时按「开通日 0 点 + 默认有效期(1 年)」兜底计算,保证老订单同样按 1 年到期;
// - 两者都无(尚未开通)返回 nil,视为无到期时间。
func ProductOrderExpireAt(order *models.PlatformProductOrder) *time.Time {
if order == nil {
return nil
}
if order.ExpireTime != nil {
t := DayStart(*order.ExpireTime)
return &t
}
if order.ActivateTime != nil {
t := DayStart(*order.ActivateTime).AddDate(0, 0, models.ProductOrderDurationDays)
return &t
}
return nil
}
// ProductOrderExpired 单品订单是否已过有效期:
// 到期日当天 0 点起即视为过期(到期后权益自动失效,需重新购买续费)。
func ProductOrderExpired(order *models.PlatformProductOrder, now time.Time) bool {
expire := ProductOrderExpireAt(order)
return expire != nil && !now.Before(*expire)
}
// GetTenantModuleCodes 获取租户已开通的功能模块编码集合(如 erp / oa / crm)。
//
// 权益由两部分组成:
@@ -189,8 +220,21 @@ func GetTenantModuleCodes(tid uint64) map[string]bool {
return codes
}
productIDs := make([]uint64, 0, len(orders))
// 只统计仍在有效期内的订单:到期后权益自动失效,需重新购买续费
now := time.Now()
validOrders := make([]models.PlatformProductOrder, 0, len(orders))
for _, order := range orders {
if ProductOrderExpired(&order, now) {
continue
}
validOrders = append(validOrders, order)
}
if len(validOrders) == 0 {
return codes
}
productIDs := make([]uint64, 0, len(validOrders))
for _, order := range validOrders {
productIDs = append(productIDs, order.ProductID)
}
var products []models.PlatformProduct