Application、Domain 与应用装配
如果说 System 解决的是“这套业务系统总体怎么配置”,那么 Application 解决的就是“这套业务系统要以什么端形态对外运行”。
在 Oak 里,一个系统往往不只对应一个前端入口。你可能同时有:
webwechatMpwechatPublicnative
每一种端形态都对应一条 Application 记录。oak-general-business 会根据当前访问环境、域名和版本,自动判断你现在到底命中了哪个应用。
主要对象
这一章实际涉及四个对象:
Application:定义应用类型、系统归属、端配置、样式、版本策略;Domain:定义域名和访问入口;ApplicationPassport:定义某个应用实际启用了哪些登录方式;Platform:系统上级平台对象,通常在系统/应用管理界面里一起出现。
其中 Application.config 是最关键的数据,它把不同端需要的配置统一放在一起:
web的微信网页登录配置;wechatMp的appId/appSecret/server;wechatPublic的公众号配置和跳小程序配置;native的微信原生登录配置。
这里要特别注意:访问入口已经不再放在 Application.config.location 里。当前版本把前端页面地址、扫码中转页和后台接口路径拆开维护:
Domain负责系统级域名、协议、端口和 API 代理路径;Application.domainId只在某个应用必须绑定指定域名时使用;System.config.App.scanPage负责微信扫码后的中转页逻辑路径;System.config.App.wechatQrCodeExpireSeconds负责微信临时二维码有效期。
组件
围绕应用管理,这个包提供的组件已经很完整:
src/components/application/detailsrc/components/application/detailForPlatformsrc/components/application/panelsrc/components/application/upsertsrc/components/application/cossrc/components/domain/detailsrc/components/domain/listsrc/components/domain/upsertsrc/components/domain/upsertItemsrc/components/applicationPassportsrc/components/config/applicationsrc/components/config/stylesrc/components/theme/setting
其中 Application 组件更偏向“应用本身”的管理,而 Domain 组件族负责维护域名入口。两者合在一起,才构成真正完整的“应用装配后台”。
如果你是在写一个管理后台,这些组件通常可以直接复用,而不需要从零搭应用管理页。
常用组件与参数
这一组组件里,最常直接包页面的通常是下面四类:
application/paneldomain/listapplicationPassportsystem/application
其中几个最关键的参数分别是:
application/panel:tabsdomain/list:systemIdapplicationPassport:systemIdsystem/application:systemId
从源码看:
application/panel和system/panel、platform/panel一样,都支持通过tabs追加项目自己的页签domain/list会直接按systemId过滤域名applicationPassport会按systemId拉这个系统下所有application与passport,再生成“每个应用启用哪些登录方式”的管理矩阵
这意味着如果项目已经有系统详情页,通常不需要自己拼:
- 应用列表
- 域名列表
- 应用级登录方式开关
而是直接把这几块公共组件挂进对应页签里。
application/panel 内置了哪些页签
这点很值得单独写出来,因为很多人会误以为 application/panel 只是一个“留给项目自己填内容的面板壳”。实际上从源码看,它默认已经包含:
detailconfigstylecos
如果 application.type === 'wechatPublic',还会自动再补:
menuautoReplytagusertemplate
如果 application.type === 'wechatMp',还会自动补:
template
也就是说,项目层在大多数情况下只需要继续往 tabs 里补“自己的业务 tab”,而不是把微信菜单、自动回复、模板管理这些基础页签再手工拼一遍。
domain/list 的真实交互
domain/list 不是一个只读列表。从 oak-general-business/src/components/domain/list/web.pc.tsx 看,它在 web 端已经把常见管理动作都包进去了:
- 组件内部始终按
systemId过滤域名 - 点击“创建”时会先
addItem({ systemId }),也就是新建行会自动挂到当前系统下 - 创建和更新都不是跳新页面,而是直接弹
DomainUpsertItem的模态框 - 列表通过启用 / 禁用动作控制域名是否参与运行时匹配,不再把删除作为常规入口
它默认编辑和展示的字段就是:
urlapiPathportprotocolableState
所以项目里如果只是做“系统域名配置”,更推荐直接把它挂到系统详情页或系统配置页里,而不是自己再写一套域名 CRUD。
Domain 的运行时语义
Domain 现在是应用访问入口的统一来源。它的字段含义不要和 src/configuration/access.ts 混在一起:
protocol和url组成站点基础地址,例如https://example.comport是可选字段,80、443 这类默认端口通常不需要写apiPath只给后台接口地址使用,常见于 nginx 反向代理路径ableState为disabled时,这条域名不会参与应用识别和二维码链接兜底
oak-general-business/src/utils/domain.ts 里有两个拼接函数,名字就体现了这个边界:
composeDomainUrl(domain, url, props):拼面向前端页面的地址,不拼apiPathcomposeServerUrl(domain, url, props):拼面向后台接口的地址,会先拼apiPath
微信扫码图文链接、邀请落地页、开发环境二维码调试链接这类“用户浏览器要打开的页面”,应该走 composeDomainUrl(...)。后台 API、endpoint、反向代理服务地址才应该走 composeServerUrl(...)。
Application.domainId 与兜底规则
getApplication 会按“端类型 + 当前域名 + 版本”识别当前应用。当前域名匹配时的顺序是:
- 先找
application.domainId指向的启用域名; - 找不到时,再找同一个
system下没有指定domainId的同类型应用; ableState === 'disabled'的域名会被排除。
所以 domainId 不需要每条 application 都配置。更推荐的做法是:
- 一个系统下某个类型只有一个应用时,可以让应用不填
domainId,由系统启用域名兜底; - 一个系统下有多个
web或多个同类型应用时,给需要区分的应用配置domainId; - 不想某条域名继续被运行时命中时,禁用它,而不是直接删除历史数据。
getApplicationDomain(application) 也遵循类似语义:如果应用有 domainId,优先取这个启用域名;否则从 system.domain$system 里挑启用域名兜底。兜底排序会优先普通域名,其次 IP,最后 localhost,避免生产环境误用本地域名。
扫码中转页与二维码有效期
微信二维码相关配置现在放在 System.config.App:
await this.features.config.updateConfig('system', systemId, {
App: {
scanPage: 'wechatQrCode/scan',
wechatQrCodeExpireSeconds: 2592000,
},
});
scanPage 写逻辑路径即可,默认是 wechatQrCode/scan。公共工具会做规范化:
- web / 公众号图文链接直接使用这个路径,例如
https://example.com/wechatQrCode/scan?scene=... - 小程序码会拼成
pages/wechatQrCode/scan/index - 如果误写成
/wechatQrCode/scan、pages/wechatQrCode/scan/index,运行时也会规整成同一逻辑路径
wechatQrCodeExpireSeconds 单位是秒,用于微信临时二维码和本地 expiresAt。不配置、配置非法值或小于等于 0 时,默认 2592000 秒;超过微信上限时也会按 2592000 秒处理。
wechatQrCode 的应用选择
wechatQrCode 创建时可以显式指定 applicationId 和 type。如果指定了,就按这个目标应用生成二维码;如果没有指定,公共 trigger 才进入自动兜底:
- 优先使用
System.config.App.qrCodeApplicationId和qrCodeType - 当前应用是服务号时,生成公众号二维码
- 当前应用是小程序时,有
qrCodePrefix就生成小程序普通链接二维码,否则生成小程序码 - 当前应用不是微信端时,优先找系统下服务号,再找小程序
这意味着项目层需要强约束二维码目标时,应该在创建 wechatQrCode 时传目标应用;只想使用系统默认策略时,再交给公共包自动兜底。
applicationPassport 的真实交互
applicationPassport 也值得单独说明,因为它并不是一个简单的“勾选启用登录方式”组件。当前源码的真实行为是:
- 必传
systemId - 进入页面时,会先拉当前系统下所有
application - 再拉当前系统下所有
enabled: true的passport,并排除password - 最终按“应用”为行、“登录方式类型”为列,生成一张配置矩阵
这个矩阵里还有两个很容易忽略的点:
default列不是展示字段,而是真正的“默认登录方式”选择器loginName一旦启用,会直接创建isDefault: true、allowPwd: true的applicationPassport
从组件内部逻辑看,allowPwd 相关行为也已经做了约束:
loginName会强制带密码,界面上不会允许你把它关掉sms、email在启用后可以额外控制allowPwd
另外,如果同一系统下同一类 passport 不止一个,组件不一定用单纯的开关。对 web 应用,或者 wechatMp / wechatPublic 下的 sms、email,它会切成下拉选择模式,让你明确指定这一类登录方式到底绑定哪一个 passport。
前端入口与 aspect
应用管理相关的前端 feature 主要有:
features.applicationfeatures.configfeatures.themefeatures.template
对应的后端 aspect 主要有:
getApplicationsignatureJsSDKupdateApplicationConfigupdateConfigupdateStylegetApplicationPassportsremoveApplicationPassportsByPIds
其中最核心的是 getApplication。features.application.initialize(...) 最终就是通过这个 aspect,按“端类型 + 域名 + 版本”确定当前应用,并把应用数据缓存到前端。
后台规则
这一章真正的复杂性,分散在四个文件里:
src/checkers/application.tssrc/triggers/application.tssrc/checkers/applicationPassport.tssrc/triggers/applicationPassport.ts
Application 相关规则包括:
- 校验
dangerousVersions、warningVersions、soaVersion的合法性; - 创建
application时校验name/type/systemId,并在缺省时补一个空config; - 创建
application时自动补config.type; - 创建
application时按type自动准备基础passport; - 根据
application.type和配置,自动创建、删除或关闭相关passport; - 删除
application时清理关联的applicationPassport和某些自动生成的passport。
ApplicationPassport 则负责“应用级登录方式选择”:
- 只允许同一个应用有一个默认登录方式;
loginName类型在创建前会自动把allowPwd置为true;- 删除或更新默认登录方式时会自动维持一致性。
注入点
应用装配能力的注入点有三个非常关键:
create(...)
oak-general-business/src/features/index.ts 在 create(...) 时会创建 application feature,并把它挂到 features.application。
initialize(...)
真正让它生效的,是后续的 initialize(...):
- 注册 selection / operation rewriter;
- 调用
features.application.initialize(...)识别当前应用; - 在 web 环境下设置微信落地地址;
- 在后续章节里还会看到,它也会顺手带动文件上传、小程序自动登录等流程。
后端装配
前端只是把 application feature 建起来,真正让 Application / Domain / ApplicationPassport 的规则生效,还需要后端运行态加载公共包能力:
ogb0Aspectsogb0Checkersogb0Triggers
当前后端装配不再靠项目手写 initialize.frontend.ts 拼接。server:start 启动的 AppLoader 会根据依赖图从项目和依赖包的 lib/... 里合并 aspects / checkers / triggers / watchers / timers / ports / routines 等模块。这样 getApplication、版本校验、默认登录方式维护、自动补 passport 等逻辑才会进入运行时。
项目中如何接入
Application 这一章是 oak-general-business 真正落到项目运行时的第一入口。项目接入它,一般有两步:
- 在
src/configuration/dependency.ts声明oak-general-business,依赖变化后依次执行project:init、make:domain和make:dep; - 前端运行时由生成的
initialize.server.ts创建createOgb0Features(...),再在启动后通过initializeFeatures.ts执行initializeOgb0Features(...),让features.application.initialize(...)自动识别当前应用。
bm-smart/src/initializeFeatures.ts 的真实写法就是:
await initializeOgb0Features(
features,
accessConfiguration,
undefined,
[Qiniu, S3, Aliyun]
);
而 oak-general-business/src/features/index.ts 内部会继续调用:
await features.application.initialize(
oakGetPackageJsonVersion(),
access.http.hostname,
undefined,
config?.applicationExtraProjection
);
所以对项目层来说,最重要的不是“怎么手工查 application 表”,而是保证这条初始化链路先走通。
真实项目里的初始化方式
如果你的项目同时接了 oak-pay-business,真实写法往往不会直接显式调用 initializeOgb0Features(...)。haina-busi 和 taicang 都是这样:
await initializeOpb1Features(
features,
accessConfiguration,
{
applicationExtraProjection: APPLICATION_PROJECTION,
},
[ALiYun, S3]
);
或者:
await initializeOpb1Features(
features,
accessConfiguration,
{
dontAutoLoginInWechatmp: true,
},
[Qiniu]
);
这不是绕过了 Application,而是 oak-pay-business/features.initialize(...) 内部已经先调用了 oak-general-business/features.initialize(...),并且会把支付域需要的 applicationProjection 和项目自定义投影合并起来。
项目里最容易漏掉的两件事
- 后端必须把
ogb0Aspects / ogb0Checkers / ogb0Triggers真正并进运行时,否则getApplication、默认登录方式维护、应用配置校验都不会生效 - 前端初始化时如果项目还要做文件上传或小程序自动登录,需要把 COS 类列表一并传给初始化函数
开发注意事项
应用装配这章还有两个很容易踩坑的地方:
application feature识别当前应用时依赖域名、版本和额外投影,所以项目自己的applicationExtraProjection不要把公共包需要的字段覆盖掉applicationPassport组件不是简单勾选框,它内部会同时读取系统下的application和passport,所以系统层登录方式没有配好时,应用级页面看起来就会“没有可选项”
使用示例
1. 页面里统一从当前应用取 systemId
这类写法在 bm-smart 里非常常见:
const systemId = this.features.application.getApplication().systemId;
return {
systemId,
};
这样写的好处是,不需要页面自己判断域名、端类型、当前 appId,全部交给 features.application。
2. 后台逻辑里统一从 context 取当前应用
在 aspect、trigger、watcher 里,更推荐直接走 context.getApplication():
const application = context.getApplication();
const { system } = application!;
oak-general-business 里大量 token、短信、文件、微信逻辑都是这么拿当前应用和系统配置的。
3. 管理台修改应用配置
如果要在项目里改某个应用的端配置,推荐直接用 features.config.updateApplicationConfig(...):
await this.features.config.updateApplicationConfig('application', applicationId, {
type: 'web',
wechat: {
appId: 'wx-app-id',
appSecret: 'wx-app-secret',
enable: true,
},
});
应用自己的配置只保留端能力,例如微信 appId、COS 默认源、邀请落地页等;站点访问地址不要再写回 Application.config.location。
4. 配置系统域名与扫码页
域名入口和扫码中转页分别配置:
await this.features.config.updateConfig('system', systemId, {
App: {
scanPage: 'wechatQrCode/scan',
wechatQrCodeExpireSeconds: 2592000,
},
});
Domain 本身建议通过 domain/list 管理组件维护。如果是初始化数据,大致形态是:
const domain = {
protocol: 'https:',
url: 'example.com',
apiPath: '/rest/aspect',
ableState: 'enabled',
systemId,
};
如果使用 80 或 443 默认端口,可以不写 port。
使用建议
新手最容易犯的错误,是把 Application 当成一个普通配置对象来手工查询。实际上更推荐的方式是:
- 在前端永远通过
features.application.getApplication()取当前应用; - 在后台通过
context.getApplication()取当前上下文应用; - 把“一个系统下不同端”的端能力差异收敛到
Application.config和ApplicationPassport上; - 把访问入口、扫码 URL 和邀请落地页绝对地址交给
Domain和System.config.App。
这样一来,登录、文件、微信、版本控制这些横向能力,才能真正围绕应用自动生效。