暂无描述
查找文件
weon ad1a76a88d 第二轮加固:配置语义、依赖健康、安全头、落地页白屏
【落地页白屏 —— 公网首页】use-platform.ts 用 q.data! 断言,而 TanStack Query 的
placeholderData 只在 pending 期间供数;请求失败时 q.data 是 undefined,断言就地爆掉,
整页 "Unexpected Application Error!"。讽刺的是那行上面的注释写着
「绝不让品牌配置成为单点故障」—— 它正是。改成 ?? FACTORY_DEFAULTS,
实测把接口打成 500 后页面正常降级到出厂名。

【配置语义】两侧对称:MCF_ENV 加合法值校验(设成空串也拒绝,那是更隐蔽的一半);
envInt 改错误累积,一次报出所有解析失败;prod 下拒绝已知示例密钥与已公开的
超管口令 Admin12345(强度校验放行它,只能靠黑名单)。
两侧黑名单是复制的,漂移检测【直接读对侧源码】—— 起初比的是手抄期望值,
那样只能挡住单边,改成读源码后任一侧改动都会失败,已做变异验证。

【频控 0 语义】三个参数原本两个「0=关闭」、一个把 0 吞掉。统一为 0=关闭,
但不能简单把 > 0 改成 >= 0:ipLimiter.Allow 判的是 len >= max,
max=0 时每个请求都拒,会把发码接口整个打死。改用 ipLimit = nil 表示关闭。

【依赖健康】补邮件探测。只做 3 秒 TCP,不 EHLO 不 AUTH 不发信 ——
健康页会被频繁刷新,登录尝试会触发对方封禁。对象存储刻意不加:
admin 侧没有任何真实信号,假绿最糟、永久红次糟、少一项最诚实。

【安全头】nginx 的 add_header 只在本层无 add_header 时才继承上层。
两个前端的 location 各自声明了 Cache-Control,导致 server 层三个安全头
对 HTML 文档整块失效。线上实测商户端一个安全头都没有。两侧 location 各补一遍,
并在宿主 nginx 的商户端 vhost 上立即补上(配 proxy_hide_header 防重复头)。

make test 退出码 0,两个前端构建通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 19:33:15 +08:00
admin-backend 第二轮加固:配置语义、依赖健康、安全头、落地页白屏 2026-08-28 19:33:15 +08:00
admin-frontend 第二轮加固:配置语义、依赖健康、安全头、落地页白屏 2026-08-28 19:33:15 +08:00
deploy Initial commit: AI 营销内容工厂(工具项目 + 运营后台) 2026-08-28 15:01:39 +08:00
docs 第二轮加固:配置语义、依赖健康、安全头、落地页白屏 2026-08-28 19:33:15 +08:00
e2e Initial commit: AI 营销内容工厂(工具项目 + 运营后台) 2026-08-28 15:01:39 +08:00
e2e-ui Initial commit: AI 营销内容工厂(工具项目 + 运营后台) 2026-08-28 15:01:39 +08:00
prompt-evals Initial commit: AI 营销内容工厂(工具项目 + 运营后台) 2026-08-28 15:01:39 +08:00
scripts Initial commit: AI 营销内容工厂(工具项目 + 运营后台) 2026-08-28 15:01:39 +08:00
shared Initial commit: AI 营销内容工厂(工具项目 + 运营后台) 2026-08-28 15:01:39 +08:00
tool-backend 第二轮加固:配置语义、依赖健康、安全头、落地页白屏 2026-08-28 19:33:15 +08:00
tool-frontend 第二轮加固:配置语义、依赖健康、安全头、落地页白屏 2026-08-28 19:33:15 +08:00
.dockerignore 安全加固:compose 默认值、SSRF、错误码缺失、演示标记、备份 2026-08-28 19:03:50 +08:00
.env.example 第二轮加固:配置语义、依赖健康、安全头、落地页白屏 2026-08-28 19:33:15 +08:00
.gitignore 测试报告移入 docs/delivery 并改名;新增 make delivery 一键汇总 2026-08-28 15:30:43 +08:00
docker-compose.yml 安全加固:compose 默认值、SSRF、错误码缺失、演示标记、备份 2026-08-28 19:03:50 +08:00
Makefile 安全加固:compose 默认值、SSRF、错误码缺失、演示标记、备份 2026-08-28 19:03:50 +08:00
README.md 第二轮加固:配置语义、依赖健康、安全头、落地页白屏 2026-08-28 19:33:15 +08:00

AI 营销内容工厂

SOW YL-SDA-SGP-20260801-001 · 主合同 YL-SDA-SGP-20260801 需求书见 docs/:A = 商户端工具项目,B = 运营管理后台

两个独立部署的应用,共用一套数据库(NFR-03)。

仓库结构

shared/            两侧共用的 Go 模块 —— 跨系统契约的实现载体,见下节
admin-backend/     运营管理后台 后端(Go + Gin + GORM)        :8081
admin-frontend/    运营管理后台 前端(React + Ant Design 5)    :8080
tool-backend/      商户端工具项目 后端(Go + Chromium 渲染)     :8082
tool-frontend/     商户端工具项目 前端(React + Tailwind)       :8083
e2e/               跨两个后端的端到端业务验收
deploy/            开发/测试环境的 AI 配置
docs/              SOW、三份需求书
docs/delivery/     合同交付物(D2 部署文档等)
docs/dev/          API 文档、测试报告

