package controllers import ( "encoding/json" "fmt" "io" "strconv" "strings" "time" "server/models" "server/pkg/jwtutil" "server/services" beego "github.com/beego/beego/v2/server/web" ) // PlatformTenantPackageController 平台端「租户套餐」管理 // // 两块能力: // 1. 功能套餐(yz_system_tenant_package + yz_system_tenant_package_module): // 勾选套餐包含的功能模块(erp / oa / crm / cms ...),租户绑定后按其开通功能。 // 2. 用户数配额(yz_system_tenant_user_quota_package + yz_system_tenant_quota_order): // 每个租户有用户数上限(默认 20),支持按单个用户或按加购套餐增购。 type PlatformTenantPackageController struct { beego.Controller } func (c *PlatformTenantPackageController) jsonErr(httpStatus, bizCode int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} _ = c.ServeJSON() } func (c *PlatformTenantPackageController) jsonOK(data interface{}, msg string) { if msg == "" { msg = "success" } resp := map[string]interface{}{"code": 200, "msg": msg} if data != nil { resp["data"] = data } c.Data["json"] = resp _ = c.ServeJSON() } // currentOperator 解析当前平台账号名(用于增购记录留痕;取不到时返回空串,不阻断请求) func (c *PlatformTenantPackageController) currentOperator() string { auth := c.Ctx.Request.Header.Get("Authorization") if auth == "" { return "" } parts := strings.SplitN(auth, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { return "" } claims, err := jwtutil.ParseToken(parts[1]) if err != nil || claims == nil { return "" } if strings.TrimSpace(claims.Username) != "" { return claims.Username } return "" } func (c *PlatformTenantPackageController) readBody(target interface{}) bool { raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return false } if err := json.Unmarshal(raw, target); err != nil { c.jsonErr(400, 400, "参数错误") return false } return true } func parseUint64Param(raw string) uint64 { v, _ := strconv.ParseUint(strings.TrimSpace(raw), 10, 64) return v } // ============================ 功能套餐 ============================ type tenantPackageModuleItem struct { ModuleCode string `json:"module_code"` ModuleName string `json:"module_name"` } type tenantPackageDTO struct { models.SystemTenantPackage Modules []tenantPackageModuleItem `json:"modules"` } type tenantPackagePayload struct { Name string `json:"name"` Code string `json:"code"` Description string `json:"description"` Price *float64 `json:"price"` DurationDays *int `json:"duration_days"` UserQuota *int `json:"user_quota"` ExtraUserPrice *float64 `json:"extra_user_price"` IsDefault *int8 `json:"is_default"` // 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(年付) func normalizePackageDuration(v *int) int { if v == nil || *v <= 0 { return services.DefaultPackageDurationDays } return *v } // loadPackageModules 批量加载套餐模块(packageID -> 模块列表) func loadPackageModules(packageIDs []uint64) map[uint64][]tenantPackageModuleItem { result := map[uint64][]tenantPackageModuleItem{} if len(packageIDs) == 0 { return result } var rows []models.SystemTenantPackageModule _, err := models.Orm.QueryTable(new(models.SystemTenantPackageModule)). Filter("package_id__in", packageIDs). OrderBy("id"). All(&rows) if err != nil { return result } for _, r := range rows { result[r.PackageID] = append(result[r.PackageID], tenantPackageModuleItem{ ModuleCode: r.ModuleCode, ModuleName: r.ModuleName, }) } return result } // GetPackageList GET /platform/tenantPackage/list func (c *PlatformTenantPackageController) GetPackageList() { keyword := strings.TrimSpace(c.GetString("keyword")) status := strings.TrimSpace(c.GetString("status")) qs := models.Orm.QueryTable(new(models.SystemTenantPackage)).Filter("delete_time__isnull", true) if keyword != "" { qs = qs.Filter("name__icontains", keyword) } if status == "0" || status == "1" { v, _ := strconv.Atoi(status) qs = qs.Filter("status", v) } var rows []models.SystemTenantPackage if _, err := qs.OrderBy("sort", "id").All(&rows); err != nil { c.jsonErr(500, 500, "获取失败:"+err.Error()) return } ids := make([]uint64, 0, len(rows)) for _, r := range rows { ids = append(ids, r.ID) } moduleMap := loadPackageModules(ids) // 每个套餐的租户数 tenantCount := map[uint64]int{} if len(ids) > 0 { var tenants []models.SystemTenant if _, err := models.Orm.QueryTable(new(models.SystemTenant)).All(&tenants, "ID", "PackageID"); err == nil { for _, t := range tenants { tenantCount[t.PackageID]++ } } } list := make([]map[string]interface{}, 0, len(rows)) for _, r := range rows { modules := moduleMap[r.ID] if modules == nil { modules = []tenantPackageModuleItem{} } list = append(list, map[string]interface{}{ "id": r.ID, "name": r.Name, "code": r.Code, "description": r.Description, "price": r.Price, "duration_days": r.DurationDays, "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, "create_time": r.CreateTime, "update_time": r.UpdateTime, "modules": modules, "module_codes": moduleCodes(modules), "tenant_count": tenantCount[r.ID], }) } c.jsonOK(map[string]interface{}{"list": list, "total": len(list)}, "获取成功") } func moduleCodes(items []tenantPackageModuleItem) []string { codes := make([]string, 0, len(items)) for _, it := range items { codes = append(codes, it.ModuleCode) } return codes } // GetPackageSelectList GET /platform/tenantPackage/select/list func (c *PlatformTenantPackageController) GetPackageSelectList() { var rows []models.SystemTenantPackage _, err := models.Orm.QueryTable(new(models.SystemTenantPackage)). Filter("delete_time__isnull", true). Filter("status", 1). OrderBy("sort", "id"). All(&rows) if err != nil { c.jsonErr(500, 500, "获取失败:"+err.Error()) return } ids := make([]uint64, 0, len(rows)) for _, r := range rows { ids = append(ids, r.ID) } moduleMap := loadPackageModules(ids) list := make([]map[string]interface{}, 0, len(rows)) for _, r := range rows { modules := moduleMap[r.ID] if modules == nil { modules = []tenantPackageModuleItem{} } list = append(list, map[string]interface{}{ "id": r.ID, "name": r.Name, "code": r.Code, "description": r.Description, "price": r.Price, "duration_days": r.DurationDays, "user_quota": r.UserQuota, "extra_user_price": r.ExtraUserPrice, "is_default": r.IsDefault, "allow_repurchase": r.AllowRepurchase, "modules": modules, "module_codes": moduleCodes(modules), }) } c.jsonOK(list, "获取成功") } // GetPackageDetail GET /platform/tenantPackage/detail/:id func (c *PlatformTenantPackageController) GetPackageDetail() { id := parseUint64Param(c.Ctx.Input.Param(":id")) if id == 0 { c.jsonErr(400, 400, "参数错误") return } var row models.SystemTenantPackage if err := models.Orm.QueryTable(new(models.SystemTenantPackage)). Filter("id", id). Filter("delete_time__isnull", true). One(&row); err != nil { c.jsonErr(404, 404, "套餐不存在") return } modules := loadPackageModules([]uint64{id})[id] if modules == nil { modules = []tenantPackageModuleItem{} } c.jsonOK(tenantPackageDTO{SystemTenantPackage: row, Modules: modules}, "获取成功") } // GetPurchaseURL GET /platform/tenantPackage/purchaseUrl // 套餐购买链接:租户端未开通功能卡片「购买」按钮的跳转地址 func (c *PlatformTenantPackageController) GetPurchaseURL() { c.jsonOK(map[string]interface{}{"purchase_url": services.GetPackagePurchaseURL()}, "获取成功") } // SavePurchaseURL POST /platform/tenantPackage/purchaseUrl func (c *PlatformTenantPackageController) SavePurchaseURL() { var p struct { PurchaseURL string `json:"purchase_url"` } if !c.readBody(&p) { return } if err := services.SavePackagePurchaseURL(p.PurchaseURL); err != nil { c.jsonErr(500, 500, "保存失败:"+err.Error()) return } c.jsonOK(nil, "保存成功") } // GetModuleOptions GET /platform/tenantPackage/moduleOptions // 可加入套餐的功能模块清单(取 yz_system_modules 中启用且未删除的模块) func (c *PlatformTenantPackageController) GetModuleOptions() { var rows []models.SystemModules _, err := models.Orm.QueryTable(new(models.SystemModules)). Filter("delete_time__isnull", true). Filter("status", 1). OrderBy("sort", "id"). All(&rows) if err != nil { c.jsonErr(500, 500, "获取失败:"+err.Error()) return } list := make([]map[string]interface{}, 0, len(rows)) for _, r := range rows { list = append(list, map[string]interface{}{ "id": r.ID, "name": r.Name, "code": r.Code, "path": r.Path, "icon": r.Icon, "description": r.Description, "type": r.Type, "sort": r.Sort, }) } c.jsonOK(map[string]interface{}{"list": list, "total": len(list)}, "获取成功") } // savePackageModules 覆盖式保存套餐模块 func savePackageModules(packageID uint64, modules []string) error { if _, err := models.Orm.QueryTable(new(models.SystemTenantPackageModule)). Filter("package_id", packageID). Delete(); err != nil { return err } if len(modules) == 0 { return nil } nameMap := map[string]string{} var allModules []models.SystemModules if _, err := models.Orm.QueryTable(new(models.SystemModules)). Filter("delete_time__isnull", true). All(&allModules, "ID", "Name", "Code"); err == nil { for _, m := range allModules { nameMap[m.Code] = m.Name } } seen := map[string]bool{} for _, code := range modules { code = strings.TrimSpace(code) if code == "" || seen[code] { continue } seen[code] = true row := &models.SystemTenantPackageModule{ PackageID: packageID, ModuleCode: code, ModuleName: nameMap[code], } if _, err := models.Orm.Insert(row); err != nil { return err } } return nil } // CreatePackage POST /platform/tenantPackage/create func (c *PlatformTenantPackageController) CreatePackage() { var p tenantPackagePayload if !c.readBody(&p) { return } p.Name = strings.TrimSpace(p.Name) p.Code = strings.TrimSpace(p.Code) if p.Name == "" { c.jsonErr(400, 400, "套餐名称不能为空") return } if p.Code == "" { c.jsonErr(400, 400, "套餐编码不能为空") return } cnt, _ := models.Orm.QueryTable(new(models.SystemTenantPackage)). Filter("code", p.Code). Filter("delete_time__isnull", true). Count() if cnt > 0 { c.jsonErr(400, 400, "套餐编码已存在") return } 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), 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()) return } if err := savePackageModules(uint64(id), p.Modules); err != nil { c.jsonErr(500, 500, "套餐功能保存失败:"+err.Error()) return } if row.IsDefault == 1 { clearOtherDefaultPackages(uint64(id)) } c.jsonOK(map[string]interface{}{"id": uint64(id)}, "创建成功") } // EditPackage POST /platform/tenantPackage/edit/:id func (c *PlatformTenantPackageController) EditPackage() { id := parseUint64Param(c.Ctx.Input.Param(":id")) if id == 0 { c.jsonErr(400, 400, "参数错误") return } var p tenantPackagePayload if !c.readBody(&p) { return } p.Name = strings.TrimSpace(p.Name) p.Code = strings.TrimSpace(p.Code) if p.Name == "" { c.jsonErr(400, 400, "套餐名称不能为空") return } if p.Code != "" { cnt, _ := models.Orm.QueryTable(new(models.SystemTenantPackage)). Filter("code", p.Code). Filter("id__ne", id). Filter("delete_time__isnull", true). Count() if cnt > 0 { c.jsonErr(400, 400, "套餐编码已存在") return } } now := time.Now() update := map[string]interface{}{ "name": p.Name, "description": strings.TrimSpace(p.Description), "extra_user_price": valueFloat(p.ExtraUserPrice, services.DefaultTenantExtraUserPrice), "sort": valueInt(p.Sort, 0), "status": valueInt8(p.Status, 1), "remark": strings.TrimSpace(p.Remark), "update_time": now, } if p.Code != "" { update["code"] = p.Code } if p.Price != nil { update["price"] = *p.Price } if p.DurationDays != nil { update["duration_days"] = normalizePackageDuration(p.DurationDays) } if p.UserQuota != nil { quota := *p.UserQuota if quota <= 0 { quota = services.DefaultTenantUserQuota } update["user_quota"] = quota } if p.IsDefault != nil { 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). Filter("delete_time__isnull", true). Update(update); err != nil { c.jsonErr(500, 500, "编辑失败:"+err.Error()) return } if p.Modules != nil { if err := savePackageModules(id, p.Modules); err != nil { c.jsonErr(500, 500, "套餐功能保存失败:"+err.Error()) return } } if p.IsDefault != nil && *p.IsDefault == 1 { clearOtherDefaultPackages(id) } c.jsonOK(nil, "编辑成功") } // clearOtherDefaultPackages 保证全局只有一个默认套餐 func clearOtherDefaultPackages(keepID uint64) { _, _ = models.Orm.QueryTable(new(models.SystemTenantPackage)). Filter("id__ne", keepID). Filter("delete_time__isnull", true). Update(map[string]interface{}{"is_default": 0}) } // 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 { c.jsonErr(400, 400, fmt.Sprintf("该套餐已被 %d 个租户使用,请先调整租户套餐", used)) return } now := time.Now() n, err := models.Orm.QueryTable(new(models.SystemTenantPackage)). Filter("id", id). Filter("delete_time__isnull", true). Update(map[string]interface{}{"delete_time": now, "update_time": now, "is_default": 0}) if err != nil { c.jsonErr(500, 500, "删除失败:"+err.Error()) return } if n == 0 { c.jsonErr(404, 404, "套餐不存在") return } _, _ = models.Orm.QueryTable(new(models.SystemTenantPackageModule)).Filter("package_id", id).Delete() c.jsonOK(nil, "删除成功") } func valueFloat(v *float64, def float64) float64 { if v == nil { return def } return *v } func valueInt(v *int, def int) int { if v == nil { return def } return *v } // ============================ 用户数加购套餐 ============================ type quotaPackagePayload struct { Name string `json:"name"` UserCount *int `json:"user_count"` Price *float64 `json:"price"` Sort *int `json:"sort"` Status *int8 `json:"status"` Remark string `json:"remark"` } // GetQuotaPackageList GET /platform/tenantQuotaPackage/list func (c *PlatformTenantPackageController) GetQuotaPackageList() { onlyEnabled := c.GetString("enabled") == "1" qs := models.Orm.QueryTable(new(models.SystemTenantUserQuotaPackage)).Filter("delete_time__isnull", true) if onlyEnabled { qs = qs.Filter("status", 1) } var rows []models.SystemTenantUserQuotaPackage if _, err := qs.OrderBy("sort", "id").All(&rows); err != nil { c.jsonErr(500, 500, "获取失败:"+err.Error()) return } c.jsonOK(map[string]interface{}{"list": rows, "total": len(rows)}, "获取成功") } // CreateQuotaPackage POST /platform/tenantQuotaPackage/create func (c *PlatformTenantPackageController) CreateQuotaPackage() { var p quotaPackagePayload if !c.readBody(&p) { return } p.Name = strings.TrimSpace(p.Name) if p.Name == "" { c.jsonErr(400, 400, "规格名称不能为空") return } if p.UserCount == nil || *p.UserCount <= 0 { c.jsonErr(400, 400, "包含用户数必须大于 0") return } now := time.Now() row := &models.SystemTenantUserQuotaPackage{ Name: p.Name, UserCount: *p.UserCount, Price: valueFloat(p.Price, 0), Sort: valueInt(p.Sort, 0), Status: valueInt8(p.Status, 1), Remark: strings.TrimSpace(p.Remark), UpdateTime: &now, } id, err := models.Orm.Insert(row) if err != nil { c.jsonErr(500, 500, "创建失败:"+err.Error()) return } c.jsonOK(map[string]interface{}{"id": uint64(id)}, "创建成功") } // EditQuotaPackage POST /platform/tenantQuotaPackage/edit/:id func (c *PlatformTenantPackageController) EditQuotaPackage() { id := parseUint64Param(c.Ctx.Input.Param(":id")) if id == 0 { c.jsonErr(400, 400, "参数错误") return } var p quotaPackagePayload if !c.readBody(&p) { return } p.Name = strings.TrimSpace(p.Name) if p.Name == "" { c.jsonErr(400, 400, "规格名称不能为空") return } if p.UserCount != nil && *p.UserCount <= 0 { c.jsonErr(400, 400, "包含用户数必须大于 0") return } now := time.Now() update := map[string]interface{}{ "name": p.Name, "sort": valueInt(p.Sort, 0), "status": valueInt8(p.Status, 1), "remark": strings.TrimSpace(p.Remark), "update_time": now, } if p.UserCount != nil { update["user_count"] = *p.UserCount } if p.Price != nil { update["price"] = *p.Price } n, err := models.Orm.QueryTable(new(models.SystemTenantUserQuotaPackage)). Filter("id", id). Filter("delete_time__isnull", true). Update(update) if err != nil { c.jsonErr(500, 500, "编辑失败:"+err.Error()) return } if n == 0 { c.jsonErr(404, 404, "规格不存在") return } c.jsonOK(nil, "编辑成功") } // DeleteQuotaPackage DELETE /platform/tenantQuotaPackage/delete/:id(软删) func (c *PlatformTenantPackageController) DeleteQuotaPackage() { id := parseUint64Param(c.Ctx.Input.Param(":id")) if id == 0 { c.jsonErr(400, 400, "参数错误") return } now := time.Now() n, err := models.Orm.QueryTable(new(models.SystemTenantUserQuotaPackage)). Filter("id", id). Filter("delete_time__isnull", true). Update(map[string]interface{}{"delete_time": now, "update_time": now}) if err != nil { c.jsonErr(500, 500, "删除失败:"+err.Error()) return } if n == 0 { c.jsonErr(404, 404, "规格不存在") return } c.jsonOK(nil, "删除成功") } // ============================ 租户套餐 / 用户数配额 ============================ type setTenantPackagePayload struct { Tid uint64 `json:"tid"` PackageID uint64 `json:"package_id"` SyncQuota *bool `json:"sync_quota"` } // SetTenantPackage POST /platform/tenant/setPackage // 为租户绑定套餐;sync_quota 为 true(默认)时按套餐包含用户数与现有上限取较大值同步。 func (c *PlatformTenantPackageController) SetTenantPackage() { var p setTenantPackagePayload if !c.readBody(&p) { return } if p.Tid == 0 || p.PackageID == 0 { c.jsonErr(400, 400, "租户与套餐不能为空") return } syncQuota := true if p.SyncQuota != nil { syncQuota = *p.SyncQuota } newQuota, err := services.ApplyPackageToTenant(p.Tid, p.PackageID, syncQuota) if err != nil { c.jsonErr(400, 400, err.Error()) return } c.jsonOK(map[string]interface{}{"user_quota": newQuota}, "套餐设置成功") } // GetTenantQuotaInfo GET /platform/tenant/quotaInfo?tid=1 // 返回租户套餐、用户数使用情况、可增购规格与单价 func (c *PlatformTenantPackageController) GetTenantQuotaInfo() { tid, _ := c.GetUint64("tid") if tid == 0 { c.jsonErr(400, 400, "tid 不能为空") return } info, err := services.GetTenantQuotaInfo(tid) if err != nil { c.jsonErr(404, 404, "租户不存在") return } var quotaPackages []models.SystemTenantUserQuotaPackage _, _ = models.Orm.QueryTable(new(models.SystemTenantUserQuotaPackage)). Filter("delete_time__isnull", true). Filter("status", 1). OrderBy("sort", "id"). All("aPackages) modules, _ := services.GetTenantPackageModules(tid) moduleList := make([]tenantPackageModuleItem, 0, len(modules)) for _, m := range modules { 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, "effective_package_id": info.EffectivePackageID, "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 套餐增购 QuotaPackageID uint64 `json:"quota_package_id"` UserCount int `json:"user_count"` UnitPrice float64 `json:"unit_price"` Remark string `json:"remark"` } // RechargeTenantQuota POST /platform/tenant/rechargeQuota // 用户数增购:单个增购(单价 x 人数)或 按加购套餐(如 5人/500元) func (c *PlatformTenantPackageController) RechargeTenantQuota() { var p rechargeQuotaPayload if !c.readBody(&p) { return } if p.Tid == 0 { c.jsonErr(400, 400, "租户不能为空") return } order := models.SystemTenantQuotaOrder{ Tid: p.Tid, Type: p.Type, Operator: c.currentOperator(), Remark: strings.TrimSpace(p.Remark), } if order.Type != 2 { order.Type = 1 } if order.Type == 2 { var pkg models.SystemTenantUserQuotaPackage if err := models.Orm.QueryTable(new(models.SystemTenantUserQuotaPackage)). Filter("id", p.QuotaPackageID). Filter("delete_time__isnull", true). One(&pkg); err != nil { c.jsonErr(400, 400, "加购规格不存在") return } if pkg.UserCount <= 0 { c.jsonErr(400, 400, "加购规格用户数配置有误") return } order.QuotaPackageID = pkg.ID order.PackageName = pkg.Name order.UserCount = pkg.UserCount order.Amount = pkg.Price order.UnitPrice = round2(pkg.Price / float64(pkg.UserCount)) } else { if p.UserCount <= 0 { c.jsonErr(400, 400, "增购用户数必须大于 0") return } unitPrice := p.UnitPrice if unitPrice <= 0 { // 未传单价时取租户套餐的单个用户增购价 if info, err := services.GetTenantQuotaInfo(p.Tid); err == nil && info.ExtraUserPrice > 0 { unitPrice = info.ExtraUserPrice } else { unitPrice = services.DefaultTenantExtraUserPrice } } order.UserCount = p.UserCount order.UnitPrice = unitPrice order.Amount = round2(unitPrice * float64(p.UserCount)) order.PackageName = "单个增购" } before, after, err := services.IncreaseTenantQuota(p.Tid, order.UserCount) if err != nil { c.jsonErr(400, 400, err.Error()) return } order.QuotaBefore = before order.QuotaAfter = after if _, err := models.Orm.Insert(&order); err != nil { c.jsonErr(500, 500, "增购记录写入失败:"+err.Error()) return } if info, err := services.GetTenantQuotaInfo(p.Tid); err == nil { c.jsonOK(map[string]interface{}{ "quota": info.Quota, "used": info.Used, "order": order, "amount": order.Amount, "add": order.UserCount, }, "增购成功") return } c.jsonOK(map[string]interface{}{"quota": after, "add": order.UserCount, "amount": order.Amount}, "增购成功") } // GetQuotaOrders GET /platform/tenant/quotaOrders?tid=1 func (c *PlatformTenantPackageController) GetQuotaOrders() { tid, _ := c.GetUint64("tid") if tid == 0 { c.jsonErr(400, 400, "tid 不能为空") return } var rows []models.SystemTenantQuotaOrder _, err := models.Orm.QueryTable(new(models.SystemTenantQuotaOrder)). Filter("tid", tid). OrderBy("-id"). All(&rows) if err != nil { c.jsonErr(500, 500, "获取失败:"+err.Error()) return } c.jsonOK(map[string]interface{}{"list": rows, "total": len(rows)}, "获取成功") }