用户体系
这篇解决什么
后台员工和 C 端会员不是同一张用户表。读完能分清 AdminUser / MemberUser、从 LoginUser 取当前人、接账号/短信/社交登录,以及在用户管理里新增管理员。
默认端口 48080。管理端前缀 /admin-api,会员端前缀 /app-api。Token 认证见 功能权限与数据权限。
示意图:管理后台和办公 UniApp 走管理员用户;会员端走会员用户。令牌解析在框架层完成。
两套用户
AdminUser 服务 B 端:PC 管理端、办公 UniApp,接口在 /admin-api/**。MemberUser 服务 C 端会员,接口在 /app-api/**。
两边登录都发 OAuth2 Token,请求头 Authorization: Bearer {accessToken}。令牌落在同一张表,用 user_type 区分。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
AdminUserDO | 管理员。账号、部门、岗位 | ruoyi-office/yudao-module-system/.../dal/dataobject/user/AdminUserDO.java |
MemberUserDO | 会员。手机号、等级、积分 | ruoyi-office/yudao-module-member/.../dal/dataobject/user/MemberUserDO.java |
UserTypeEnum | MEMBER = 1,ADMIN = 2 | ruoyi-office/yudao-framework/yudao-common/.../enums/UserTypeEnum.java |
| 用户管理 | 左部门树、右用户表 | ruoyi-office-vben/apps/web-antd/src/views/system/user/ |
| 会员用户 | 后台查看、改等级与积分 | ruoyi-office-vben/apps/web-antd/src/views/member/user/ |
| 办公登录 | UniApp 调 /system/auth/login | ruoyi-office-uniapp/src/api/login.ts |

本系统截图:系统管理 → 用户管理。左侧按部门过滤,右侧维护管理员账号。
表结构
管理员表是 system_users(SQL Server 里 system_user 是关键字)。会员表是 member_user。两张表都继承 TenantBaseDO,带 tenant_id。
按本仓库生成的 ER:管理员挂部门;令牌表用 user_id + user_type 同时指向两类用户。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
system_users | @TableName("system_users")。密码 BCrypt,无独立盐字段 | .../dal/dataobject/user/AdminUserDO.java |
member_user | @TableName("member_user")。uk_mobile 按手机号唯一 | .../dal/dataobject/user/MemberUserDO.java |
system_dept | 部门树。orgType:0 部门,1 公司 | ruoyi-office/yudao-module-system/.../dal/dataobject/dept/DeptDO.java |
system_user_post | 用户—岗位 | .../dal/dataobject/dept/UserPostDO.java |
system_oauth2_access_token | 访问令牌。userInfo 里放昵称、部门 | .../dal/dataobject/oauth2/OAuth2AccessTokenDO.java |
AdminUserDO.postIds 是 JSON 数组,列表展示用;真正的岗位关联在 system_user_post。deptId 是主部门。多组织开启后,会话里的当前部门可以和主部门不同。
为什么拆两张表
管理员要部门、岗位、角色;会员要等级、积分、标签。字段和迭代节奏都不一样,拆表比加 type 再挂扩展表更干净。共享表(令牌、社交绑定)用 user_type 指向 UserTypeEnum。
用户类型
UserTypeEnum 是全局枚举。写跨类型表时不要自己发明数字。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
MEMBER | 1,C 端会员 | yudao-framework/yudao-common/.../enums/UserTypeEnum.java |
ADMIN | 2,管理后台 / 办公端 | 同上 |
登录成功后,OAuth2TokenServiceImpl#createAccessToken 把 userType 写入令牌。过滤器还原 LoginUser 时带着这个类型。
当前登录用户
业务代码用 SecurityFrameworkUtils,不要自己解 Token。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
getLoginUser() | 完整 LoginUser | ruoyi-office/yudao-framework/yudao-spring-boot-starter-security/.../util/SecurityFrameworkUtils.java |
getLoginUserId() | 用户编号,最常用 | 同上 |
getLoginUserNickname() | 昵称。读 LoginUser.info | 同上 |
getLoginUserDeptId() | 当前部门。读 LoginUser.info | 同上 |
LoginUser | id / userType / info / tenantId | .../security/core/LoginUser.java |
Long userId = SecurityFrameworkUtils.getLoginUserId();
Long deptId = SecurityFrameworkUtils.getLoginUserDeptId();昵称和部门只对管理员有值
OAuth2TokenServiceImpl#buildUserInfo 在 ADMIN 时写入 nickname、deptId。MEMBER 目前返回空 Map。会员端不要用这两个方法当业务字段,去查 member_user。
多组织开启时,buildUserInfo 会用刷新令牌对应的会话部门覆盖 AdminUserDO.deptId。getLoginUserDeptId() 拿到的是当前任职部门,不一定是主部门。
要在 Token 里多带字段(例如手机号),改 buildUserInfo,再在 SecurityFrameworkUtils 加对应 getXxx()。会员侧要先把读取补上,不要只加工具方法。
if (userType.equals(UserTypeEnum.ADMIN.getValue())) {
AdminUserDO user = adminUserService.getUser(userId);
return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getNickname())
.put(LoginUser.INFO_KEY_DEPT_ID, StrUtil.toStringOrNull(deptId)).build();
} else if (userType.equals(UserTypeEnum.MEMBER.getValue())) {
return Collections.emptyMap();
}路径:ruoyi-office/yudao-module-system/.../service/oauth2/OAuth2TokenServiceImpl.java。
账号密码登录
管理后台用 username + password。会员端用 mobile + password。返回体都是 userId / accessToken / refreshToken / expiresTime。
验证码开关见 验证码。开发环境可关 yudao.captcha.enable。
管理后台
POST /admin-api/system/auth/login,@PermitAll。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 接口 | @RequestMapping("/system/auth") | ruoyi-office/yudao-module-system/.../controller/admin/auth/AuthController.java |
| 请求 | username / password,可选 socialType + socialCode + socialState | .../controller/admin/auth/vo/AuthLoginReqVO.java |
| 响应 | AuthLoginRespVO | .../controller/admin/auth/vo/AuthLoginRespVO.java |
| 实现 | 校验验证码 → authenticate → 发令牌 | .../service/auth/AdminAuthServiceImpl.java |
@PostMapping("/login")
@PermitAll
@Operation(summary = "使用账号密码登录")
public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
return success(authService.login(reqVO));
}authenticate 按用户名查 system_users,用 BCryptPasswordEncoder 比对密码。账号不存在、密码错、状态停用都会记登录日志。
请求里带上 socialType 时,同一请求会把社交账号绑到这个管理员。
会员端
POST /app-api/member/auth/login。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 接口 | @RequestMapping("/member/auth") | ruoyi-office/yudao-module-member/.../controller/app/auth/AppAuthController.java |
| 请求 | mobile / password,可选社交绑定字段 | .../controller/app/auth/vo/AppAuthLoginReqVO.java |
| 响应 | 多一个 openid(社交登录时) | .../controller/app/auth/vo/AppAuthLoginRespVO.java |
@PostMapping("/login")
@PermitAll
@Operation(summary = "使用手机 + 密码登录")
public CommonResult<AppAuthLoginRespVO> login(@RequestBody @Valid AppAuthLoginReqVO reqVO) {
return success(authService.login(reqVO));
}办公 UniApp 不是会员登录,它调的是 /system/auth/login,用户仍在 system_users。
短信验证码登录
先发码,再带 mobile + code 登录。场景号在 SmsSceneEnum:管理员登录 21,会员登录 1。
管理后台
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 发码 | POST /system/auth/send-sms-code | AuthController#sendLoginSmsCode |
| 登录 | POST /system/auth/sms-login | AuthController#smsLogin |
| 场景 | ADMIN_MEMBER_LOGIN = 21 | yudao-module-system-api/.../enums/sms/SmsSceneEnum.java |
管理员发码前会查手机号是否已在 system_users。没有对应账号直接报错,不会顺手注册。
会员端
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 发码 | POST /member/auth/send-sms-code | AppAuthController#sendSmsCode |
| 登录 | POST /member/auth/sms-login | AppAuthController#smsLogin |
| 自动建用户 | MemberUserService#createUserIfAbsent | .../service/user/MemberUserServiceImpl.java |
短信登录时,手机号若不存在会插入一条 member_user,昵称默认 用户 + 6 位随机数。这个接口同时承担注册。
管理员短信登录不会自动开户
会员可以「先登录再落档」。管理员必须先在用户管理里建号,或打开注册开关走 /system/auth/register。不要把会员的 createUserIfAbsent 抄到管理端。
三方登录
两端流程相同:先拿授权地址,再用 code 换登录态。未绑定的管理员不能快捷登录;未绑定的会员会先建用户再绑定。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 管理端跳转 | GET /system/auth/social-auth-redirect | AuthController |
| 管理端快捷登录 | POST /system/auth/social-login | 同上 |
| 会员跳转 | GET /member/auth/social-auth-redirect | AppAuthController |
| 会员快捷登录 | POST /member/auth/social-login | 同上 |
| 微信小程序 | POST /member/auth/weixin-mini-app-login | 同上;phoneCode + loginCode |
管理员绑定登录复用账号密码接口:/system/auth/login 额外传 socialType / socialCode / socialState。会员绑定登录同样把这三项加到 /member/auth/login 或短信登录请求里。
小程序一键登录会按手机号 createUserIfAbsent,再绑定微信。细节见 OAuth 2.0。
注册
管理员默认不开放自助注册,在用户管理里新增。会员靠短信或社交登录自动建档。
管理后台
POST /admin-api/system/user/create,权限 system:user:create。
@PostMapping("/create")
@PreAuthorize("@ss.hasPermission('system:user:create')")
public CommonResult<Long> createUser(@Valid @RequestBody UserSaveReqVO reqVO) {
return success(userService.createUser(reqVO));
}路径:ruoyi-office/yudao-module-system/.../controller/admin/user/UserController.java。
另有 POST /system/auth/register。参数管理里 system.user.register-enabled 必须是 true,否则抛「注册关闭」。通过后会赋默认普通角色,并写登录令牌。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 后台新增 | 用户管理「新增用户」 | UserController#createUser |
| 自助注册 | 配置键 system.user.register-enabled | AdminUserServiceImpl#registerUser |
| 前端页 | 左树右表、导入导出、分配角色 | ruoyi-office-vben/apps/web-antd/src/views/system/user/ |
会员端
没有单独的「注册页」接口。sms-login、社交未绑定、微信小程序一键登录都会走 createUserIfAbsent 或 createUser。
后台「会员用户」只能改资料、等级、积分,不能当开户入口。
登出
登出就是删 Token。库和 Redis 一起清,已发出的 accessToken 立刻失效。
示意图:管理端 POST /system/auth/logout,会员端 POST /member/auth/logout。都是 @PermitAll。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 管理端登出 | 从请求头取 Token 再删 | AuthController#logout |
| 会员登出 | 同上 | AppAuthController#logout |
| 删令牌 | 同时清刷新令牌 | OAuth2TokenServiceImpl#removeAccessToken |
改密码也会作废令牌,所以这里不用无状态 JWT。过期时间看 system_oauth2_client 里 client_id = default。
停用账号不会自动踢线
status 改成停用后,已发出的 Token 要等过期或主动登出。需要立刻踢人时,调 removeAccessToken(userId, userType)。
配置与操作
管理员在 系统管理 → 用户管理 建账号,必填用户名、密码、部门;角色在用户页或角色页互绑。保存后立刻能登录,已发出的 Token 不会因为改部门自动失效。
停用只改 status,要立刻踢人再调删令牌。租户套餐裁的是菜单,不是这张用户表。开启见 系统功能设计。截图见上文用户列表。