快速开始

cp .env.example .env      # 填入开发中转的密钥(见下方「AI 供应商的环境差异」)
make up                   # 起全栈五个容器
入口 地址
商户端(商户使用) http://localhost:8083
运营管理后台 http://localhost:8080

首次启动会自动完成:建表 → 建超管 → 灌 7 套提示词模板 → 配好 AI 供应商与四条能力绑定。 起来就能用,不需要手工点任何配置。

⚠️ 启动顺序是固定的:admin-backend 负责建表与迁移(NFR-09), tool-backend 只校验表存在。compose 已用 depends_on: service_healthy 编排好, 但甲方在 ECS 上是分别拉起的 —— 这一条写在 D2 部署文档 第 3 节。

默认超管:admin@scash.local / Admin12345(由 MCF_BOOTSTRAP_ADMIN_* 决定, 已存在则跳过,不会覆盖改过的密码)。

不用容器的话:

make db-up          # PostgreSQL :55432
make run-backend    # 运营后台后端 :8081
make run-frontend   # 运营后台前端 :5174
make run-tool       # 商户端后端  :8082

AI 供应商的环境差异

这是本项目最容易在交付时出错的一处,单独说明。

环境 AI 供应商来源 怎么配
开发 / 测试 乙方提供的中转(deploy/ai-bootstrap.dev.json) 启动时按该文件自动建好,密钥从 .env 的 MCF_BOOTSTRAP_AI_KEY 取
生产(甲方环境) 甲方自备 不设 MCF_BOOTSTRAP_AI_*;由甲方运营在管理后台「AI 供应商」页自行配置

三条硬约束,代码层面已经落实:

  1. MCF_ENV=prod 时自动配置被显式拒绝,只记一条 WARN。生产环境的 AI 配置必须 经由管理后台,这样才会留下审计(FR-ADM-13)——被一个环境变量悄悄写进去是不行的。
  2. 只做「不存在才建」,绝不覆盖运营在后台改过的任何东西。重启一次就把人家改的 打回去,是最难排查的一类问题。
  3. 单价一律留 0。单价直接进甲方的成本核算(FR-ADM-16),必须按真实账单填, 由运营在「模型与单价」页维护。

deploy/ai-bootstrap.dev.json 里的密钥写成 ${MCF_BOOTSTRAP_AI_KEY} 占位, 所以这个文件可以进仓库,密钥只走环境变量(.env 已在 .gitignore 中)。

交付给甲方时,AI 相关需要甲方提供

  1. 一个或多个 AI 供应商账号与 API Key(官方平台或中转均可,管理后台内置了常见平台的预设)
  2. 四类能力各自要用哪个模型:文案生成 / 图片视觉理解 / 图片生成 / 多语言本地化改写
  3. 各模型的单价(用于 FR-ADM-16 成本折算,须与甲方实际账单口径一致)

甲方运营的配置动线是:AI 供应商(选平台 → 贴 Key → 测试连接)→ 模型与单价(从测试连接拉回的模型列表里挑,填单价)→ 能力绑定(四类能力各绑主备模型)。

为工具项目预留的复用面

shared/ 不是「顺手抽出来的公共代码」,而是跨系统契约的实现载体。 商户端工具项目直接 import 这些包,两侧行为由同一份代码保证一致:

包 契约 工具项目怎么用
shared/model 共用数据库实体,含读写职责注释 直接用,不再定义一套自己的结构体
shared/modelcfg 契约 #5 AI 模型配置热更新(FR-ADM-12) resolver.Resolve(ctx, cap) 拿到主备模型 + 参数 + 明文 Key;版本号轮询,后台保存即生效
shared/quota 契约 #3 配额预检与余额(FR-BAT-08、FR-WS-02) Precheck() 生成前拦截;GetBalance() 展示剩余额度
shared/usage 契约 #4 用量流水唯一事实来源 每次生成后 Record(),成本按当时单价冻结
shared/authx 登录规则(FR-AUTH-01~04、11~13) JWT 签发校验、密码哈希、登录锁定;aud 隔离两侧令牌
shared/errcode FR-I18N-04 语义化错误码 后端只返回 code + params,前端语言包渲染
shared/httpx 统一响应体与分页 两个项目的前端共用一套解包逻辑与错误码命名空间

典型用法(工具项目侧):

snap, err := resolver.Resolve(ctx, model.CapCopy)   // 拿生效配置
if err := quotaSvc.Precheck(ctx, mid, quota.Need{Copy: 50}); err != nil { ... }  // 生成前预检
job.ParamsSnapshot = snap.Fingerprint()             // 入队时冻结配置快照
// …调用 AI…
recorder.Record(ctx, usage.Event{ /* … */ }, snap)  // 生成后必须写流水

