Files
yunzerwebsiteallinone/go/controllers/backend_admin_user.go
T
2026-09-29 18:25:09 +08:00

720 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controllers
import (
"encoding/json"
"errors"
"fmt"
"io"
"strconv"
"strings"
"server/models"
"server/pkg/jwtutil"
"server/pkg/passwordutil"
"server/services"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
type BackendAdminUserController struct {
beego.Controller
}
type backendUserInfoDTO struct {
ID uint64 `json:"id"`
Tid uint64 `json:"tid"`
Uid uint64 `json:"uid"`
GroupID uint64 `json:"group_id"`
Account *string `json:"account"`
Name *string `json:"name"`
Phone *string `json:"phone"`
Email *string `json:"email"`
Avatar *string `json:"avatar"`
Sex uint8 `json:"sex"`
Birth *string `json:"birth"`
IsDefault int8 `json:"is_default"`
Status int8 `json:"status"`
Remark *string `json:"remark"`
CreateTime string `json:"create_time"`
UpdateTime *string `json:"update_time"`
TenantName string `json:"tenant_name"`
TenantCode string `json:"tenant_code"`
}
type backendTenantUserPayload struct {
Tid uint64 `json:"tid"`
Uid uint64 `json:"uid"`
GroupID *uint64 `json:"group_id"`
Account *string `json:"account"`
Name *string `json:"name"`
Phone *string `json:"phone"`
Email *string `json:"email"`
Sex *uint8 `json:"sex"`
Birth *string `json:"birth"`
Password *string `json:"password"`
IsDefault *int8 `json:"is_default"`
Status *int8 `json:"status"`
Remark *string `json:"remark"`
}
type backendChangePasswordPayload struct {
ID uint64 `json:"id"`
Password string `json:"password"`
}
func formatBackendBirth(birth *string) *string {
if birth == nil {
return nil
}
s := strings.TrimSpace(*birth)
if s == "" {
return nil
}
if len(s) >= 10 {
date := s[:10]
return &date
}
return &s
}
func toBackendUserInfoDTO(u models.SystemTenantUser) backendUserInfoDTO {
var updateTime *string
if u.UpdateTime != nil {
s := u.UpdateTime.Format("2006-01-02 15:04:05")
updateTime = &s
}
tenantName := "未知租户"
tenantCode := ""
tenant, err := services.GetTenantByID(u.Tid)
if err == nil && tenant != nil {
tenantName = tenant.TenantName
tenantCode = tenant.TenantCode
}
// 头像取自认证中心身份(业务用户表 yz_system_tenant_user 不存头像)
var avatar *string
if a := services.GetIdentityAvatar(u.Uid); a != "" {
avatar = &a
}
return backendUserInfoDTO{
ID: u.ID,
Tid: u.Tid,
Uid: u.Uid,
GroupID: u.GroupID,
Account: u.Account,
Name: u.Name,
Phone: u.Phone,
Email: u.Email,
Avatar: avatar,
Sex: u.Sex,
Birth: formatBackendBirth(u.Birth),
IsDefault: u.IsDefault,
Status: u.Status,
Remark: u.Remark,
CreateTime: u.CreateTime.Format("2006-01-02 15:04:05"),
UpdateTime: updateTime,
TenantName: tenantName,
TenantCode: tenantCode,
}
}
// getJWTUidTid 取当前登录者标识:
// - uid:令牌里的 user_id,即认证中心 identity_id
// - tid:令牌里的 tenant_id
//
// 键名历史上有两套写法(uid/tid 与 userId/tenantId),这里都兼容;
// 中间件在 warn/off 模式下可能没有注入,故再兜底自行解析 Authorization 头,
// 否则会静默拿到 0(曾导致按 identity_id 直查老表、显示到别人的资料)。
func (c *BackendAdminUserController) getJWTUidTid() (uint64, uint64) {
var uid uint64
var tid uint64
data := c.Ctx.Input.Data()
for _, key := range []string{"uid", "userId"} {
if v, ok := data[key].(uint64); ok && v > 0 {
uid = v
break
}
}
for _, key := range []string{"tid", "tenantId"} {
if v, ok := data[key].(uint64); ok && v > 0 {
tid = v
break
}
}
if uid == 0 || tid == 0 {
auth := c.Ctx.Request.Header.Get("Authorization")
if parts := strings.SplitN(auth, " ", 2); len(parts) == 2 && parts[0] == "Bearer" {
if claims, err := jwtutil.ParseToken(parts[1]); err == nil {
if uid == 0 {
uid = uint64(claims.UserID)
}
if tid == 0 {
tid = uint64(claims.TenantId)
}
}
}
}
return uid, tid
}
func (c *BackendAdminUserController) parseTenantUserPayload() (backendTenantUserPayload, bool) {
var p backendTenantUserPayload
raw, _ := io.ReadAll(c.Ctx.Request.Body)
if err := json.Unmarshal(raw, &p); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return backendTenantUserPayload{}, false
}
return p, true
}
// findBackendTenantUser 按「用户在认证中心的 identity_id」或「记录主键 id」查租户用户。
//
// 注意查询顺序:令牌/前端传来的用户标识是认证中心 identity_id,而
// yz_system_tenant_user.uid 仍是老 uid(老表不在 uid 全量迁移范围内),
// 因此必须先换算再按 uid 查;先按主键 id 查会把 identity_id 命中的是
// 主键恰好相同的**别人**的记录(个人中心显示他人资料就是这么来的)。
func findBackendTenantUser(idOrUid uint64, jwtTid uint64) (*models.SystemTenantUser, error) {
if idOrUid == 0 {
return nil, errors.New("用户ID不能为空")
}
base := func() orm.QuerySeter {
qs := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("delete_time__isnull", true)
if jwtTid > 0 {
qs = qs.Filter("tid", jwtTid)
}
return qs
}
// 1) 当作 identity_id:换算成本租户老 uid 后匹配
var row models.SystemTenantUser
if uid := services.ResolveTenantUID(jwtTid, idOrUid); uid > 0 {
if err := base().Filter("uid", uid).One(&row); err == nil {
return &row, nil
}
}
// 2) 当作记录主键 id(用户管理页传的就是 row.id)
if err := base().Filter("id", idOrUid).One(&row); err == nil {
return &row, nil
}
// 3) 兜底:本来就是老 uid 的场景
err := base().Filter("uid", idOrUid).One(&row)
if err != nil {
return nil, err
}
return &row, nil
}
// GetAllUsers 获取当前租户后台用户列表
// GET /backend/getAllUsers
func (c *BackendAdminUserController) GetAllUsers() {
_, jwtTid := c.getJWTUidTid()
keyword := strings.TrimSpace(c.GetString("keyword"))
tid, _ := c.GetUint64("tid")
if jwtTid > 0 {
tid = jwtTid
}
cond := orm.NewCondition().And("delete_time__isnull", true)
if tid > 0 {
cond = cond.And("tid", tid)
}
if keyword != "" {
kwCond := orm.NewCondition().
Or("name__icontains", keyword).
Or("phone__icontains", keyword).
Or("email__icontains", keyword).
Or("account__icontains", keyword)
cond = cond.AndCond(kwCond)
}
var rows []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
SetCond(cond).
OrderBy("-is_default", "-id").
All(&rows)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败: " + err.Error()}
_ = c.ServeJSON()
return
}
list := make([]backendUserInfoDTO, 0, len(rows))
for _, row := range rows {
list = append(list, toBackendUserInfoDTO(row))
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"list": list,
"total": len(list),
},
}
_ = c.ServeJSON()
}
// GetTenantUsers 获取指定租户后台用户
// GET /backend/getTenantUsers/:tid
func (c *BackendAdminUserController) GetTenantUsers() {
tidStr := c.Ctx.Input.Param(":tid")
tid, _ := strconv.ParseUint(tidStr, 10, 64)
_, jwtTid := c.getJWTUidTid()
if jwtTid > 0 {
tid = jwtTid
}
if tid == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tid 不能为空"}
_ = c.ServeJSON()
return
}
var rows []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).
Filter("delete_time__isnull", true).
OrderBy("-is_default", "-id").
All(&rows)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败: " + err.Error()}
_ = c.ServeJSON()
return
}
list := make([]backendUserInfoDTO, 0, len(rows))
for _, row := range rows {
list = append(list, toBackendUserInfoDTO(row))
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"list": list,
"total": len(list),
},
}
_ = c.ServeJSON()
}
// GetUserInfo 获取后台租户用户详情
// GET /backend/getUserInfo/:id
func (c *BackendAdminUserController) GetUserInfo() {
jwtUid, jwtTid := c.getJWTUidTid()
idStr := c.Ctx.Input.Param(":id")
id, _ := strconv.ParseUint(idStr, 10, 64)
if id == 0 {
id = jwtUid
}
if id == 0 {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "未登录或非法请求"}
_ = c.ServeJSON()
return
}
u, err := findBackendTenantUser(id, jwtTid)
if err != nil || u == nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "后台用户信息不存在"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": toBackendUserInfoDTO(*u),
}
_ = c.ServeJSON()
}
// AddUser 添加后台租户用户
// POST /backend/addUser
func (c *BackendAdminUserController) AddUser() {
p, ok := c.parseTenantUserPayload()
if !ok {
return
}
_, jwtTid := c.getJWTUidTid()
if jwtTid > 0 {
p.Tid = jwtTid
}
if p.Tid == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tid 不能为空"}
_ = c.ServeJSON()
return
}
if p.Account == nil || strings.TrimSpace(*p.Account) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "account 不能为空"}
_ = c.ServeJSON()
return
}
if p.Password == nil || strings.TrimSpace(*p.Password) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "password 不能为空"}
_ = c.ServeJSON()
return
}
account := strings.TrimSpace(*p.Account)
p.Account = &account
hashed, err := passwordutil.Hash(strings.TrimSpace(*p.Password))
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
_ = c.ServeJSON()
return
}
p.Password = &hashed
if p.Uid == 0 {
// 统一走 uid:优先用认证中心 identity_id,与已全量迁移的业务表口径一致
uid, err := ensureTenantUIDForNewUser(
p.Tid,
strOrEmpty(p.Account),
strOrEmpty(p.Name),
strOrEmpty(p.Phone),
strOrEmpty(p.Email),
strOrEmpty(p.Password),
)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "生成用户ID失败"}
_ = c.ServeJSON()
return
}
p.Uid = uid
}
isDefault := int8(0)
if p.IsDefault != nil {
isDefault = *p.IsDefault
}
status := int8(1)
if p.Status != nil {
status = *p.Status
}
// 用户数配额校验:达到上限后需要平台端为租户增购用户数才能继续添加
if check, err := services.CheckTenantUserQuota(p.Tid, p.Uid); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "用户数校验失败: " + err.Error()}
_ = c.ServeJSON()
return
} else if !check.Allowed {
c.Data["json"] = map[string]interface{}{
"code": 400,
"msg": fmt.Sprintf("当前租户用户数已达上限(%d/%d),请联系平台管理员增购用户数", check.Used, check.Quota),
}
_ = c.ServeJSON()
return
}
id, err := services.BindTenantUser(
p.Tid,
p.Uid,
p.Account,
p.Name,
p.Phone,
p.Email,
p.Sex,
p.Birth,
p.Password,
isDefault,
status,
p.Remark,
)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "添加失败: " + err.Error()}
_ = c.ServeJSON()
return
}
if isDefault == 1 {
_ = services.SetDefaultTenant(p.Uid, p.Tid)
}
// 关联角色(group_id):0 表示未分配,前端展示为"未分配/全权限"
if p.GroupID != nil && *p.GroupID > 0 {
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("id", id).
Update(map[string]interface{}{"group_id": *p.GroupID})
} else if isDefault == 1 {
// 租户的第一个账号即租户管理员:自动绑定全局「租户管理员」角色(拥有 backend 全权限)。
if adminRoleID, err := models.GetTenantAdminRole(); err == nil && adminRoleID > 0 {
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("id", id).
Update(map[string]interface{}{"group_id": adminRoleID})
}
}
// 双写:同步到统一认证中心(读回最新记录,确保 group_id 已落库)
var created models.SystemTenantUser
if e := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).One(&created); e == nil {
syncTenantUserToAuth(&created)
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{"id": id},
}
_ = c.ServeJSON()
}
// EditUser 编辑后台租户用户
// POST /backend/editUser/:id
func (c *BackendAdminUserController) EditUser() {
idStr := c.Ctx.Input.Param(":id")
id, _ := strconv.ParseUint(idStr, 10, 64)
if id == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
_ = c.ServeJSON()
return
}
p, ok := c.parseTenantUserPayload()
if !ok {
return
}
_, jwtTid := c.getJWTUidTid()
row, err := findBackendTenantUser(id, jwtTid)
if err != nil || row == nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "用户不存在"}
_ = c.ServeJSON()
return
}
update := map[string]interface{}{}
if p.Tid > 0 && jwtTid == 0 {
update["tid"] = p.Tid
}
if p.Uid > 0 {
update["uid"] = p.Uid
}
if p.Account != nil {
account := strings.TrimSpace(*p.Account)
if account != "" {
update["account"] = account
}
}
if p.Name != nil {
update["name"] = *p.Name
}
if p.Phone != nil {
update["phone"] = *p.Phone
}
if p.Email != nil {
update["email"] = *p.Email
}
if p.Sex != nil {
update["sex"] = *p.Sex
}
if p.Birth != nil {
birth := strings.TrimSpace(*p.Birth)
if birth == "" {
update["birth"] = nil
} else {
update["birth"] = birth
}
}
if p.Password != nil && strings.TrimSpace(*p.Password) != "" {
hashed, err := passwordutil.Hash(strings.TrimSpace(*p.Password))
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
_ = c.ServeJSON()
return
}
update["password"] = hashed
}
if p.IsDefault != nil {
update["is_default"] = *p.IsDefault
}
if p.Status != nil {
update["status"] = *p.Status
}
if p.Remark != nil {
update["remark"] = *p.Remark
}
if p.GroupID != nil {
update["group_id"] = *p.GroupID
}
if len(update) == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无更新字段"}
_ = c.ServeJSON()
return
}
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("id", row.ID).
Update(update)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "编辑失败: " + err.Error()}
_ = c.ServeJSON()
return
}
if p.IsDefault != nil && *p.IsDefault == 1 {
_ = services.SetDefaultTenant(row.Uid, row.Tid)
}
// 双写:同步到统一认证中心
var edited models.SystemTenantUser
if e := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", row.ID).One(&edited); e == nil {
syncTenantUserToAuth(&edited)
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// DeleteUser 删除后台租户用户
// DELETE /backend/deleteUser/:id
//
// 用户管理里的账号属于租户层面的登录凭证(yz_system_tenant_user),与员工
//(yz_backend_employee)、人事档案(yz_backend_employee_file)、通讯录
//(yz_backend_erp_contact)是三份不同用途的数据:账号只用于登录系统,员工与档案属于企业数据。
// 因此这里只解除登录绑定,不做任何级联操作——删除账号不会影响员工档案和通讯录记录。
// 员工侧的离职/禁用走 deleteEmployee,档案侧的离职走 employeefile/delete。
func (c *BackendAdminUserController) DeleteUser() {
idStr := c.Ctx.Input.Param(":id")
id, _ := strconv.ParseUint(idStr, 10, 64)
if id == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
_ = c.ServeJSON()
return
}
_, jwtTid := c.getJWTUidTid()
row, err := findBackendTenantUser(id, jwtTid)
if err != nil || row == nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "用户不存在"}
_ = c.ServeJSON()
return
}
if err := services.UnbindTenantUser(row.ID); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败: " + err.Error()}
_ = c.ServeJSON()
return
}
// 双写:删除认证中心中的企业绑定
removeTenantUserFromAuth(row)
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// ChangePassword 修改后台租户用户密码
// POST /backend/changePassword
func (c *BackendAdminUserController) ChangePassword() {
var p backendChangePasswordPayload
raw, _ := io.ReadAll(c.Ctx.Request.Body)
if err := json.Unmarshal(raw, &p); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return
}
if p.ID == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
_ = c.ServeJSON()
return
}
if strings.TrimSpace(p.Password) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "password 不能为空"}
_ = c.ServeJSON()
return
}
_, jwtTid := c.getJWTUidTid()
row, err := findBackendTenantUser(p.ID, jwtTid)
if err != nil || row == nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "用户不存在"}
_ = c.ServeJSON()
return
}
hashed, err := passwordutil.Hash(strings.TrimSpace(p.Password))
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
_ = c.ServeJSON()
return
}
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("id", row.ID).
Update(map[string]interface{}{
"password": hashed,
})
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "修改失败: " + err.Error()}
_ = c.ServeJSON()
return
}
// 双写:同步新密码到统一认证中心
var updated models.SystemTenantUser
if e := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", row.ID).One(&updated); e == nil {
syncTenantUserToAuth(&updated)
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "修改成功"}
_ = c.ServeJSON()
}