Files
yunzerwebsiteallinone/go/controllers/backend_admin_user.go
T
2026-10-19 01:13:17 +08:00

803 lines
22 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/identity"
"server/pkg/passwordutil"
"server/services"
authsvc "server/services/auth"
"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"`
// IdentityID + VerifyPassword:绑定已有认证身份时使用。
// 归并键(手机/邮箱/账号)命中已有身份时,接口返回 409 要求验证;
// 前端补上 identity_id 与该账号的登录密码(verify_password)重新提交,
// 验证通过只建企业绑定,不新建身份、不覆盖原密码。
IdentityID uint64 `json:"identity_id"`
VerifyPassword string `json:"verify_password"`
}
type backendChangePasswordPayload struct {
ID uint64 `json:"id"`
Password string `json:"password"`
By string `json:"by"`
}
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,
}
}
// requireBackendIdentity 解析并校验 backend 端登录态;失败时已写好响应,调用方必须立即 return。
//
// 取代原 getJWTUidTid:那版从中间件注入的 ctx data 里取 uid/tid,
// 既不判端、取不到时又静默返回 0,而 0 会让「按企业过滤」退化成全局查询——
// 平台或小程序的令牌即可枚举、编辑别家企业的用户。
// 现在 uid/tid 只来自令牌,且必须是 backend 端且已绑定企业。
func (c *BackendAdminUserController) requireBackendIdentity() (uint64, uint64, bool) {
claims, err := identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndBackend)
if err != nil {
code := identity.Status(err)
c.Data["json"] = map[string]interface{}{"code": code, "msg": err.Error()}
_ = c.ServeJSON()
return 0, 0, false
}
if claims.TenantId <= 0 {
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "登录态未绑定企业"}
_ = c.ServeJSON()
return 0, 0, false
}
return uint64(claims.UserID), uint64(claims.TenantId), true
}
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 命中的是
// 主键恰好相同的**别人**的记录(个人中心显示他人资料就是这么来的)。
//
// byID=true 表示调用方明确传入的是 yz_system_tenant_user 主键
// (用户管理页的行数据,列表接口返回的 id 就是主键),
// 此时只按主键精确查询,禁止 identity_id/uid 换算——否则主键值
// 恰好等于别人老 uid 时会查到**别人**的记录(编辑李增炎却回显李志强)。
func findBackendTenantUser(idOrUid uint64, jwtTid uint64, byID bool) (*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
}
if byID {
var row models.SystemTenantUser
if err := base().Filter("id", idOrUid).One(&row); err != nil {
return nil, err
}
return &row, nil
}
// 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, ok := c.requireBackendIdentity()
if !ok {
return
}
keyword := strings.TrimSpace(c.GetString("keyword"))
// 用户列表只按登录态隔离:?tid= 不再生效(此前不传 tid 会直接返回全部企业的用户)
tid := jwtTid
if tid == 0 {
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "登录态未绑定企业"}
_ = c.ServeJSON()
return
}
cond := orm.NewCondition().And("delete_time__isnull", true).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, ok := c.requireBackendIdentity()
if !ok {
return
}
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[?by=id]
// by=id:前端明确传主键(用户管理页),只按主键查询;
// 缺省时兼容 identity_id / 老 uid(个人中心等场景)。
func (c *BackendAdminUserController) GetUserInfo() {
jwtUid, jwtTid, ok := c.requireBackendIdentity()
if !ok {
return
}
idStr := c.Ctx.Input.Param(":id")
id, _ := strconv.ParseUint(idStr, 10, 64)
byID := c.GetString("by") == "id"
if id == 0 && !byID {
id = jwtUid
}
if id == 0 {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "未登录或非法请求"}
_ = c.ServeJSON()
return
}
u, err := findBackendTenantUser(id, jwtTid, byID)
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, ok := c.requireBackendIdentity()
if !ok {
return
}
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
// ── 统一认证:一人一套账号密码,跨企业只建绑定 ─────────────────────────
// 分支①:前端携带 identity_id(409 验证后的二次提交)——
// 验证该身份的登录密码,通过后只建企业绑定,不新建身份、不覆盖原密码。
if p.IdentityID > 0 {
if strings.TrimSpace(p.VerifyPassword) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "请输入该账号在原企业的登录密码以验证身份"}
_ = c.ServeJSON()
return
}
ident, err := authsvc.GetIdentityByID(p.IdentityID)
if err != nil || ident == nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "认证身份不存在"}
_ = c.ServeJSON()
return
}
if !authsvc.VerifyIdentityPassword(ident, strings.TrimSpace(p.VerifyPassword)) {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "密码不正确,无法确认是同一账号"}
_ = c.ServeJSON()
return
}
// uid 统一为 identity_id,与已全量迁移的业务表口径一致
p.Uid = ident.ID
// 老表镜像身份层现有密码哈希(登录仍以认证中心为准)
if ident.PasswordHash != nil && *ident.PasswordHash != "" {
h := *ident.PasswordHash
p.Password = &h
} else {
// 身份层无密码(历史数据),仅镜像到老表,不影响统一认证
hashed, hErr := passwordutil.Hash(strings.TrimSpace(*p.Password))
if hErr != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": hErr.Error()}
_ = c.ServeJSON()
return
}
p.Password = &hashed
}
} else if p.Uid == 0 {
// 分支②:归并键(手机 > 邮箱 > 账号)命中已有身份 → 不覆盖,
// 返回 409 让前端提示「该账号已在哪些企业存在」,
// 用户输入原企业登录密码验证本人后,携 identity_id 重新提交(走分支①)。
if existed, hit := authsvc.LocateIdentityByKey(account, strOrEmpty(p.Phone), strOrEmpty(p.Email)); hit {
hints := make([]map[string]interface{}, 0)
if tenants, err := authsvc.ListTenantOptions(existed.ID); err == nil {
for _, t := range tenants {
hints = append(hints, map[string]interface{}{"tid": t.Tid, "tenant_name": t.TenantName})
}
}
c.Data["json"] = map[string]interface{}{
"code": 409,
"msg": "该账号已在其他企业存在,请输入原企业登录密码验证身份后绑定本企业",
"data": map[string]interface{}{
"need_verify": true,
"identity_id": existed.ID,
"tenants": hints,
},
}
_ = c.ServeJSON()
return
}
// 分支③:全新身份 —— password 必填,先建认证身份再拿 identity_id 当 uid
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
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, false)
}
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, ok := c.requireBackendIdentity()
if !ok {
return
}
row, err := findBackendTenantUser(id, jwtTid, c.GetString("by") == "id")
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, false)
}
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, ok := c.requireBackendIdentity()
if !ok {
return
}
row, err := findBackendTenantUser(id, jwtTid, c.GetString("by") == "id")
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, ok := c.requireBackendIdentity()
if !ok {
return
}
row, err := findBackendTenantUser(p.ID, jwtTid, p.By == "id")
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, true)
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "修改成功"}
_ = c.ServeJSON()
}