M3 平台与 SaaS 实施记录
# M3 平台与 SaaS 实施记录
2026-10-23,M3 工程与自动化验收完成,schema v11。M1、M2 已按用户人工确认结项;本页不另行声称 M3 获得了用户人工确认,也不表示 M4 发布门槛已完成。
# 交付与架构
| 工作包 | 已实现行为 | 证据入口 |
|---|---|---|
| 平台租户 | 独立登录、列表/详情、PENDING 创建、幂等初始化、停用恢复、到期续期 | tenancy_test.go、tenancy_http_test.go、平台租户页 |
| 套餐能力 | 发布能力与权限关联、版本化套餐、受影响租户数、降级保留数据/选择、扩容后恢复有效选择 | TestIntegrationTenancyPackageQuota、平台套餐页 |
| 权限上限 | inherit/custom;custom 空集合明确拒绝;不能加入平台或套餐外权限 | 服务层交集、真实 HTTP 与前端事件回归 |
| 配额 | 初始管理员及停用账号计入;软删除释放;超额只拒绝新增;并发不超额 | 两位不同管理员争夺最后一个名额 |
| 隔离与审计 | audience/组件区域隔离、拒绝租户切换头、平台无租户业务通道、独立平台审计表和页面 | M1/M2 全量回归、M3 HTTP 与 Redis 键测试 |
fago-tenancy 拥有套餐、租户状态和平台操作审计,通过 PlatformGuard、Initializer 窄端口协作,由 app 显式装配;不反向依赖 identity/system 实现。系统模块消费自己定义的 TenantPolicy,检查存活和配额。未增加循环依赖、跨模块 internal 导入或全局服务定位器。
套餐是已发布能力的集合,不复制菜单。有效权限仍是启用角色授予、租户选择、套餐能力与启用目录的交集;菜单从同一集合投影。平台 /permissions 保留本人平台目录,新的 /tenant-permission-options 返回带 capabilityCode 的 admin 发布选项,不能借目录跨 audience 授权。
# 事务与失败边界
- 创建只提交 PENDING。初始化锁定租户,在同一个本地事务内写管理员、四个保留角色、安全策略、ACTIVE 状态和审计。注入“真实种子写入之后失败”的测试确认所有表一起回滚为 PENDING。
initialized_at是幂等标记,重复请求 ACTIVE、停用或到期的已初始化租户都不重建账号、不覆盖凭证;首次执行仍要求当前版本。并发两次初始化只产生一位管理员、四个保留角色和一条初始化成功审计。- 平台写锁序是平台账号→当前平台会话→目标套餐→租户 ID 升序。守卫在同一事务复核会话、账号、密码有效期和当前权限;写隔离级别 READ COMMITTED,避免等待资源锁后读旧快照。
- 套餐修改锁住所有引用租户,与分配套餐和账号创建串行;能力及租户授权版本同事务变更。角色、租户选择和业务数据不因降级被删除,重新扩容后原有效选择可重新生效。
- 配额与新增在同一个租户行锁内,用有索引的未删除用户 count 作权威用量,避免双写计数器漂移。停用不释放配额,初始管理员计入;现有用户超额仍能正常读、改、删,新增返回
409005。 - 启停或到期时间实际改变会推进
session_version,会话固定签发时的tenant_session_version。每次主库认证和敏感系统写入比较代次,恢复或续期不会复活旧会话。到期是时间判定,不依赖定时任务。 - 未知字段、重复/未知查询、非规范 ID、错误 Origin、错误 realm 及租户切换头拒绝。错误只输出登记的业务码、中文提示和 requestId;密码、token、Cookie、散列不进入响应或平台变更快照。
具体扩展取舍见 总体架构。当前 R1 不引入分布式事务、异步权威配额或新的通用任务系统;大规模套餐批量更新需另行验证锁范围与容量,不把当前测试写成吞吐承诺。
# 实际验证
| 检查 | 实际结果 |
|---|---|
| 完整应用集成 | MySQL 8.4、PostgreSQL 16 的独立随机测试库,各 29 个顶层集成测试通过,均启用 race;耗时分别约 259.3s / 260.4s |
| M3 专项 | 两库 TestIntegrationTenancyLifecycle/PackageQuota/HTTP/LoginKeys;在完整回归之后补强不同操作者配额竞争、保留选择恢复和 Redis 键隔离 |
| 真实 HTTP 合同 | 每库 plain/rsa-aes/sm2-sm4 各 51 条,共 306 条实际响应,102 明文、204 加密;解密后统一 OpenAPI 校验通过 |
| 迁移 | 两库空库、v1/v4/v5/v6/v8/v9 有数据升级、重复迁移、历史 dirty/校验和/缺号拒绝通过;v9 保留口令散列、会话、摘要、custom 选择和期限 |
| 默认 workspace | 14 个声明模块逐模块 race/vet 通过;缓存与未接入试验目录不混入构建清单 |
| 独立模块 | 14 个默认模块通过 GOWORK=off、新缓存、本地源码代理及副本 tidy/test;DAG 与 internal 边界检查通过 |
| 前端 | OpenAPI lint、627 个示例、生成类型、typecheck、91 项回归、生产构建通过;构建页面使用真实 API |
| 浏览器 | 独立 8082/5175 实例核对平台导航、列表/详情、初始化/权限表单及取消、套餐影响预览、独立审计与重载恢复;临时夹具正常释放清理 |
真实响应证据:MySQL、PostgreSQL。机器汇总与文件校验和见 M3 验证结果。记录的测试库与端口不构成生产兼容认证;Oracle/SQL Server/DM、完整消息后端和通用工作流不在本次支持声明内。
# 命名用例与隔离范围
TestIntegrationTenancyLifecycle 覆盖初始化失败后无半成品、并发重试、受限初始管理员、停用恢复与到期续期、旧凭证 fencing、过期版本和不覆盖原账号。TestIntegrationTenancyPackageQuota 使用两位不同管理员竞争配额,验证降级不删数据、删除释放、菜单/API 同时收窄、选择保留与恢复、空 custom、平台/租户服务隔离及事务内会话复核。实际测试曾发现权限更新响应版本多加一次的缺陷,已改为更新和响应共用预计算版本,并加入落库版本断言。
TestIntegrationTenancyLoginKeys 在同一 IP 对两个同名 alice 账号发真实 Redis 限流请求:A 达到账号阈值后 B 仍可登录;对应账号计数分别为 21 与 1。只清理当前随机 IP 和本用例账号键,不使用 FLUSHDB。当前没有以 Redis 权限缓存作为权威授权源;权限/菜单始终从主库同一快照计算。
完整回归继续执行 TestIntegrationOrganization、TestIntegrationUserRoleLifecycle、TestIntegrationPermissionRoleScopes、TestIntegrationRoleBoundaries、TestIntegrationAuditQueries、TestIntegrationIdentityAuthorization 等:两个租户相同用户名、不同部门/角色,详情、修改、关联替换、分页/count、日志范围及同租户数据库约束均拒绝跨租户提权;保留管理员、再鉴别、改密/撤销/刷新竞争同时回归。平台 token 调租户 API 与租户 token 调平台 API 均拒绝。X-Tenant-ID、X-Tenant-Code、tenant-id、visit-tenant-id、X-Visit-Tenant-ID 均在真实 HTTP 链拒绝。
# 可复现命令
先从外层仓库根目录执行 source scripts/go-env.sh,显式设置 FAGO_INTEGRATION=1、APP_DATABASE_DRIVER、APP_DATABASE_DSN 和 APP_REDIS_ADDR;DSN 必须指向专用可清理测试库,迁移测试账号需有创建随机测试库权限。报告不保存这些凭证。
bash scripts/test-backend.sh
bash scripts/test-independent.sh
# 环境变量已在当前终端按专用测试环境设置;MySQL、PostgreSQL 分别执行。
bash scripts/integration.sh
# M3 专项,仍须先在根目录 source 工具链环境。
cd fago/apps/fago-server
# MySQL 使用 fago_mysql;PostgreSQL 使用 fago_postgres。
go test -tags=fago_mysql -race -count=1 -v -run '^TestIntegrationTenancy' -timeout=12m .
导出 HTTP 证据时,给 TestIntegrationTenancyHTTP 设置 FAGO_M3_HTTP_EVIDENCE 到新建的私有本机文件;测试只采集通过泄漏检查的响应,不写请求或凭证。随后在根目录验证:
node scripts/check-live-contracts.mjs docs/evidence/m3-http-mysql.json docs/evidence/m3-http-postgres.json
bash scripts/web.sh contract
bash scripts/web.sh typecheck
bash scripts/web.sh exec vitest run apps/web-antd/src
bash scripts/web.sh build
TestBrowserPlatformAcceptance 仅以 browser 构建标签进入专用测试程序,要求 APP_ENV=test、显式集成开关和新的私有目录/0600 凭证输出。创建 <输出文件>.release 后清理;十分钟未释放则执行清理并判定失败。本轮没有在浏览器输入新密码或提交权限变更;这些动作的真实写入由 HTTP 测试证明,不能表述为用户人工完成了同样操作。




