# 模块专属 PostgreSQL 账号与 schema(v0.16) 模块可以继续使用底座 records.*,也可以申请自己的 PostgreSQL 账号和 schema,或组合使用。启用专属数据库不会自动迁移现有记录;CMS 0.5.0 已通过显式迁移改用 PostgreSQL;其他未申请的存量应用保持原存储。 ## 架构与申请 OA 使用独立的 PostgreSQL 17 实例,与服务器上其他项目的既有 PostgreSQL 服务分开;只监听 Unix socket,不开启 TCP 端口。实例数据库为 oa_modules,底座控制服务按“运行环境 scope + 模块 ID”生成独立账号和 schema,应用不能指定任意数据库、用户名、schema、连接地址或挂载目录。 ```json "database": {"engine": "postgres"}, "capabilities": ["identity.get", "database.connect"] ``` 模块仍声明读/写动作与角色权限,并提供自定义 H5。管理员审核并批准 database.connect 后,启动时分配或恢复账号和空间。未配置控制服务、未批准能力或环境不是 Linux Docker 时拒绝启动该数据库模块,不回退到其他存储。当前专属数据库面向有 list/create 契约的普通模块;read_only 通讯录发布模式暂不支持该声明。 ```text 模块容器 ──专用 Unix socket──→ OA 专用 PostgreSQL │ ├─ 应用 A 账号 → A schema │ database.connect └─ 应用 B 账号 → B schema ▼ 底座(校验身份/动作/能力) │ 启动时申请、停用、轮换 ▼ 受控管理服务(宿主 Unix socket,应用不挂载) ``` 只有申请并获批的应用额外挂载 PostgreSQL socket 目录到 `/database`,只读挂载且不含数据库数据文件。应用仍是 network none,不能自由出网、接触 Docker socket 或控制服务 socket。账号密码不写清单、环境变量或应用包,只经请求绑定的后端反向 RPC 返回。 ## 后端接入 ```go reply, err := host.Call(modulekit.HostRequest{ Service: "database.connect", ActorID: req.ActorID, RequestID: req.RequestID, }) // 先检查 err、reply.Code 和 reply.Error,再解析;不要打印 reply.Data。 var cfg modulekit.DatabaseConnection err = json.Unmarshal(reply.Data, &cfg) // cfg 包含 Host、Port、Database、User、Password、Schema、MaxConnections。 // 使用 pgx/database/sql 等驱动连接,连接池最多 cfg.MaxConnections(当前 2)。 ``` 连接配置只给当前模块后端,不给 H5;不应将配置放入业务返回、日志、错误正文或文档。模块应复用连接池并在退出时关闭。凭据轮换会终止连接并重启运行中的应用,新的进程重新获取配置;不要自行修改受控账号密码。 默认 search_path 指向本模块 schema 与 pg_catalog,可以直接建表、建索引并使用数据库事务。业务程序负责 SQL、参数化查询、迁移、版本条件更新、幂等和审计。例如“插入单据 + 幂等记录 + 审计/outbox”可以在自己的一个事务中完成;与底座 JSON 审计、其他模块和外部系统之间不会自动形成事务。 **员工记录权限必须重新接入。** schema 只隔离应用。数据库账号属于整个模块,不能区分编辑、总编或部门;模块应从可信请求身份获取操作者,用参数化 SQL/RLS 等实现记录范围。底座 records.* 的范围过滤不会自动作用于自有 SQL。示例探针仅演示按可信 actor_id 查询本人记录,不是完整的企业数据权限库。 ## 实际权限与资源边界 账号没有超级用户、建库、建角色、复制、绕过 RLS、读宿主文件或运行宿主程序权限,不加入其他应用角色。schema 由 postgres 管理,应用仅获自己的 USAGE/CREATE,并拥有自己创建的表;public schema 的默认权限已撤销。其他 schema 的读写、SET ROLE 等应被拒绝。角色/表名等数据库目录元数据不承诺完全隐藏。 每账号最多 2 个连接。默认 statement_timeout=5s、lock_timeout=1s、idle_in_transaction_session_timeout=10s,temp_file_limit=32MB。部分会话超时可由应用自行修改,所以它们是默认保护,不能当成对恶意 SQL 的硬期限。应用容器 CPU 限制不约束数据库进程;专用 PostgreSQL 服务整体限制为内存 160 MiB、CPU 50%、64 个任务,所有申请应用共享预算。当前没有每 schema 的硬磁盘、CPU、IO 或查询容量配额,恶意/失控 SQL 仍可能影响其他 OA 数据库应用。 停止/卸载保留 schema 数据;正常停止时账号设置 NOLOGIN 并终止连接,再启动恢复。程序异常退出时连接由操作系统和数据库处理,不保证任意恶意长查询立即终止;需要时管理员可停止应用并轮换凭据。底座或 PostgreSQL 维护会中断事务,未提交事务由 PostgreSQL 回滚;客户端遇到未知写结果仍应查询或使用原业务请求号重试。 ## 模拟与验收 每个模拟目录使用独立 scope,获得独立测试账号和 schema;不会获取生产账号。模拟验证后端接口校验、授权、业务幂等和重启保持,并在结束时清理测试 schema/角色。进程异常导致清理失败的空间保留在受保护控制注册表,可由运维按 scope 清理;生产 scope 禁止自动 cleanup。 PostgreSQL 模块应在 list/create 返回兼容的记录结构以使用现有模拟契约。独立 SQL 的事务审计由模块负责,底座模拟不再声称其业务写入与 JSON 审计是同一个原子提交。任意 API 路由、迁移和自定义 SQL 规则仍需专门验收。 源码中的 `cmd/database-probe` 与 `cmd/database-acceptance` 使用真实两个容器验证独立空间、事务幂等、跨 schema/角色/宿主能力拒绝、连接上限、重启和凭据轮换;只用于标识清楚的临时环境,不作为生产业务应用安装。 ## 管理与备份 后台「应用数据库」需 modules.manage,只显示账号/schema 名称、连接数、容量与状态,不返回密码。可轮换已获批应用凭据;运行中的应用会重启,已提交数据保留。HTTP 接口:GET /api/databases,POST /api/databases/{id}/rotate(浏览器还需 CSRF)。 专用实例由 oa-module-postgres.service 管理,控制服务为 oa-module-db-control.service。每日 UTC 04:20(另有最多两分钟随机延迟)逻辑备份,保留最近 7 份,计时器为 oa-module-db-backup.timer。备份同时保存 pg_dump 自定义格式和角色/凭据注册信息,仅放宿主受保护目录 `/var/lib/oa-module-db-control/backups/`,目录 0700、文件 0600,不提供浏览器下载;这是本机备份,尚无异地副本。 恢复必须由运维在维护窗口完成:停止相关应用、验证备份指纹与可读性,在隔离数据库先恢复相应角色和权限,再使用 pg_restore 恢复 schema/数据,验证条数/幂等/业务关键查询后切换。跨主机恢复需要同时恢复受保护的账号注册信息,确保角色、密码、权限和 scope 与目标环境一致;不能只还原 SQL 表而漏掉账号。已有 JSON 状态、账号、注册和会话仍需按原流程单独备份。 参考:[PostgreSQL schema 权限](https://www.postgresql.org/docs/17/ddl-schemas.html)、[角色属性](https://www.postgresql.org/docs/17/role-attributes.html)、[认证规则](https://www.postgresql.org/docs/17/auth-pg-hba-conf.html)。 ## CMS 实际接入(底座 v0.16.1) CMS 0.5.0 使用专属账号/schema,表定义在 examples/cms/schema.sql;后端数据库访问在 examples/cms/database.go。articles、article_revisions、audit_events 共同事务提交,旧 JSON 历史经停服导入并逐条核对。identity.get 的 record_access 提供按动作计算的可信范围,CMS 将 read 与 action 范围同时用于 SQL,并保留作者及业务状态校验;范围不是数据库自动 RLS。后台“应用数据库”可查看分配结果。CMS 业务审计保存在其 audit_events,暂未汇总进底座“操作审计”页面。完整路径见 /docs/rpc-and-storage.md 的 4.10。 ## 提交包中的数据库说明与 SQL 审核 在 manifest.json 的 database 中可附 schema_version、description 和 scripts。每个 script 声明 path(必须是 database/ 下的 .sql)、kind(schema/init/migration)、execution(module/operator)与 description。脚本必须随 ZIP 提交,UTF-8、每份不超过 64KiB,总计不超过 256KiB,最多 16 份。缺少文件、路径越界、未声明文件或非法内容会拒绝提交。旧包未声明脚本时明确显示“未附带”。 ```json "database": { "engine": "postgres", "schema_version": "1", "description": "业务表、索引、初始化和迁移策略", "scripts": [{ "path": "database/schema.sql", "kind": "schema", "execution": "module", "description": "应用首次请求初始化,不生成业务样例、不删除旧数据" }] } ``` 后台“模块发布审核 → 查看详情 → 数据库定义与初始化脚本”展示结构版本、用途、执行者、SQL 正文、字节数与 SHA256。内容从该候选 ZIP 按包指纹校验后读取,只有 modules.manage 管理员可访问 GET /api/publishing/submissions/{id}/database;普通编辑和提交令牌不能读取此审核接口。展示脚本不触发 SQL 执行;底座不会以管理员账号代应用执行上传脚本。应用后端执行初始化,旧数据迁移由说明中指定的维护流程完成。声明不能证明业务二进制一定执行了相同 SQL,仍需源码审查和隔离验收。 ## 模块管理中的查看入口(v0.17.1) 后台“模块管理 → 选择应用 → 数据库与脚本”可查看存储方式、结构版本、实际 PostgreSQL 数据库/账号/schema、连接数和容量,以及当前安装包的表结构、初始化或迁移 SQL。仅 modules.manage 可调用 GET /api/modules/{id}/database,不返回连接密码或业务记录正文。 脚本读取自当前安装文件,核对匹配安装清单的已批准包,并显示当前/批准 SHA256;修改、缺失、无可核验包均单独标记,不用候选包文本替代缺失的安装文件。只读展示不会执行 SQL,也不代表已验证实际表结构与声明完全一致。 records.* 模式显示持久化文件、模块集合命名空间和已写入的集合名;首次登记前可能暂无集合,不虚构业务表。申请专属数据库后仍留存的 JSON 集合也会显示为底座留存数据,不据此认定应用当前仍写 JSON。运行配置暂不可用时仍可查看安装脚本,页面会说明状态。