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() }