【落地页白屏 —— 公网首页】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> |
||
|---|---|---|
| admin-backend | ||
| admin-frontend | ||
| deploy | ||
| docs | ||
| e2e | ||
| e2e-ui | ||
| prompt-evals | ||
| scripts | ||
| shared | ||
| tool-backend | ||
| tool-frontend | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Makefile | ||
| README.md | ||
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 供应商」页自行配置 |
三条硬约束,代码层面已经落实:
MCF_ENV=prod时自动配置被显式拒绝,只记一条 WARN。生产环境的 AI 配置必须 经由管理后台,这样才会留下审计(FR-ADM-13)——被一个环境变量悄悄写进去是不行的。- 只做「不存在才建」,绝不覆盖运营在后台改过的任何东西。重启一次就把人家改的 打回去,是最难排查的一类问题。
- 单价一律留 0。单价直接进甲方的成本核算(
FR-ADM-16),必须按真实账单填, 由运营在「模型与单价」页维护。
deploy/ai-bootstrap.dev.json 里的密钥写成 ${MCF_BOOTSTRAP_AI_KEY} 占位,
所以这个文件可以进仓库,密钥只走环境变量(.env 已在 .gitignore 中)。
交付给甲方时,AI 相关需要甲方提供
- 一个或多个 AI 供应商账号与 API Key(官方平台或中转均可,管理后台内置了常见平台的预设)
- 四类能力各自要用哪个模型:文案生成 / 图片视觉理解 / 图片生成 / 多语言本地化改写
- 各模型的单价(用于
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 白名单)供甲方选择。