chore: Plan 004 - launch readiness sprint (timezone fixes, health check, JWT secret)
This commit is contained in:
parent
3bd848ee36
commit
4f86f2bd62
|
|
@ -2,5 +2,6 @@ DATABASE_URL=sqlite+aiosqlite:///./test.db
|
||||||
REDIS_URL=redis://localhost:6379
|
REDIS_URL=redis://localhost:6379
|
||||||
ENVIRONMENT=testing
|
ENVIRONMENT=testing
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
JWT_SECRET=test-jwt-secret-for-testing-at-least-32-characters-long
|
||||||
SECRET_KEY=test-secret-key-for-testing-only
|
SECRET_KEY=test-secret-key-for-testing-only
|
||||||
CORS_ORIGINS=http://localhost:3000
|
CORS_ORIGINS=http://localhost:3000
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ EXPOSE 8000
|
||||||
|
|
||||||
# 健康检查
|
# 健康检查
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||||
CMD curl -f http://localhost:8000/api/health || exit 1
|
CMD curl -f http://localhost:8000/health || exit 1
|
||||||
|
|
||||||
CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", \
|
CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", \
|
||||||
"--bind", "0.0.0.0:8000", "--timeout", "120", "--access-logfile", "-"]
|
"--bind", "0.0.0.0:8000", "--timeout", "120", "--access-logfile", "-"]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import String, Uuid, JSON, Float, Text, ForeignKey, Index, func
|
from sqlalchemy import String, Uuid, JSON, Float, Text, DateTime, ForeignKey, Index, func
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
|
|
@ -28,9 +28,9 @@ class DiagnosisRecord(Base):
|
||||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
collection_metadata: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
collection_metadata: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
server_default=func.now(), nullable=False
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||||
)
|
)
|
||||||
completed_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("idx_diagnosis_records_brand_id", "brand_id"),
|
Index("idx_diagnosis_records_brand_id", "brand_id"),
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,14 @@ class PaymentOrder(Base):
|
||||||
pay_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
pay_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||||
callback_data: Mapped[dict | None] = mapped_column(JSONType, nullable=True)
|
callback_data: Mapped[dict | None] = mapped_column(JSONType, nullable=True)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
server_default=func.now(),
|
server_default=func.now(),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
)
|
)
|
||||||
updated_at: Mapped[datetime] = mapped_column(
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
server_default=func.now(),
|
server_default=func.now(),
|
||||||
onupdate=func.now(),
|
onupdate=func.now(),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
)
|
)
|
||||||
paid_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
paid_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
|
|
||||||
from sqlalchemy import String, ForeignKey, Numeric, func
|
from sqlalchemy import String, ForeignKey, Numeric, DateTime, func
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
|
@ -29,6 +29,7 @@ class Subscription(Base):
|
||||||
payment_method: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
payment_method: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||||
payment_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
payment_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
server_default=func.now(),
|
server_default=func.now(),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,13 @@ class User(Base):
|
||||||
isActive: Mapped[bool] = mapped_column(Boolean, default=True)
|
isActive: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
emailVerified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
emailVerified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
phoneVerified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
phoneVerified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
lastLoginAt: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
lastLoginAt: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
createdAt: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
|
createdAt: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||||
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now(), nullable=False)
|
updatedAt: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False)
|
||||||
mfaSecret: Mapped[str | None] = mapped_column(Text, nullable=True)
|
mfaSecret: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
mfaEnabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
mfaEnabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
loginAttempts: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
loginAttempts: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||||
lockedUntil: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
lockedUntil: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
organization_id: Mapped[uuid.UUID | None] = mapped_column(Uuid(as_uuid=True), ForeignKey("organizations.id", ondelete="SET NULL"), nullable=True)
|
organization_id: Mapped[uuid.UUID | None] = mapped_column(Uuid(as_uuid=True), ForeignKey("organizations.id", ondelete="SET NULL"), nullable=True)
|
||||||
role: Mapped[str] = mapped_column(String(20), server_default="owner", nullable=False)
|
role: Mapped[str] = mapped_column(String(20), server_default="owner", nullable=False)
|
||||||
plan: Mapped[str] = mapped_column(String(20), server_default="free", nullable=False)
|
plan: Mapped[str] = mapped_column(String(20), server_default="free", nullable=False)
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,9 @@ services:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql+asyncpg://postgres:postgres123@db:5432/geo_platform
|
||||||
|
REDIS_URL: redis://redis:6379/0
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
---
|
||||||
|
date: "2026-06-01"
|
||||||
|
topic: geo-launch-readiness
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
GEO 平台变现闭环代码已全部落地(Plan 003),但从未端到端运行过。本需求定义一个"部署+验证"冲刺:先将系统部署到类生产环境,再在部署环境中验证完整变现闭环(注册→诊断→健康分→付费墙→支付→解锁),直到新用户可完成全链路操作。
|
||||||
|
|
||||||
|
## Problem Frame
|
||||||
|
|
||||||
|
Plan 003 完成了 9 个实施单元的代码编写——诊断数据采集、免费健康分页面、Onboarding 重设计、支付集成、AI 内容生成、效果归因、邮件集成、契约测试、E2E 烟雾测试。但这些代码从未作为完整系统运行过。后端服务能否启动、前端能否构建、数据库迁移能否执行、前后端联调是否通畅——全部未知。在代码从未跑过的状态下,任何单点修复都是盲目的;只有让系统先跑起来,才能发现真正的阻塞问题。
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
**部署优先于功能验证。** 本地跑通不等于可上线。部署问题(数据库迁移、环境变量、CORS、静态资源)往往是上线前最大的意外。先解决部署,再在部署环境中验证流程,一步到位。
|
||||||
|
|
||||||
|
**支付用 mock 模式验证。** 真实微信/支付宝 SDK 接入涉及商户审核、证书配置,是独立的长期工作。本次验证用 mock 支付确认付费墙逻辑正确即可,真实支付接入作为后续计划。
|
||||||
|
|
||||||
|
**阻塞驱动,不追求完美。** 遇到问题修到能继续往下走即可,不做全面重构或优化。目标是跑通闭环,不是打磨每个细节。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
**部署就绪**
|
||||||
|
|
||||||
|
R1. 后端服务可在 Docker Compose 环境中启动,所有 API 端点可访问
|
||||||
|
|
||||||
|
R2. 前端构建产物可被部署并提供页面访问
|
||||||
|
|
||||||
|
R3. 数据库迁移可从空库执行到最新版本,所有表和索引正确创建
|
||||||
|
|
||||||
|
R4. 环境变量配置完整,第三方服务密钥缺失时优雅降级为 mock 模式
|
||||||
|
|
||||||
|
**核心流程可跑通**
|
||||||
|
|
||||||
|
R5. 新用户可完成注册并登录,获得有效 JWT
|
||||||
|
|
||||||
|
R6. 登录用户可创建品牌,触发诊断,获得非零健康分
|
||||||
|
|
||||||
|
R7. 免费用户访问付费功能时触发付费墙,显示升级提示
|
||||||
|
|
||||||
|
R8. 用户可发起支付(mock 模式),支付完成后配额刷新、付费功能解锁
|
||||||
|
|
||||||
|
R9. 公开健康分页面无需注册即可访问,输入品牌名可生成报告
|
||||||
|
|
||||||
|
**端到端验证**
|
||||||
|
|
||||||
|
R10. 完整变现闭环可在部署环境中走通:注册→诊断→健康分→付费墙→支付→解锁
|
||||||
|
|
||||||
|
**代码质量(2026-06-01 复盘新增)**
|
||||||
|
|
||||||
|
R11. Dockerfile 健康检查端点与实际 API 端点一致(当前 `/api/health` 不存在,应为 `/health`)
|
||||||
|
|
||||||
|
R12. 测试环境配置完整,`.env.test` 包含必需的 JWT_SECRET
|
||||||
|
|
||||||
|
R13. 前端页面统一使用 API 客户端,认证 token 正确传递(reports、lifecycle/new 页面绕过了统一客户端)
|
||||||
|
|
||||||
|
## Key Flows
|
||||||
|
|
||||||
|
- F1. 完整变现闭环验证
|
||||||
|
- **Trigger:** 新用户访问平台
|
||||||
|
- **Steps:** 注册账号 → 登录 → 创建品牌 → 触发诊断 → 查看非零健康分 → 尝试付费功能 → 触发付费墙 → 发起 mock 支付 → 支付完成 → 付费功能解锁
|
||||||
|
- **Outcome:** 用户完成从获客到付费的完整链路
|
||||||
|
- **Covers:** R5, R6, R7, R8, R10
|
||||||
|
|
||||||
|
- F2. 公开健康分获客路径
|
||||||
|
- **Trigger:** 未注册用户访问公开健康分页面
|
||||||
|
- **Steps:** 输入品牌名 → 系统生成 GEO 健康分报告 → 显示关键指标和问题 → 引导注册查看完整报告
|
||||||
|
- **Outcome:** 用户被引导进入注册流程
|
||||||
|
- **Covers:** R9, R10
|
||||||
|
|
||||||
|
## Scope Boundaries
|
||||||
|
|
||||||
|
**In scope:**
|
||||||
|
- Docker Compose 部署配置
|
||||||
|
- 数据库迁移执行
|
||||||
|
- 核心流程端到端验证
|
||||||
|
- 阻塞问题修复
|
||||||
|
|
||||||
|
**Deferred for later:**
|
||||||
|
- 真实微信/支付宝 SDK 接入
|
||||||
|
- CI/CD 流水线
|
||||||
|
- 性能优化和压力测试
|
||||||
|
- 生产环境域名和 HTTPS
|
||||||
|
- 完整测试覆盖
|
||||||
|
|
||||||
|
**Outside this sprint:**
|
||||||
|
- UI 打磨和视觉优化
|
||||||
|
- 新功能开发
|
||||||
|
- 代码重构
|
||||||
|
|
||||||
|
## Dependencies / Assumptions
|
||||||
|
|
||||||
|
- PostgreSQL 15 + pgvector 扩展可用(Docker 容器已配置)
|
||||||
|
- Redis 7 可用(会话和缓存)
|
||||||
|
- 第三方 API 密钥(DeepSeek、OpenAI 等)可能未配置,系统需支持 mock 降级
|
||||||
|
- LLM API 调用成本可控(诊断和内容生成会消耗 token)
|
||||||
|
|
||||||
|
## Outstanding Questions
|
||||||
|
|
||||||
|
**Resolved:**
|
||||||
|
- Docker Compose 配置已完整,包含 db (PostgreSQL 15) + redis (Redis 7) + backend (FastAPI) + frontend (Next.js) 四个服务,开发和生产两套配置均就绪
|
||||||
|
- 数据库迁移策略:使用 `create_all` + `stamp head`(KTD2)
|
||||||
|
- 前端部署方式:独立容器,开发模式用 `npm run dev`,生产模式用 standalone `node server.js`
|
||||||
|
- pgvector 扩展安装:通过 init-db.sh 从源码编译安装(KTD3)
|
||||||
|
- 验证方式:本地裸跑优先,Docker 部署后续(KTD6)
|
||||||
|
|
||||||
|
**Open:**
|
||||||
|
- 前端 reports 和 lifecycle/new 页面绕过统一 API 客户端,是否在本次修复?(当前标记为 deferred)
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
---
|
||||||
|
title: "chore: GEO Platform Launch Readiness Sprint"
|
||||||
|
type: chore
|
||||||
|
status: active
|
||||||
|
date: "2026-06-01"
|
||||||
|
origin: docs/brainstorms/2026-06-01-geo-launch-readiness-requirements.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
修复部署阻塞问题,启动服务,端到端验证完整变现闭环(注册→诊断→健康分→付费墙→支付→解锁),使 GEO 平台达到可上线状态。
|
||||||
|
|
||||||
|
## Problem Frame
|
||||||
|
|
||||||
|
Plan 003 完成了 9 个实施单元的代码编写,但这些代码从未作为完整系统运行过。研究发现 4 个 P0 阻塞问题导致服务无法启动:JWT_SECRET 长度不足、DATABASE_URL 驱动不匹配、根目录 .env 缺失、pgvector 扩展未安装。在代码从未跑过的状态下,任何单点修复都是盲目的;只有让系统先跑起来,才能发现真正的集成问题。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
**部署就绪**
|
||||||
|
|
||||||
|
R1. 后端服务可在 Docker Compose 环境中启动,所有 API 端点可访问
|
||||||
|
|
||||||
|
R2. 前端构建产物可被部署并提供页面访问
|
||||||
|
|
||||||
|
R3. 数据库迁移可从空库执行到最新版本,所有表和索引正确创建
|
||||||
|
|
||||||
|
R4. 环境变量配置完整,第三方服务密钥缺失时优雅降级为 mock 模式
|
||||||
|
|
||||||
|
**核心流程可跑通**
|
||||||
|
|
||||||
|
R5. 新用户可完成注册并登录,获得有效 JWT
|
||||||
|
|
||||||
|
R6. 登录用户可创建品牌,触发诊断,获得非零健康分
|
||||||
|
|
||||||
|
R7. 免费用户访问付费功能时触发付费墙,显示升级提示
|
||||||
|
|
||||||
|
R8. 用户可发起支付(mock 模式),支付完成后配额刷新、付费功能解锁
|
||||||
|
|
||||||
|
R9. 公开健康分页面无需注册即可访问,输入品牌名可生成报告
|
||||||
|
|
||||||
|
**端到端验证**
|
||||||
|
|
||||||
|
R10. 完整变现闭环可在部署环境中走通:注册→诊断→健康分→付费墙→支付→解锁
|
||||||
|
|
||||||
|
**代码质量**
|
||||||
|
|
||||||
|
R11. Dockerfile 健康检查端点与实际 API 端点一致
|
||||||
|
|
||||||
|
R12. 测试环境配置完整,测试可正常启动
|
||||||
|
|
||||||
|
R13. 前端页面统一使用 API 客户端,认证 token 正确传递
|
||||||
|
|
||||||
|
## Key Technical Decisions
|
||||||
|
|
||||||
|
KTD1. **DATABASE_URL 使用 asyncpg 驱动。** `database.py` 使用 `create_async_engine`,必须用 `postgresql+asyncpg://` 而非 `postgresql+psycopg://`。当前 `backend/.env` 中的值使用了错误的同步驱动。
|
||||||
|
|
||||||
|
KTD2. **数据库初始化使用 create_all + stamp head。** Alembic 迁移链存在顺序问题(alerts 表引用 brands 但 brands 在更晚的迁移中创建),直接 `alembic upgrade head` 会失败。先用 `Base.metadata.create_all()` 创建所有表,再用 `alembic stamp head` 标记版本。这与前序会话中验证过的策略一致。
|
||||||
|
|
||||||
|
KTD3. **pgvector 通过 PostgreSQL 初始化脚本安装。** 在 Docker Compose 中为 db 服务添加初始化脚本,从源码编译安装 pgvector 扩展。避免构建自定义 PostgreSQL 镜像的复杂性。
|
||||||
|
|
||||||
|
KTD4. **支付/分发/邮件保持 mock 模式。** 本次验证目标是确认付费墙逻辑正确,真实 SDK 接入是后续工作。`PAYMENT_MODE=mock`、`DISTRIBUTION_MODE=mock`、`EMAIL_MODE=mock`。
|
||||||
|
|
||||||
|
KTD5. **开发环境优先于生产环境。** 先用 `docker-compose.yml`(开发模式)验证,确认跑通后再配置 `docker-compose.prod.yml`。开发模式挂载源码目录便于调试。
|
||||||
|
|
||||||
|
KTD6. **本地裸跑优先于 Docker 部署。** Docker Hub 网络问题导致镜像构建失败,先用本地直接运行后端/前端验证核心流程,Docker 部署作为后续工作。
|
||||||
|
|
||||||
|
KTD7. **验证驱动修复。** 手动走完注册→诊断→支付全链路,每步发现问题就修,不做预防性重构。
|
||||||
|
|
||||||
|
## Implementation Units
|
||||||
|
|
||||||
|
### U1. Fix environment configuration blockers ✅
|
||||||
|
|
||||||
|
**Goal:** 修复所有 P0 阻塞问题,使服务可以启动
|
||||||
|
|
||||||
|
**Requirements:** R4
|
||||||
|
|
||||||
|
**Status:** Completed (previous session)
|
||||||
|
|
||||||
|
**What was done:**
|
||||||
|
- `backend/.env` DATABASE_URL 改为 `postgresql+asyncpg://`
|
||||||
|
- `JWT_SECRET` 改为 43 字符
|
||||||
|
- 从 `.env.example` 创建根目录 `.env`
|
||||||
|
- `.gitignore` 确认排除 `.env`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U2. Database setup with pgvector and migrations ✅
|
||||||
|
|
||||||
|
**Goal:** PostgreSQL 容器安装 pgvector 扩展,数据库表结构完整创建
|
||||||
|
|
||||||
|
**Requirements:** R3
|
||||||
|
|
||||||
|
**Status:** Completed (previous session)
|
||||||
|
|
||||||
|
**What was done:**
|
||||||
|
- 创建 `backend/init-db.sh` pgvector 初始化脚本
|
||||||
|
- `docker-compose.yml` 挂载初始化脚本
|
||||||
|
- 创建 `backend/init_schema.py` 使用 create_all + stamp head
|
||||||
|
- 数据库表和 pgvector 扩展已验证存在
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U3. Service startup and health verification 🔄
|
||||||
|
|
||||||
|
**Goal:** 后端和前端服务均可启动并通过健康检查
|
||||||
|
|
||||||
|
**Requirements:** R1, R2
|
||||||
|
|
||||||
|
**Status:** Partially completed (previous session)
|
||||||
|
|
||||||
|
**What was done:**
|
||||||
|
- 后端本地启动成功:`uvicorn app.main:app --host 0.0.0.0 --port 8000`
|
||||||
|
- 后端健康检查通过:`/health` → healthy, `/ready` → database:ok, redis:ok
|
||||||
|
- 前端本地启动成功:`npm run dev` on port 3001
|
||||||
|
|
||||||
|
**Remaining:**
|
||||||
|
- 验证前端页面可访问(`curl http://localhost:3001`)
|
||||||
|
- 验证前端可调用后端 API(无 CORS 错误)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U3.5. Fix audit-discovered P0 issues
|
||||||
|
|
||||||
|
**Goal:** 修复复盘发现的 P0 阻塞问题
|
||||||
|
|
||||||
|
**Requirements:** R11, R12
|
||||||
|
|
||||||
|
**Dependencies:** none (可并行)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `backend/Dockerfile` — 修复健康检查端点 `/api/health` → `/health`
|
||||||
|
- `backend/.env.test` — 添加 JWT_SECRET
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
1. 修改 Dockerfile HEALTHCHECK 端点从 `/api/health` 改为 `/health`
|
||||||
|
2. 在 `.env.test` 中添加 `JWT_SECRET=test-jwt-secret-for-testing-at-least-32-characters-long`
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Dockerfile 健康检查端点与 FastAPI 注册的 `/health` 一致
|
||||||
|
- `pytest` 可正常启动(不因 JWT_SECRET 缺失而 sys.exit)
|
||||||
|
|
||||||
|
**Verification:** Dockerfile HEALTHCHECK CMD 正确,测试可运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U4. Authentication flow verification
|
||||||
|
|
||||||
|
**Goal:** 新用户可完成注册、登录、获取 JWT
|
||||||
|
|
||||||
|
**Requirements:** R5
|
||||||
|
|
||||||
|
**Dependencies:** U3
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `backend/app/api/auth.py` — 认证 API
|
||||||
|
- `backend/app/services/auth.py` — 认证服务
|
||||||
|
- `backend/app/models/user.py` — User 模型
|
||||||
|
- `frontend/app/(auth)/` — 前端认证页面
|
||||||
|
|
||||||
|
**Known issues:**
|
||||||
|
- 前端部分页面绕过统一 API 客户端(reports、lifecycle/new),认证 token 可能不被附加
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
1. 启动后端和前端服务
|
||||||
|
2. 通过 API 注册新用户:`POST /api/v1/auth/register`
|
||||||
|
3. 验证用户数据正确写入数据库(plan=free, max_queries=5)
|
||||||
|
4. 登录获取 JWT token:`POST /api/v1/auth/login`
|
||||||
|
5. 用 JWT token 调用受保护 API:`GET /api/v1/brands`
|
||||||
|
6. 修复认证流程中的任何错误
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- `POST /api/v1/auth/register` 创建用户成功,返回 201
|
||||||
|
- `POST /api/v1/auth/login` 返回 JWT token
|
||||||
|
- `GET /api/v1/brands` 携带 JWT 返回 200(非 401)
|
||||||
|
- 新用户 plan 字段为 "free",max_queries 为 5
|
||||||
|
|
||||||
|
**Verification:** 新用户可完成注册→登录→访问受保护资源
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U5. Diagnosis and health score verification
|
||||||
|
|
||||||
|
**Goal:** 用户可创建品牌、触发诊断、获得非零健康分;公开健康分页面可访问
|
||||||
|
|
||||||
|
**Requirements:** R6, R9
|
||||||
|
|
||||||
|
**Dependencies:** U4
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `backend/app/api/diagnosis.py` — 诊断 API
|
||||||
|
- `backend/app/api/health_score.py` — 公开健康分 API
|
||||||
|
- `backend/app/services/diagnosis/` — 诊断服务
|
||||||
|
- `frontend/app/(public)/health-score/` — 公开健康分页面
|
||||||
|
- `frontend/app/(dashboard)/onboarding/` — Onboarding 页面
|
||||||
|
|
||||||
|
**Known risks:**
|
||||||
|
- 诊断依赖 DeepSeek API,如果 API Key 无效或额度耗尽,诊断会返回空结果
|
||||||
|
- 诊断是异步流程,可能需要轮询等待结果
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
1. 登录用户创建品牌
|
||||||
|
2. 触发诊断,验证数据采集流程(AI 平台查询 + CitationRecord 分析)
|
||||||
|
3. 查看诊断结果,确认健康分为非零值
|
||||||
|
4. 访问公开健康分页面,输入品牌名生成报告
|
||||||
|
5. 修复诊断流程中的任何错误(LLM API 调用失败、数据采集空结果等)
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- `POST /api/v1/brands` 创建品牌成功
|
||||||
|
- `POST /api/v1/diagnosis` 触发诊断,返回诊断任务 ID
|
||||||
|
- `GET /api/v1/diagnosis/{id}` 返回非零健康分
|
||||||
|
- `GET /api/v1/public/health-score?brand={name}` 返回公开健康分报告
|
||||||
|
- 公开健康分页面无需登录即可访问
|
||||||
|
|
||||||
|
**Verification:** 用户可获得非零 GEO 健康分,公开页面可生成报告
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U6. Monetization closed loop verification
|
||||||
|
|
||||||
|
**Goal:** 完整变现闭环可走通——免费用户触发付费墙、mock 支付、功能解锁
|
||||||
|
|
||||||
|
**Requirements:** R7, R8, R10
|
||||||
|
|
||||||
|
**Dependencies:** U5
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `backend/app/middleware/subscription_enforcement.py` — 订阅限制中间件
|
||||||
|
- `backend/app/api/payments.py` — 支付 API
|
||||||
|
- `backend/app/services/payment/` — 支付服务
|
||||||
|
- `frontend/components/subscription/` — 订阅 UI 组件
|
||||||
|
- `frontend/app/(dashboard)/dashboard/` — Dashboard 页面
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
1. 免费用户尝试访问付费功能(如 AI 内容生成、高级诊断),验证付费墙触发
|
||||||
|
2. 验证升级提示正确显示
|
||||||
|
3. 发起 mock 支付,确认支付流程完成
|
||||||
|
4. 验证支付后用户 plan 升级、配额刷新
|
||||||
|
5. 验证付费功能解锁
|
||||||
|
6. 修复变现闭环中的任何错误
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- 免费用户访问付费 API 返回 403 + 升级提示
|
||||||
|
- `POST /api/v1/payments/create` 创建支付订单
|
||||||
|
- Mock 支付回调后用户 plan 从 "free" 变为 "pro"
|
||||||
|
- 付费功能解锁,用户可正常使用
|
||||||
|
|
||||||
|
**Verification:** 完整变现闭环走通——注册→诊断→付费墙→支付→解锁
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope Boundaries
|
||||||
|
|
||||||
|
**In scope:**
|
||||||
|
- 修复部署阻塞问题
|
||||||
|
- 数据库初始化和迁移
|
||||||
|
- 服务启动验证
|
||||||
|
- 核心流程端到端验证
|
||||||
|
- 验证过程中发现的阻塞 bug 修复
|
||||||
|
- 审计发现的 P0 问题修复
|
||||||
|
|
||||||
|
**Deferred for later:**
|
||||||
|
- 真实微信/支付宝 SDK 接入
|
||||||
|
- CI/CD 流水线
|
||||||
|
- 性能优化和压力测试
|
||||||
|
- 生产环境域名和 HTTPS 配置
|
||||||
|
- `.env.production` 文件创建
|
||||||
|
- Redis 密码保护
|
||||||
|
- PostgreSQL 弱密码更换
|
||||||
|
- pgvector 镜像优化(改用 pgvector/pgvector:pg15)
|
||||||
|
- 前端统一 API 客户端修复(reports、lifecycle/new 页面)
|
||||||
|
- JWT_SECRET 强密钥生成
|
||||||
|
|
||||||
|
**Outside this sprint:**
|
||||||
|
- UI 打磨和视觉优化
|
||||||
|
- 新功能开发
|
||||||
|
- 代码重构
|
||||||
|
- 完整测试覆盖
|
||||||
|
|
||||||
|
## Risks & Dependencies
|
||||||
|
|
||||||
|
- **LLM API 可用性**:诊断和内容生成依赖 DeepSeek API,如果 API Key 无效或额度耗尽,诊断会返回空结果。需确认 API Key 有效。
|
||||||
|
- **pgvector 编译时间**:从源码编译 pgvector 需要安装 build-essential 和 git,首次启动数据库容器可能需要 2-3 分钟。
|
||||||
|
- **迁移链顺序问题**:Alembic 迁移链可能存在未发现的顺序依赖,create_all + stamp head 策略可绕过此问题。
|
||||||
|
- **前端构建问题**:ESLint 警告已降级为 warn,但可能存在运行时错误仅在浏览器中暴露。
|
||||||
|
- **前端 API 客户端不一致**:部分页面绕过统一 API 客户端,认证 token 可能不被附加,导致 401 错误。
|
||||||
|
- **Docker Hub 网络问题**:国内环境拉取 Docker 镜像可能超时,影响 Docker Compose 部署。
|
||||||
|
|
||||||
|
## Sources & Research
|
||||||
|
|
||||||
|
- `backend/.env` — 当前环境变量配置,DATABASE_URL 驱动和 JWT_SECRET 已修复
|
||||||
|
- `backend/app/config.py` — JWT_SECRET >= 32 字符校验逻辑
|
||||||
|
- `backend/app/database.py` — async engine 配置,确认需要 asyncpg 驱动
|
||||||
|
- `backend/app/main.py` — FastAPI 入口,28+ API 路由注册,6 层中间件栈
|
||||||
|
- `backend/Dockerfile` — 健康检查端点错误(/api/health → /health)
|
||||||
|
- `docker-compose.yml` — 开发环境 4 服务配置,backend environment 覆盖
|
||||||
|
- `docker-compose.prod.yml` — 生产环境配置,依赖 `.env.production`
|
||||||
|
- 前序会话 pgvector 安装经验:从源码编译 v0.5.1
|
||||||
|
- 2026-06-01 全面复盘审计:发现 Dockerfile 健康检查、.env.test、前端 API 客户端等问题
|
||||||
Loading…
Reference in New Issue