// +====================================================================== // | 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 "禁用" }