⚠️ usage.Record 漏写会同时导致配额失准与甲方对账对不上。 这是契约 #4 的原话。

可运行的参照实现: shared/examples/toolproject 把上面五步写成了一个能跑的程序, 已在真实供应商上验证通过(见 docs/delivery/D3-Test-Report.md)。工具项目团队照着抄即可:

MCF_DATABASE_DSN=... MCF_SECRET_KEY=... MCF_DEMO_LIVE=1 go run ./examples/toolproject

不设 MCF_DEMO_LIVE=1 时只跑到第 3 步(不产生 API 调用费用)。

数据库迁移的归属(NFR-09)

两个项目共用一套库,迁移只能由一侧执行。本项目是负责方:

  • 它持有全部管控类表(model_config、prompt_template、merchant_quota、admin_user…)
  • 工具项目一启动就要读 model_config,表不存在会直接起不来

因此部署顺序固定为:先起 admin-backend,再起工具项目。 这一条必须写进 D2 文档。 工具项目侧不应调用 AutoMigrate。

环境变量

配置分三层(PRD-B「配置分层」)。本文件只列第一层——第二层业务配置(模型、参数、 配额、单价)在后台界面里配,不进环境变量,否则「保存即生效」(FR-ADM-12)就是假的。

变量 必填 默认 说明
MCF_DATABASE_DSN ✅ — PostgreSQL 连接串
MCF_SECRET_KEY ✅ — JWT 签名 + API Key 加密。≥16 字符
MCF_ENV dev dev / staging / prod
MCF_ADMIN_HTTP_ADDR :8081 监听地址
MCF_ADMIN_CORS_ORIGINS http://localhost:5174 逗号分隔。同源部署时可留空
MCF_SESSION_TTL_HOURS 12 会话有效期
MCF_SESSION_REMEMBER_TTL_DAYS 7 勾选「记住我」
MCF_LOGIN_MAX_FAILS 5 登录失败锁定阈值(FR-AUTH-13)
MCF_LOGIN_LOCK_MINUTES 15 锁定时长
MCF_BOOTSTRAP_ADMIN_EMAIL / _PASSWORD — 首个超管,幂等
MCF_BOOTSTRAP_PROVIDER_KEYS — code=key,code2=key2,仅注入尚无 Key 的供应商
MCF_DB_LOG_LEVEL warn silent/error/warn/info
MCF_LOG_LEVEL / MCF_LOG_JSON info / false 生产建议 MCF_LOG_JSON=true

⚠️ MCF_SECRET_KEY 视为一次性设定。 换掉它会让已加密的供应商 API Key 全部无法解密(表现为所有生成调用失败),且已签发的会话全部失效。 确需轮换必须同步在后台重录所有供应商 Key。

权限模型(FR-ADM-31)

角色 统计查看 租户/配额管理 模型配置与提示词 单价 管理员账号
超级管理员 super ✅ ✅ ✅ ✅ ✅
运营 operator ✅ ✅ ❌ ❌ ❌
只读 viewer ✅ ❌ ❌ ❌ ❌

「运营不可改模型配置与单价」是需求原文的硬约束,所以模型配置与单价是两个独立权限点 而非笼统的写权限——合并成一个看起来更整洁,但那样这条约束会在某次重构中被顺手抹掉。 回归测试:admin-backend/internal/middleware/rbac_test.go。

测试

make test              # 单元 + 集成 + 前端(不花 AI 额度)
make test-e2e          # 端到端业务验收(需两个后端在跑,会真实调用 AI)
make lint              # go vet + tsc

e2e/ 是跨两个后端的业务验收套件,只走 HTTP,不 import 任何一侧的内部包 —— 测的是接口契约与真实业务链路,与甲方验收时的视角一致。 覆盖 M0 账号、M1 品牌、M1B 商品、M2 五语文案、M3 十套模板/两种尺寸/泰文实拍、 M4 社媒与 EDM、M5 编辑还原审批导出、M6 批量 50 行、M8 配额可见。

集成测试通过 MCF_TEST_DSN 开关:未设置则跳过,保证没有数据库的环境也能跑单元测试。

已知边界

  • 登录失败锁定是单机内存实现(shared/authx/lockout.go)。多副本部署时同一账号的 失败计数会分散在各副本,相当于把阈值放大到 5 × 副本数。管理后台用量极低, 单副本完全够用;若甲方要多副本,需换 Redis 后端。这一点须写进 D2 文档。
  • 依赖健康页不主动探测 LLM/生图,取的是供应商页最近一次「测试连接」的落库结果。 主动探测会在页面刷新时产生真实 API 费用,而这笔费用由甲方承担(SOW 第三条第 8 项)。
  • 本项目对商户业务数据一律只读(brand_profile、product、generation_job、 usage_record)。唯一写入方向是管控类字段。

网络边界(NFR-10)

管理后台不应与商户端共享公网入口。独立部署的核心价值之一就是甲方运营后台不与 商户端共享攻击面;部署时若又合并到同一个公网入口,这层收益就没有了。 D2 需给出建议形态(独立域名 / 内网 / IP 白名单)供甲方选择。