本文主要介绍一类自托管媒体库(以 Octans 后端为例)在两件看似无关、实则都决定「长期可维护性」的决策上如何收口:一是内部结果模型与错误契约统一,二是配置可视化首版的边界。前者解决 Controller / Service 各造一套 *ServiceResult 的分裂;后者解决「配置能改、能看、能审计」与「部署覆盖、启动链路、权限面」之间的张力。讨论的是可被 supersede 的工程边界,不是某一版本的接口字段全集。

媒体库后端会同时长出两套压力:对前端要有稳定的 { data, meta?, error? } 消费面;对运维与 Root 用户又要能在控制台看见配置从哪来、能否改、改完要不要重启。如果内部结果模型继续按模块私有演进,错误码在字面量里散落,配置定义又以数据库行或临时 JSON 为「事实来源」,后续任何重构都会同时伤到 API 契约与部署习惯。下面按两份正式决策的口径合成一版可对外表述的主线。

一、结果模型与错误契约

问题:四套并行结果 + 查询双层包装

在结果模型统一之前,后端容易出现按模块各自生长的结果类型,例如认证、库管理、任务查询、通用查询各有一套 *ServiceResult<T>。查询主线还可能再套一层只负责「把 Data 和 Meta 绑在一起」的转运 DTO,形成:

QueryServiceResult<QueryResultDto<T>>

直接后果是:

  1. Controller 与 mapper 反复拆 result.Data!.Data / result.Data.Meta
  2. 分页与能力类元数据本就来自 query service,却被塞进临时壳,语义不稳定。
  3. 为迁移方便引入的过渡结构,会在没有明确删除决策时变成「事实标准」。

与此同时,前端其实只依赖线协议:HTTP 响应仍是稳定的 data / meta? / error.code / error.message。内部结果模型怎么设计,不该再牵动这层消费契约。

决策:内统一、外不变

正式口径可以压成四条:

层面决策
对外线协议继续 ApiResponse<T> / QueryApiResponse<T>;前端只依赖 datametaerror.codeerror.message
内部结果内核统一到共享三类:Result<T>(中性)、CommandResult<T>(命令 / 动作 / 管理)、QueryResult<T>(查询)
查询主线删除纯转运的 QueryResultDto<T>QueryResult<T> 直接承载 DataMetaStatusCodeErrorCodeErrorMessage
错误码API 面向外仍是字符串;只允许从集中目录(如 ApiErrorCodes)引用,禁止 Controller / Service 再散落写字面量

内部结果模型直接成为公开 API schema;映射由共享 mapper 完成,例如:

QueryResult<T>  →  QueryApiResponse<T>
CommandResult<T> →  ApiResponse<T>

是否用继承、record 或组合属于实现细节;「query 结果直接带 Meta」与「错误码集中出口」是契约层决策。

为什么 Meta 必须挂在 QueryResult 上

  • Meta 是查询语义的一部分totalskiptakecapabilitiesflags 等不是 Controller 临时拼出来的 HTTP 装饰,而是 query service 在业务查询时一并产出的正式结果。
  • 转运层没有独立领域语义。只把 Data/Meta 绑一下再拆一次,只会增加样板,不会增加边界清晰度。
  • 共享 mapper 需要稳定输入形状。若仍是「服务结果再包一层 DTO」,query 路径会长期停留在临时过渡态,和 command / auth / libraries 路径长得不一样。

明确拒绝的替代方案

方案拒绝原因
长期保留 QueryResultDto<T>纯转运;保留 Data!.Data 样板;query 与其它路径不一致
Meta 只存在于 QueryApiResponse来源是 service 不是 Controller;内部不带 Meta 只会逼出 tuple / out / 再包装
QueryResult 混入 HttpContext / ActionResult把 service 绑回 ASP.NET 运行时;毁掉共享 mapper 与进程无关测试

决策后果与非目标

后果:这是改动面较广的机械收口,不是局部试点;查询、认证、库管理、系统任务读取侧应同时替换到共享结果模型。对前端 API 契约不应产生结构性变化;变化的是后端内部统一方式。长期约束应沉淀到开发规范,不能只留在某次改造计划里。

非目标(刻意不做):

  • 把全部业务日志结果码、任务明细码并进同一层结果模型。
  • 顺手重命名全部业务 DTO 命名空间。
  • 把内部结果类型直接文档化为公开 OpenAPI 的唯一 schema 来源。

对同类 .NET / 多模块后端,可复用的一句话是:线协议稳定给前端,结果内核稳定给服务层,错误码只从一个集中出口出去

