增加租户套餐
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// DefaultTenantUserQuota 租户默认用户数上限(未配置套餐/套餐未设置时兜底)
|
||||
const DefaultTenantUserQuota = 20
|
||||
|
||||
// DefaultTenantExtraUserPrice 单个用户增购默认单价(元/人)
|
||||
const DefaultTenantExtraUserPrice = 200.00
|
||||
|
||||
// ErrTenantQuotaExceeded 用户数已达上限
|
||||
var ErrTenantQuotaExceeded = errors.New("tenant user quota exceeded")
|
||||
|
||||
// CountTenantUsers 统计租户有效用户数(未软删)
|
||||
func CountTenantUsers(tid uint64) (int, error) {
|
||||
if tid == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
cnt, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
return int(cnt), err
|
||||
}
|
||||
|
||||
// GetTenantPackage 获取租户生效套餐:优先取绑定套餐,未绑定/已删除时回退到默认套餐
|
||||
func GetTenantPackage(tid uint64) (*models.SystemTenantPackage, error) {
|
||||
if tid == 0 {
|
||||
return nil, errors.New("tid 不能为空")
|
||||
}
|
||||
var tenant models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", tid).One(&tenant); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenant.PackageID > 0 {
|
||||
var pkg models.SystemTenantPackage
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||
Filter("id", tenant.PackageID).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&pkg); err == nil {
|
||||
return &pkg, nil
|
||||
}
|
||||
}
|
||||
return GetDefaultTenantPackage()
|
||||
}
|
||||
|
||||
// GetDefaultTenantPackage 获取默认套餐(is_default=1 优先,其次按排序取第一个启用套餐)
|
||||
func GetDefaultTenantPackage() (*models.SystemTenantPackage, error) {
|
||||
var pkg models.SystemTenantPackage
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("status", 1).
|
||||
Filter("is_default", 1).
|
||||
OrderBy("sort", "id").
|
||||
One(&pkg)
|
||||
if err == nil {
|
||||
return &pkg, nil
|
||||
}
|
||||
var fallback models.SystemTenantPackage
|
||||
if err2 := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("status", 1).
|
||||
OrderBy("sort", "id").
|
||||
One(&fallback); err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
return &fallback, nil
|
||||
}
|
||||
|
||||
// GetTenantPackageModules 获取租户套餐包含的功能模块(按套餐未绑定则取默认套餐)
|
||||
func GetTenantPackageModules(tid uint64) ([]models.SystemTenantPackageModule, error) {
|
||||
pkg, err := GetTenantPackage(tid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []models.SystemTenantPackageModule
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantPackageModule)).
|
||||
Filter("package_id", pkg.ID).
|
||||
OrderBy("id").
|
||||
All(&rows)
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// GetTenantModuleCodes 获取租户已开通的功能模块编码集合(如 erp / oa / crm)
|
||||
func GetTenantModuleCodes(tid uint64) map[string]bool {
|
||||
codes := map[string]bool{}
|
||||
rows, err := GetTenantPackageModules(tid)
|
||||
if err != nil {
|
||||
return codes
|
||||
}
|
||||
for _, r := range rows {
|
||||
code := strings.TrimSpace(r.ModuleCode)
|
||||
if code != "" {
|
||||
codes[code] = true
|
||||
}
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
// TenantQuotaCheck 用户数配额校验结果
|
||||
type TenantQuotaCheck struct {
|
||||
Quota int `json:"quota"`
|
||||
Used int `json:"used"`
|
||||
Allowed bool `json:"allowed"`
|
||||
}
|
||||
|
||||
// CheckTenantUserQuota 校验租户是否还能新增用户。
|
||||
// uid > 0 且该用户已绑定到本租户时视为「编辑/复用已有绑定」,不占用新增名额。
|
||||
func CheckTenantUserQuota(tid, uid uint64) (*TenantQuotaCheck, error) {
|
||||
result := &TenantQuotaCheck{Quota: DefaultTenantUserQuota, Allowed: true}
|
||||
if tid == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
quota := DefaultTenantUserQuota
|
||||
var tenant models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", tid).One(&tenant); err == nil && tenant.UserQuota > 0 {
|
||||
quota = tenant.UserQuota
|
||||
}
|
||||
result.Quota = quota
|
||||
|
||||
if uid > 0 {
|
||||
exist, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("uid", uid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if err == nil && exist > 0 {
|
||||
result.Allowed = true
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
used, err := CountTenantUsers(tid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Used = used
|
||||
result.Allowed = used < quota
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// EnsureTenantUserQuota 用户数已满时返回 ErrTenantQuotaExceeded
|
||||
func EnsureTenantUserQuota(tid, uid uint64) error {
|
||||
check, err := CheckTenantUserQuota(tid, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !check.Allowed {
|
||||
return ErrTenantQuotaExceeded
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
used, err := CountTenantUsers(tid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info.Used = used
|
||||
info.Remaining = info.Quota - used
|
||||
if info.Remaining < 0 {
|
||||
info.Remaining = 0
|
||||
}
|
||||
if pkg, err := GetTenantPackage(tid); err == nil && pkg != nil {
|
||||
info.EffectivePackageID = pkg.ID
|
||||
info.PackageName = pkg.Name
|
||||
if pkg.ExtraUserPrice > 0 {
|
||||
info.ExtraUserPrice = pkg.ExtraUserPrice
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// IncreaseTenantQuota 增加租户用户数上限,返回(变更前上限、变更后上限)
|
||||
func IncreaseTenantQuota(tid uint64, count int) (int, int, error) {
|
||||
if tid == 0 || count <= 0 {
|
||||
return 0, 0, errors.New("参数错误")
|
||||
}
|
||||
var tenant models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", tid).One(&tenant); err != nil {
|
||||
return 0, 0, errors.New("租户不存在")
|
||||
}
|
||||
before := tenant.UserQuota
|
||||
if before <= 0 {
|
||||
before = DefaultTenantUserQuota
|
||||
}
|
||||
after := before + count
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("id", tid).
|
||||
Update(map[string]interface{}{"user_quota": after})
|
||||
if err != nil {
|
||||
return before, after, err
|
||||
}
|
||||
return before, after, nil
|
||||
}
|
||||
|
||||
// ApplyPackageToTenant 为租户绑定套餐:同步套餐包含的用户数(不低于当前已开用户数,且不降低已有上限)
|
||||
func ApplyPackageToTenant(tid, packageID uint64, syncQuota bool) (int, error) {
|
||||
if tid == 0 || packageID == 0 {
|
||||
return 0, errors.New("参数错误")
|
||||
}
|
||||
var pkg models.SystemTenantPackage
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
|
||||
Filter("id", packageID).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&pkg); err != nil {
|
||||
return 0, errors.New("套餐不存在")
|
||||
}
|
||||
|
||||
update := map[string]interface{}{"package_id": packageID}
|
||||
newQuota := 0
|
||||
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
|
||||
}
|
||||
newQuota = pkg.UserQuota
|
||||
if newQuota <= 0 {
|
||||
newQuota = DefaultTenantUserQuota
|
||||
}
|
||||
// 不低于当前上限:换套餐不回收已购用户数
|
||||
if newQuota < current {
|
||||
newQuota = current
|
||||
}
|
||||
update["user_quota"] = newQuota
|
||||
}
|
||||
|
||||
if _, err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("id", tid).
|
||||
Update(update); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return newQuota, nil
|
||||
}
|
||||
|
||||
// sortModulesByPathDesc 模块按 path 长度倒序:长路径优先匹配,避免 /apps 误吞 /apps/erp
|
||||
func sortModulesByPathDesc(modules []models.SystemModules) []models.SystemModules {
|
||||
list := make([]models.SystemModules, 0, len(modules))
|
||||
for _, m := range modules {
|
||||
if strings.TrimSpace(m.Path) != "" {
|
||||
list = append(list, m)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
return len(list[i].Path) > len(list[j].Path)
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
// ownerModuleCode 返回菜单路径归属的模块编码(按模块 path 前缀匹配)。
|
||||
// 不属于任何模块时 ok=false —— 视为公共菜单,不参与套餐过滤。
|
||||
func ownerModuleCode(sortedModules []models.SystemModules, path string) (string, bool) {
|
||||
p := strings.TrimSuffix(strings.TrimSpace(path), "/")
|
||||
if p == "" {
|
||||
return "", false
|
||||
}
|
||||
for _, m := range sortedModules {
|
||||
mp := strings.TrimSuffix(strings.TrimSpace(m.Path), "/")
|
||||
if mp == "" {
|
||||
continue
|
||||
}
|
||||
if p == mp || strings.HasPrefix(p, mp+"/") {
|
||||
return m.Code, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// FilterMenusByTenantModules 按租户套餐包含的功能模块过滤菜单:
|
||||
// - 菜单不属于任何模块(公共/基础菜单)→ 保留;
|
||||
// - 菜单归属某模块 → 仅当该模块已被租户套餐开通时保留。
|
||||
func FilterMenusByTenantModules(menus []models.SystemMenu, allModules []models.SystemModules, allowedCodes map[string]bool) []models.SystemMenu {
|
||||
if len(menus) == 0 || len(allModules) == 0 {
|
||||
return menus
|
||||
}
|
||||
sorted := sortModulesByPathDesc(allModules)
|
||||
out := 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 {
|
||||
out = append(out, m)
|
||||
continue
|
||||
}
|
||||
if allowedCodes[code] {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ListEnabledModules 获取全部启用且未删除的模块(用于菜单归属判定)
|
||||
func ListEnabledModules() ([]models.SystemModules, error) {
|
||||
var rows []models.SystemModules
|
||||
_, err := models.Orm.QueryTable(new(models.SystemModules)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("status", 1).
|
||||
OrderBy("sort", "id").
|
||||
All(&rows)
|
||||
return rows, err
|
||||
}
|
||||
Reference in New Issue
Block a user