1020 lines
30 KiB
Go
1020 lines
30 KiB
Go
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)}, "获取成功")
|
|
}
|