二、配置可视化首版边界

背景:从 appsettings 到「可看、可改、可审计」

配置一旦从「改文件重启」演进到控制台可视化,就会同时碰到:定义从哪来、数据库写什么、环境变量如何仍能压过 UI、改完要不要重启、哪些 key 能热生效、Secret 能不能回显、启动链路配置能不能被 UI 改坏。旧方案若带过宽的 Dynamic 分类、进程内软重启,或把 CORS / 权限面扩得过大,会和「同源 publish、个人开发者主线、单实例」的产品形态不匹配。

首版可视化的目标不是「所有配置热更新 + 多租户 scope」,而是把长期边界钉死,避免计划里的候选方案被误写成已落地事实。

决策摘要

1. Registry 是定义的唯一事实来源

配置定义以代码侧 registry(定义注册表)为 canonical source。数据库中的定义表只是运行期镜像,用于查询、审计和 inactive 标记。

这样可以避免:

  • 定义被运行时数据悄悄改写;
  • 配置 provider 把 factory default 误当成真实 override。

2. 数据库只保存用户 override

值表只存用户写入的覆盖项。恢复默认 = 删除 override,不做 factory default 复制进库。

生效优先级固定为:

环境变量 > 命令行参数 > 数据库 override > User Secrets > 环境配置文件 > 基础配置文件

命令行参数与环境变量保持最高优先级,并在 UI 中显示为 locked(部署侧最终覆盖,控制台不可与之对打)。

3. 首版只做全局配置

不做 tenant / user / library scope;配置键直接作为主键维度。当前主线按「个人开发者 + 单一真实用户 + 单一主线环境」决策,没有必要为假想多租户提前引入复合 scope 模型。

4. 只提供 Root-only 应用层重启

  • 不做进程内 Host 软重启。
  • 不暴露 Docker API 或 systemd 重启控制面。
  • 保存后需要重启的配置可写 pending restart 状态;Root 可通过设置页或专用 restart API 请求应用层重启。

重启链路约定:应用进程按约定退出码停止;Docker 场景下由 entrypoint 监督子进程,约定退出码时重新拉起、容器保持运行docker stop / compose stop / systemd stop 等外部停止仍按正常停机,不进入重启循环。非 Docker 部署不承诺自动拉起,由 systemd、supervisor 或手工启动承接。

拒绝软重启与 Docker socket 的原因:

  • 进程内重启易与 Host 生命周期、后台任务、实时连接、播放会话、数据库上下文深度耦合。
  • 暴露 Docker / systemd 控制面会显著放大权限边界,不符合当前单实例主线。
  • 配置变更后的重启是真实产品闭环,用 Root 确认 + 容器内 exit-code supervisor 即可覆盖,不必引入热更新复杂度。

5. Dynamic 必须逐 key 审计

只有消费端完成审计与迁移的 key 才能标为 Dynamic(运行中可读到新值)。扩大 allowlist 前,必须证明消费端不再在构造期缓存旧值。

大量与文件、扫描、刮削、播放、认证、存储相关的关键配置,首版仍应保持 AppRestart(改完需应用重启才完全生效),避免「UI 显示已保存、进程仍用旧缓存」的假热更新。

6. Secret 不回显明文

Secret key 不通过 API 返回明文;审计记录也不存明文,只存 hash。首版不承诺数据库 secret 加密落盘;若要做 encrypted-at-rest,需另开决策明确密钥来源、备份恢复与启动解锁。

7. Bootstrap 配置禁止 UI 写入

数据库连接、监听地址、静态资源根目录等启动链路配置不允许 UI 写入。数据库配置 provider 也应过滤需外部重启或只读的 key,避免 override 反过来破坏 provider 自身与启动顺序。

正向影响与代价

收益

  • Root 可在控制台查看来源、override 状态、locked 状态与生效模式。
  • 变更可走 revision、并发令牌、changeset 与条目级审计。
  • 数据库 override 不污染 appsettings*.json
  • 环境变量与命令行仍适合部署侧最终覆盖。
  • Docker 下应用层重启可保持容器存活。

代价

  • 多数配置仍需重启后完全生效;重启期间播放会话与后台任务可能中断。
  • pending restart 状态随下次启动清理;标记为外部重启的项仍按部署层处理。
  • Secret 当前是「不回显、不审计明文」,不是落盘加密。

后续能力的准入条件

