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

Application、Domain 与应用装配

如果说 System 解决的是“这套业务系统总体怎么配置”,那么 Application 解决的就是“这套业务系统要以什么端形态对外运行”。

在 Oak 里,一个系统往往不只对应一个前端入口。你可能同时有:

  • web
  • wechatMp
  • wechatPublic
  • native

每一种端形态都对应一条 Application 记录。oak-general-business 会根据当前访问环境、域名和版本,自动判断你现在到底命中了哪个应用。

主要对象

这一章实际涉及四个对象:

  • Application:定义应用类型、系统归属、端配置、样式、版本策略;
  • Domain:定义域名和访问入口;
  • ApplicationPassport:定义某个应用实际启用了哪些登录方式;
  • Platform:系统上级平台对象,通常在系统/应用管理界面里一起出现。

其中 Application.config 是最关键的数据,它把不同端需要的配置统一放在一起:

  • web 的微信网页登录配置;
  • wechatMpappId/appSecret/server
  • wechatPublic 的公众号配置和跳小程序配置;
  • native 的微信原生登录配置。

这里要特别注意:访问入口已经不再放在 Application.config.location 里。当前版本把前端页面地址、扫码中转页和后台接口路径拆开维护:

  • Domain 负责系统级域名、协议、端口和 API 代理路径;
  • Application.domainId 只在某个应用必须绑定指定域名时使用;
  • System.config.App.scanPage 负责微信扫码后的中转页逻辑路径;
  • System.config.App.wechatQrCodeExpireSeconds 负责微信临时二维码有效期。

组件

围绕应用管理,这个包提供的组件已经很完整:

  • src/components/application/detail
  • src/components/application/detailForPlatform
  • src/components/application/panel
  • src/components/application/upsert
  • src/components/application/cos
  • src/components/domain/detail
  • src/components/domain/list
  • src/components/domain/upsert
  • src/components/domain/upsertItem
  • src/components/applicationPassport
  • src/components/config/application
  • src/components/config/style
  • src/components/theme/setting

其中 Application 组件更偏向“应用本身”的管理,而 Domain 组件族负责维护域名入口。两者合在一起,才构成真正完整的“应用装配后台”。

如果你是在写一个管理后台,这些组件通常可以直接复用,而不需要从零搭应用管理页。

常用组件与参数

这一组组件里,最常直接包页面的通常是下面四类:

  • application/panel
  • domain/list
  • applicationPassport
  • system/application

其中几个最关键的参数分别是:

  • application/paneltabs
  • domain/listsystemId
  • applicationPassportsystemId
  • system/applicationsystemId

从源码看:

  • application/panelsystem/panelplatform/panel 一样,都支持通过 tabs 追加项目自己的页签
  • domain/list 会直接按 systemId 过滤域名
  • applicationPassport 会按 systemId 拉这个系统下所有 applicationpassport,再生成“每个应用启用哪些登录方式”的管理矩阵

这意味着如果项目已经有系统详情页,通常不需要自己拼:

  • 应用列表
  • 域名列表
  • 应用级登录方式开关

而是直接把这几块公共组件挂进对应页签里。

application/panel 内置了哪些页签

这点很值得单独写出来,因为很多人会误以为 application/panel 只是一个“留给项目自己填内容的面板壳”。实际上从源码看,它默认已经包含:

  • detail
  • config
  • style
  • cos

如果 application.type === 'wechatPublic',还会自动再补:

  • menu
  • autoReply
  • tag
  • user
  • template

如果 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 的模态框
  • 列表通过启用 / 禁用动作控制域名是否参与运行时匹配,不再把删除作为常规入口

它默认编辑和展示的字段就是:

  • url
  • apiPath
  • port
  • protocol
  • ableState

所以项目里如果只是做“系统域名配置”,更推荐直接把它挂到系统详情页或系统配置页里,而不是自己再写一套域名 CRUD。

Domain 的运行时语义

Domain 现在是应用访问入口的统一来源。它的字段含义不要和 src/configuration/access.ts 混在一起:

  • protocolurl 组成站点基础地址,例如 https://example.com
  • port 是可选字段,80、443 这类默认端口通常不需要写
  • apiPath 只给后台接口地址使用,常见于 nginx 反向代理路径
  • ableStatedisabled 时,这条域名不会参与应用识别和二维码链接兜底

oak-general-business/src/utils/domain.ts 里有两个拼接函数,名字就体现了这个边界:

  • composeDomainUrl(domain, url, props):拼面向前端页面的地址,不拼 apiPath
  • composeServerUrl(domain, url, props):拼面向后台接口的地址,会先拼 apiPath

