面向"包装升级决策"的 SaaS。用户递交项目 → AI 顾问引导确认 → 10 阶段流水线生成 19 章报告 + 6 个方向配图 → 会员/积分/付费解锁交付。 本文件基于对 gitee packhorizon/pack-horizon@master 的全量代码通读,还原当前生产架构、数据模型、业务流、部署形态与在建改造。
前端 SPA 与后端 FastAPI 是唯二"服务",PostgreSQL 是唯一事实源,Redis 同时充当 Celery broker、分布式锁与反暴力计数。 报告和图片两条独立队列各自跑 Celery worker(--concurrency 3 · --concurrency 9), 两个 15 秒 poller(scheduler)内嵌在 API 进程里,从 DB 抓 pending 抢 Redis lock 后向 Celery 投递 —— 不是 Celery beat。
flowchart LR
subgraph Client["浏览器"]
UI["React SPA
Workspace / Admin / Payment"]
end
subgraph Edge["frontend 容器 · Nginx"]
NG["nginx.conf
SPA fallback + /api 反代"]
end
subgraph API["backend · FastAPI 进程"]
FA["create_app()
main.py:26"]
SCH1["ReportScheduler
report_scheduler.py:313"]
SCH2["ImageScheduler
image_scheduler.py:188"]
SVC["services/*
ai_workflow · intake_consultant
commerce · credit"]
end
subgraph Queue["Redis 7"]
RED[("Broker · Lock · AuthAbuse")]
end
subgraph Workers["Celery workers · 独立容器"]
RW["report-worker
run_report_workflow"]
IW["image-worker
run_image_generation_job"]
end
subgraph DB["PostgreSQL 16"]
PG[("workflow_runs · stage_artifacts
packaging_reports · image_generation_jobs
prompt_versions · users · orders · credit_grants")]
end
subgraph AI["外部 AI"]
P1["OpenAI /v1"]
P2["DeepSeek /v1"]
P3["小米 MiMo /v1"]
P4["OpenAI Compatible"]
end
subgraph Pay["外部支付"]
WX["微信支付 V3"]
end
UI -- fetch --> NG
NG -- /api --> FA
FA <---> PG
FA -- publish --> RED
SCH1 -- claim/publish --> RED
SCH2 -- claim/publish --> RED
RED --> RW
RED --> IW
RW -- HTTPS --> P1 & P2 & P3 & P4
IW -- HTTPS --> P1 & P3
RW <---> PG
IW <---> PG
SVC <---> PG
FA -. WeChat Pay V3 .-> WX
React 18 + Vite 8 SPA
frontend/src/main.tsx → App.tsx
FastAPI · 17 router
backend/app/main.py:26-95 create_app()
Celery 5.4 · 2 队列
report_generation · image_generation
Postgres 16 · 本地文件系统
3 bind mount 卷 · 无 OSS/MinIO
Python 后端选型偏"标准 boring stack"。ORM 走 SQLAlchemy 2.0 声明式,DB 驱动锁到 psycopg V3(_normalize_database_url 强制 postgresql+psycopg:// 方言), 迁移工具 Alembic 现有 33 个 revision。异步任务是 Celery 5.4 + Redis broker,result_backend 关掉了 —— 业务态由 DB 承担。 Scheduler 是自研 15 秒 poller,不是 Celery beat。认证走签名 Cookie session,反暴力靠 Redis 计数 + Cloudflare Turnstile。
| 维度 | 选型 | 关键位置 |
|---|---|---|
| Web 框架 | FastAPI 0.115+ | backend/app/main.py:26-95 · create_app() 挂 17 router |
| ORM | SQLAlchemy 2.0 · Mapped/DeclarativeBase | backend/app/db/session.py:1-22 · engine/SessionLocal, 池参从 settings.database_* |
| DB 驱动 | psycopg V3 | backend/app/core/config.py:11-43 · _normalize_database_url 强制方言 |
| 迁移工具 | Alembic 1.13 · 33 revision | backend/alembic/versions/6749f7a6d790_initial_core_schema.py 起 |
| 异步任务 | Celery 5.4 + Redis broker · 2 queue | backend/app/tasks/celery_app.py:8-29 · task_routes 分流 · result_backend 关闭 |
| Scheduler | 自研 poller · 15 秒 | report_scheduler.py:313 · image_scheduler.py:188 · 内嵌 API 进程 |
| 缓存 / 锁 | Redis 7 · packhorizon:* | report_queue.py:60-138 · ReportExecutionLock.claim/heartbeat/release |
| 认证 | Cookie session (签名 token) + 微信 | auth_service.py:30-53 · create_signed_token · scrypt 校验 |
| 反暴力 / 验证码 | Redis 计数 + Cloudflare Turnstile | config.py:82-86 · 用户 5/10 次触发 · 管理员 3/8 次 |
| 密码算法 | scrypt | models/access.py:73 · User.password_algorithm 默认值 |
纯 SPA,无 SSR/SSG。无第三方 state store,由 30+ 个 useWorkspace*.ts custom hook 按域拆分状态。UI 是 shadcn 风格 —— Radix primitives + Tailwind 4 + CVA。 业务 API 客户端按域拆到 frontend/src/lib/aiClient*.ts(Auth · Workspace · AdminModels · Analytics)。 部署时 Vite 打 dist/,拷进 nginx:alpine 容器 —— 对 /api/ 反代到 backend:8000,其余路径 fallback 到 index.html。
| 维度 | 选型 |
|---|---|
| 框架 / 语言 | React 18.3.1 + TS 5.8 (strict) |
| 构建 | Vite 8.0.16 · @vitejs/plugin-react · 别名 @ → ./src |
| 路由 | react-router-dom 6.30.4 · 二级用 URL search param |
| 状态管理 | 纯 React Hooks + Context · 无第三方 store |
| UI / 样式 | Radix UI + TailwindCSS 4 + CVA · shadcn 风格 |
| 图标 / 字体 | lucide-react + @fontsource-variable/geist |
| HTTP 客户端 | 原生 fetch · apiClient.ts · credentials: "include" |
| 部署 | nginx:alpine + dist/ · /api 反代 · 其余 fallback index.html |
| 渲染模式 | 纯 SPA · 无 SSR/SSG |
核心模型按业务域拆到 backend/app/models/{access, workflow, intake, commerce, system}.py。 设计上把 项目 → workflow_run → stage_artifacts → packaging_reports 建成不可变链,历史 run 永远绑定当时的 prompt_version_id —— 版本切换不会追溯影响已排队的报告。 商业化侧订单双状态机(payment_status · entitlement_status),积分走 credit_grants → credit_ledger_entries 账本模式。
| roles | 系统角色,code 唯一 |
| registration_invites | 邀请码 · users.registration_invite_id 一对多 |
| wechat_pending_registrations | 微信扫码到绑定完成之间的暂存 |
| users | 主账号 · 内嵌 login_failed_count/locked_until · openid/unionid/jsapi_openid 独立列 |
| projects | 项目主表 · has_legacy_packaging · current_workflow_run_id 反向指针 |
| project_assets | 项目文件资产(旧包装 · logo · 参考) |
| ai_provider_catalog | 供应商目录 · capability_profile_json 存能力画像 |
| ai_models | 模型配置 · provider_id → catalog(保留 provider_name 兼容) |
| stage_definitions | 阶段元数据 · system/AI 阶段区分 |
| prompt_versions | 整套版本 · is_current 单例 · based_on_version_id 自引用版本链 |
| prompt_definitions | 版本内每阶段三段 prompt · prompt_hash 用于比对 |
| workflow_runs | 报告生成运行 · 绑定 prompt_version_id · pending → queued → running → completed/failed |
| workflow_stage_runs | 每阶段快照 · gate_status 存质量门 |
| image_generation_jobs | 6 方向配图 · direction_id · charge_mode · celery_task_id · worker_heartbeat_at |
| stage_artifacts | 阶段中间产物 · is_selected / is_final |
| packaging_reports | 最终报告快照 · schema_version · content_json |
| model_call_logs | 模型调用审计 · latency · tokens · 错误 |
| intake_sessions | AI 顾问会话 · kind ∈ {new-packaging, upgrade} · confirmation_json |
| intake_messages | 顾问对话消息 · role ∈ {user, assistant} |
| intake_assets | 会话内上传素材 · asset_role: old-packaging / logo / reference / competitor |
| plans | 订阅计划 · 月/年价 · 赠积分 · 日限报 |
| credit_packs | 积分包 |
| usage_rules | 消耗规则 · 报告 200 · 付费图 20 |
| user_subscriptions | 订阅明细 · plan_code · starts_at / expires_at |
| credit_grants | 积分授权 · source_type ∈ {subscription, credit_pack} |
| orders | 订单 · payment_status · entitlement_status 双状态机 |
| order_items / order_payments | 订单明细与支付流水 |
| entitlement_deliveries | 权益投递记录 |
| credit_ledger_entries | 积分账本 · delta · balance_after |
| refunds | 退款记录 · 最新提交 aea1413b |
| system_configs | 后台可修改的运行参数键值对 |
| audit_events | 审计事件流 |
| analytics_events | 增长/漏斗埋点 |
| feedback_entries | 内测反馈 · survey_json · feedback_attachments/ |
users ─┬─(1..n)→ projects ─(1..n)→ project_assets
├─(1..n)→ intake_sessions ─(1..n)→ intake_messages / intake_assets
├─(1..n)→ orders ─(1..n)→ order_items / order_payments / entitlement_deliveries
├─(1..n)→ user_subscriptions ─→ plans
├─(1..n)→ credit_grants ─(1..n)→ credit_ledger_entries
└─(1..1)→ file_assets (avatar_file_id)
projects ─┬─(1..n)→ workflow_runs ─┬─(1..n)→ workflow_stage_runs ─(1..n)→ stage_artifacts
│ ├─(1..n)→ image_generation_jobs ─(0..1)→ generated_files
│ └─(0..1)→ packaging_reports (result_payload 快照)
└─(1..n)→ project_assets
prompt_versions ─(1..n)→ prompt_definitions ─(n..1)→ stage_definitions (by stage_code)
workflow_runs.prompt_version_id ─→ prompt_versions.id
ai_models.provider_id ─→ ai_provider_catalog.id
端到端主流程分四段:项目 intake(AI 顾问对话)→ 报告 workflow(10 阶段 · Celery 拉起)→ 6 方向配图(独立队列)→ 交付/解锁(订单 · 积分 · 微信支付)。 两个 scheduler 都是"每 15 秒抢 Redis owner lock,把 DB pending 转 queued 后 publish 到 Celery",worker 消费时再抢一次 ReportExecutionLock 防重跑。
SYS 系统阶段 · AI AI 阶段(prompt_definitions 版本化) · OPT 仅升级项目跑 · "19 章"是 report-presentation 输出 JSON 的顶层数组结构,由 prompt 约束,版本记录在 packaging_reports.schema_version。
前端页面 WorkspacePage.tsx (tab=chat),后端路由 backend/app/api/intake.py:
顾问 turn 当作一次 AI 阶段调用,prompt 用 stage_code = intake-dialogue,走 ModelRuntimeService.invoke_stage_json 输出结构化 JSON:assistantReply / summary / confirmation / missingFields / shouldShowConfirmation。confirm_intake_session 落库时把 confirmation_json 拷进 projects 完成转换。
前端触发 requestUserReportEnqueue → POST /api/ai/report-runs/enqueue:
designer-directions 阶段完成后,_execute_stage 为每个方向建 image_generation_jobs(ai_workflow_service.py:660),charge_mode 依方向序号决定:前 N 属报告包含(included-auto),其余走付费解锁(paid-auto)。
ImageScheduler(image_scheduler.py:188)与报告队列同款 —— 每 15 秒 publish_pending_image_jobs_once(:108),image-worker 消费 run_image_generation_job(image_tasks.py:9),内部同样调 ModelRuntimeService.generate_image(:151),图片落 generated_files + 本地存储卷,回写 image_url / revised_prompt。
报告付费:会员免费 · 超额或非会员下单 · commerce_service.py 建 orders,微信支付回调 wechat_payment_notify(commerce.py:112)→ entitlement_deliveries 记录 → 更新 orders.entitlement_status = fulfilled。
图片解锁:报告页点解锁 → POST /api/commerce/checkout → create_checkout_order(commerce.py:37)→ 支付成功后 CreditService.consume_credits 扣 20 积分, image_generation_jobs.charge_mode 从 paid-auto → paid-unlocked。
积分账本:每次消耗/授权在 credit_ledger_entries 落一行,balance_after 便于对账。
ProviderCapabilityService 用 capability profile(结构化输出模式 / developer role 支持 / 视觉能力)统一封装差异 —— 只有 OpenAI 官方支持 developer role,DeepSeek/MiMo 必须把 developer_prompt 合并进 system,否则报 unknown variant developer。 Prompt 侧 schema 由 Alembic 管理,内容用 DbSql 手工 SQL 补丁走灰度 —— schema 与内容变更解耦。
resolve_provider_profile(model) · provider_capability_service.py:79 · 先按 model.provider_id 查表命中直接返回,未命中调 _infer_provider_key(:118) 按名字/URL 关键字(api.openai.com · deepseek · mimo · lconai.com)回退到内置 profile。
运行时消费点在 ModelRuntimeService._merge_system_and_developer_prompt(model_runtime_service.py:505)—— 根据 profile 决定 developer_prompt 合并方式,直接对应 docs/AI供应商适配改造任务清单.md §1 描述的原始 bug。
_structured_output_mode(:514)区分 json_schema(OpenAI)与 prompt_and_repair(先请求 → 失败调 repair_stage_json(:80)修复,给 DeepSeek/MiMo 兜底)。
schema 由 3 个迁移落成:a1b2c3d4e5f6_add_prompt_versions · a7b8c9d0e1f2_simplify_prompt_versions_current_only · b8c9d0e1f2a3_globalize_report_stage_and_prompt_versions。
版本切换 API 在 prompt_version_service.py:35-341 · ensure_initial_versions() 启动兜底,resolve_version(workflow_run) 保证已排队报告不被切换影响。
灰度靠 DbSql/ 手工 SQL:
⚠ 同一时刻只有一条 is_current=true,由服务代码保证 —— 不是 DB unique index(见迁移 a7b8c9d0e1f2 的简化)。
docker-compose.yml 定义 7 服务,3 个 worker/flower 挂 profiles: [workers] —— 需 --profile workers 才启。全部 restart: unless-stopped,端口全绑 127.0.0.1,生产暴露靠外层反向代理(ECS + Nginx)。
| 服务 | Image / 命令 | 宿主端口 | depends_on | Volume · 备注 |
|---|---|---|---|---|
| postgres | postgres:16-alpine |
127.0.0.1:55432 → 5432 |
— | packhorizon-postgres |
| redis | redis:7-alpine · append-only |
127.0.0.1:56379 → 6379 |
— | packhorizon-redis |
| backend | 本地 build · ./backend/Dockerfile | expose 8000(不映射) | postgres:healthy · redis:healthy | 3 卷:storage · avatars · feedback-attachments |
| report-worker profile | 同 backend · celery -Q report_generation --concurrency ${CELERY_REPORT_WORKER_CONCURRENCY:-3} | — | postgres:healthy · redis:healthy | 与 backend 同 3 卷 |
| image-worker profile | 同 backend · celery -Q image_generation --concurrency ${CELERY_IMAGE_WORKER_CONCURRENCY:-9} | — | postgres:healthy · redis:healthy | 同上 |
| flower profile | celery -A app.tasks.celery_app flower --port=5555 | 127.0.0.1:5555 → 5555 |
redis · report-worker · image-worker | BASIC_AUTH from FLOWER_BASIC_AUTH |
| frontend | 本地 build · nginx:alpine + dist/ · ARG VITE_API_BASE_URL=/ | 127.0.0.1:8088 → 80 |
backend:healthy | — |
flowchart TB
Internet(("外部流量")) --> Reverse["外层 Nginx / CDN"]
subgraph Host["ECS 主机"]
subgraph Compose["docker-compose"]
direction TB
FE["frontend
nginx:alpine + dist/
127.0.0.1:8088→80"]
BE["backend
FastAPI + uvicorn
expose 8000"]
RW["report-worker
celery -Q report_generation
--concurrency 3"]
IW["image-worker
celery -Q image_generation
--concurrency 9"]
FL["flower
127.0.0.1:5555→5555"]
PG[("postgres:16-alpine
127.0.0.1:55432→5432
vol packhorizon-postgres")]
RD[("redis:7-alpine
127.0.0.1:56379→6379
vol packhorizon-redis")]
subgraph Vols["共享 bind mounts"]
V1["/opt/packhorizon-storage → /app/storage"]
V2["/opt/packhorizon-avatars → /app/avatar-storage"]
V3["/opt/packhorizon-feedback-attachments"]
end
end
end
Reverse --> FE
FE -- /api /health /version --> BE
FE -- /auth/wechat/callback --> BE
BE --> PG
BE --> RD
BE -. writes .-> V1
BE -. writes .-> V2
BE -. writes .-> V3
RW --> PG
RW --> RD
RW -. writes .-> V1
IW --> PG
IW --> RD
IW -. writes .-> V1
FL --> RD
BE -- HTTPS --> OpenAI["OpenAI"]
RW -- HTTPS --> OpenAI
RW -- HTTPS --> DeepSeek["DeepSeek"]
RW -- HTTPS --> MiMo["小米 MiMo"]
IW -- HTTPS --> OpenAI
IW -- HTTPS --> MiMo
BE -. WeChat Pay V3 .-> WX["mp.weixin.qq.com"]
postgres · pg_isready · 20s
redis · redis-cli ping · 20s
backend · HTTP /health · 30s
report-worker · celery inspect ping · 60s
image-worker · celery inspect ping · 60s
broker 探活,非业务探活
flower · TCP 5555 探活
frontend · wget / · 30s
ECS + 外层 Nginx 反代 · 单机 docker-compose
文档:docs/ops/FRONTEND_BACKEND_DEPLOYMENT.md
Redis 一个实例同时做 Celery broker/backend + 分布式锁 + 反暴力计数,visibility_timeout=21600(6h)与长报告 job 匹配。 对象存储走本地文件系统,3 个 bind mount 卷分别接报告图、头像、反馈附件 —— 无 OSS/MinIO。 邮件、短信均未接。验证码用 Cloudflare Turnstile,微信登录 + 支付 V3 走官方 OAuth。
| 用途 | 选型 | 关键位置 / 注解 |
|---|---|---|
| 消息队列 | Redis(同一实例做 broker + 锁) | celery_app.py:14-23 · visibility_timeout=21600 |
| 分布式锁 | Redis SET NX + TTL | report_queue.py:79-138 · ReportExecutionLock · report_lock_ttl_seconds=21600 |
| 缓存 | Redis(主要给 auth-abuse 计数) | auth_abuse_service.py · 前缀 packhorizon:auth-abuse:* |
| 对象存储 | 本地文件系统(无 OSS/MinIO) | 3 bind mount:/app/storage · /app/avatar-storage · /app/feedback-attachment-storage · 元数据落 generated_files / file_assets |
| 邮件 | — 无 | 仓库无 SMTP 相关配置 |
| 短信 | — 无 | config/env 未发现供应商 key |
| 验证码 | Cloudflare Turnstile | captcha_provider.py · env CAPTCHA_PROVIDER=turnstile + TURNSTILE_SITE_KEY/SECRET_KEY |
| 微信登录 | 官方 OAuth(扫码 + JSAPI) | wechat_auth_service.py · 双 openid(PC 扫码 openid + JSAPI openid) |
| 微信支付 | 官方 V3(JSAPI + Native) | wechat_pay_service.py · 回调 /api/payments/wechat/notify(:111)· /api/payments/wechat/refund-notify(:140) |
| 联网搜索 | 可选 · DuckDuckGo / Tavily / Bing | env PACKHORIZON_SEARCH_PROVIDER · 默认关(RESEARCH_ENABLED=0)· packaging_research_service.py |
| 任务监控 | Celery Flower | 127.0.0.1:5555 · FLOWER_BASIC_AUTH |
| 分析埋点 | 自建 analytics_events 表 | aiClientAnalytics.ts 上报 · analytics_service.py 存表 |
| IP 地理 | 本地 CSV 库(可自动刷新) | env PACKHORIZON_IP_GEO_PROVIDER=local · IP_GEO_DB_PATH |
从 docs/ 中"改造方案 / 待办清单 / 重构计划"类文档 + git log + Alembic 迁移记录反推正在填的坑。 标签:P0 阻塞内测 · 进行中 · 基本完成 · 运维/流程。
image_scheduler + image_generation_jobs 刚落成(3 个迁移 merge head),但 REPORT_LEGACY_WORKER_ENABLED 开关仍留 —— 旧 report_worker.py 未删,双轨过渡。
方案要求"API 可多副本 + scheduler 多副本抢锁",锁机制已上,但未见 K8s / systemd 多副本部署证据。
当前 10 阶段"大部分实现仍是本地模板/本地占位逻辑",report_snapshot 直写通道未移除 —— 导致"点进入研究后瞬间出报告"。
状态:待彻底移除 stub,阻塞内测转正。
已完成:ai_provider_catalog(迁移 ab12cd34ef56)· ProviderCapabilityService · developer_prompt 分离合并。
遗留:后台"新增模型"仍支持自由输入 provider_name 兜底;profile_overrides_json 明确列为"第一版不做"。
schema 已就位(3 迁移),但版本变更靠人肉 SQL(DbSql/create_prompt_version_v36_*.sql + set_..._current_*.sql)。后台 /admin/prompts 只做只读比对,无写入路径。
潜在债:生产版本切换未走审计,依赖运维手工执行 SQL,备份靠 DbSql/prompt_backups/ 目录约定。
迁移 f7a8b9c0d1e2_membership_subscriptions_and_credit_grants · 4 档订阅 + 2 种积分包 + 报告 200 积分 / 付费图 20 积分。
失败不退款,积分按订阅到期一起过期(credit_grants.expires_at 与 user_subscriptions.expires_at 对齐)。
git log:aea1413b 增加退款功能 · 18806e3e 增加退款功能。
表:refunds(commerce.py:183)。状态已上,未见对应"退款方案 md",属于运维闭环补齐。
auth_abuse_service.py + Turnstile 集成完毕 · 阈值可通过 env 调优。
遗留:AUTH_ABUSE_FAIL_OPEN=1 默认放行 Redis 故障,生产需按运维策略决定。
意图:消除 localStorage 里的业务缓存,强制"后端为唯一数据源"。
现状:30+ useWorkspace*.ts hook 都是"每次进入 tab 重新拉",与规范一致;仍存在少量前端过滤/排序逻辑未逐一核查。
AdminPage.tsx(62 KB)已经拆到 18 个 section,但单文件仍偏大 —— 关键子模块 admin/* 组件下沉到 frontend/src/components/admin/。
P0: /api/projects 旧路由未鉴权(需核实是否已修)。
P1: "DB queued 但 Celery 无任务"的假排队(Redis lock 释放时机与 scheduler 抢锁时序问题,与 9.1 强相关)。
其它:多语言混语、图片计数不一致、上游模型额度不足。
重构计划.md(Windows/CRLF 环境编辑过)保留意义在于说明"两服务架构"分家计划 —— 已基本落地。
旧后端参考清单.md 承认"旧后端未删除,作为迁移参考保留" —— backend/ 里已是新代码,旧路径以本文档索引。需要一次性 sweep。