Users、Mobile 与账号体系
oak-general-business 的“用户系统”并不是一个单表模型,而是一套拆得很细的身份体系。
User 只负责保存用户本身;手机号、账号名、验证码、改密过程、登录方式约束,则分别落在不同对象和不同规则层里。这样做的好处是:你可以非常清晰地控制“谁是用户本体”“哪些是登录凭证”“哪些只是一次性的验证过程”。
主要对象
这一章最重要的对象有:
User:保存姓名、昵称、性别、实名认证状态、密码相关状态、头像文件、地址等;Mobile:手机号凭证;LoginName:账号名凭证;Captcha:短信/邮箱验证码;ChangePasswordTemp:改密过程记录。
从业务角度看,这里最值得记住的一点是:User 不等于“所有登录信息”。登录凭证被拆在 Mobile、LoginName 等对象里,这恰好也是 Oak 后续能够灵活切换登录方式的基础。
组件
这部分已经提供了不少现成组件:
src/components/user/infosrc/components/user/managesrc/components/user/registersrc/components/user/passwordsrc/components/mobile/loginsrc/components/mobile/manageListsrc/components/mobile/upsertsrc/components/changePassword/byPasswordsrc/components/changePassword/byMobilesrc/components/my/infosrc/components/my/avatar
这些组件大多不是独立运行的,它们通常和后面的 Token 能力一起使用。
mobile/upsert 常用参数
oak-general-business/src/components/mobile/upsert/index.ts 最常用的三个参数是:
userId:要给哪个用户绑手机号maxNum:同一个用户最多允许绑定多少手机号onFinish:绑定完成后的回调
taicang/src/pages/frontend/user/authentication/web.pc.tsx 里就是:
<MobileUpsert
maxNum={5}
userId={userId}
oakPath={oakFullpath + '.mobiles'}
onFinish={() => {
onFinishByMobile();
}}
/>
mobile/login 常用参数
mobile/login 更适合做“只有手机号这一路登录”的页面或弹窗。当前常用参数有:
onlyCaptchaonlyPasswordcallbackeventLoggedIn
从组件源码看,它当前最真实的行为是:
- 发验证码时固定走
features.token.sendCaptcha('mobile', mobile, 'login') - 验证码登录时固定走
features.token.loginByMobile(mobile, captcha) - 小程序环境可以直接调
features.token.getWechatMpUserPhoneNumber(code)拉手机号 - 验证码发送节流时间会写到本地存储里,开发环境默认 10 秒,生产环境默认 60 秒
这意味着如果项目里已经确定只允许手机号登录,不需要再用大而全的 user/login 做裁剪,直接用 mobile/login 更直接。
mobile/manageList 的适用场景
这个组件比 mobile/upsert 更轻,它更像“当前数据节点里的手机号数组编辑器”。当前源码层面的行为非常简单:
addItem({ mobile: '' })直接增加一条空手机号updateItem(...)就地修改某条手机号removeItem(...)删除当前项
所以它更适合:
- 后台用户详情页里的手机号维护
- 不需要验证码校验的内部资料编辑页
- 纯 Oak 数据编辑流里的手机号列表节点
如果场景要求真正发验证码、绑定当前登录用户、控制最大绑定数,还是应该优先用 mobile/upsert。
userAuth/upsert 常用参数
当前实名认证编辑组件位于 oak-general-business/src/components/userAuth/upsert/index.ts,旧的 components/user/authenticate 已不存在。它最关键的参数是:
userId:创建认证记录时绑定的用户;origin:证件照片上传走哪个 COS 来源idCardType:可选的固定证件类型;传入后前端不允许切换autoUpload:是否自动上传证件图片needUploadPhotos:是否要求必须上传证件照片
组件在没有 oakId 的创建态下,会结合 userId 与当前 application 的 systemId 初始化 userAuth;提交动作仍由挂载它的页面或父组件通过当前 Oak 路径执行,不再通过旧的 onFinish 回调提交。
近期用户证件校验类型已经修正,认证链路会按 idCardType 区分身份证、护照和港澳台通行证等场景。项目层不要把证件类型当成普通字符串随意扩展;如果确实要新增类型,需要同时补实体枚举、entityDesc.locales.v.idCardType、证件照片标签和 checker 校验。
user/register 的真实依赖
user/register 不是一个固定规则的静态注册表单,它会在 ready() 时动态读取两套配置:
- 当前应用的
applicationPassport,找出loginName对应的passport.config - 当前系统的
system.config.Password
这意味着它会直接受这些配置影响:
loginName的最小/最大长度loginName是否启用正则校验- 当前应用是否允许注册
- 密码最小/最大长度
- 密码是否强校验
- 密码存储模式是否为
sha1
所以项目里如果发现“注册页规则和登录页规则不一致”,优先去看:
ApplicationPassport对应的loginName配置System.config.Password
而不是先怀疑前端组件本身。
user/info / user/manage / user/manage/detail
这三组组件分别解决不同层级的问题:
user/info:给当前用户自己改资料,常传changeMobileUrl、changePasswordUrl、authenticateUrl、onConfirmuser/manage:给后台做用户列表,常传userDetailUrl、createUserUrluser/manage/detail:给后台做单个用户详情,常传updateUserUrl、onUserUpdate、onUserPlay
taicang 前台个人资料页多处都把:
changePasswordUrl="/user/password/update"
直接传给 user/info,这样用户资料页和改密页就接起来了。
这三组组件还有几个源码里很明确的行为,值得直接写在文档里:
user/info会直接读取mobile$user、extraFile$entity(tag1='avatar')、wechatUser$useruser/info如果当前 token 对应的就是本应用的微信用户,会允许同步微信资料user/manage的搜索不是只搜昵称,而是同时按$text和mobile$user.mobile $startsWith查user/manage/detail在 root 查看他人时会额外暴露play动作,底层直接调用features.token.switchTo(...)
也就是说,如果项目想复用这些组件,实体关系最好保持和公共包一致;尤其头像、手机号、微信绑定关系不要随意改名。
my/info 更适合做轻量个人中心
如果页面只是“我的资料卡片”,不需要完整的 user/info 编辑表单,那么更适合直接用 my/info。它和 user/info 的区别在于:
- 数据直接从
features.token.getUserInfo()取 - 默认展示昵称/姓名、手机号、实名状态、用户状态、性别
- 可以直接调用
features.token.logout() - 支持用
updateAttribute(attr, value)就地更新当前用户字段
它还带一个很实用的参数:
showLogout
所以项目里的:
- “我的”首页
- 个人中心头部卡片
- 轻量版账户信息页
通常更适合挂 my/info,而不是一开始就上完整的 user/info。
user/password/update / user/password/verify
这两组组件项目里通常成对出现:
user/password/update:常用参数是once、onSuccessuser/password/verify:常用参数是onVerified
源码里这两个组件都会主动读取 system.config.Password,所以密码长度、正则、是否需要校验都应该在 System.config 里配,而不是写死在页面上。
user/password/update 还有一个很容易忽略的参数语义:
once: true时只输入一次密码,适合后台直接重置用户密码once: false时才会要求重复确认密码,适合用户自己修改密码
而 user/password/verify 则更像一个“敏感操作前置确认器”,它不会自己改密码,而是:
- 读取当前系统的密码模式
- 必要时按
sha1处理输入 - 调用
features.token.verifyPassword(...) - 验证成功后再执行
onVerified
所以删账号、切身份、提现申请前确认之类的动作,都很适合先包一层这个组件。
changePassword/byPassword / changePassword/byMobile
这两组组件和 user/password/update 不完全是一回事。前者更像“完整找回/修改密码流程页”,后者更像“密码输入控件”。
changePassword/byPassword 的真实行为是:
- 读取当前系统密码策略
- 调
updateUserPassword({ userId, prevPassword, newPassword }) - 如果密码模式是
sha1,会先做加密 - 后端返回失败次数时,会把
times写回页面状态
它更适合“用户已登录,知道旧密码,走常规改密”的页面。
changePassword/byMobile 的真实行为则是:
- 从当前用户的
mobile$user里取启用中的手机号 - 发送用途为
changePassword的验证码 - 调
updateUserPassword({ userId, mobile, captcha, newPassword }) - 同样会跟随系统密码模式决定是否
sha1
它更适合:
- 忘记旧密码但还能验证手机号
- 需要走短信校验后改密
- 前台个人中心里的“手机验证改密”
前端入口与 aspect
这一章没有单独的 user feature,用户体系的前端入口主要通过 features.token 间接暴露。
与用户资料和账号体系直接相关的 aspect 包括:
registerUserByLoginNamegetChangePasswordChannelsupdateUserPasswordmergeUserbindByMobilebindByEmailsendCaptchaByMobilesendCaptchaByEmail
也就是说,用户体系并不是靠“前端直接操作数据行”来完成的,很多关键动作已经被封装成了命名业务接口。
后台规则
这一章最需要熟悉的文件是 src/triggers/user.ts 和 src/checkers/user.ts。
默认规则包括:
- 新建用户时,初始状态默认是
shadow; - 系统里创建出的第一个用户默认会成为
root; - 更新密码相关字段时,会自动维护
hasPassword; - 用户激活后,会把相关的
parasite失效; - 实名认证时,会根据系统配置决定是否自动通过。
对应 checker 则负责限制敏感操作:
- 非 root 用户不能任意禁用、删除关键用户;
- 实名认证所需数据必须满足要求;
- 某些敏感字段不能随意更新。
此外,src/triggers/mobile.ts 还会在删除手机号前清理相关的失效 token。
注入点
用户体系的注入点主要有两个:
- 后端:通过
ogb0Triggers、ogb0Checkers合并进入项目; - 前端:通过
features.token暴露登录、绑手机号、发验证码等动作;注册和改密则通过对应 aspect 由cache.exec(...)调用。
所以如果你在项目里依赖了 oak-general-business,这些规则通常已经默认生效了,不需要再自己补一套重复逻辑。
项目中如何接入
用户体系在项目里通常不是直接 operate('user') 完事,而是走“组件 + aspect + token feature”的组合:
- 注册页直接复用
src/components/user/register - 手机号登录与绑定复用
src/components/mobile/login、src/components/mobile/manageList - 改密复用
src/components/changePassword/byPassword、src/components/changePassword/byMobile - 发验证码、绑手机、绑邮箱统一走
features.token - 注册和改密则分别走
registerUserByLoginName、updateUserPassword这些 aspect
这也是为什么用户体系虽然没有单独的 user feature,但项目侧仍然很容易用起来。
一个推荐的新手接法
如果你要在现有项目里补一套“先绑手机号、再实名、再改资料”的前台流程,taicang 已经给了一个很好的参考:
/user/authentication页面里先用mobile/upsert- 绑定完成后切到
userAuth/upsert - 资料页用
user/info - 改密页单独挂
user/password/update - 对敏感动作再加一层
user/password/verify
这样每一步都复用公共组件,页面层只负责路由和跳转,不去重写账号体系本身。
使用示例
1. 按账号名注册用户
src/components/user/register/index.ts 的真实调用方式是:
await this.features.cache.exec('registerUserByLoginName', {
loginName,
password: pwd,
});
如果系统密码策略要求 sha1,公共组件还会先按 Password.mode 做加密再提交。
2. 发送验证码并绑定手机号/邮箱
在项目组件里,推荐直接走 features.token:
await this.features.token.sendCaptcha('mobile', mobile, 'login');
await this.features.token.bindByMobile(mobile, captcha);
await this.features.token.sendCaptcha('email', email, 'confirm');
await this.features.token.bindByEmail(email, captcha);
这条链路会自动复用当前应用的验证码模板、渠道配置和校验规则。
3. 修改密码
src/components/changePassword/byPassword/index.ts 的调用方式是:
await this.features.cache.exec('updateUserPassword', {
userId,
prevPassword,
newPassword,
});
如果项目安全级别较高,还可以配合 features.token.verifyPassword(...) 先做敏感动作前的密码确认。
4. 在页面里组合手机号绑定和实名认证
下面这段就是 taicang 的真实思路,先手机号,后实名:
{activeIndex === 0 ? (
<MobileUpsert
maxNum={5}
userId={userId}
oakPath={oakFullpath + '.mobiles'}
onFinish={() => onFinishByMobile()}
/>
) : null}
{activeIndex === 1 ? (
<UserAuthenticate
oakId={userId}
oakPath={oakFullpath + '.user'}
onFinish={() => onFinishByAuthentication()}
/>
) : null}
使用建议
对新手来说,最重要的一条经验是:
把
User当成用户主体,把Mobile/LoginName当成登录凭证,把Captcha/ChangePasswordTemp当成流程记录。
这样你在读源码时就不会混乱,也更容易理解为什么有些逻辑写在 user.ts,有些逻辑却写在 token.ts。
另外还有一个很实际的注意点:
- 如果项目层扩展了
User,尽量不要改掉公共组件依赖的关系名和字段习惯,例如mobile$user、extraFile$entity(tag1='avatar')、wechatUser$user
因为 user/info、user/manage、token/me 这些组件都直接按这套投影和关系去读数据。字段名改掉了,页面不会自己适配。