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 }