微信扫码图文链接、邀请落地页、开发环境二维码调试链接这类“用户浏览器要打开的页面”,应该走 composeDomainUrl(...)。后台 API、endpoint、反向代理服务地址才应该走 composeServerUrl(...)

Application.domainId 与兜底规则

getApplication 会按“端类型 + 当前域名 + 版本”识别当前应用。当前域名匹配时的顺序是:

  1. 先找 application.domainId 指向的启用域名;
  2. 找不到时,再找同一个 system 下没有指定 domainId 的同类型应用;
  3. 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/scanpages/wechatQrCode/scan/index,运行时也会规整成同一逻辑路径

wechatQrCodeExpireSeconds 单位是秒,用于微信临时二维码和本地 expiresAt。不配置、配置非法值或小于等于 0 时,默认 2592000 秒;超过微信上限时也会按 2592000 秒处理。

wechatQrCode 的应用选择

wechatQrCode 创建时可以显式指定 applicationIdtype。如果指定了,就按这个目标应用生成二维码;如果没有指定,公共 trigger 才进入自动兜底:

  1. 优先使用 System.config.App.qrCodeApplicationIdqrCodeType
  2. 当前应用是服务号时,生成公众号二维码
  3. 当前应用是小程序时,有 qrCodePrefix 就生成小程序普通链接二维码,否则生成小程序码
  4. 当前应用不是微信端时,优先找系统下服务号,再找小程序

这意味着项目层需要强约束二维码目标时,应该在创建 wechatQrCode 时传目标应用;只想使用系统默认策略时,再交给公共包自动兜底。

applicationPassport 的真实交互

applicationPassport 也值得单独说明,因为它并不是一个简单的“勾选启用登录方式”组件。当前源码的真实行为是:

  • 必传 systemId
  • 进入页面时,会先拉当前系统下所有 application
  • 再拉当前系统下所有 enabled: truepassport,并排除 password
  • 最终按“应用”为行、“登录方式类型”为列,生成一张配置矩阵

这个矩阵里还有两个很容易忽略的点:

  • default 列不是展示字段,而是真正的“默认登录方式”选择器
  • loginName 一旦启用,会直接创建 isDefault: trueallowPwd: trueapplicationPassport

从组件内部逻辑看,allowPwd 相关行为也已经做了约束:

  • loginName 会强制带密码,界面上不会允许你把它关掉
  • smsemail 在启用后可以额外控制 allowPwd

另外,如果同一系统下同一类 passport 不止一个,组件不一定用单纯的开关。对 web 应用,或者 wechatMp / wechatPublic 下的 smsemail,它会切成下拉选择模式,让你明确指定这一类登录方式到底绑定哪一个 passport

前端入口与 aspect

应用管理相关的前端 feature 主要有:

  • features.application
  • features.config
  • features.theme
  • features.template

对应的后端 aspect 主要有:

  • getApplication
  • signatureJsSDK
  • updateApplicationConfig
  • updateConfig
  • updateStyle
  • getApplicationPassports
  • removeApplicationPassportsByPIds

其中最核心的是 getApplicationfeatures.application.initialize(...) 最终就是通过这个 aspect,按“端类型 + 域名 + 版本”确定当前应用,并把应用数据缓存到前端。

后台规则

这一章真正的复杂性,分散在四个文件里:

  • src/checkers/application.ts
  • src/triggers/application.ts
  • src/checkers/applicationPassport.ts
  • src/triggers/applicationPassport.ts

Application 相关规则包括:

  • 校验 dangerousVersionswarningVersionssoaVersion 的合法性;
  • 创建 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.tscreate(...) 时会创建 application feature,并把它挂到 features.application

initialize(...)

真正让它生效的,是后续的 initialize(...)

  • 注册 selection / operation rewriter;
  • 调用 features.application.initialize(...) 识别当前应用;
  • 在 web 环境下设置微信落地地址;
  • 在后续章节里还会看到,它也会顺手带动文件上传、小程序自动登录等流程。

后端装配

前端只是把 application feature 建起来,真正让 Application / Domain / ApplicationPassport 的规则生效,还需要后端运行态加载公共包能力:

  • ogb0Aspects
  • ogb0Checkers
  • ogb0Triggers

当前后端装配不再靠项目手写 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:initmake:domainmake: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-busitaicang 都是这样:

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 组件不是简单勾选框,它内部会同时读取系统下的 applicationpassport,所以系统层登录方式没有配好时,应用级页面看起来就会“没有可选项”

使用示例

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.configApplicationPassport 上;
  • 把访问入口、扫码 URL 和邀请落地页绝对地址交给 DomainSystem.config.App

这样一来,登录、文件、微信、版本控制这些横向能力,才能真正围绕应用自动生效。