能力先决条件
扩大 Dynamic消费端迁到可监控选项或入口实时读取,并做端到端验证
引入 scope先确认真实存在多用户 / 多库独立配置需求,再设计迁移
扩展重启控制面若接 Docker API、systemd、多实例编排或重启审计,须另开决策写清权限、失败恢复与用户确认
Secret 加密落盘先确定密钥来源与备份恢复流程

两条决策如何叠在一起

结果契约解决的是模块内部如何返回、如何映射、如何报错;配置可视化解决的是实例级参数如何被看见与被安全修改。叠在一起时,有几条值得显式对齐:

  1. 配置读写 API 也走统一结果内核。设置页保存、列表查询、重启请求不应再发明第四套 ConfigServiceResult;查询类带 Meta(分页 / 能力),命令类走 CommandResult,错误码进集中目录。
  2. 线协议对前端仍是同一套封套。设置页与媒体查询页可以共用请求层对 error.code / error.message 的处理,而不是配置模块单独一套错误形状。
  3. 权限面与角色模型衔接。配置可视化与应用层重启应落在 Root(或等价的实例超级用户)能力上,而不是「能登录就能改启动参数」;角色分层见同系列 IAM 文。
  4. 部署覆盖高于 UI。结果契约保证 API 稳定;配置优先级保证 env / CLI 在可视化之上,避免控制台「保存成功」却与编排层意图对打。

架构上可以粗略画成:

前端 / 控制台
  │  只消费 { data, meta?, error? }
API 封套(ApiResponse / QueryApiResponse)
  │  共享 mapper
内部结果内核(Result / CommandResult / QueryResult)
  ├─ 业务查询 / 命令服务
  └─ 系统配置读写、审计、pending restart
定义 registry(canonical) + 值 override(DB)
  + 环境变量 / 命令行(最高优先级,UI locked)

对同类产品的可复用清单

结果 / 错误

  • 是否已消灭「每模块一套 ServiceResult」?
  • 查询结果是否直接带 Meta,而不是转运 DTO?
  • 对外 JSON 是否与内部结果类型解耦?
  • 错误码是否字符串契约 + 集中目录,禁止字面量散落?
  • 是否明确「不把内部 Result 当 OpenAPI 唯一真相」?

配置可视化

  • 定义是否在代码 registry,而不是以库表为 canonical?
  • DB 是否只存 override,恢复默认是否等于删除?
  • 优先级是否写死,且 env / CLI 在 UI 上 locked?
  • 首版是否拒绝假想多租户 scope?
  • 重启是否收敛为 Root-only 应用层路径,而非进程内热重启 / Docker socket?
  • Dynamic 是否按 key 审计,默认偏 AppRestart?
  • Secret 是否不回显、审计不存明文?
  • Bootstrap / 启动链路 key 是否禁止 UI 写入?

局限与注意事项

  • 本文是决策边界与选型说明,不是接口字段字典、迁移 runbook 或配置项全表。
  • 结果统一往往是大范围机械替换;验收应同时看内部类型收敛与前端契约零结构破坏,而不是只看编译通过。
  • 配置可视化首版刻意保守:多数 key 仍需重启;播放与后台任务会中断,产品文案与 Root 确认流程需要配套。
  • Secret 与加密落盘、多实例滚动重启、库级配置 scope 等,均属后续条件触发的议题,不应被本文写成已交付能力。
  • 具体类名、路径与退出码以目标版本实现为准;公开表述只保留机制与边界。

小结

  1. 对外封套稳定,对内结果统一ApiResponse / QueryApiResponse 不变;内部收敛为 Result / CommandResult / QueryResult,查询直接带 Meta,错误码集中出口。
  2. 配置定义在代码,值覆盖在库:registry 为事实来源;DB 只存 override;env / CLI 最高且 locked。
  3. 首版全局、Root 重启、Dynamic 慎开:不做假想 scope;应用层退出码重启替代软重启与 Docker 控制面;热生效必须逐 key 证明。
  4. 两条线共用同一套 API 纪律:配置读写也走统一结果与错误契约,权限与部署覆盖另由角色与优先级保证。

把「怎么返回」和「怎么改配置」同时钉在可审计、可映射、可部署的边界上,比先做满功能再回头拆临时模型更便宜。

相关阅读

同系列还可对照:

来源

本文综合改写自 Octans 项目决策层文档中关于后端结果模型与错误契约统一,以及后端配置可视化首版边界的说明。表述面向公开架构选型,已去掉内部仓库路径、计划编号与未发布实现细节;具体字段与 key 清单以目标版本开发规范与实现为准。