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

Oak 通用业务逻辑

oak-general-business 是 Oak 生态里最重要的公共业务包之一。它并不是一个“示例仓库”,而是一套已经拆好了对象、触发器、校验器、前端组件、aspect、endpoint、feature 和后台例程的通用业务底座。用户、登录、文件、微信、OAuth、消息、文章、地址这些看上去彼此独立的能力,在这个包里都已经被组织成了可复用的模块。

如果你只是把它当成“顺手依赖一下的工具包”,那么很快就会迷路。更好的理解方式是:

  • src/entities 定义这套通用业务到底有哪些对象;
  • src/checkerssrc/triggers 定义这些对象的默认业务规则;
  • src/aspectssrc/endpoints 暴露可调用的业务入口;
  • src/features 把这些入口包装成前端可直接使用的能力;
  • src/components 提供已经写好的 Oak 组件;
  • src/watcherssrc/routines/start.ts 则把后台补偿任务和启动注入点也一起准备好了。

近期 oak-general-business 又补上了系统翻译相关对象、组件和触发链路,并把 Area 数据推进到全球 locale 化。也就是说,通用业务包现在不只是“登录、文件、微信、系统配置”,还承担了一部分多语言内容生产和系统翻译基础能力。

这部分为什么要重新拆分

按照目录名直接理解 oak-general-business,很容易把能力划分错。

例如:

  • PassportApplicationPassport 明明和登录相关,但它们一部分依赖 System,另一部分又和 Application 强绑定;
  • Token 虽然是一个对象,但真正的登录逻辑主要写在 aspects/token.tsfeatures/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/register
  • changePassword/*
  • wechatLogin/*
  • oauth/*

它们覆盖的其实不是一件事,而是“正式登录入口”这一整层:

  • 用户名 / 密码 / 手机验证码登录
  • 微信扫码登录
  • OAuth 授权登录
  • 账号密码修改和补齐

所以项目层做登录页时,通常不是从零画,而是先从这几组现成组件里挑合适的入口。

2. 授权分享、临时入口与关系分发

这组最值得优先看的组件是:

  • invite/landing
  • my/invite
  • userEntityGrant/list
  • userEntityGrant/upsert
  • userEntityGrant/claim
  • parasite/list
  • parasite/upsert
  • parasite/detail
  • parasite/excess

这三组能力虽然都和“分享一个入口出去”有关,但语义完全不同:

  • invite 更偏邀请来源归因,最终记录谁邀请了哪个 token
  • userEntityGrant 更偏正式授权、对象关系分发
  • parasite 更偏临时身份、一次性或短期访问入口

如果项目里有“邀请成员”“分享授权链接”“给外部用户一个临时访问口”这类需求,通常先从这里选。

3. 会话、消息与通知面板

这一组最常直接复用的是:

  • session/list
  • session/forMessage
  • sessionMessage/list
  • sessionMessage/upsert
  • message/list
  • message/detail
  • my/message

实际项目里,最稳的拆法通常是:

  • 左侧会话列表复用 session/list
  • 中间消息主体复用 session/forMessage
  • 单条消息输入或补发再按需用 sessionMessage/upsert

这样新项目很快就能先把“能聊起来”的骨架搭出来。

4. 文件、素材与对象存储

这组最常先用的是:

  • extraFile/upload
  • extraFile/commit
  • extraFile/forUrl
  • extraFile/avatar
  • extraFile/gallery
  • wechatMaterialLibrary

其中真正最值得先理解的,还是前面几章已经详细写过的:

  • upload 负责选文件和上传临时对象
  • commit 负责把上传结果落成正式 extraFile
  • forUrl 负责纯展示或 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.tsmake:dep 会在类似 bm-smart/src/initialize.server.ts 这样的文件里,把 oak-general-business 的:

  • checkers
  • common configuration
  • render configuration
  • features

和项目自己的实现合并起来,然后再创建 oak-general-business 的 features:

const ogb0Features = createOgb0Features(totalFeatures);
Object.assign(totalFeatures, ogb0Features);

这一步决定了前端运行时能不能直接调用 features.tokenfeatures.extraFilefeatures.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:initmake:domainmake: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.token
  • features.application
  • features.extraFile
  • features.wechatSdk
  • features.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-busitaicang,会发现这两个项目接 oak-general-business 的方式其实很接近,而且都不是“单独只初始化 general-business 一次”这么简单。

1. 初始化阶段先同时创建 ogb0opb1

这两个项目都会在:

  • src/initialize.server.ts
  • src/initializeFeatures.ts

里创建和初始化:

  • createOgb0Features(...)
  • createOpb1Features(...)

前端运行时负责合并 checkers / common / render / features,后台侧的 aspects / triggers / watchers / timers / data / ports / routines 则由 AppLoader 按依赖图合并进服务端运行态。

2. 很多项目最终只调用 initializeOpb1Features(...)

这点非常容易让新手困惑。haina-busi/src/initializeFeatures.tstaicang/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-busitaicang 这类项目,还会在启动例程里继续注册:

  • 短信实现:registerSms(...) / registSms(...)
  • COS 实现:registerCosBackend(...) / registerCos(...)
  • 消息类型:registerMessageType(...)
  • 消息/通知处理器:registerMessageHandler(...)registerNotificationHandler(...)

所以新手读公共包时,最好把“初始化 feature”和“启动时注册具体供应商实现”一起看,不要只看页面层。

阅读源码时建议按这个顺序

第一次阅读 oak-general-business,建议按下面的顺序走:

  1. 先看 src/features/index.ts
  2. 再看 src/aspects/index.ts
  3. 然后看 src/endpoints/index.ts
  4. 再看 src/watchers/index.ts
  5. 然后回到 src/entities
  6. 最后才去翻 src/components

原因很简单:新手最容易被组件数量吓住,但真正决定模块能力边界的,其实是 feature / aspect / endpoint / trigger / checker / watcher 这些运行时入口。

接下来的每一章,我都会把下面这些信息明确写出来:

  • 这一块对应哪些实体;
  • 已经有哪些可直接复用的组件;
  • 前端通过哪些 feature 调用;
  • 后端通过哪些 aspect 或 endpoint 暴露;
  • 后台还有哪些 trigger / checker / watcher / routine 在兜底;
  • 这些能力究竟是在哪里被注入到项目里的。