# 退出判定与后续边界
M3 的平台建租户/成功初始化/失败重试、停用与到期拒绝、套餐收窄、配额及隔离工程退出条件已通过上述实际代码和自动验证。平台页面已经可操作,操作说明见 平台租户与套餐。浏览器展示证据与真实写入测试分别列出,不将局部页面核对或虚拟 DOM 代替完整 E2E。
M4 仍须执行「平台创建租户→租户授权→普通用户操作」全链路 E2E、固定数据量容量基准、备份恢复、升级回滚、部署加固和发布交付。当前没有生产发布、备份恢复完成或用户独立人工验收 M3 的声明。M1/M2 已有人工确认继续有效,不重复请求或恢复旧暂缓状态。
# 数据库按需构建与达梦复核
按用户澄清,数据库选择用于裁剪同一个项目的产物体积,不维护两种发布包。当前 scripts/build-go.sh 只生成当前项目数据库、当前 OS/架构的一套 API/worker/CLI,固定输出 .cache/release/,同时移除调试符号。默认不附带额外 Linux 版本,也不附带商业库探针。旧 .cache/bin/<driver>/ 是首次验证的历史开发缓存,不属于当前发布目录。
bash scripts/test-database-profiles.sh 只在临时目录验证两种配置:嵌入资源只有选定一套完整 SQL,错误方言在取连接之前拒绝,三个实际二进制只含对应驱动,平台一致且目录中无额外文件。临时产物随验证退出清理。开发和集成入口继续复用相同数据库选择,迁移和初始化仍走统一 CLI。
2026-10-23 以 Go 1.27.3、darwin/arm64 和同一后端源版本对照 API:双驱动/双方言且含调试信息为 43.73 MiB;双驱动/双方言去掉调试信息后为 29.64 MiB;只含 MySQL 并去掉调试信息后为 26.28 MiB。在同样去掉调试信息的条件下,数据库裁剪减少 11.35%;两项合计减少 39.91%。前两项通过临时 overlay 还原未裁剪链接,仅用于体积比较,不作为业务支持构建。机器数值与校验和见 体积对照;不同平台、工具链与依赖版本的体积需重新测量。
达梦按用户提供的专用开发实例复跑,补测 GORM CRUD、主键回填、中文分页、DML 回滚,发现并修复供应商方言缺少唯一键错误映射的问题。实例事实、修复与剩余边界见 M0 达梦复核 和 实际探针证据。这不扩张 M3 的业务数据库支持范围,DM 的完整业务迁移与租户认证仍待后续实施。