Files
yunzerwebsiteallinone/go/controllers/platform_tenant.go
T
2026-10-08 15:30:37 +08:00

598 lines
19 KiB
Go

package controllers
import (
"encoding/json"
"errors"
"fmt"
"io"
"math"
"strconv"
"strings"
"time"
"server/models"
"server/services"
authsvc "server/services/auth"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformTenantController 平台端租户管ç?
type PlatformTenantController struct {
beego.Controller
}
type tenantDTO struct {
ID uint64 `json:"id"`
TenantCode string `json:"tenant_code"`
TenantName string `json:"tenant_name"`
ContactPerson string `json:"contact_person"`
ContactPhone string `json:"contact_phone"`
ContactEmail string `json:"contact_email"`
Address string `json:"address"`
Worktime string `json:"worktime"`
Status int8 `json:"status"`
// 套餐与用户数配额
PackageID uint64 `json:"package_id"`
PackageName string `json:"package_name"`
// PackageExpireTime 套餐计费到期时间(NULL-永久有效);DaysRemaining 剩余天数
PackageExpireTime *time.Time `json:"package_expire_time"`
DaysRemaining int `json:"days_remaining"`
UserQuota int `json:"user_quota"`
UserUsed int `json:"user_used"`
Remark string `json:"remark"`
CreateTime *time.Time `json:"create_time,omitempty"`
UpdateTime *time.Time `json:"update_time,omitempty"`
DeleteTime *time.Time `json:"delete_time,omitempty"`
}
// tenantPackageNameMap 套餐ID -> 套餐名称
func tenantPackageNameMap() map[uint64]string {
result := map[uint64]string{}
var rows []models.SystemTenantPackage
if _, err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
Filter("delete_time__isnull", true).
All(&rows, "ID", "Name"); err == nil {
for _, r := range rows {
result[r.ID] = r.Name
}
}
return result
}
// tenantUserCountMap 租户ID -> 有效用户数
func tenantUserCountMap() map[uint64]int {
result := map[uint64]int{}
var rows []models.SystemTenantUser
if _, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("delete_time__isnull", true).
All(&rows, "ID", "Tid"); err == nil {
for _, r := range rows {
result[r.Tid]++
}
}
return result
}
func stringValue(s *string) string {
if s == nil {
return ""
}
return *s
}
func stringPtr(s string) *string {
return &s
}
func toTenantDTO(t models.SystemTenant) tenantDTO {
ct := t.CreateTime
ut := t.UpdateTime
daysRemaining := 0
if t.PackageExpireTime != nil {
daysRemaining = int(math.Ceil(time.Until(*t.PackageExpireTime).Hours() / 24))
if daysRemaining < 0 {
daysRemaining = 0
}
}
return tenantDTO{
ID: t.ID,
TenantCode: t.TenantCode,
TenantName: t.TenantName,
ContactPerson: stringValue(t.ContactPerson),
ContactPhone: stringValue(t.ContactPhone),
ContactEmail: stringValue(t.ContactEmail),
Address: stringValue(t.Address),
Worktime: stringValue(t.Worktime),
Status: t.Status,
PackageID: t.PackageID,
PackageExpireTime: t.PackageExpireTime,
DaysRemaining: daysRemaining,
UserQuota: t.UserQuota,
Remark: stringValue(t.Remark),
CreateTime: &ct,
UpdateTime: &ut,
DeleteTime: t.DeleteTime,
}
}
// GetTenant 获取租户列表
// GET /platform/tenant/getTenant?page=1&pageSize=10&tenant_name=...&tenant_code=...&contact_person=...&contact_phone=...
func (c *PlatformTenantController) GetTenant() {
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 10)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
tenantName := strings.TrimSpace(c.GetString("tenant_name"))
tenantCode := strings.TrimSpace(c.GetString("tenant_code"))
contactPerson := strings.TrimSpace(c.GetString("contact_person"))
contactPhone := strings.TrimSpace(c.GetString("contact_phone"))
qs := models.Orm.QueryTable(new(models.SystemTenant))
if tenantName != "" {
qs = qs.Filter("tenant_name__icontains", tenantName)
}
if tenantCode != "" {
qs = qs.Filter("tenant_code__icontains", tenantCode)
}
if contactPerson != "" {
qs = qs.Filter("contact_person__icontains", contactPerson)
}
if contactPhone != "" {
qs = qs.Filter("contact_phone__icontains", contactPhone)
}
total, err := qs.Count()
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取租户失败: " + err.Error()}
_ = c.ServeJSON()
return
}
var rows []models.SystemTenant
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取租户失败: " + err.Error()}
_ = c.ServeJSON()
return
}
packageNames := tenantPackageNameMap()
userCounts := tenantUserCountMap()
list := make([]tenantDTO, 0, len(rows))
for _, t := range rows {
dto := toTenantDTO(t)
dto.PackageName = packageNames[t.PackageID]
dto.UserUsed = userCounts[t.ID]
list = append(list, dto)
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"list": list,
"total": total,
},
}
_ = c.ServeJSON()
}
// GetTenantDetail 获取租户详ćƒ
// GET /platform/tenant/getTenantDetail/:id
func (c *PlatformTenantController) GetTenantDetail() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
_ = c.ServeJSON()
return
}
var t models.SystemTenant
err = models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", id).One(&t)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "租户不存在"}
_ = c.ServeJSON()
return
}
dto := toTenantDTO(t)
if pkg, err := services.GetTenantPackage(t.ID); err == nil && pkg != nil {
if pkg.ID != t.PackageID {
// 套餐已因到期自动降级,原到期时间不再有效
dto.PackageExpireTime = nil
dto.DaysRemaining = 0
}
dto.PackageID = pkg.ID
dto.PackageName = pkg.Name
}
if used, err := services.CountTenantUsers(t.ID); err == nil {
dto.UserUsed = used
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": dto,
}
_ = c.ServeJSON()
}
type tenantPayload struct {
TenantCode string `json:"tenant_code"`
TenantName string `json:"tenant_name"`
ContactPerson string `json:"contact_person"`
ContactPhone string `json:"contact_phone"`
ContactEmail string `json:"contact_email"`
Address string `json:"address"`
Worktime string `json:"worktime"`
Status *int8 `json:"status"`
// 套餐与用户数上限(可选)
PackageID *uint64 `json:"package_id"`
UserQuota *int `json:"user_quota"`
Remark string `json:"remark"`
// 指定租户管理员(可选):把已有认证身份绑定为该租户管理员。
// AdminIdentityID 直接指定身份;也可只传 AdminAccount/AdminPhone/AdminEmail 按归并键定位。
// 统一认证原则:不新建账号、不覆盖原密码,只建「身份-企业」绑定并授予租户管理员角色。
AdminIdentityID uint64 `json:"admin_identity_id"`
AdminAccount string `json:"admin_account"`
AdminPhone string `json:"admin_phone"`
AdminEmail string `json:"admin_email"`
}
func (c *PlatformTenantController) parseTenantPayload() (tenantPayload, error) {
// 优先从表单读取(createTenant 使用 multipart/form-dataďź?
p := tenantPayload{
TenantCode: strings.TrimSpace(c.GetString("tenant_code")),
TenantName: strings.TrimSpace(c.GetString("tenant_name")),
ContactPerson: strings.TrimSpace(c.GetString("contact_person")),
ContactPhone: strings.TrimSpace(c.GetString("contact_phone")),
ContactEmail: strings.TrimSpace(c.GetString("contact_email")),
Address: strings.TrimSpace(c.GetString("address")),
Worktime: strings.TrimSpace(c.GetString("worktime")),
Remark: strings.TrimSpace(c.GetString("remark")),
}
if s := strings.TrimSpace(c.GetString("status")); s != "" {
if v, err := strconv.ParseInt(s, 10, 8); err == nil {
tmp := int8(v)
p.Status = &tmp
}
}
if s := strings.TrimSpace(c.GetString("package_id")); s != "" {
if v, err := strconv.ParseUint(s, 10, 64); err == nil {
p.PackageID = &v
}
}
if s := strings.TrimSpace(c.GetString("user_quota")); s != "" {
if v, err := strconv.Atoi(s); err == nil {
p.UserQuota = &v
}
}
if s := strings.TrimSpace(c.GetString("admin_identity_id")); s != "" {
if v, err := strconv.ParseUint(s, 10, 64); err == nil {
p.AdminIdentityID = v
}
}
p.AdminAccount = strings.TrimSpace(c.GetString("admin_account"))
p.AdminPhone = strings.TrimSpace(c.GetString("admin_phone"))
p.AdminEmail = strings.TrimSpace(c.GetString("admin_email"))
// 如果关键字段为空,尝试从 JSON body 解析(editTenant 靘莤 JSONďź?
if p.TenantName == "" && p.TenantCode == "" {
raw, _ := io.ReadAll(c.Ctx.Request.Body)
if len(raw) > 0 {
_ = json.Unmarshal(raw, &p)
}
}
return p, nil
}
// bindTenantAdminFromPayload 把已有认证身份绑定为新租户的管理员(不新建账号、不覆盖密码)。
// 优先 AdminIdentityID;否则按 AdminAccount/AdminPhone/AdminEmail 归并键(手机>邮箱>账号)定位。
// 返回 (是否绑定, 错误);未指定管理员时返回 (false, nil)。
func bindTenantAdminFromPayload(tid uint64, p tenantPayload) (bool, error) {
identityID := p.AdminIdentityID
if identityID == 0 && (p.AdminAccount != "" || p.AdminPhone != "" || p.AdminEmail != "") {
existed, hit := authsvc.LocateIdentityByKey(p.AdminAccount, p.AdminPhone, p.AdminEmail)
if !hit {
return false, errors.New("未找到该账号对应的认证身份")
}
identityID = existed.ID
}
if identityID == 0 {
return false, nil // 未指定管理员,跳过
}
identity, err := authsvc.GetIdentityByID(identityID)
if err != nil || identity == nil {
return false, errors.New("认证身份不存在")
}
// 企业内账号:显式指定 > 身份手机号 > user{id};姓名取身份昵称
account := p.AdminAccount
if account == "" && identity.Mobile != nil {
account = strings.TrimSpace(*identity.Mobile)
}
if account == "" {
account = fmt.Sprintf("user%d", identity.ID)
}
name := "租户管理员"
if identity.Nickname != nil && strings.TrimSpace(*identity.Nickname) != "" {
name = strings.TrimSpace(*identity.Nickname)
}
// 老表镜像身份层现有密码哈希(登录仍以认证中心为准,不在建租户时改密)
var pwd *string
if identity.PasswordHash != nil {
h := *identity.PasswordHash
pwd = &h
}
// isDefault=0:不改变该用户当前的默认企业
if _, err := services.BindTenantUser(tid, identity.ID, &account, &name, identity.Mobile, identity.Email, nil, nil, pwd, 0, 1, nil); err != nil {
return false, err
}
// 绑定全局「租户管理员」角色(拥有 backend 全权限)
if adminRoleID, err := models.GetTenantAdminRole(); err == nil && adminRoleID > 0 {
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).
Filter("uid", identity.ID).
Update(map[string]interface{}{"group_id": adminRoleID})
}
// 双写:同步到统一认证中心(建 yz_auth_tenant_user 绑定)
var row models.SystemTenantUser
if e := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).
Filter("uid", identity.ID).
One(&row); e == nil {
syncTenantUserToAuth(&row, false)
}
return true, nil
}
// CreateTenant 创建租户
// POST /platform/tenant/createTenant
func (c *PlatformTenantController) CreateTenant() {
p, _ := c.parseTenantPayload()
if strings.TrimSpace(p.TenantName) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "租户名称不能为空"}
_ = c.ServeJSON()
return
}
if strings.TrimSpace(p.TenantCode) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "租户编码不能为空"}
_ = c.ServeJSON()
return
}
// 校验编码唯一
cnt, err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("tenant_code", p.TenantCode).Count()
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败: " + err.Error()}
_ = c.ServeJSON()
return
}
if cnt > 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "租户编码已存在"}
_ = c.ServeJSON()
return
}
status := int8(1)
if p.Status != nil {
status = *p.Status
}
// 套餐与用户数上限:未指定套餐时使用默认套餐,用户数取其包含用户数(兜底 20)
packageID := uint64(0)
userQuota := services.DefaultTenantUserQuota
// 显式选择的套餐按套餐有效时长计费;未指定而兜底绑定默认套餐的不设置到期时间
explicitPackage := p.PackageID != nil && *p.PackageID > 0
var packageExpire *time.Time
if explicitPackage {
packageID = *p.PackageID
}
if packageID == 0 {
if pkg, err := services.GetDefaultTenantPackage(); err == nil && pkg != nil {
packageID = pkg.ID
if pkg.UserQuota > 0 {
userQuota = pkg.UserQuota
}
}
}
if packageID > 0 {
var pkg models.SystemTenantPackage
if err := models.Orm.QueryTable(new(models.SystemTenantPackage)).
Filter("id", packageID).
Filter("delete_time__isnull", true).
One(&pkg); err == nil {
if pkg.UserQuota > 0 {
userQuota = pkg.UserQuota
}
if explicitPackage {
e := services.PackageExpireFromNow(&pkg)
packageExpire = &e
}
}
}
if p.UserQuota != nil && *p.UserQuota > 0 {
userQuota = *p.UserQuota
}
if userQuota <= 0 {
userQuota = services.DefaultTenantUserQuota
}
t := models.SystemTenant{
TenantCode: p.TenantCode,
TenantName: p.TenantName,
ContactPerson: stringPtr(p.ContactPerson),
ContactPhone: stringPtr(p.ContactPhone),
ContactEmail: stringPtr(p.ContactEmail),
Address: stringPtr(p.Address),
Worktime: stringPtr(p.Worktime),
Status: status,
PackageID: packageID,
PackageExpireTime: packageExpire,
UserQuota: userQuota,
Remark: stringPtr(p.Remark),
}
id, err := models.Orm.Insert(&t)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败: " + err.Error()}
_ = c.ServeJSON()
return
}
// 指定已有身份为租户管理员(可选):只建绑定,不新建账号、不改密码。
// 绑定失败不影响租户创建结果,仅返回警告信息。
adminBound := false
adminWarn := ""
if bound, bErr := bindTenantAdminFromPayload(uint64(id), p); bErr != nil {
adminWarn = bErr.Error()
} else {
adminBound = bound
}
msg := "success"
if adminWarn != "" {
msg = "租户已创建,但指定管理员绑定失败: " + adminWarn
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": msg,
"data": map[string]interface{}{"id": id, "admin_bound": adminBound},
}
_ = c.ServeJSON()
}
// EditTenant 编辑租户
// POST /platform/tenant/editTenant/:id
func (c *PlatformTenantController) EditTenant() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
_ = c.ServeJSON()
return
}
p, _ := c.parseTenantPayload()
if strings.TrimSpace(p.TenantName) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "租户名称不能为空"}
_ = c.ServeJSON()
return
}
update := map[string]interface{}{
"tenant_name": p.TenantName,
"contact_person": p.ContactPerson,
"contact_phone": p.ContactPhone,
"contact_email": p.ContactEmail,
"address": p.Address,
"worktime": p.Worktime,
"remark": p.Remark,
}
if p.Status != nil {
update["status"] = *p.Status
}
if p.PackageID != nil {
pkgID := *p.PackageID
if pkgID > 0 {
// 套餐切换/续费:按套餐有效时长重新计费(同套餐未到期在原到期时间上顺延),不动用户数上限
if _, err := services.ApplyPackageToTenant(id, pkgID, false); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
_ = c.ServeJSON()
return
}
} else {
// 解绑套餐:回退未绑定状态(视为默认套餐),清除计费到期时间
update["package_id"] = 0
update["package_expire_time"] = nil
}
}
if p.UserQuota != nil {
// 用户数上限只增不减:不允许调低到当前已开用户数以下
used, _ := services.CountTenantUsers(id)
quota := *p.UserQuota
if quota > 0 && quota < used {
c.Data["json"] = map[string]interface{}{
"code": 400,
"msg": fmt.Sprintf("用户数上限不能小于该租户已开用户数(%d)", used),
}
_ = c.ServeJSON()
return
}
if quota > 0 {
update["user_quota"] = quota
}
}
_, err = models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", id).Update(update)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// DeleteTenant 删除租户
// DELETE /platform/tenant/deleteTenant/:id
func (c *PlatformTenantController) DeleteTenant() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
_ = c.ServeJSON()
return
}
_, err = models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", id).Delete()
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败: " + err.Error()}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// FindTenantCode 校验租户编码是否重复
// GET /platform/tenant/findTenantCode?tenant_code=xxxxxx
// 返回 code=200 表示可用;非200表示重复/不可用(前端会自动重新生成)
func (c *PlatformTenantController) FindTenantCode() {
code := strings.TrimSpace(c.GetString("tenant_code"))
if code == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tenant_code 不能为空"}
_ = c.ServeJSON()
return
}
cnt, err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("tenant_code", code).Count()
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "校验失败: " + err.Error()}
_ = c.ServeJSON()
return
}
if cnt > 0 {
c.Data["json"] = map[string]interface{}{"code": 409, "msg": "租户编码已存在"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok"}
_ = c.ServeJSON()
}