Oak 通用业务逻辑
oak-general-business 是 Oak 生态里最重要的公共业务包之一。它并不是一个“示例仓库”,而是一套已经拆好了对象、触发器、校验器、前端组件、aspect、endpoint、feature 和后台例程的通用业务底座。用户、登录、文件、微信、OAuth、消息、文章、地址这些看上去彼此独立的能力,在这个包里都已经被组织成了可复用的模块。
如果你只是把它当成“顺手依赖一下的工具包”,那么很快就会迷路。更好的理解方式是:
src/entities定义这套通用业务到底有哪些对象;src/checkers和src/triggers定义这些对象的默认业务规则;src/aspects和src/endpoints暴露可调用的业务入口;src/features把这些入口包装成前端可直接使用的能力;src/components提供已经写好的 Oak 组件;src/watchers和src/routines/start.ts则把后台补偿任务和启动注入点也一起准备好了。
近期 oak-general-business 又补上了系统翻译相关对象、组件和触发链路,并把 Area 数据推进到全球 locale 化。也就是说,通用业务包现在不只是“登录、文件、微信、系统配置”,还承担了一部分多语言内容生产和系统翻译基础能力。
这部分为什么要重新拆分
按照目录名直接理解 oak-general-business,很容易把能力划分错。
例如:
Passport、ApplicationPassport明明和登录相关,但它们一部分依赖System,另一部分又和Application强绑定;Token虽然是一个对象,但真正的登录逻辑主要写在aspects/token.ts和features/token.ts;HumanVerify不是captcha验证码实体,而是挂在登录、注册、发送验证码入口之前的人机校验策略;OAuth既包含“用第三方账号登录 Oak 应用”的能力,也包含“把 Oak 应用作为 OAuth 服务端”的能力;ToDo甚至不是一组自动注入的触发器,而是一组需要你在项目层手工调用的辅助函数。
所以本章不再沿用“看到一个对象就开一章”的机械拆法,而是按真实的功能域来组织:
System、Passport 与系统级配置Application、Domain 与应用装配Users、Mobile 与账号体系Token 与多端登录态HumanVerify 人机校验Invite 邀请归因UserEntityGrant 授权分享Parasite 寄生登录Session、Message 与通知ExtraFile 文件与对象存储WeChat 公众号/小程序能力Article 与内容树Address、Area 与地图能力SMS 与消息模板Subscription 订阅源ToDo 协作待办Livestream 直播流OAuth 客户端与第三方登录
这样的拆法更贴近实际开发时的思考顺序。
先建立组件地图
第一次接 oak-general-business,不要一头扎进 src/components 逐个翻。更高效的做法,是先建立一张“这类业务先看哪组组件”的脑图。
1. 登录、注册与身份绑定
这一组通常先看:
passport/*user/login/*user/registerchangePassword/*wechatLogin/*oauth/*
它们覆盖的其实不是一件事,而是“正式登录入口”这一整层:
- 用户名 / 密码 / 手机验证码登录
- 微信扫码登录
- OAuth 授权登录
- 账号密码修改和补齐
所以项目层做登录页时,通常不是从零画,而是先从这几组现成组件里挑合适的入口。
2. 授权分享、临时入口与关系分发
这组最值得优先看的组件是:
invite/landingmy/inviteuserEntityGrant/listuserEntityGrant/upsertuserEntityGrant/claimparasite/listparasite/upsertparasite/detailparasite/excess
这三组能力虽然都和“分享一个入口出去”有关,但语义完全不同:
invite更偏邀请来源归因,最终记录谁邀请了哪个 tokenuserEntityGrant更偏正式授权、对象关系分发parasite更偏临时身份、一次性或短期访问入口
如果项目里有“邀请成员”“分享授权链接”“给外部用户一个临时访问口”这类需求,通常先从这里选。
3. 会话、消息与通知面板
这一组最常直接复用的是:
session/listsession/forMessagesessionMessage/listsessionMessage/upsertmessage/listmessage/detailmy/message
实际项目里,最稳的拆法通常是:
- 左侧会话列表复用
session/list - 中间消息主体复用
session/forMessage - 单条消息输入或补发再按需用
sessionMessage/upsert
这样新项目很快就能先把“能聊起来”的骨架搭出来。
4. 文件、素材与对象存储
这组最常先用的是:
extraFile/uploadextraFile/commitextraFile/forUrlextraFile/avatarextraFile/gallerywechatMaterialLibrary
其中真正最值得先理解的,还是前面几章已经详细写过的:
upload负责选文件和上传临时对象commit负责把上传结果落成正式extraFileforUrl负责纯展示或 URL 场景复用
如果项目层只想做头像上传、附件上传、图片选择,通常先从这三组开始就够了。
5. 系统、应用、域与后台配置
后台管理页最常先复用的则是:
system/*application/*domain/*config/*platform/*subscription/*
这组组件的特点不是“直接面向终端用户”,而是适合:
- 系统配置后台
- 应用接入后台
- 域名与 COS 配置页
- 订阅源和内容配置页
所以项目里一旦要做“平台后台”或“系统管理台”,先看这组通常比先写页面壳更快。
6. 哪些能力本来就没有成品组件
文档里必须把这一点说死,否则新手很容易找半天:
ToDo当前没有现成 UI 组件,主要靠createToDo(...)/completeToDo(...)helper- 某些微信菜单、自动回复、标签管理能力虽然有后台组件,但前台业务页通常还是项目层自己组合
也就是说,oak-general-business 不是“所有对象都配了成品页面”,而是:
- 一部分能力给了完整组件
- 一部分能力只给实体、trigger、checker、aspect 或 helper
理解这条边界之后,阅读源码时就不容易误判。
先看接入点,再看具体模块
一个业务项目真正“接入” oak-general-business,至少会经过三个位置。
前端运行时装配
在当前模板里,前端运行时的主入口通常是 src/initialize.ts -> src/initialize.server.ts。make:dep 会在类似 bm-smart/src/initialize.server.ts 这样的文件里,把 oak-general-business 的:
checkerscommon configurationrender configurationfeatures
和项目自己的实现合并起来,然后再创建 oak-general-business 的 features:
const ogb0Features = createOgb0Features(totalFeatures);
Object.assign(totalFeatures, ogb0Features);
这一步决定了前端运行时能不能直接调用 features.token、features.extraFile、features.application 等能力。旧的 initialize.frontend.ts 只剩历史 DebugConnector 场景,不再是当前推荐的纯前台入口。
前端启动后的初始化
在类似 bm-smart/src/initializeFeatures.ts 的文件里,还会继续调用:
await initializeOgb0Features(features, accessConfiguration, undefined, [Qiniu, S3, Aliyun]);
这个初始化过程非常关键,它至少做了几件事:
- 注册 selection / operation rewriter;
- 调用
features.application.initialize(...)识别当前应用; - 在 web 环境下设置微信网页授权落地地址;
- 注册 COS 类,打通
extraFile上传; - 在小程序环境下按需自动登录。
后台启动注入
oak-general-business/src/routines/start.ts 当前注入了三个后台启动逻辑:
- 注册 selection / operation rewriter;
- 向
oak-common-aspect注入地图服务获取逻辑。 - 注册系统翻译 timer 的运行时环境,并按已有 system 配置重建调度任务。
对应的 src/routines/stop.ts 会注销全部系统翻译 timer 并清理运行时环境。翻译调度因此是完整的启动/停止生命周期能力,不只是某个页面发起的临时任务。
后台侧不需要在 initialize.frontend.ts 中手工拼接。server:start 使用的 AppLoader 会根据依赖图从项目和依赖包的 lib/... 中合并 trigger、checker、aspect、endpoint、watcher、timer、port 和 routine。也就是说,oak-general-business 不只是“提供几个对象”,而是真的会改造项目的前后端运行时。
最小接入示例
如果你是在自己的 Oak 项目里第一次接 oak-general-business,最小可用接法通常就三步。
1. 在依赖配置和生成文件中接入公共能力
当前推荐先在 src/configuration/dependency.ts 声明 oak-general-business,再依次执行 project:init、make:domain 和 make:dep。以 bm-smart 为例,生成后的 initialize.server.ts 会把项目自己的前端运行时配置和 oak-general-business 合并:
const totalCheckers = mergeConcatMany([checkers, ogb0Checkers])!;
const totalCommon = mergeConcatMany([common, ogb0Common])!;
const totalRender = mergeConcatMany([render, ogb0Render])!;
const ogb0Features = createOgb0Features(totalFeatures);
Object.assign(totalFeatures, ogb0Features);
这一步做完后,项目里才真正拥有:
features.tokenfeatures.applicationfeatures.extraFilefeatures.wechatSdkfeatures.template- 以及对应的 checker / common / render 装配
后端 trigger、checker、aspect、endpoint、watcher、timer、port 和 routine 则由 AppLoader 在服务端运行态按依赖图合并。
2. 应用启动后继续执行 initializeOgb0Features(...)
在 bm-smart/src/initializeFeatures.ts 里,真实写法是:
await initializeOgb0Features(
features,
accessConfiguration,
undefined,
[Qiniu, S3, Aliyun]
);
这一步会继续做四件关键的事:
- 注册 selection / operation rewriter;
- 调用
features.application.initialize(...)识别当前应用; - web 环境下设置微信落地地址;
- 给
features.extraFile注册 COS 实现; - 小程序环境下按需自动登录。
如果项目启用系统翻译,还需要在初始化、静态数据同步和权限升级脚本里一起考虑翻译相关实体和动作。只执行 make:locale 只能生成 i18n 数据,不能替代系统翻译状态和数据升级。
3. 业务页面里统一从 feature 取能力
项目层真正使用时,通常会像 bm-smart 这样写:
const application = this.features.application.getApplication();
const systemId = application.systemId;
const userId = this.features.token.getUserId(true);
const imageUrl = this.features.extraFile.getUrl(extraFile);
也就是说,项目层一般不会自己去重写“应用识别”“token 管理”“文件 URL 拼接”这些公共逻辑,而是直接站在 oak-general-business 的 feature 上继续写业务。
参考项目里的真实接法
如果你已经在看 haina-busi 和 taicang,会发现这两个项目接 oak-general-business 的方式其实很接近,而且都不是“单独只初始化 general-business 一次”这么简单。
1. 初始化阶段先同时创建 ogb0 和 opb1
这两个项目都会在:
src/initialize.server.tssrc/initializeFeatures.ts
里创建和初始化:
createOgb0Features(...)createOpb1Features(...)
前端运行时负责合并 checkers / common / render / features,后台侧的 aspects / triggers / watchers / timers / data / ports / routines 则由 AppLoader 按依赖图合并进服务端运行态。
2. 很多项目最终只调用 initializeOpb1Features(...)
这点非常容易让新手困惑。haina-busi/src/initializeFeatures.ts 和 taicang/src/initializeFeatures.ts / initializeFeatures.web.ts 实际都只调用了:
await initializeOpb1Features(features, accessConfiguration, config, cosClazzes);
之所以成立,不是因为它们“没用 general-business”,而是因为 oak-pay-business/src/features/index.ts 里的 initialize(...) 内部本来就先调用了:
await initializeGeneral(features, access, mergedConfig, clazzes);
也就是说:
- 只接
oak-general-business的项目,自己调用initializeOgb0Features(...) - 同时接
oak-pay-business的项目,通常直接调用initializeOpb1Features(...)就够了
3. 真实项目还会把启动注册写在 routines 里
oak-general-business 不只是组件和 feature。像 haina-busi、taicang 这类项目,还会在启动例程里继续注册:
- 短信实现:
registerSms(...)/registSms(...) - COS 实现:
registerCosBackend(...)/registerCos(...) - 消息类型:
registerMessageType(...) - 消息/通知处理器:
registerMessageHandler(...)、registerNotificationHandler(...)
所以新手读公共包时,最好把“初始化 feature”和“启动时注册具体供应商实现”一起看,不要只看页面层。
阅读源码时建议按这个顺序
第一次阅读 oak-general-business,建议按下面的顺序走:
- 先看
src/features/index.ts - 再看
src/aspects/index.ts - 然后看
src/endpoints/index.ts - 再看
src/watchers/index.ts - 然后回到
src/entities - 最后才去翻
src/components
原因很简单:新手最容易被组件数量吓住,但真正决定模块能力边界的,其实是 feature / aspect / endpoint / trigger / checker / watcher 这些运行时入口。
接下来的每一章,我都会把下面这些信息明确写出来:
- 这一块对应哪些实体;
- 已经有哪些可直接复用的组件;
- 前端通过哪些 feature 调用;
- 后端通过哪些 aspect 或 endpoint 暴露;
- 后台还有哪些 trigger / checker / watcher / routine 在兜底;
- 这些能力究竟是在哪里被注入到项目里的。