增加功能报价
This commit is contained in:
+191
-17
@@ -2,8 +2,10 @@ package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
@@ -11,6 +13,9 @@ import (
|
||||
// DefaultTenantUserQuota 租户默认用户数上限(未配置套餐/套餐未设置时兜底)
|
||||
const DefaultTenantUserQuota = 20
|
||||
|
||||
// DefaultPackageDurationDays 套餐默认有效时长(天):按年计费
|
||||
const DefaultPackageDurationDays = 365
|
||||
|
||||
// DefaultTenantExtraUserPrice 单个用户增购默认单价(元/人)
|
||||
const DefaultTenantExtraUserPrice = 200.00
|
||||
|
||||
@@ -29,7 +34,26 @@ func CountTenantUsers(tid uint64) (int, error) {
|
||||
return int(cnt), err
|
||||
}
|
||||
|
||||
// GetTenantPackage 获取租户生效套餐:优先取绑定套餐,未绑定/已删除时回退到默认套餐
|
||||
// PackageDurationDays 套餐有效时长(天),未配置时按 365(年付)
|
||||
func PackageDurationDays(pkg *models.SystemTenantPackage) int {
|
||||
if pkg == nil || pkg.DurationDays <= 0 {
|
||||
return DefaultPackageDurationDays
|
||||
}
|
||||
return pkg.DurationDays
|
||||
}
|
||||
|
||||
// PackageExpireFromNow 从当前时间起算套餐到期时间
|
||||
func PackageExpireFromNow(pkg *models.SystemTenantPackage) time.Time {
|
||||
return time.Now().AddDate(0, 0, PackageDurationDays(pkg))
|
||||
}
|
||||
|
||||
// tenantPackageExpired 租户套餐是否已超过时效(未设置到期时间视为永久有效)
|
||||
func tenantPackageExpired(tenant *models.SystemTenant) bool {
|
||||
return tenant.PackageExpireTime != nil && tenant.PackageExpireTime.Before(time.Now())
|
||||
}
|
||||
|
||||
// GetTenantPackage 获取租户生效套餐:优先取绑定套餐,未绑定/已删除时回退到默认套餐。
|
||||
// 绑定套餐已超过时效时,自动切换回基础套餐并落库(到期时间清空),返回基础套餐。
|
||||
func GetTenantPackage(tid uint64) (*models.SystemTenantPackage, error) {
|
||||
if tid == 0 {
|
||||
return nil, errors.New("tid 不能为空")
|
||||
@@ -39,6 +63,10 @@ func GetTenantPackage(tid uint64) (*models.SystemTenantPackage, error) {
|
||||
return nil, err
|
||||
}
|
||||
if tenant.PackageID > 0 {
|
||||
// 套餐到期:自动切换基础套餐(降级落库,保证只执行一次)
|
||||
if tenantPackageExpired(&tenant) {
|
||||
return downgradeExpiredTenantPackage(tid)
|
||||
}
|
||||
var pkg models.SystemTenantPackage
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||
Filter("id", tenant.PackageID).
|
||||
@@ -50,6 +78,35 @@ func GetTenantPackage(tid uint64) (*models.SystemTenantPackage, error) {
|
||||
return GetDefaultTenantPackage()
|
||||
}
|
||||
|
||||
// GetBasicTenantPackage 获取基础套餐(code=basic;不存在时回退默认套餐)
|
||||
func GetBasicTenantPackage() (*models.SystemTenantPackage, error) {
|
||||
var pkg models.SystemTenantPackage
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||
Filter("code", "basic").
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("status", 1).
|
||||
One(&pkg)
|
||||
if err == nil {
|
||||
return &pkg, nil
|
||||
}
|
||||
return GetDefaultTenantPackage()
|
||||
}
|
||||
|
||||
// downgradeExpiredTenantPackage 套餐到期:切换基础套餐并清除到期时间,返回基础套餐
|
||||
func downgradeExpiredTenantPackage(tid uint64) (*models.SystemTenantPackage, error) {
|
||||
basic, err := GetBasicTenantPackage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("id", tid).
|
||||
Update(map[string]interface{}{
|
||||
"package_id": basic.ID,
|
||||
"package_expire_time": nil,
|
||||
})
|
||||
return basic, nil
|
||||
}
|
||||
|
||||
// GetDefaultTenantPackage 获取默认套餐(is_default=1 优先,其次按排序取第一个启用套餐)
|
||||
func GetDefaultTenantPackage() (*models.SystemTenantPackage, error) {
|
||||
var pkg models.SystemTenantPackage
|
||||
@@ -160,24 +217,26 @@ func EnsureTenantUserQuota(tid, uid uint64) error {
|
||||
|
||||
// GetTenantQuotaInfo 租户用户数使用情况
|
||||
type TenantQuotaInfo struct {
|
||||
Tid uint64 `json:"tid"`
|
||||
PackageID uint64 `json:"package_id"`
|
||||
PackageName string `json:"package_name"`
|
||||
Quota int `json:"quota"`
|
||||
Used int `json:"used"`
|
||||
Remaining int `json:"remaining"`
|
||||
ExtraUserPrice float64 `json:"extra_user_price"`
|
||||
EffectivePackageID uint64 `json:"effective_package_id"`
|
||||
Tid uint64 `json:"tid"`
|
||||
PackageID uint64 `json:"package_id"`
|
||||
PackageName string `json:"package_name"`
|
||||
Quota int `json:"quota"`
|
||||
Used int `json:"used"`
|
||||
Remaining int `json:"remaining"`
|
||||
ExtraUserPrice float64 `json:"extra_user_price"`
|
||||
EffectivePackageID uint64 `json:"effective_package_id"`
|
||||
DurationDays int `json:"duration_days"`
|
||||
PackageExpireTime *time.Time `json:"package_expire_time"`
|
||||
DaysRemaining int `json:"days_remaining"`
|
||||
}
|
||||
|
||||
// GetTenantQuotaInfo 查询租户用户数使用情况(含生效套餐信息)
|
||||
// GetTenantQuotaInfo 查询租户用户数使用情况(含生效套餐信息与计费到期时间)
|
||||
func GetTenantQuotaInfo(tid uint64) (*TenantQuotaInfo, error) {
|
||||
info := &TenantQuotaInfo{Tid: tid, Quota: DefaultTenantUserQuota, ExtraUserPrice: DefaultTenantExtraUserPrice}
|
||||
var tenant models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", tid).One(&tenant); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info.PackageID = tenant.PackageID
|
||||
if tenant.UserQuota > 0 {
|
||||
info.Quota = tenant.UserQuota
|
||||
}
|
||||
@@ -190,13 +249,29 @@ func GetTenantQuotaInfo(tid uint64) (*TenantQuotaInfo, error) {
|
||||
if info.Remaining < 0 {
|
||||
info.Remaining = 0
|
||||
}
|
||||
// 生效套餐(到期会自动降级基础套餐并落库)
|
||||
if pkg, err := GetTenantPackage(tid); err == nil && pkg != nil {
|
||||
info.EffectivePackageID = pkg.ID
|
||||
info.PackageName = pkg.Name
|
||||
info.DurationDays = PackageDurationDays(pkg)
|
||||
if pkg.ExtraUserPrice > 0 {
|
||||
info.ExtraUserPrice = pkg.ExtraUserPrice
|
||||
}
|
||||
}
|
||||
// 重新读取租户绑定信息(GetTenantPackage 可能已触发到期降级落库)
|
||||
var fresh models.SystemTenant
|
||||
if e := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", tid).One(&fresh); e == nil {
|
||||
tenant = fresh
|
||||
}
|
||||
info.PackageID = tenant.PackageID
|
||||
info.PackageExpireTime = tenant.PackageExpireTime
|
||||
if tenant.PackageExpireTime != nil {
|
||||
remain := int(math.Ceil(time.Until(*tenant.PackageExpireTime).Hours() / 24))
|
||||
if remain < 0 {
|
||||
remain = 0
|
||||
}
|
||||
info.DaysRemaining = remain
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
@@ -223,7 +298,10 @@ func IncreaseTenantQuota(tid uint64, count int) (int, int, error) {
|
||||
return before, after, nil
|
||||
}
|
||||
|
||||
// ApplyPackageToTenant 为租户绑定套餐:同步套餐包含的用户数(不低于当前已开用户数,且不降低已有上限)
|
||||
// ApplyPackageToTenant 为租户绑定/续费套餐:
|
||||
// - 同步套餐包含的用户数(不低于当前已开用户数,且不降低已有上限,syncQuota=true 时)
|
||||
// - 按套餐有效时长(duration_days,默认 365 天 = 年付)设置计费到期时间;
|
||||
// 同套餐未到期时再次绑定视为续费,在原到期时间上顺延
|
||||
func ApplyPackageToTenant(tid, packageID uint64, syncQuota bool) (int, error) {
|
||||
if tid == 0 || packageID == 0 {
|
||||
return 0, errors.New("参数错误")
|
||||
@@ -235,14 +313,14 @@ func ApplyPackageToTenant(tid, packageID uint64, syncQuota bool) (int, error) {
|
||||
One(&pkg); err != nil {
|
||||
return 0, errors.New("套餐不存在")
|
||||
}
|
||||
var tenant models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", tid).One(&tenant); err != nil {
|
||||
return 0, errors.New("租户不存在")
|
||||
}
|
||||
|
||||
update := map[string]interface{}{"package_id": packageID}
|
||||
newQuota := 0
|
||||
newQuota := tenant.UserQuota
|
||||
if syncQuota {
|
||||
var tenant models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", tid).One(&tenant); err != nil {
|
||||
return 0, errors.New("租户不存在")
|
||||
}
|
||||
current := tenant.UserQuota
|
||||
if current <= 0 {
|
||||
current = DefaultTenantUserQuota
|
||||
@@ -258,6 +336,13 @@ func ApplyPackageToTenant(tid, packageID uint64, syncQuota bool) (int, error) {
|
||||
update["user_quota"] = newQuota
|
||||
}
|
||||
|
||||
// 计费时效:同套餐未到期续费 → 从原到期时间顺延;换套餐/已到期 → 从当前时间起算
|
||||
base := time.Now()
|
||||
if tenant.PackageID == packageID && tenant.PackageExpireTime != nil && tenant.PackageExpireTime.After(base) {
|
||||
base = *tenant.PackageExpireTime
|
||||
}
|
||||
update["package_expire_time"] = base.AddDate(0, 0, PackageDurationDays(&pkg))
|
||||
|
||||
if _, err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("id", tid).
|
||||
Update(update); err != nil {
|
||||
@@ -325,6 +410,95 @@ func FilterMenusByTenantModules(menus []models.SystemMenu, allModules []models.S
|
||||
return out
|
||||
}
|
||||
|
||||
// MarkMenusLockedByTenantModules 按租户套餐标记未开通模块的菜单:菜单保留,返回锁定菜单ID集合。
|
||||
// lockedIDs 包含未开通模块对应的菜单及其全部后代菜单ID,供前端展示锁定态(而非直接剔除导致 404)。
|
||||
func MarkMenusLockedByTenantModules(menus []models.SystemMenu, allModules []models.SystemModules, allowedCodes map[string]bool) ([]models.SystemMenu, map[uint64]bool) {
|
||||
lockedIDs := map[uint64]bool{}
|
||||
if len(menus) == 0 || len(allModules) == 0 {
|
||||
return menus, lockedIDs
|
||||
}
|
||||
sorted := sortModulesByPathDesc(allModules)
|
||||
kept := make([]models.SystemMenu, 0, len(menus))
|
||||
for _, m := range menus {
|
||||
path := ""
|
||||
if m.Path != nil {
|
||||
path = *m.Path
|
||||
}
|
||||
code, ok := ownerModuleCode(sorted, path)
|
||||
if ok && !allowedCodes[code] {
|
||||
lockedIDs[m.ID] = true
|
||||
}
|
||||
kept = append(kept, m)
|
||||
}
|
||||
if len(lockedIDs) == 0 {
|
||||
return kept, lockedIDs
|
||||
}
|
||||
// 锁定传递给所有后代菜单
|
||||
childrenOf := map[uint64][]uint64{}
|
||||
for _, m := range kept {
|
||||
if m.Pid > 0 {
|
||||
pid := uint64(m.Pid)
|
||||
childrenOf[pid] = append(childrenOf[pid], m.ID)
|
||||
}
|
||||
}
|
||||
queue := make([]uint64, 0, len(lockedIDs))
|
||||
for id := range lockedIDs {
|
||||
queue = append(queue, id)
|
||||
}
|
||||
for len(queue) > 0 {
|
||||
id := queue[0]
|
||||
queue = queue[1:]
|
||||
for _, child := range childrenOf[id] {
|
||||
if !lockedIDs[child] {
|
||||
lockedIDs[child] = true
|
||||
queue = append(queue, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
return kept, lockedIDs
|
||||
}
|
||||
|
||||
// PackagePurchaseURLCode 平台通用配置(yz_platform_normal_setting)中「套餐购买链接」的配置 code
|
||||
const PackagePurchaseURLCode = "package_purchase_url"
|
||||
|
||||
// GetPackagePurchaseURL 读取平台配置的套餐购买链接。
|
||||
// 租户端 home 页未开通(锁定)功能卡片的「购买」按钮跳转到该地址;未配置时返回空串。
|
||||
func GetPackagePurchaseURL() string {
|
||||
var s models.PlatformNormalSetting
|
||||
if err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
|
||||
Filter("code", PackagePurchaseURLCode).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&s); err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(s.Value)
|
||||
}
|
||||
|
||||
// SavePackagePurchaseURL 保存平台配置的套餐购买链接(不存在则创建)
|
||||
func SavePackagePurchaseURL(url string) error {
|
||||
url = strings.TrimSpace(url)
|
||||
var s models.PlatformNormalSetting
|
||||
err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
|
||||
Filter("code", PackagePurchaseURLCode).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&s)
|
||||
if err != nil {
|
||||
s = models.PlatformNormalSetting{
|
||||
Name: "套餐购买链接",
|
||||
Code: PackagePurchaseURLCode,
|
||||
Value: url,
|
||||
Remark: "租户端未开通功能卡片「购买」按钮跳转地址",
|
||||
}
|
||||
_, err = models.Orm.Insert(&s)
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
s.Value = url
|
||||
s.UpdateTime = &now
|
||||
_, err = models.Orm.Update(&s)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListEnabledModules 获取全部启用且未删除的模块(用于菜单归属判定)
|
||||
func ListEnabledModules() ([]models.SystemModules, error) {
|
||||
var rows []models.SystemModules
|
||||
|
||||
Reference in New Issue
Block a user