Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Users、Mobile 与账号体系

oak-general-business 的“用户系统”并不是一个单表模型,而是一套拆得很细的身份体系。

User 只负责保存用户本身;手机号、账号名、验证码、改密过程、登录方式约束,则分别落在不同对象和不同规则层里。这样做的好处是:你可以非常清晰地控制“谁是用户本体”“哪些是登录凭证”“哪些只是一次性的验证过程”。

主要对象

这一章最重要的对象有:

  • User:保存姓名、昵称、性别、实名认证状态、密码相关状态、头像文件、地址等;
  • Mobile:手机号凭证;
  • LoginName:账号名凭证;
  • Captcha:短信/邮箱验证码;
  • ChangePasswordTemp:改密过程记录。

从业务角度看,这里最值得记住的一点是:User 不等于“所有登录信息”。登录凭证被拆在 MobileLoginName 等对象里,这恰好也是 Oak 后续能够灵活切换登录方式的基础。

组件

这部分已经提供了不少现成组件:

  • src/components/user/info
  • src/components/user/manage
  • src/components/user/register
  • src/components/user/password
  • src/components/mobile/login
  • src/components/mobile/manageList
  • src/components/mobile/upsert
  • src/components/changePassword/byPassword
  • src/components/changePassword/byMobile
  • src/components/my/info
  • src/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 更适合做“只有手机号这一路登录”的页面或弹窗。当前常用参数有:

  • onlyCaptcha
  • onlyPassword
  • callback
  • eventLoggedIn

从组件源码看,它当前最真实的行为是:

  • 发验证码时固定走 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:给当前用户自己改资料,常传 changeMobileUrlchangePasswordUrlauthenticateUrlonConfirm
  • user/manage:给后台做用户列表,常传 userDetailUrlcreateUserUrl
  • user/manage/detail:给后台做单个用户详情,常传 updateUserUrlonUserUpdateonUserPlay

taicang 前台个人资料页多处都把:

changePasswordUrl="/user/password/update"

直接传给 user/info,这样用户资料页和改密页就接起来了。

这三组组件还有几个源码里很明确的行为,值得直接写在文档里:

  • user/info 会直接读取 mobile$userextraFile$entity(tag1='avatar')wechatUser$user
  • user/info 如果当前 token 对应的就是本应用的微信用户,会允许同步微信资料
  • user/manage 的搜索不是只搜昵称,而是同时按 $textmobile$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:常用参数是 onceonSuccess
  • user/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 包括:

  • registerUserByLoginName
  • getChangePasswordChannels
  • updateUserPassword
  • mergeUser
  • bindByMobile
  • bindByEmail
  • sendCaptchaByMobile
  • sendCaptchaByEmail

也就是说,用户体系并不是靠“前端直接操作数据行”来完成的,很多关键动作已经被封装成了命名业务接口。

后台规则

这一章最需要熟悉的文件是 src/triggers/user.tssrc/checkers/user.ts

默认规则包括:

  • 新建用户时,初始状态默认是 shadow
  • 系统里创建出的第一个用户默认会成为 root
  • 更新密码相关字段时,会自动维护 hasPassword
  • 用户激活后,会把相关的 parasite 失效;
  • 实名认证时,会根据系统配置决定是否自动通过。

对应 checker 则负责限制敏感操作:

  • 非 root 用户不能任意禁用、删除关键用户;
  • 实名认证所需数据必须满足要求;
  • 某些敏感字段不能随意更新。

此外,src/triggers/mobile.ts 还会在删除手机号前清理相关的失效 token。

注入点

用户体系的注入点主要有两个:

  • 后端:通过 ogb0Triggersogb0Checkers 合并进入项目;
  • 前端:通过 features.token 暴露登录、绑手机号、发验证码等动作;注册和改密则通过对应 aspect 由 cache.exec(...) 调用。

所以如果你在项目里依赖了 oak-general-business,这些规则通常已经默认生效了,不需要再自己补一套重复逻辑。

项目中如何接入

用户体系在项目里通常不是直接 operate('user') 完事,而是走“组件 + aspect + token feature”的组合:

  • 注册页直接复用 src/components/user/register
  • 手机号登录与绑定复用 src/components/mobile/loginsrc/components/mobile/manageList
  • 改密复用 src/components/changePassword/byPasswordsrc/components/changePassword/byMobile
  • 发验证码、绑手机、绑邮箱统一走 features.token
  • 注册和改密则分别走 registerUserByLoginNameupdateUserPassword 这些 aspect

这也是为什么用户体系虽然没有单独的 user feature,但项目侧仍然很容易用起来。

一个推荐的新手接法

如果你要在现有项目里补一套“先绑手机号、再实名、再改资料”的前台流程,taicang 已经给了一个很好的参考:

  1. /user/authentication 页面里先用 mobile/upsert
  2. 绑定完成后切到 userAuth/upsert
  3. 资料页用 user/info
  4. 改密页单独挂 user/password/update
  5. 对敏感动作再加一层 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$userextraFile$entity(tag1='avatar')wechatUser$user

因为 user/infouser/managetoken/me 这些组件都直接按这套投影和关系去读数据。字段名改掉了,页面不会自己适配。