44406b72db
Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
612 lines
18 KiB
Markdown
612 lines
18 KiB
Markdown
# img-infinite-canvas 初期架构与演进方案
|
||
|
||
## 目标
|
||
|
||
当前阶段不要直接建设 Kafka、多微服务、多区域部署等终局复杂架构。推荐采用“可演进的模块化单体”:
|
||
|
||
- 初期保持一个 go-zero API 项目,一个 PostgreSQL,一个 Redis,一个对象存储。
|
||
- API 与 worker 可以先共用同一代码仓库,但部署为两个进程。
|
||
- 所有关键边界按未来可拆服务设计,避免用户增长后推倒重来。
|
||
- 新建项目必须登录,所有项目、资产、消息、任务、账本都必须绑定用户。
|
||
|
||
初期目标规模:
|
||
|
||
- 1 万注册用户。
|
||
- 数百级并发在线。
|
||
- 数十到数百级并发生成任务。
|
||
- 支持后续平滑演进到 Kafka 事件总线、独立 Billing/Agent/Realtime 服务。
|
||
|
||
## 当前主要问题
|
||
|
||
1. 没有用户维度
|
||
当前 `projects` 没有 `user_id`,项目列表和详情天然是全局资源。后续必须改成用户或租户隔离。
|
||
|
||
2. 新建项目没有登录要求
|
||
项目创建、保存、删除、生成、上传都应受登录态保护。未来新建项目必须登录。
|
||
|
||
3. 画布保存是全量覆盖
|
||
当前保存项目时会重写 nodes、connections、messages。用户增长后会造成写放大,并带来并发覆盖风险。
|
||
|
||
4. 聊天消息和画布保存耦合
|
||
`agent_messages` 应该 append-only,不应在保存画布时被删除重插。
|
||
|
||
5. 异步任务依赖进程内 goroutine
|
||
服务重启会丢任务,多实例部署时无法统一调度,也无法做全局并发控制、重试、死信队列。
|
||
|
||
6. 实时通道轮询数据库
|
||
WebSocket/SSE 当前通过高频读取完整项目推送状态。并发连接变多后会放大数据库和 JSON 序列化压力。
|
||
|
||
7. 点数扣费尚未形成账本
|
||
后续需要扣除点数,必须有账户、冻结、扣费、退款、流水和幂等设计。
|
||
|
||
8. 配置和生产环境边界不足
|
||
生产环境必须禁用 memory storage,密钥必须走环境变量或 secret,不应提交明文 API key。
|
||
|
||
## 初期简化架构
|
||
|
||
```text
|
||
Next.js frontend
|
||
-> go-zero API
|
||
-> PostgreSQL: 用户、项目、画布、消息、点数账本、任务状态
|
||
-> Redis: 缓存、限流、Asynq 队列、短期实时事件
|
||
-> Object Storage: MinIO / S3 / R2 / OSS
|
||
-> Worker process: 图片生成、图层合并、资产清理、扣费结算
|
||
```
|
||
|
||
初期暂不引入:
|
||
|
||
- Kafka。
|
||
- NATS。
|
||
- 微服务拆分。
|
||
- 多区域部署。
|
||
- 复杂 organization/workspace。
|
||
- 数据仓库事件流。
|
||
|
||
但要预留这些能力:
|
||
|
||
- `outbox_events` 表,为未来接 Kafka 做准备。
|
||
- 模块化目录和接口,未来可拆服务。
|
||
- append-only 消息和账本,未来可审计、可回放。
|
||
- 可替换 JobQueue 接口,未来可从 Asynq 切到 Kafka command topic。
|
||
|
||
## 初期技术选型
|
||
|
||
### API
|
||
|
||
- 继续使用 go-zero REST API。
|
||
- handler 保持薄层,只做请求解析和调用 logic。
|
||
- logic 调 application service。
|
||
- application service 只依赖领域接口,不依赖具体 Redis/Kafka/Asynq 实现。
|
||
|
||
### 数据库
|
||
|
||
- PostgreSQL 作为权威业务数据源。
|
||
- 余额、扣点、冻结、退款必须在 PostgreSQL 事务内完成。
|
||
- 所有核心数据表增加 `user_id`。
|
||
|
||
### 缓存与任务
|
||
|
||
- Redis 用于缓存、限流、短期实时事件。
|
||
- 使用 Asynq 作为初期任务队列。
|
||
|
||
### 对象存储
|
||
|
||
- 本地开发可用 MinIO。
|
||
- 生产使用 S3 / R2 / OSS / 私有 MinIO。
|
||
- 数据库只保存 object key、public URL、owner、project 归属和元数据。
|
||
|
||
## 初期模块边界
|
||
|
||
建议在单体内按未来服务边界拆模块:
|
||
|
||
```text
|
||
server/internal/modules/auth
|
||
server/internal/modules/project
|
||
server/internal/modules/canvas
|
||
server/internal/modules/agent
|
||
server/internal/modules/billing
|
||
server/internal/modules/asset
|
||
server/internal/modules/job
|
||
server/internal/modules/realtime
|
||
server/internal/shared
|
||
```
|
||
|
||
也可以先不大规模移动现有文件,但新增代码应按这些边界组织。
|
||
|
||
模块职责:
|
||
|
||
- `auth`: 登录、注册、JWT、refresh token、用户上下文。
|
||
- `project`: 项目元数据、项目列表、项目删除、所有权校验。
|
||
- `canvas`: 画布快照、节点保存、版本冲突。
|
||
- `agent`: 会话线程、消息、生成编排。
|
||
- `billing`: 点数账户、冻结、扣费、释放、退款、流水。
|
||
- `asset`: 上传签名、对象存储、资产归属、清理。
|
||
- `job`: 任务队列接口、Asynq/Postgres jobs 实现。
|
||
- `realtime`: WebSocket/SSE 推送接口。
|
||
|
||
## 初期数据模型
|
||
|
||
最小可运营版本需要这些表:
|
||
|
||
```sql
|
||
users (
|
||
id TEXT PRIMARY KEY,
|
||
email TEXT NOT NULL UNIQUE,
|
||
password_hash TEXT NOT NULL,
|
||
name TEXT NOT NULL DEFAULT '',
|
||
status TEXT NOT NULL DEFAULT 'active',
|
||
created_at TIMESTAMPTZ NOT NULL,
|
||
updated_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
projects (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL REFERENCES users(id),
|
||
title TEXT NOT NULL,
|
||
brief TEXT NOT NULL,
|
||
status TEXT NOT NULL,
|
||
thumbnail TEXT NOT NULL DEFAULT '',
|
||
current_snapshot_id TEXT NOT NULL DEFAULT '',
|
||
version BIGINT NOT NULL DEFAULT 1,
|
||
updated_at TIMESTAMPTZ NOT NULL,
|
||
deleted_at TIMESTAMPTZ
|
||
);
|
||
|
||
canvas_snapshots (
|
||
id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
user_id TEXT NOT NULL REFERENCES users(id),
|
||
version BIGINT NOT NULL,
|
||
viewport_json JSONB NOT NULL,
|
||
nodes_json JSONB NOT NULL,
|
||
created_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
agent_threads (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL REFERENCES users(id),
|
||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
mode TEXT NOT NULL DEFAULT 'design',
|
||
status TEXT NOT NULL DEFAULT 'running',
|
||
created_at TIMESTAMPTZ NOT NULL,
|
||
updated_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
agent_messages (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL REFERENCES users(id),
|
||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
thread_id TEXT NOT NULL REFERENCES agent_threads(id) ON DELETE CASCADE,
|
||
role TEXT NOT NULL,
|
||
message_type TEXT NOT NULL DEFAULT '',
|
||
content TEXT NOT NULL,
|
||
status TEXT NOT NULL DEFAULT '',
|
||
created_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
credit_accounts (
|
||
user_id TEXT PRIMARY KEY REFERENCES users(id),
|
||
balance BIGINT NOT NULL DEFAULT 0,
|
||
reserved BIGINT NOT NULL DEFAULT 0,
|
||
updated_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
credit_reservations (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL REFERENCES users(id),
|
||
project_id TEXT NOT NULL REFERENCES projects(id),
|
||
job_id TEXT NOT NULL,
|
||
amount BIGINT NOT NULL,
|
||
status TEXT NOT NULL,
|
||
expires_at TIMESTAMPTZ NOT NULL,
|
||
created_at TIMESTAMPTZ NOT NULL,
|
||
updated_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
credit_ledger (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL REFERENCES users(id),
|
||
project_id TEXT,
|
||
job_id TEXT,
|
||
reservation_id TEXT,
|
||
ledger_type TEXT NOT NULL,
|
||
amount BIGINT NOT NULL,
|
||
balance_after BIGINT NOT NULL,
|
||
idempotency_key TEXT NOT NULL UNIQUE,
|
||
reason TEXT NOT NULL DEFAULT '',
|
||
created_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
generation_jobs (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL REFERENCES users(id),
|
||
project_id TEXT NOT NULL REFERENCES projects(id),
|
||
thread_id TEXT NOT NULL REFERENCES agent_threads(id),
|
||
reservation_id TEXT,
|
||
job_type TEXT NOT NULL,
|
||
status TEXT NOT NULL,
|
||
payload_json JSONB NOT NULL,
|
||
attempts INTEGER NOT NULL DEFAULT 0,
|
||
idempotency_key TEXT NOT NULL UNIQUE,
|
||
created_at TIMESTAMPTZ NOT NULL,
|
||
updated_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
assets (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL REFERENCES users(id),
|
||
project_id TEXT REFERENCES projects(id),
|
||
object_key TEXT NOT NULL,
|
||
public_url TEXT NOT NULL,
|
||
content_type TEXT NOT NULL DEFAULT '',
|
||
size_bytes BIGINT NOT NULL DEFAULT 0,
|
||
created_at TIMESTAMPTZ NOT NULL,
|
||
deleted_at TIMESTAMPTZ
|
||
);
|
||
|
||
outbox_events (
|
||
id TEXT PRIMARY KEY,
|
||
aggregate_type TEXT NOT NULL,
|
||
aggregate_id TEXT NOT NULL,
|
||
event_type TEXT NOT NULL,
|
||
payload_json JSONB NOT NULL,
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
published_at TIMESTAMPTZ,
|
||
created_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
```
|
||
|
||
关键索引:
|
||
|
||
```sql
|
||
CREATE INDEX projects_user_updated_idx ON projects(user_id, updated_at DESC) WHERE deleted_at IS NULL;
|
||
CREATE INDEX canvas_snapshots_project_version_idx ON canvas_snapshots(project_id, version DESC);
|
||
CREATE INDEX agent_threads_project_updated_idx ON agent_threads(project_id, updated_at DESC);
|
||
CREATE INDEX agent_messages_thread_created_idx ON agent_messages(thread_id, created_at ASC);
|
||
CREATE INDEX generation_jobs_status_updated_idx ON generation_jobs(status, updated_at);
|
||
CREATE INDEX assets_user_project_created_idx ON assets(user_id, project_id, created_at DESC);
|
||
CREATE INDEX outbox_events_status_created_idx ON outbox_events(status, created_at);
|
||
```
|
||
|
||
## 关键接口约束
|
||
|
||
所有项目相关 repository 方法必须带 `userID`:
|
||
|
||
```go
|
||
GetProject(ctx context.Context, userID string, projectID string) (Project, error)
|
||
ListProjects(ctx context.Context, userID string, cursor string, limit int64) (ProjectPage, error)
|
||
SaveCanvas(ctx context.Context, userID string, projectID string, req SaveCanvasRequest) (Project, error)
|
||
DeleteProject(ctx context.Context, userID string, projectID string) error
|
||
```
|
||
|
||
不要再使用只带 `projectID` 的业务方法。这样后续拆服务时权限边界不需要重做。
|
||
|
||
任务队列只暴露接口:
|
||
|
||
```go
|
||
type JobQueue interface {
|
||
EnqueueGeneration(ctx context.Context, job GenerationJob) error
|
||
EnqueueAssetCleanup(ctx context.Context, job AssetCleanupJob) error
|
||
}
|
||
```
|
||
|
||
初期实现可以是 Asynq。未来如果切 Kafka,业务层不改,只替换 infrastructure。
|
||
|
||
计费模块只暴露业务语义:
|
||
|
||
```go
|
||
type BillingService interface {
|
||
ReserveCredits(ctx context.Context, req ReserveCreditsRequest) (Reservation, error)
|
||
ChargeCredits(ctx context.Context, req ChargeCreditsRequest) error
|
||
ReleaseCredits(ctx context.Context, req ReleaseCreditsRequest) error
|
||
}
|
||
```
|
||
|
||
生成模块不直接修改余额,只调用 BillingService。
|
||
|
||
## 登录与项目创建流程
|
||
|
||
```text
|
||
1. 用户登录或注册。
|
||
2. API 生成 access token 和 refresh token。
|
||
3. 前端调用 POST /api/projects。
|
||
4. JWT middleware 解析 user_id。
|
||
5. ProjectService.CreateProject(ctx, userID, req)。
|
||
6. 创建 project 和初始 canvas_snapshot。
|
||
7. 写 outbox_events.ProjectCreated。
|
||
8. 返回项目。
|
||
```
|
||
|
||
硬性要求:
|
||
|
||
- 未登录不能创建项目。
|
||
- 未登录不能上传资产。
|
||
- 未登录不能触发生成。
|
||
- 用户只能访问自己的项目,除非后续增加协作授权。
|
||
|
||
## 生成与扣点流程
|
||
|
||
```text
|
||
1. 用户点击生成。
|
||
2. API 解析 user_id、project_id、prompt、model。
|
||
3. ProjectService 校验项目归属。
|
||
4. BillingService 计算预估点数。
|
||
5. 开启 PostgreSQL 事务。
|
||
6. SELECT credit_accounts ... FOR UPDATE 锁定账户。
|
||
7. 检查 balance - reserved 是否足够。
|
||
8. 创建 credit_reservation,增加 reserved。
|
||
9. 创建 generation_job。
|
||
10. 写 credit_ledger(reserve)。
|
||
11. 写 outbox_events.GenerationRequested 和 CreditsReserved。
|
||
12. 提交事务。
|
||
13. JobQueue 入队。
|
||
14. worker 执行图片生成。
|
||
15. 成功后写节点/快照,ChargeCredits,释放多余冻结。
|
||
16. 失败后 ReleaseCredits。
|
||
17. 写 outbox_events.GenerationCompleted 或 GenerationFailed。
|
||
18. Realtime 推送前端。
|
||
```
|
||
|
||
扣点规则:
|
||
|
||
- 生成前冻结点数,不直接扣除。
|
||
- 成功后按实际消耗扣除。
|
||
- 失败后释放冻结。
|
||
- 所有扣点和释放必须写 `credit_ledger`。
|
||
- 所有扣点操作必须有 `idempotency_key`。
|
||
|
||
## 画布保存流程
|
||
|
||
初期先用快照方式,不急着做复杂增量节点表:
|
||
|
||
```text
|
||
1. 前端保存时带 base_version 和 snapshot。
|
||
2. API 校验 user_id + project_id。
|
||
3. 对 project 做乐观锁检查。
|
||
4. 如果 base_version != current version,返回 409。
|
||
5. 写入新的 canvas_snapshot。
|
||
6. 更新 projects.current_snapshot_id 和 version。
|
||
7. 不修改 agent_messages。
|
||
8. 写 outbox_events.CanvasSnapshotCreated。
|
||
```
|
||
|
||
这样可以先解决“保存画布重写消息”的问题,同时为未来增量节点保存预留空间。
|
||
|
||
## 实时推送初期方案
|
||
|
||
初期可以分两步:
|
||
|
||
第一步:
|
||
|
||
- 保留现有 WebSocket/SSE 形态。
|
||
- 降低数据库轮询频率。
|
||
- 只在生成中开启轮询。
|
||
- 项目静止时不轮询。
|
||
|
||
第二步:
|
||
|
||
- worker 写 outbox 或 Redis pub/sub(用 outbox 保底、Redis 做实时加速)。
|
||
- realtime 模块订阅事件。
|
||
- WebSocket/SSE 只推增量事件,不再轮询完整 project。
|
||
|
||
事件示例:
|
||
|
||
```json
|
||
{
|
||
"type": "generation.completed",
|
||
"projectId": "project-id",
|
||
"threadId": "thread-id",
|
||
"snapshotId": "snapshot-id",
|
||
"messageId": "message-id"
|
||
}
|
||
```
|
||
|
||
## Outbox 设计
|
||
|
||
初期即使不用 Kafka,也必须先建 `outbox_events`。
|
||
|
||
写业务数据和写 outbox 必须在同一个数据库事务内:
|
||
|
||
```text
|
||
BEGIN;
|
||
update credit_accounts;
|
||
insert credit_ledger;
|
||
insert generation_jobs;
|
||
insert outbox_events;
|
||
COMMIT;
|
||
```
|
||
|
||
初期可以用 Go poller 读取 `outbox_events where status='pending'`,发布到 Redis 或直接调用 realtime broadcaster。
|
||
|
||
未来接 Kafka 时:
|
||
|
||
- 使用 Debezium Outbox,或自研 outbox publisher。
|
||
- Kafka topic 可以按 `billing.events`、`generation.events`、`project.events` 拆分。
|
||
- 业务事务无需重写。
|
||
|
||
## 架构演进路线
|
||
|
||
### 阶段 1:可运营单体
|
||
|
||
目标:能安全上线给真实用户使用。
|
||
|
||
改造项:
|
||
|
||
- 新增用户登录。
|
||
- 项目创建必须登录。
|
||
- 所有核心表加 `user_id`。
|
||
- 所有项目接口按 `user_id + project_id` 查询。
|
||
- 新增点数账户、冻结、流水。
|
||
- 生成前冻结点数,成功后扣除,失败后释放。
|
||
- 生产禁用 memory storage。
|
||
- 明文密钥移出仓库。
|
||
|
||
部署:
|
||
|
||
```text
|
||
api process
|
||
worker process
|
||
postgres
|
||
redis
|
||
object storage
|
||
```
|
||
|
||
### 阶段 2:可扩展单体
|
||
|
||
目标:API 和 worker 可独立扩容。
|
||
|
||
改造项:
|
||
|
||
- 引入 Asynq。
|
||
- 所有慢任务进入队列。
|
||
- `agent_messages` append-only。
|
||
- `canvas_snapshots` 替换全量 project 保存。
|
||
- outbox poller 发布内部事件。
|
||
- 基础限流:按 user、IP、model。
|
||
- 基础指标:QPS、p95、DB pool、队列长度、生成耗时、扣点失败率。
|
||
|
||
部署:
|
||
|
||
```text
|
||
api replicas
|
||
worker-image replicas
|
||
worker-cleanup replicas
|
||
postgres
|
||
redis
|
||
object storage
|
||
```
|
||
|
||
### 阶段 3:准终局架构
|
||
|
||
目标:将增长压力从 API 层剥离。
|
||
|
||
改造项:
|
||
|
||
- realtime 独立进程。
|
||
- worker 按任务类型拆池。
|
||
- Redis pub/sub 或 stream 推送增量事件。
|
||
- 画布保存支持乐观锁和冲突处理。
|
||
- 历史快照清理或归档。
|
||
- 点数账本增加审计后台。
|
||
- 资产增加生命周期管理。
|
||
|
||
部署:
|
||
|
||
```text
|
||
api replicas
|
||
realtime replicas
|
||
worker-image replicas
|
||
worker-billing replicas
|
||
worker-asset replicas
|
||
postgres primary/replica
|
||
redis
|
||
object storage + CDN
|
||
```
|
||
|
||
### 阶段 4:终局平滑拆分
|
||
|
||
触发条件:
|
||
|
||
- API 单体发布频率互相影响。
|
||
- Billing 需要独立合规和权限边界。
|
||
- Agent worker 资源消耗显著高于其他模块。
|
||
- Realtime 连接规模对 API 进程造成影响。
|
||
- 多业务系统需要消费项目、计费、生成事件。
|
||
|
||
拆分顺序:
|
||
|
||
1. 拆 Realtime 服务。
|
||
2. 拆 Worker 服务。
|
||
3. 拆 Billing 服务。
|
||
4. 拆 Agent 服务。
|
||
5. 接 Kafka 作为跨系统事件总线。
|
||
|
||
终局形态:
|
||
|
||
```text
|
||
API Gateway
|
||
-> Auth Service
|
||
-> Project Service
|
||
-> Canvas Service
|
||
-> Billing Service
|
||
-> Agent Service
|
||
-> Asset Service
|
||
-> Realtime Service
|
||
|
||
PostgreSQL / Redis / Kafka / Object Storage / CDN / Observability
|
||
```
|
||
|
||
## 现在不要做死的设计
|
||
|
||
- 不要再出现全局项目列表。
|
||
- 不要只用 `project_id` 查询项目。
|
||
- 不要把扣点逻辑写在生成逻辑里。
|
||
- 不要让生成任务只存在 goroutine 里。
|
||
- 不要让保存画布重写消息。
|
||
- 不要把实时事件只放内存里。
|
||
- 不要让业务层依赖 Asynq/Kafka 具体实现。
|
||
- 不要让 assets 只有 URL,没有 owner 和 project 归属。
|
||
|
||
## 初期实施顺序
|
||
|
||
1. 清理密钥
|
||
移除配置文件中的真实 API key,统一使用环境变量或 secret。
|
||
|
||
2. 登录鉴权
|
||
新增 `users`、登录接口、JWT middleware。项目创建、保存、删除、生成、上传全部要求登录。
|
||
|
||
3. 数据隔离
|
||
`projects` 加 `user_id`,所有项目 repository 方法带 `userID`。迁移现有项目到默认管理员用户。
|
||
|
||
4. 点数账本
|
||
新增 `credit_accounts`、`credit_reservations`、`credit_ledger`。生成前冻结点数。
|
||
|
||
5. 任务队列
|
||
新增 `generation_jobs`,引入 Asynq worker。替换进程内 `go func()` 生成任务。
|
||
|
||
6. 消息解耦
|
||
`agent_messages` append-only,画布保存不再重写消息。
|
||
|
||
7. 画布快照
|
||
新增 `canvas_snapshots`,保存时使用版本号和 `base_version`。
|
||
|
||
8. Outbox
|
||
新增 `outbox_events`,业务事务内写事件,初期用 poller 发布。
|
||
|
||
9. 实时优化
|
||
由轮询完整 project 逐步改为推送增量事件。
|
||
|
||
10. 压测和运维
|
||
建立基础指标、日志、告警、备份恢复和压测脚本。
|
||
|
||
## 验收标准
|
||
|
||
上线前必须满足:
|
||
|
||
- 未登录不能创建项目。
|
||
- 用户不能读取、保存、删除别人的项目。
|
||
- 生成任务重复提交不会重复扣点。
|
||
- worker 重启后任务不会永久丢失。
|
||
- 图片生成失败会释放冻结点数。
|
||
- 保存画布不会重写聊天消息。
|
||
- 项目列表按 `user_id` 分页查询。
|
||
- 生产配置不使用 memory repository/cache/object storage。
|
||
- 真实密钥不在仓库配置文件中。
|
||
|
||
增长到 1 万用户前应满足:
|
||
|
||
- API 和 worker 可独立水平扩容。
|
||
- 队列长度和失败率可观测。
|
||
- PostgreSQL 慢查询可观测。
|
||
- 点数账本可审计。
|
||
- 资产可按用户和项目追踪。
|
||
- WebSocket/SSE 不再高频拉完整 project。
|
||
|
||
## 参考方向
|
||
|
||
- AWS Well-Architected: 持续演进、可靠性、可观测性。
|
||
- Strangler Fig Pattern: 从模块化单体平滑演进到微服务。
|
||
- Debezium Outbox Pattern: 业务事务与事件发布解耦。
|
||
- PostgreSQL row lock: 点数账户冻结与扣费需要数据库事务和行锁。
|
||
- OWASP Authentication Cheat Sheet: 登录和会话安全基线。
|