### 4.4 页面由模块定义,为什么由底座调用模块后端? 一个应用包中同时包含两种运行在不同位置的程序:H5 的 HTML/CSS/JavaScript 下载到用户浏览器,在 iframe 中执行;Go 后端留在服务器的应用容器中执行。“由模块定义页面”说明界面的代码归属,不代表浏览器能直接调用服务器进程中的 Go 方法。 当前应用容器没有公开 HTTP 端口,H5 又被 CSP 限制直接 fetch/XHR/WebSocket,不能自由请求后端或数据库。应用 H5 通过底座注入的 PlatformUI 发送一个受限消息,由父页面代为调用经过身份认证的底座动作接口,再由底座通过 RPC 请求指定的应用进程。HTTP 是浏览器到服务器的这一段,RPC 是服务器进程之间的这一段。 ```text 浏览器内 服务器内 CMS 自定义 H5 点击“提交审核” PlatformUI.request('/articles//submit', ...) │ MessageChannel 消息 ▼ 底座父页面(统一工作空间) │ HTTP POST /apps/cms/api/articles//submit │ 浏览器会话 + CSRF + request_id └───────────────────────────────→ Go 底座 │ RPC:Execute(submit, ...) ▼ CMS 后端容器 │ 反向 RPC:identity.get / database.connect ▼ Go 底座返回可信身份、数据范围和连接配置 │ CMS 通过 /database Unix socket ▼ PostgreSQL:CMS 专属 schema ``` 因此,用户是业务操作的发起人,CMS H5 是交互发起点,底座是认证和路由入口,CMS Go 程序是规则执行者。说“底座向模块发起业务 RPC”仅描述服务器间这一段,绝不表示底座定时主动替用户提交稿件,也不表示底座生成了 CMS 的表单或按钮。 统一入口让用户身份、API Key 限权、动作授权、限流、超时、Trace ID 和运行实例都由底座校验和绑定。模块不需要另开端口或实现独立登录;页面也拿不到平台登录凭据。前端按钮是否显示只改善体验,服务端仍必须拒绝伪造或越权操作。 ### 4.5 两个 RPC 方向分别处理什么? | 方向 | 发起者→接收者 | 当前主要调用 | 责任 | |---|---|---|---| | 模块生命周期/业务调用 | 底座→模块后端 | Describe、Initialize、Execute | 识别程序、为本次调用建立反向通道、执行业务动作 | | 公共能力调用 | 模块后端→底座 | identity.get、records.list、records.create、directory.read、cloud.request | 以已绑定模块和当前用户身份访问批准的公共服务 | Describe 用于声明身份、版本和动作,不负责生成业务页面。Initialize 为执行建立宿主回调连接。Execute 收到 action、payload、request_id 与由底座填写的 ActorID,再由模块判断业务规则。只有 Execute 所处理的 list/create/submit/publish 等属于这里说的“业务 RPC”;不能把每个 Describe 都算作业务请求。 当前请求级回调通过单次调用专用连接绑定模块、ActorID、动作、request_id、Key 限权和取消上下文。反向 RPC 不能任意改称其他用户或其他模块,底座还会重读账号启用状态、角色和记录范围。执行结束或取消后通道关闭,迟到回调不能继承下一位用户身份。它不是一个任何应用都能随意访问的通用管理后门。 #### CMS “提交审核”的完整路径 1. 编辑在 CMS 自带页面点击“提交审核”。CMS 0.5.0 的 `examples/cms/ui/cms.js` 调用 `ui.request('/articles/'+article_id+'/submit', {method:'POST', body:{revision:当前修订号}, requestID})`,通过桥转交父页面。 2. 父页面向 `POST /apps/cms/api/articles//submit` 发送业务请求号与参数。底座核验登录/Key、`cms.submit`、实例状态和请求额度,不接受前端指定操作者身份。 3. 底座等待该模块执行槽位,建立本次专属反向连接,再调用 CMS 的 Execute;绑定该编辑为 ActorID,而不是使用系统管理员身份。 4. CMS 反向调用 `identity.get` 获取可信身份、动作权限及 record_access(读与当前动作分别计算的数据范围),调用 `database.connect` 获取仅供后端的受限连接配置。CMS 复用连接池,经专用 Unix socket 直接查询自己的 PostgreSQL schema,SQL 同时满足读范围与当前动作范围。 5. CMS 从 articles 表读取稿件当前版本,校验确实是本人稿件、状态为“草稿”或“已退回”、传入 revision 与当前版本一致,并检查同一 request_id 是否已经处理。提交动作不允许夹带修改正文。 6. CMS 在同一个 PostgreSQL 事务中检查请求号、版本和状态,按 revision 条件更新 articles,并写入 article_revisions(含幂等结果)与 audit_events。schema 内的事务锁防止并发请求都通过旧版本检查;SQL 失败时整笔回滚。原作者和 department_id 保持不变。 7. 数据库提交后,结果沿“CMS 后端→底座网关→父页面→CMS H5”返回。当前状态行更新,历史修订保留;未知写结果以原 request_id 重试,返回原提交结果。CMS 业务审计位于自己的 audit_events,底座统一审计仍记录平台管理操作,二者没有跨库原子事务。 #### CMS “审核并发布”的完整路径 总编在 CMS 自定义页面点击“审核并发布”,同样通过消息桥请求 `POST /apps/cms/api/articles//publish`。底座校验的是总编当前的 `cms.publish`,并以该总编身份向 CMS 发起 Execute;不会借用编辑此前的会话。 CMS 同样反向获取身份与数据库配置,并在自己的 SQL 事务中核验审核权限、读/发布数据范围、当前修订号及“待审核”状态。随后更新状态并追加历史修订、审核人、发布时间和审计,事务一起提交。审核人是总编,原作者与 owner_id 保持不变。 总编要审核哪些人的稿件,还需给其角色配置正确的 CMS 数据范围;只有发布按钮权限而范围仅为“本人”,并不能自然看到全公司的稿件。稿件发布是 CMS 内容状态变更,与“批准发布一个应用安装包”是两种操作,后者属于模块发布管理。 读写超时必须区分:执行前限流没有产生业务;已进入写调用后返回 OUTCOME_UNKNOWN,不能认为稿件一定没有提交。应查询状态或在支持幂等时以原 request_id 重试,不能无条件换新号再次执行。 ### 4.6 为什么同一个模块暂时仍排队? 这是当前协议兼容与业务一致性的选择,Go 语言本身并不要求串行。不同应用可以同时执行,例如 CMS 发布与请假查询;同一个模块当前只有一个执行槽位。 第一,已有模块由 Initialize 连接宿主服务。若两个 Execute 同时交错执行并改变实例回调引用,可能使用错误的回调连接。每次请求独立绑定已经解决跨请求通道复用,但旧模块的连接初始化约定仍需要同模块串行保护。 第二,旧 records.* 应用的多步读写依赖同模块串行。CMS 0.5.0 已改成 PostgreSQL 事务、schema 内写事务锁、revision 条件更新和唯一请求号;但本次没有移除协议层的同模块执行槽位。数据库事务不等于 RPC 协议已支持并发,也不提供跨模块或跨系统事务。 将来增加单模块并发或多副本,需要同时改为显式请求上下文协议,以及在持久层实现原子条件更新/CAS、事务、唯一约束或按业务记录加锁。不同单据可并发,同一稿件的竞争操作应只允许一个成功;不能只移除执行锁,也不能依赖前端禁用按钮保证一致性。 ### 4.7 模块可以使用自己的数据库吗? **公共记录模式。** 未申请专属数据库的应用由底座 records.* 管理持久化,数据位于宿主 JSON 状态文件。应用容器只挂本应用只读程序和独立 IPC 目录,根只读、网络为 none;默认没有数据库持久目录或可达数据库端点;v0.16 申请并获批后可挂专用 PostgreSQL Unix socket,仍不挂数据库数据文件。容器临时 /tmp 和可写 IPC 目录不是获准的业务数据库。当前清单可申请受控 PostgreSQL 账号/schema,但不能申请任意挂载或放开网络。 **当前可选模式(v0.16)。** 已支持受控 PostgreSQL 为每个申请应用分配独立账号和 schema;使用 OA 专用实例,应用通过专用 Unix socket 连接。与原底座记录服务可组合使用,不自动迁移既有数据;上传应用不能自行更改 Docker 参数。 | 项目 | 方案 A:继续使用底座数据服务 | 方案 B:模块拥有专属账号/schema(已支持) | |---|---|---| | 数据访问 | 模块→反向 RPC→底座 records.* | 模块→获准数据库连接→自己的 schema/库 | | 表结构与事务 | 当前底座统一记录模型;后续可升级存储实现 | 模块管理业务表、索引、迁移和本地事务 | | 数据权限 | 底座 records.* 执行归属和角色范围过滤 | 模块查询必须接入可信身份与记录范围;底座现有过滤不会自动生效 | | 凭据 | 模块无需直接持有数据库凭据 | 由运维分配专用受限身份,限制可访问库与操作;不能暴露给 H5 | | 容器连接 | 继续断网,以 Unix socket 调公共服务 | 获批容器固定挂载专用数据库 socket;不开放自由出网 | | 审计与恢复 | 当前记录、去重和审计共同提交 | 模块事务与平台审计跨边界,需设计事务 outbox/可靠事件及幂等 | | 生命周期 | 底座统一备份状态 | 需定义建库、备份、恢复、迁移、删除与版本回退策略 | 建议先用受控 PostgreSQL 服务为应用分配独立数据库账号和 schema,禁止跨应用读写;仅允许该应用接入指定数据库目标,保留云元数据、其他应用及公网访问限制。凭据可由受保护配置或短期凭据机制下发给受信任执行环境;有凭据的原生模块能使用其被授予的范围,因此账号权限和资源限制必须最小化,不能承诺模块永远无法读取自己进程内的凭据。 也可先增加“受控数据服务”而非直接数据库连接,由底座提供支持事务与条件更新的专用 API,使模块不持数据库密码;这需要新的数据契约,不能把现有简单 records.* 当成任意 SQL 代理。嵌入式 SQLite 则需要独立持久卷、容量限额、备份和单写者规则,不能随容器删除丢失,也不适合多个副本共享文件写入。 无论采用哪种模式,底座负责可信身份、动作入口和运行策略,模块负责领域规则;跨模块通过已定义服务交换数据,不共享超级数据库账号。数据库改造必须与并发、多副本、数据权限、审计和恢复一起设计,不能只增加一个挂载目录。 实现依据:`examples/cms/ui/cms.js`、`examples/cms/main.go`、`internal/platform/module_ui.go`、`modulekit/module.go`、`internal/platform/execution.go`、`internal/platform/data_scopes.go` 与 `internal/platform/docker.go`。 ### 4.8 模块 API 网关(v0.15)的落地 模块通过 api_routes 自定义 JSON 路径,H5 使用 PlatformUI.request,底座鉴权后仍以 Execute RPC 转发,并提供 req.API 路由元数据。CMS 0.5.0 已采用该模式,旧动作接口兼容。该网关没有增加内部 HTTP 服务,也不开放容器网络;当前不支持上传、流式协议或任意状态码。完整声明与开发例子见 /docs/module-api.md。 ### 4.9 专属 PostgreSQL 已实现(v0.16) 按环境scope和模块分配独立账号/schema,OA独立实例只用Unix socket,不改动服务器既有PostgreSQL。每账号2连接,专用实例整体160MiB/CPU50%;应用不能建库建角色、读宿主文件或跨schema。支持模拟空间、正常停止停用账号、凭据轮换和本机每日备份,已完成实际恢复验证。数据范围、业务迁移和事务审计由自有SQL应用显式实现,其他旧应用继续原存储;CMS 0.5.0 已显式迁移到专属 PostgreSQL。完整边界见 /docs/module-database.md。 ### 4.10 CMS PostgreSQL 表结构与迁移(底座 v0.16.1 / CMS 0.5.0) CMS 清单仅申请 identity.get、database.connect,不再调用 records.*。表结构定义在 examples/cms/schema.sql,由后端嵌入:articles 保存当前稿件;article_revisions 保存每次修订与幂等响应;audit_events 保存同事务业务审计;schema_migrations 和 legacy_imports 记录版本及导入指纹。后台“应用数据库”显示实际账号、schema、容量和连接数。 底座 identity.get 新增 record_access:read 和 action 分别给出 all/self/department_ids 的有效并集,考虑当前角色、部门树及 API Key 限权;CMS SQL 取两者交集,再执行作者与审核状态规则。宽范围阅读角色不会扩大发布范围。新建稿件的部门来自可信身份,修改保留原归属;H5 不能指定作者、部门或状态。该字段是供应用实现过滤的可信策略,拥有数据库账号的恶意模块仍可绕过其自身 SQL,schema 隔离只保护其他应用。 迁移在停服窗口执行:备份旧状态与应用包,按旧事件顺序导入全部修订、原记录 ID、作者/部门、请求号、内容哈希与审核审计,单事务提交;核对每条历史响应和总数,重复导入相同指纹不会再写。旧 JSON 记录保留为迁移归档,后续 CMS 只写 PostgreSQL。上线前按现有每个用户的实际角色比较迁移前后的可见稿件;已开始 PG 写入后不能直接回退旧 JSON 程序,应停写后做反向迁移或修复。 ### 4.11 语言无关协议(v0.17) 公开协议 v2 使用双向 JSON-RPC 2.0 标准输入输出,不依赖 Go/gob;已提供 Go ServeJSON 与 Python SDK/受控运行配置,并通过两种语言的真实 Docker 发布、身份范围和重启验收。原 Go 协议 v1 兼容通道继续服务 CMS 等现有模块。底座按独立随机 context_id 绑定每次宿主回调,不能只相信模块发送的 actor_id;后台管理、H5/API、数据库能力和超时/限流策略共用。当前仍单模块单执行槽位,不宣称自动获得多副本或更高性能。详见 /docs/module-jsonrpc.md。 ### 4.12 开发环境管理的请求调度(模块1.1.1) cloud-admin 页面曾在启动时并发请求配置、转发、安全组和流量,超过同模块250ms等待预算,导致“模块繁忙,本次请求未执行”。现改为页面内单请求调度、关键读取优先、同参数读取合并、仅明确未执行的读取做有限重试;写操作不自动重试。未更改后端身份绑定、锁或准入预算;详见 /docs/cloud-admin.md。