Files
EasyGoAdmin_GoFrame_AntdVue/internal/logic/user.go
T
2026-10-20 19:07:36 +08:00

988 lines
31 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.
// +======================================================================
// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2019-2026 半城风雨团队 版权所有
// +----------------------------------------------------------------------
// | 开源协议: Apache-2.0
// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
// +----------------------------------------------------------------------
// | 官方网站: https://www.easygoadmin.vip
// +----------------------------------------------------------------------
// | 研发团队: 小蚂蚁云团队 荣誉出品
// +======================================================================
// | 版权和免责声明:
// | 一、知识产权声明
// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
// | 二、使用限制与禁止条款
// | 您理解并同意,禁止将本产品用于以下任何行为:
// | 1. 任何违反中华人民共和国法律法规的用途;
// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
// | 三、责任豁免
// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
// | 四、违约与法律责任
// | 如发现任何违反本声明的行为,本团队有权:
// | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
// | 3. 保留进一步采取刑事控告等法律措施的权利。
// | 五、许可范围说明
// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
// | 成的一切经济损失及商誉损害。
// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
// | 许可协议》附件为准。
// +======================================================================
package logic
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"easygoadmin/api/v1"
"easygoadmin/internal/consts"
"easygoadmin/internal/dao"
"easygoadmin/internal/service"
"easygoadmin/utility/cache"
"easygoadmin/utility/crypto"
"easygoadmin/utility/excel"
"easygoadmin/utility/upload"
)
// ============================== 用户业务实现 ==============================
// sUser 用户业务实现
// 实现 service.IUser 接口,承载用户模块的业务逻辑
type sUser struct{}
func init() {
service.RegisterUser(&sUser{})
}
// userSerializeMaps 枚举字段到字典编码的映射
// {field}Text 由 dict_item 解析,字典管理页可修改文案
var userSerializeMaps = map[string]string{
"gender": "user_gender",
"status": "user_status",
}
// userSpec 用户模块声明
//
// Add、Update、Detail 涉及密码、城市、头像与角色装配,采用手写实现
var userSpec = ModuleSpec[v1.UserItem, v1.UserListItem, v1.UserDetailRes]{
Model: dao.User.Ctx,
LikeFields: []string{"realname"}, // querySchemas 姓名输入 → 模糊
EqFields: []string{"status", "dept_id", "level_id", "position_id", "tenant_id"},
OrderBy: "id DESC",
ListOrderBy: "sort, id",
SerializeMaps: userSerializeMaps,
FileFields: []string{"avatar"}, // 读侧自动补域名(file.domainUrl)
FileSaveDir: "user",
Filter: userFilter, // keyword 列 Page/List 有别 + role_id 子查询
DataScope: ApplyDataScope("dept_id"), // 角色数据权限(Page/List/导出生效)
BeforeDelete: userDeleteGuard,
AfterDelete: userCleanRelations, // 清理 user_role + 身份缓存
AfterBatchDelete: func(ctx context.Context, ids []int64) error {
return userCleanRelationsBatch(ctx, ids)
},
AfterPageList: userExpandNames, // 行内角色/部门/职级/岗位名批量补全
}
// userFilter 复杂筛选
//
// 关键词按入参分支,分页请求额外匹配手机号,并支持按角色ID过滤
func userFilter(ctx context.Context, m *gdb.Model, req any) *gdb.Model {
switch r := req.(type) {
case *v1.UserPageReq:
if r.Keyword != "" {
kw := "%" + r.Keyword + "%"
m = m.Where("(username LIKE ? OR realname LIKE ? OR mobile LIKE ?)", kw, kw, kw)
}
roleId := r.RoleId
if roleId == 0 {
roleId = r.Role // 前端传参名兼容(role)
}
if roleId > 0 {
m = m.WhereIn("id", dao.UserRole.Ctx(ctx).Where("role_id", roleId).Fields("user_id"))
}
case *v1.UserListReq:
if r.Keyword != "" {
kw := "%" + r.Keyword + "%"
m = m.Where("(username LIKE ? OR realname LIKE ?)", kw, kw)
}
}
return m
}
// userDeleteGuard 删除守卫
// 校验记录存在性
func userDeleteGuard(ctx context.Context, id int64) error {
if n, err := dao.User.Ctx(ctx).Where("id", id).Count(); err != nil {
return err
} else if n == 0 {
return gerror.NewCode(consts.CodeRecordNotFound)
}
return nil
}
// userCleanRelations 单条删除后清理
// 清理 user_role 关联并失效身份缓存
func userCleanRelations(ctx context.Context, id int64) error {
return userCleanRelationsBatch(ctx, []int64{id})
}
// userCleanRelationsBatch 批量删除后清理
// 清理 user_role 关联并失效身份缓存
func userCleanRelationsBatch(ctx context.Context, ids []int64) error {
if _, err := dao.UserRole.Ctx(ctx).WhereIn("user_id", ids).Delete(); err != nil {
return err
}
for _, id := range ids {
cache.Del(ctx,
consts.CacheKeyUserIdentity(int(id)),
consts.CacheKeyUserPerms(int(id)),
consts.CacheKeyUserRoles(int(id)),
)
}
return nil
}
// userExpandNames 分页行冗余装配
//
// 补全角色引用数组、城市编码数组、部门、职级、岗位名称及头像域名
func userExpandNames(ctx context.Context, list []*v1.UserItem, req any) error {
if len(list) == 0 {
return nil
}
ids := make([]int64, 0, len(list))
for _, item := range list {
ids = append(ids, item.Id)
}
// UserItem 无 dept/level/position/city id 列 → 按主键回查装配所需外键
rows, err := dao.User.Ctx(ctx).WhereIn("id", ids).
Fields("id, dept_id, level_id, position_id, province_code, city_code, district_code, street_code").All()
if err != nil {
return err
}
u := &sUser{}
levelIds := make([]int64, 0)
positionIds := make([]int64, 0)
deptIds := make([]int64, 0)
fkOf := make(map[int64]gdb.Record, len(rows))
for _, r := range rows {
fkOf[r["id"].Int64()] = r
if v := r["level_id"].Int64(); v > 0 {
levelIds = append(levelIds, v)
}
if v := r["position_id"].Int64(); v > 0 {
positionIds = append(positionIds, v)
}
if v := r["dept_id"].Int64(); v > 0 {
deptIds = append(deptIds, v)
}
}
nameOfLevel := u.namesById(ctx, dao.Level.Ctx(ctx), levelIds)
nameOfPosition := u.namesById(ctx, dao.Position.Ctx(ctx), positionIds)
nameOfDept := u.namesById(ctx, dao.Dept.Ctx(ctx), deptIds)
roleRefMap, err := u.userRoleRefs(ctx, ids)
if err != nil {
return err
}
for _, item := range list {
item.Roles = roleRefMap[item.Id]
if item.Roles == nil {
item.Roles = []v1.UserRoleRef{}
}
if fk, ok := fkOf[item.Id]; ok {
item.LevelName = nameOfLevel[fk["level_id"].Int64()]
item.PositionName = nameOfPosition[fk["position_id"].Int64()]
item.DeptName = nameOfDept[fk["dept_id"].Int64()]
city := make([]string, 0, 4)
for _, col := range []string{"province_code", "city_code", "district_code", "street_code"} {
if v := fk[col].String(); v != "" {
city = append(city, v)
}
}
item.City = city
}
if item.City == nil {
item.City = []string{}
}
}
return nil
}
// Page 用户分页
//
// 支持关键词、状态、部门、角色、职级、岗位等条件,行内冗余名称批量补全
func (s *sUser) Page(ctx context.Context, req *v1.UserPageReq) (res *v1.UserPageRes, err error) {
list, total, err := userSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize())
if err != nil {
return nil, err
}
return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil
}
// List 用户全量列表
//
// 供下拉选择使用,角色名在委托层后处理
func (s *sUser) List(ctx context.Context, req *v1.UserListReq) (res v1.UserListRes, err error) {
list, err := userSpec.List(ctx, req)
if err != nil {
return nil, err
}
// 精简列表不挂基座钩子 → 角色名在委托层后处理
roleMap, err := s.userRoleNames(ctx)
if err != nil {
return nil, err
}
for _, item := range list {
item.Role = roleMap[item.Id]
}
return derefSlice(list), nil
}
// Detail 用户详情
//
// 含角色ID数组与四级区划编码,城市信息为空时按编码回填名称
func (s *sUser) Detail(ctx context.Context, req *v1.UserDetailReq) (res *v1.UserDetailRes, err error) {
one, err := dao.User.Ctx(ctx).Where("id", req.Id).One()
if err != nil {
return nil, err
}
if len(one) == 0 {
return nil, gerror.NewCode(consts.CodeRecordNotFound)
}
roleIds, err := s.userRoleIds(ctx, req.Id)
if err != nil {
return nil, err
}
detail := s.toDetail(ctx, one, roleIds)
if detail.CityInfo == "" && len(detail.City) > 0 {
detail.CityInfo = ResolveCityNames(ctx, detail.City, ",")
}
return detail, nil
}
// Add 新增用户
//
// 用户名与手机号租户内唯一,校验租户用户上限,自动生成用户编码并写入 user_role
func (s *sUser) Add(ctx context.Context, req *v1.UserAddReq) (res *v1.IdRes, err error) {
tenantId := consts.GetTenantID(ctx)
if exists, e := s.usernameExists(ctx, req.Username, tenantId, 0); e != nil {
return nil, e
} else if exists {
return nil, gerror.NewCodef(consts.CodeRecordExists, "用户名已存在")
}
if req.Mobile != "" {
if exists, e := s.mobileExists(ctx, req.Mobile, tenantId, 0); e != nil {
return nil, e
} else if exists {
return nil, gerror.NewCodef(consts.CodeRecordExists, "手机号已被使用")
}
}
if err = s.checkTenantUserMax(ctx, tenantId); err != nil {
return nil, err
}
// 自动生成用户编码:{租户编码}-{租户内6位序号}(如 T0001-000020;平台租户用 SYS)
code, err := genUserCode(ctx, tenantId)
if err != nil {
return nil, err
}
// 双重加盐:外部 salt + bcrypt(password+salt),两值分别入 password/salt 列
salt := crypto.GenerateSalt()
hashed, err := crypto.HashPassword(req.Password, salt)
if err != nil {
return nil, err
}
status := req.Status
if status == 0 {
status = 1
}
data := map[string]any{
"tenant_id": tenantId,
"code": code,
"username": req.Username,
"password": hashed,
"salt": salt,
"realname": req.Realname,
"gender": req.Gender,
"mobile": req.Mobile,
"email": req.Email,
"dept_id": req.DeptId,
"level_id": req.LevelId,
"position_id": req.PositionId,
"city_info": req.CityInfo,
"address": req.Address,
"avatar": req.Avatar,
"intro": req.Intro,
"status": status,
"sort": req.Sort,
"note": req.Note,
"province_code": cityCodeAt(req.City, 0),
"city_code": cityCodeAt(req.City, 1),
"district_code": cityCodeAt(req.City, 2),
"street_code": cityCodeAt(req.City, 3),
}
// 头像 temp → 正式目录(入库形态为相对 uploadDir 的路径,剥离域名)
data["avatar"] = SaveFileField(ctx, req.Avatar, "user")
// 城市信息:优先由编码数组解析名称
if len(req.City) > 0 {
if info := ResolveCityNames(ctx, req.City, ","); info != "" {
data["city_info"] = info
}
}
result, err := dao.User.Ctx(ctx).Data(data).Insert()
if err != nil {
return nil, err
}
idv, _ := result.LastInsertId()
if err = s.AssignRoles(ctx, idv, req.Roles); err != nil {
return nil, err
}
return &v1.IdRes{Id: idv}, nil
}
// Update 修改用户
//
// 密码留空时不做修改,角色关联全量重写
func (s *sUser) Update(ctx context.Context, req *v1.UserUpdateReq) (res *v1.UserUpdateRes, err error) {
if exists, e := s.usernameExists(ctx, req.Username, consts.GetTenantID(ctx), req.Id); e != nil {
return nil, e
} else if exists {
return nil, gerror.NewCodef(consts.CodeRecordExists, "用户名已存在")
}
if req.Mobile != "" {
if exists, e := s.mobileExists(ctx, req.Mobile, consts.GetTenantID(ctx), req.Id); e != nil {
return nil, e
} else if exists {
return nil, gerror.NewCodef(consts.CodeRecordExists, "手机号已被使用")
}
}
data := map[string]any{
"username": req.Username,
"realname": req.Realname,
"gender": req.Gender,
"mobile": req.Mobile,
"email": req.Email,
"dept_id": req.DeptId,
"level_id": req.LevelId,
"position_id": req.PositionId,
"city_info": req.CityInfo,
"address": req.Address,
"avatar": req.Avatar,
"intro": req.Intro,
"status": req.Status,
"sort": req.Sort,
"note": req.Note,
"province_code": cityCodeAt(req.City, 0),
"city_code": cityCodeAt(req.City, 1),
"district_code": cityCodeAt(req.City, 2),
"street_code": cityCodeAt(req.City, 3),
}
if len(req.City) > 0 {
if info := ResolveCityNames(ctx, req.City, ","); info != "" {
data["city_info"] = info
}
}
data["avatar"] = SaveFileField(ctx, req.Avatar, "user")
if req.Password != "" {
salt := crypto.GenerateSalt()
hashed, e := crypto.HashPassword(req.Password, salt)
if e != nil {
return nil, e
}
data["password"] = hashed
data["salt"] = salt
}
if _, err = dao.User.Ctx(ctx).Where("id", req.Id).Data(data).Update(); err != nil {
return nil, err
}
if err = s.AssignRoles(ctx, req.Id, req.Roles); err != nil {
return nil, err
}
return &v1.UserUpdateRes{}, nil
}
// Status 用户状态
// 设置用户的正常或禁用状态
func (s *sUser) Status(ctx context.Context, req *v1.UserStatusReq) (res *v1.UserStatusRes, err error) {
if _, err = dao.User.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": req.Status}).Update(); err != nil {
return nil, err
}
return &v1.UserStatusRes{}, nil
}
// ResetPwd 重置用户密码
// 未传新密码时重置为系统默认密码
func (s *sUser) ResetPwd(ctx context.Context, req *v1.UserResetPwdReq) (res *v1.UserResetPwdRes, err error) {
if n, e := dao.User.Ctx(ctx).Where("id", req.UserId).Count(); e != nil {
return nil, e
} else if n == 0 {
return nil, gerror.New("用户不存在")
}
password := req.Password
if password == "" {
password = getDefaultPassword(ctx)
}
salt := crypto.GenerateSalt()
hashed, err := crypto.HashPassword(password, salt)
if err != nil {
return nil, err
}
if _, err = dao.User.Ctx(ctx).Where("id", req.UserId).Data(map[string]any{
"password": hashed,
"salt": salt,
}).Update(); err != nil {
return nil, err
}
return &v1.UserResetPwdRes{}, nil
}
// Delete 删除用户
// 软删记录并清理 user_role 关联
func (s *sUser) Delete(ctx context.Context, req *v1.UserDeleteReq) (res *v1.UserDeleteRes, err error) {
if err = userSpec.Delete(ctx, req.Id); err != nil {
return nil, err
}
return &v1.UserDeleteRes{}, nil
}
// BatchDelete 批量删除用户
// 按用户ID列表批量删除记录
func (s *sUser) BatchDelete(ctx context.Context, req *v1.UserBatchDeleteReq) (res *v1.UserBatchDeleteRes, err error) {
if err = userSpec.BatchDelete(ctx, req.Ids); err != nil {
return nil, err
}
return &v1.UserBatchDeleteRes{}, nil
}
// Import 导入用户
//
// 表格列序:姓名、性别、手机号、邮箱、部门、职级、岗位、城市、地址、用户名、简介、状态、备注;
// 密码统一取系统默认密码,用户编码自动生成;
// 性别与状态支持文本或数字,部门、职级、岗位支持名称或ID,城市为逗号分隔的名称串
func (s *sUser) Import(ctx context.Context, req *v1.UserImportReq) (res *v1.UserImportRes, err error) {
_, rows, err := excel.ReadSheet(req.File)
if err != nil {
return nil, err
}
res = &v1.UserImportRes{Total: len(rows)}
tenantId := consts.GetTenantID(ctx)
for i, row := range rows {
username := cellAt(row, 9)
if username == "" {
res.Failed++
res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:用户名为空", i+2))
continue
}
if exists, e := s.usernameExists(ctx, username, tenantId, 0); e != nil {
return nil, e
} else if exists {
res.Failed++
res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:用户名已存在", i+2))
continue
}
salt := crypto.GenerateSalt()
hashed, e := crypto.HashPassword(getDefaultPassword(ctx), salt)
if e != nil {
res.Failed++
res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:密码加密失败", i+2))
continue
}
code, e := genUserCode(ctx, tenantId)
if e != nil {
return nil, e
}
cityInfo := cellAt(row, 7)
codes := ResolveCityCodes(ctx, cityInfo)
if _, e = dao.User.Ctx(ctx).Data(map[string]any{
"username": username,
"realname": cellAt(row, 0),
"code": code,
"password": hashed,
"salt": salt,
"gender": parseGenderCell(cellAt(row, 1)),
"mobile": cellAt(row, 2),
"email": cellAt(row, 3),
"dept_id": resolveRefID(ctx, dao.Dept.Ctx(ctx), cellAt(row, 4)),
"level_id": resolveRefID(ctx, dao.Level.Ctx(ctx), cellAt(row, 5)),
"position_id": resolveRefID(ctx, dao.Position.Ctx(ctx), cellAt(row, 6)),
"city_info": cityInfo,
"address": cellAt(row, 8),
"intro": cellAt(row, 10),
"status": parseStatusCell(cellAt(row, 11)),
"note": cellAt(row, 12),
"tenant_id": tenantId,
"province_code": codes[0],
"city_code": codes[1],
"district_code": codes[2],
"street_code": codes[3],
}).Insert(); e != nil {
res.Failed++
res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:写入失败 %v", i+2, e))
continue
}
res.Success++
}
return res, nil
}
// Export 导出用户
//
// 列序对齐导出模板,每批 500 条防 OOM,写出 xlsx 并返回访问地址
func (s *sUser) Export(ctx context.Context, req *v1.UserExportReq) (res *v1.UserExportRes, err error) {
// 部门/职级/岗位 ID→名称(小维表全量加载,行内引用)
deptNames, err := idNameMap(ctx, dao.Dept.Ctx(ctx))
if err != nil {
return nil, err
}
levelNames, err := idNameMap(ctx, dao.Level.Ctx(ctx))
if err != nil {
return nil, err
}
positionNames, err := idNameMap(ctx, dao.Position.Ctx(ctx))
if err != nil {
return nil, err
}
headers := []string{"用户编码", "姓名", "性别", "手机号", "邮箱", "部门", "职级", "岗位", "城市", "地址", "用户名", "简介", "状态", "备注"}
var rows [][]any
// 分块遍历导出(每页重建查询防 OOM;并发写入下可能有翻页漂移,导出场景可接受)
if err = ChunkQuery(ctx, func(page, size int) *gdb.Model {
m := userSpec.applyFilters(ctx, dao.User.Ctx(ctx), req)
return m.Page(page, size).Order("id DESC")
}, 500, func(part gdb.Result) error {
for _, r := range part {
codes := make([]string, 0, 4)
for _, col := range []string{"province_code", "city_code", "district_code", "street_code"} {
if v := r[col].String(); v != "" {
codes = append(codes, v)
}
}
// 城市优先由四级编码解析名称,编码缺失时回退 city_info 存量值
city := ResolveCityNames(ctx, codes, ",")
if city == "" {
city = r["city_info"].String()
}
rows = append(rows, []any{
r["code"].String(), r["realname"].String(), genderText(r["gender"].Int()),
r["mobile"].String(), r["email"].String(),
deptNames[r["dept_id"].Int64()], levelNames[r["level_id"].Int64()],
positionNames[r["position_id"].Int64()], city, r["address"].String(),
r["username"].String(), r["intro"].String(),
userStatusText(r["status"].Int()), r["note"].String(),
})
}
return nil
}); err != nil {
return nil, err
}
fileName, _, err := excel.WriteSheet(upload.BaseDir(ctx), "用户数据", headers, rows)
if err != nil {
return nil, err
}
url := upload.GetFileUrl(ctx, fileName)
return &url, nil
}
// Document 用户打印信息
//
// 在用户详情基础上追加角色、部门、职级、岗位名称
func (s *sUser) Document(ctx context.Context, req *v1.UserDocumentReq) (res *v1.UserDocumentRes, err error) {
one, err := dao.User.Ctx(ctx).Where("id", req.Id).One()
if err != nil {
return nil, err
}
if len(one) == 0 {
return nil, gerror.NewCode(consts.CodeRecordNotFound)
}
roleIds, err := s.userRoleIds(ctx, req.Id)
if err != nil {
return nil, err
}
detail := s.toDetail(ctx, one, roleIds)
res = &v1.UserDocumentRes{UserDetailRes: *detail, RoleNames: []string{}}
if len(roleIds) > 0 {
roleRows, e := dao.Role.Ctx(ctx).WhereIn("id", roleIds).All()
if e == nil {
for _, r := range roleRows {
res.RoleNames = append(res.RoleNames, r["name"].String())
}
}
}
res.DeptName = s.dictName(ctx, dao.Dept.Ctx(ctx), one["dept_id"].Int())
res.LevelName = s.dictName(ctx, dao.Level.Ctx(ctx), one["level_id"].Int())
res.PositionName = s.dictName(ctx, dao.Position.Ctx(ctx), one["position_id"].Int())
return res, nil
}
// AssignRoles 重写用户角色关联
//
// 供新增与修改调用,完成后失效该用户的身份缓存
func (s *sUser) AssignRoles(ctx context.Context, userId int64, roleIds []int64) error {
if _, err := dao.UserRole.Ctx(ctx).Where("user_id", userId).Delete(); err != nil {
return err
}
for _, rid := range roleIds {
if rid <= 0 {
continue
}
if _, err := dao.UserRole.Ctx(ctx).Data(map[string]any{
"user_id": userId,
"role_id": rid,
}).Insert(); err != nil {
return err
}
}
s.clearIdentityCache(ctx, userId)
return nil
}
// ---------- 私有辅助 ----------
// clearIdentityCache 权限相关缓存失效
// 采用尽力而为策略
func (s *sUser) clearIdentityCache(ctx context.Context, userId int64) {
cache.Del(ctx,
consts.CacheKeyUserIdentity(int(userId)),
consts.CacheKeyUserPerms(int(userId)),
consts.CacheKeyUserRoles(int(userId)),
)
}
// usernameExists 判断用户名是否已存在
// tenantId 限定租户,excludeId 排除自身
func (s *sUser) usernameExists(ctx context.Context, username string, tenantId int, excludeId int64) (bool, error) {
m := dao.User.Ctx(ctx).Where("username", username).Where("tenant_id", tenantId)
if excludeId > 0 {
m = m.WhereNot("id", excludeId)
}
count, err := m.Count()
return count > 0, err
}
// mobileExists 判断手机号是否已存在
func (s *sUser) mobileExists(ctx context.Context, mobile string, tenantId int, excludeId int64) (bool, error) {
m := dao.User.Ctx(ctx).Where("mobile", mobile).Where("tenant_id", tenantId)
if excludeId > 0 {
m = m.WhereNot("id", excludeId)
}
count, err := m.Count()
return count > 0, err
}
// checkTenantUserMax 校验租户用户数上限
func (s *sUser) checkTenantUserMax(ctx context.Context, tenantId int) error {
if tenantId == 0 {
return nil
}
one, err := dao.Tenant.Ctx(ctx).Where("id", tenantId).One()
if err != nil || len(one) == 0 {
return nil
}
maxNum := one["max_user_num"].Int()
if maxNum <= 0 {
return nil
}
count, err := dao.User.Ctx(ctx).Where("tenant_id", tenantId).Count()
if err != nil {
return err
}
if count >= maxNum {
return gerror.NewCode(consts.CodeTenantUserLimit)
}
return nil
}
// genUserCode 生成用户编码
//
// 格式为 {租户编码}-{租户内6位序号},如 T0001-000020;平台租户前缀为 SYS。
// 序号取租户内未删除用户数加一,与现有编码顺延
func genUserCode(ctx context.Context, tenantId int) (string, error) {
prefix := "SYS"
if tenantId > 0 {
one, err := dao.Tenant.Ctx(ctx).Where("id", tenantId).Fields("code").One()
if err != nil {
return "", err
}
if len(one) == 0 {
return "", gerror.NewCode(consts.CodeTenantNotFound)
}
prefix = one["code"].String()
}
n, err := dao.User.Ctx(ctx).Where("tenant_id", tenantId).Count()
if err != nil {
return "", err
}
return fmt.Sprintf("%s-%06d", prefix, n+1), nil
}
// userRoleIds 查询用户的角色ID列表
func (s *sUser) userRoleIds(ctx context.Context, userId int64) ([]int64, error) {
rows, err := dao.UserRole.Ctx(ctx).Where("user_id", userId).All()
if err != nil {
return nil, err
}
ids := make([]int64, 0, len(rows))
for _, r := range rows {
ids = append(ids, r["role_id"].Int64())
}
return ids, nil
}
// userRoleRefs 批量查询用户角色引用数组
//
// 返回用户ID到角色引用的映射,一次查询避免 N+1
func (s *sUser) userRoleRefs(ctx context.Context, userIds []int64) (map[int64][]v1.UserRoleRef, error) {
result := make(map[int64][]v1.UserRoleRef)
if len(userIds) == 0 {
return result, nil
}
// 注意:不可给主表起别名——dao.Ctx 注入的软删条件使用完整表名,带别名时 MySQL 无法解析
urTable := dao.UserRole.Table()
roleData, err := dao.UserRole.Ctx(ctx).
LeftJoin(dao.Role.Table()+" r", urTable+".role_id = r.id AND r.del_flag=0").
WhereIn(urTable+".user_id", userIds).
Fields(urTable + ".user_id, r.id, r.name").
Order(urTable + ".role_id").
All()
if err != nil {
return nil, err
}
for _, rd := range roleData {
uid := rd["user_id"].Int64()
result[uid] = append(result[uid], v1.UserRoleRef{
Id: rd["id"].Int64(),
Name: rd["name"].String(),
})
}
return result, nil
}
// userRoleNames 查询用户ID到角色名的映射
// 多个角色名以逗号分隔
func (s *sUser) userRoleNames(ctx context.Context) (map[int64]string, error) {
result := make(map[int64]string)
urRows, err := dao.UserRole.Ctx(ctx).All()
if err != nil {
return nil, err
}
if len(urRows) == 0 {
return result, nil
}
byUser := make(map[int64][]int64)
roleIds := make([]int64, 0, len(urRows))
for _, r := range urRows {
uid, rid := r["user_id"].Int64(), r["role_id"].Int64()
byUser[uid] = append(byUser[uid], rid)
roleIds = append(roleIds, rid)
}
nameById := make(map[int64]string)
roleRows, err := dao.Role.Ctx(ctx).WhereIn("id", roleIds).All()
if err != nil {
return nil, err
}
for _, r := range roleRows {
nameById[r["id"].Int64()] = r["name"].String()
}
for uid, ids := range byUser {
names := ""
for i, rid := range ids {
if i > 0 {
names += ","
}
names += nameById[rid]
}
result[uid] = names
}
return result, nil
}
// toDetail 数据库记录转换为用户详情
func (s *sUser) toDetail(ctx context.Context, row gdb.Record, roleIds []int64) *v1.UserDetailRes {
if roleIds == nil {
roleIds = []int64{}
}
city := make([]string, 0, 4)
for _, col := range []string{"province_code", "city_code", "district_code", "street_code"} {
if v := row[col].String(); v != "" {
city = append(city, v)
}
}
return &v1.UserDetailRes{
Id: row["id"].Int64(),
Username: row["username"].String(),
Realname: row["realname"].String(),
Avatar: upload.GetFileUrl(ctx, row["avatar"].String()), // 逃生舱手拼 DTO,直接调底层公共助手补域名
Gender: row["gender"].Int(),
GenderText: DictGetText(ctx, userSerializeMaps["gender"], row["gender"].Int()),
Mobile: row["mobile"].String(),
Email: row["email"].String(),
Roles: roleIds,
DeptId: row["dept_id"].Int(),
LevelId: row["level_id"].Int(),
PositionId: row["position_id"].Int(),
City: city,
CityInfo: row["city_info"].String(),
Address: row["address"].String(),
Intro: row["intro"].String(),
Status: row["status"].Int(),
StatusText: DictGetText(ctx, userSerializeMaps["status"], row["status"].Int()),
Sort: row["sort"].Int(),
Note: row["note"].String(),
}
}
// namesById 查询ID到名称的映射
// 传入的模型需已构造好上下文
func (s *sUser) namesById(ctx context.Context, m *gdb.Model, ids []int64) map[int64]string {
result := make(map[int64]string)
if len(ids) == 0 {
return result
}
rows, err := m.WhereIn("id", ids).Fields("id, name").All()
if err != nil {
return result
}
for _, r := range rows {
result[r["id"].Int64()] = r["name"].String()
}
return result
}
// dictName 按ID查询名称
// 适用于部门、职级、岗位等通用维表
func (s *sUser) dictName(ctx context.Context, m *gdb.Model, idv int) string {
if idv <= 0 {
return ""
}
one, err := m.Where("id", idv).Fields("name").One()
if err != nil || len(one) == 0 {
return ""
}
return one["name"].String()
}
// cityCodeAt 按下标取城市编码
// 下标越界时返回空串
func cityCodeAt(cities []string, i int) string {
if i < len(cities) {
return cities[i]
}
return ""
}
// cellAt 按下标取单元格文本
// 下标越界时返回空串
func cellAt(row []string, i int) string {
if i < len(row) {
return row[i]
}
return ""
}
// cellInt 按下标取单元格整数
// 解析失败时返回默认值
func cellInt(row []string, i, def int) int {
if v := cellAt(row, i); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}
// idNameMap 加载维表的ID到名称映射
// 供部门、职级、岗位导出人读化使用
func idNameMap(ctx context.Context, m *gdb.Model) (map[int64]string, error) {
rows, err := m.Fields("id, name").All()
if err != nil {
return nil, err
}
result := make(map[int64]string, len(rows))
for _, r := range rows {
result[r["id"].Int64()] = r["name"].String()
}
return result, nil
}
// resolveRefID 解析引用列
// 数字按ID处理,非数字按名称反查,未命中时返回 0
func resolveRefID(ctx context.Context, m *gdb.Model, v string) int64 {
v = strings.TrimSpace(v)
if v == "" {
return 0
}
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
return n
}
id, err := m.Where("name", v).Fields("id").Value()
if err != nil {
return 0
}
return id.Int64()
}
// parseGenderCell 解析性别单元格
// 支持男、女、保密文本或 1、2、3 数字,其余取保密
func parseGenderCell(v string) int {
switch strings.TrimSpace(v) {
case "男":
return 1
case "女":
return 2
case "保密":
return 3
}
if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n >= 1 && n <= 3 {
return n
}
return 3
}
// parseStatusCell 解析状态单元格
// 支持正常、禁用文本或 1、2 数字,其余取正常
func parseStatusCell(v string) int {
switch strings.TrimSpace(v) {
case "正常":
return 1
case "禁用":
return 2
}
if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n > 0 {
return n
}
return 1
}
// genderText 性别值转文本
func genderText(g int) string {
switch g {
case 1:
return "男"
case 2:
return "女"
default:
return "保密"
}
}
// userStatusText 用户状态值转文本
func userStatusText(st int) string {
if st == 1 {
return "正常"
}
return "禁用"
}