feat: add knowledge management functionality with CRUD operations and database schema
- Implemented KnowledgeHandler for managing knowledge groups and items, including listing, creating, updating, and deleting operations. - Added database migration scripts to create necessary tables for knowledge management, including knowledge_groups, knowledge_items, knowledge_parse_tasks, and knowledge_chunks_meta. - Introduced prompt_rule_knowledge_groups table to associate prompt rules with knowledge groups.
This commit is contained in:
@@ -1,675 +0,0 @@
|
||||
# GEO admin-web 后端核心链路设计
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 目标
|
||||
|
||||
搭建 GEO 平台 tenant-api 后端核心链路,为 admin-web 前端提供可用的 API 服务。第一阶段聚焦四个核心模块:认证、工作台、文章/模板、品牌词库,让前端能跑通内容生成主流程。
|
||||
|
||||
### 1.2 决策摘要
|
||||
|
||||
| 决策项 | 选择 | 理由 |
|
||||
|--------|------|------|
|
||||
| 进程模型 | 单进程 tenant-api | 当前只有 tenant 侧,gateway 无分发对象;鉴权中间件写在 shared 层,后续零成本拆出 |
|
||||
| 脚手架 | 手工初始化 | sponge 生成的 handler/dao/service 命名与本项目 transport/app/domain/repository 四层结构不符,手工初始化成本更低 |
|
||||
| Web 框架 | Gin | 架构文档指定 |
|
||||
| DB 访问 | pgx + sqlc | 类型安全、性能最优、SQL 控制力强 |
|
||||
| Migration | golang-migrate | 架构文档 §13.13.8 推荐 |
|
||||
| 认证 | JWT (access + refresh) | 架构文档 §6 指定 |
|
||||
| 日志 | zap 结构化 JSON | 架构文档 §13.13.10 推荐 |
|
||||
| 配置 | YAML + env 覆盖 (viper) | 架构文档 §13.13.6 指定 |
|
||||
| 错误码 | 统一格式 `{code, message, detail, request_id}` | 架构文档 §13.6.4 |
|
||||
|
||||
### 1.3 不包含
|
||||
|
||||
第一阶段明确不做:
|
||||
- gateway 独立进程
|
||||
- platform-api
|
||||
- scheduler / worker-*
|
||||
- prompt-rule、schedule、optimization、knowledge、media、tracking、account 模块
|
||||
- RabbitMQ / Qdrant / 对象存储集成
|
||||
- SSE 推送
|
||||
|
||||
这些在后续迭代中加入。
|
||||
|
||||
## 2. 项目结构
|
||||
|
||||
### 2.1 项目初始化
|
||||
|
||||
手工初始化 Go 项目(放弃 sponge),按架构文档 §5.2 的分层要求直接建目录结构:
|
||||
|
||||
- `transport` — HTTP handler、路由注册
|
||||
- `app` — 业务逻辑 service
|
||||
- `domain` — 领域模型(纯 Go 结构体,无框架依赖)
|
||||
- `repository` — 数据访问(pgx + sqlc)
|
||||
|
||||
### 2.2 目录结构
|
||||
|
||||
```
|
||||
server/
|
||||
├── cmd/
|
||||
│ └── tenant-api/
|
||||
│ └── main.go # 入口:初始化依赖、注册路由、启动 HTTP
|
||||
├── internal/
|
||||
│ ├── bootstrap/
|
||||
│ │ └── bootstrap.go # 依赖容器:DB/Redis/Config 初始化
|
||||
│ ├── shared/
|
||||
│ │ ├── auth/
|
||||
│ │ │ ├── jwt.go # JWT 颁发 (access 15min + refresh 7day)
|
||||
│ │ │ ├── middleware.go # Gin 中间件:Token 校验 + 身份注入 ctx
|
||||
│ │ │ └── claims.go # JWT Claims 结构体
|
||||
│ │ ├── config/
|
||||
│ │ │ └── config.go # Viper 配置加载
|
||||
│ │ ├── middleware/
|
||||
│ │ │ ├── cors.go
|
||||
│ │ │ ├── request_id.go # 生成/复用 request_id
|
||||
│ │ │ ├── recovery.go
|
||||
│ │ │ ├── logger.go # 请求日志 (zap)
|
||||
│ │ │ └── tenant_scope.go # 从 JWT 提取 tenant_id 注入 ctx
|
||||
│ │ ├── response/
|
||||
│ │ │ ├── response.go # 统一响应封装 Success/Error
|
||||
│ │ │ └── errors.go # 错误码枚举与 AppError 类型
|
||||
│ │ ├── observability/
|
||||
│ │ │ └── logger.go # zap 初始化
|
||||
│ │ └── repository/
|
||||
│ │ ├── postgres/
|
||||
│ │ │ └── postgres.go # pgx 连接池
|
||||
│ │ └── redis/
|
||||
│ │ └── redis.go # Redis 连接
|
||||
│ ├── tenant/
|
||||
│ │ ├── transport/
|
||||
│ │ │ ├── router.go # 注册所有 tenant 路由组
|
||||
│ │ │ ├── auth_handler.go # /api/auth/* handlers
|
||||
│ │ │ ├── workspace_handler.go
|
||||
│ │ │ ├── template_handler.go
|
||||
│ │ │ ├── article_handler.go
|
||||
│ │ │ └── brand_handler.go
|
||||
│ │ ├── app/
|
||||
│ │ │ ├── auth_service.go
|
||||
│ │ │ ├── workspace_service.go
|
||||
│ │ │ ├── template_service.go
|
||||
│ │ │ ├── article_service.go
|
||||
│ │ │ └── brand_service.go
|
||||
│ │ ├── domain/
|
||||
│ │ │ ├── user.go
|
||||
│ │ │ ├── tenant.go
|
||||
│ │ │ ├── article.go
|
||||
│ │ │ ├── article_template.go
|
||||
│ │ │ ├── article_version.go
|
||||
│ │ │ ├── generation_task.go
|
||||
│ │ │ ├── brand.go
|
||||
│ │ │ ├── brand_keyword.go
|
||||
│ │ │ ├── brand_question.go
|
||||
│ │ │ └── competitor.go
|
||||
│ │ └── repository/
|
||||
│ │ ├── queries/ # sqlc SQL 文件
|
||||
│ │ │ ├── user.sql
|
||||
│ │ │ ├── tenant.sql
|
||||
│ │ │ ├── article.sql
|
||||
│ │ │ ├── article_template.sql
|
||||
│ │ │ ├── brand.sql
|
||||
│ │ │ └── ...
|
||||
│ │ ├── sqlc.yaml # sqlc 配置
|
||||
│ │ ├── generated/ # sqlc 生成的 Go 代码
|
||||
│ │ ├── user_repo.go # Repository 接口 + 实现
|
||||
│ │ ├── article_repo.go
|
||||
│ │ ├── template_repo.go
|
||||
│ │ └── brand_repo.go
|
||||
│ └── gateway/ # 预留空目录
|
||||
├── migrations/ # golang-migrate SQL 文件
|
||||
│ ├── 20260331100000_create_tenants.up.sql
|
||||
│ ├── 20260331100000_create_tenants.down.sql
|
||||
│ ├── 20260331100001_create_users.up.sql
|
||||
│ ├── ...
|
||||
├── configs/
|
||||
│ ├── config.yaml # 默认配置
|
||||
│ └── config.local.yaml # 本地开发覆盖 (gitignore)
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── Makefile # dev-init, dev-api, migrate-up, sqlc-generate
|
||||
└── docker-compose.yaml # PostgreSQL + Redis (本地开发)
|
||||
```
|
||||
|
||||
## 3. 数据库 Schema
|
||||
|
||||
### 3.1 Migration 文件规划
|
||||
|
||||
按架构文档 §13.11,第一阶段需要以下表。命名规则:`YYYYMMDDHHMMSS_description.up.sql`。
|
||||
|
||||
#### 3.1.1 身份与租户
|
||||
|
||||
```sql
|
||||
-- 20260331100000_create_tenants.up.sql
|
||||
CREATE TABLE tenants (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active, frozen
|
||||
frozen_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uk_tenant_name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- 20260331100001_create_users.up.sql
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
phone VARCHAR(20),
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
avatar_url TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uk_users_email UNIQUE (email)
|
||||
);
|
||||
|
||||
-- 20260331100002_create_tenant_memberships.up.sql
|
||||
CREATE TABLE tenant_memberships (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
tenant_role VARCHAR(20) NOT NULL, -- tenant_admin, editor, kol
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uk_tenant_user UNIQUE (tenant_id, user_id)
|
||||
);
|
||||
|
||||
-- 20260331100003_create_platform_user_roles.up.sql
|
||||
CREATE TABLE platform_user_roles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
platform_role VARCHAR(20) NOT NULL, -- super_admin, ops, sre
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uk_platform_user_role UNIQUE (user_id, platform_role)
|
||||
);
|
||||
|
||||
-- 20260331100004_create_plans.up.sql
|
||||
CREATE TABLE plans (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
plan_code VARCHAR(50) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
quota_policy_json JSONB NOT NULL DEFAULT '{}',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uk_plan_code UNIQUE (plan_code)
|
||||
);
|
||||
|
||||
-- 20260331100005_create_tenant_plan_subscriptions.up.sql
|
||||
CREATE TABLE tenant_plan_subscriptions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
plan_id BIGINT NOT NULL REFERENCES plans(id),
|
||||
start_at TIMESTAMPTZ NOT NULL,
|
||||
end_at TIMESTAMPTZ NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_tenant_plan_active ON tenant_plan_subscriptions(tenant_id) WHERE status = 'active'; -- 同一时间只有一个活跃订阅
|
||||
|
||||
-- 20260331100006_create_tenant_quota_ledgers.up.sql
|
||||
CREATE TABLE tenant_quota_ledgers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
quota_type VARCHAR(50) NOT NULL, -- article_generation
|
||||
delta INT NOT NULL,
|
||||
balance_after INT NOT NULL,
|
||||
reason VARCHAR(255),
|
||||
reference_type VARCHAR(50),
|
||||
reference_id BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_tenant_quota_tenant_type_created ON tenant_quota_ledgers(tenant_id, quota_type, created_at DESC);
|
||||
|
||||
-- 20260331100007_create_quota_reservations.up.sql
|
||||
CREATE TABLE quota_reservations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
quota_type VARCHAR(50) NOT NULL,
|
||||
resource_type VARCHAR(50) NOT NULL,
|
||||
resource_id BIGINT NOT NULL,
|
||||
reserved_amount INT NOT NULL,
|
||||
consumed_amount INT NOT NULL DEFAULT 0,
|
||||
refunded_amount INT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, confirmed, refunded, expired
|
||||
expire_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_quota_reservation_tenant_status ON quota_reservations(tenant_id, status);
|
||||
```
|
||||
|
||||
#### 3.1.2 模板与文章
|
||||
|
||||
```sql
|
||||
-- 20260331100010_create_article_templates.up.sql
|
||||
CREATE TABLE article_templates (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scope VARCHAR(20) NOT NULL, -- platform, tenant
|
||||
tenant_id BIGINT REFERENCES tenants(id),
|
||||
origin_type VARCHAR(20) NOT NULL DEFAULT 'platform', -- platform, tenant_custom, kol_share
|
||||
share_code_id BIGINT,
|
||||
template_key VARCHAR(100) NOT NULL,
|
||||
template_name VARCHAR(200) NOT NULL,
|
||||
schema_json JSONB NOT NULL DEFAULT '{}',
|
||||
prompt_template TEXT,
|
||||
prompt_visibility VARCHAR(20) NOT NULL DEFAULT 'visible', -- visible, sealed
|
||||
protected_prompt_asset_key TEXT,
|
||||
card_config_json JSONB NOT NULL DEFAULT '{}',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
version_no INT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uk_template_scope_key_version UNIQUE (scope, tenant_id, template_key, version_no)
|
||||
);
|
||||
|
||||
-- 20260331100011_create_articles.up.sql
|
||||
CREATE TABLE articles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
source_type VARCHAR(30) NOT NULL, -- template, prompt_rule, optimization
|
||||
template_id BIGINT REFERENCES article_templates(id),
|
||||
prompt_rule_id BIGINT,
|
||||
current_version_id BIGINT,
|
||||
generate_status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, generating, completed, failed
|
||||
publish_status VARCHAR(20) NOT NULL DEFAULT 'unpublished', -- unpublished, publishing, success, failed, pending_review
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_article_tenant_created ON articles(tenant_id, created_at DESC);
|
||||
CREATE INDEX idx_article_tenant_status ON articles(tenant_id, generate_status, publish_status, created_at DESC);
|
||||
|
||||
-- 20260331100012_create_article_versions.up.sql
|
||||
CREATE TABLE article_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
article_id BIGINT NOT NULL REFERENCES articles(id),
|
||||
version_no INT NOT NULL,
|
||||
title VARCHAR(500),
|
||||
html_content TEXT,
|
||||
markdown_content TEXT,
|
||||
word_count INT NOT NULL DEFAULT 0,
|
||||
source_label VARCHAR(100),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uk_article_version_no UNIQUE (article_id, version_no)
|
||||
);
|
||||
|
||||
-- 20260331100013_create_generation_tasks.up.sql
|
||||
CREATE TABLE generation_tasks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
article_id BIGINT REFERENCES articles(id),
|
||||
task_batch_id VARCHAR(100),
|
||||
quota_reservation_id BIGINT REFERENCES quota_reservations(id),
|
||||
task_type VARCHAR(50) NOT NULL, -- template, prompt, batch_item, schedule_item, optimize
|
||||
request_hash VARCHAR(64),
|
||||
input_params_json JSONB,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'queued', -- queued, running, completed, failed
|
||||
error_message TEXT,
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_generation_task_status_created ON generation_tasks(status, created_at);
|
||||
CREATE INDEX idx_generation_task_tenant ON generation_tasks(tenant_id, created_at DESC);
|
||||
```
|
||||
|
||||
#### 3.1.3 品牌
|
||||
|
||||
```sql
|
||||
-- 20260331100020_create_brands.up.sql
|
||||
CREATE TABLE brands (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uk_brand_tenant_name UNIQUE (tenant_id, name)
|
||||
);
|
||||
|
||||
-- 20260331100021_create_brand_keywords.up.sql
|
||||
CREATE TABLE brand_keywords (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
brand_id BIGINT NOT NULL REFERENCES brands(id),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uk_brand_keyword_name UNIQUE (brand_id, name)
|
||||
);
|
||||
|
||||
-- 20260331100022_create_brand_questions.up.sql
|
||||
CREATE TABLE brand_questions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
brand_id BIGINT NOT NULL REFERENCES brands(id),
|
||||
keyword_id BIGINT NOT NULL REFERENCES brand_keywords(id),
|
||||
current_version_id BIGINT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_brand_questions_keyword ON brand_questions(keyword_id);
|
||||
|
||||
-- 20260331100023_create_brand_question_versions.up.sql
|
||||
CREATE TABLE brand_question_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
question_id BIGINT NOT NULL REFERENCES brand_questions(id),
|
||||
question_text TEXT NOT NULL,
|
||||
question_hash VARCHAR(64) NOT NULL,
|
||||
version_no INT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uk_question_version_no UNIQUE (question_id, version_no)
|
||||
);
|
||||
CREATE INDEX idx_question_hash ON brand_question_versions(question_hash);
|
||||
|
||||
-- 20260331100024_create_competitors.up.sql
|
||||
CREATE TABLE competitors (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
brand_id BIGINT NOT NULL REFERENCES brands(id),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
website TEXT,
|
||||
description TEXT,
|
||||
product_lines_json JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_competitor_brand ON competitors(brand_id);
|
||||
```
|
||||
|
||||
#### 3.1.4 基础设施
|
||||
|
||||
```sql
|
||||
-- 20260331100030_create_task_records.up.sql
|
||||
CREATE TABLE task_records (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
task_type VARCHAR(50) NOT NULL,
|
||||
resource_type VARCHAR(50) NOT NULL,
|
||||
resource_id BIGINT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
retry_count INT NOT NULL DEFAULT 0,
|
||||
task_batch_id VARCHAR(100),
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_task_record_status_type ON task_records(status, task_type);
|
||||
|
||||
-- 20260331100031_create_audit_logs.up.sql
|
||||
CREATE TABLE audit_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
operator_id BIGINT NOT NULL REFERENCES users(id),
|
||||
tenant_id BIGINT,
|
||||
module VARCHAR(50) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
before_json JSONB,
|
||||
after_json JSONB,
|
||||
result VARCHAR(20),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_audit_module_created ON audit_logs(module, created_at DESC);
|
||||
```
|
||||
|
||||
### 3.2 种子数据
|
||||
|
||||
`make dev-init` 需要写入最小种子数据:
|
||||
|
||||
- 1 个默认租户
|
||||
- 1 个 tenant_admin 用户 (admin@geo.local / 默认密码)
|
||||
- 1 个 tenant_membership
|
||||
- 1 个免费套餐 (plan)
|
||||
- 1 个订阅关系
|
||||
- 初始额度记录
|
||||
- 3-4 个平台预设模板 (Top X 文章、产品评测文章、研究报告文章、品牌词搜索扩写)
|
||||
|
||||
## 4. API 设计
|
||||
|
||||
### 4.1 统一响应格式
|
||||
|
||||
所有 API 返回:
|
||||
|
||||
```json
|
||||
// 成功
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": { ... },
|
||||
"request_id": "req_xxx"
|
||||
}
|
||||
|
||||
// 失败
|
||||
{
|
||||
"code": 40301,
|
||||
"message": "quota_insufficient",
|
||||
"detail": "生成额度不足,请升级套餐或等待额度重置",
|
||||
"request_id": "req_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
错误码段 (§13.6.4):`400xx` 参数错误, `401xx` 认证失败, `403xx` 权限/额度不足, `404xx` 资源不存在, `409xx` 状态冲突, `429xx` 频率限制, `500xx` 内部错误, `503xx` 下游不可用。
|
||||
|
||||
### 4.2 分页格式
|
||||
|
||||
请求:`?page=1&page_size=20&sort=created_at&order=desc`
|
||||
|
||||
响应 data 字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [...],
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Auth API
|
||||
|
||||
| Method | Path | 说明 |
|
||||
|--------|------|------|
|
||||
| POST | `/api/auth/login` | 邮箱+密码登录,返回 access_token + refresh_token |
|
||||
| POST | `/api/auth/refresh` | 刷新 access_token |
|
||||
| GET | `/api/auth/me` | 返回当前用户信息、tenant_id、tenant_role、permissions[] |
|
||||
| POST | `/api/auth/logout` | 登出 (可选:加入 token 黑名单) |
|
||||
|
||||
**JWT Claims**:
|
||||
```go
|
||||
type Claims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
Role string `json:"role"` // tenant_admin, editor
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
```
|
||||
|
||||
- Access token TTL: 15 分钟
|
||||
- Refresh token TTL: 7 天
|
||||
- Refresh token 存 Redis,支持主动失效
|
||||
|
||||
### 4.4 Workspace API
|
||||
|
||||
| Method | Path | 说明 |
|
||||
|--------|------|------|
|
||||
| GET | `/api/tenant/workspace/overview` | 聚合统计:文章数、发布数、品牌数、媒体平台数 |
|
||||
| GET | `/api/tenant/workspace/recent-articles` | 最近 10 篇文章 (带生成状态/发布状态) |
|
||||
| GET | `/api/tenant/workspace/quota-summary` | 当前套餐额度:已用/总量/重置时间 |
|
||||
| GET | `/api/tenant/workspace/template-cards` | 可用模板卡片列表 (平台预设 + 租户自建) |
|
||||
|
||||
workspace_service 聚合多个 repository 数据,单次返回,避免前端首屏发起过多散请求 (§13.2)。
|
||||
|
||||
### 4.5 Template API
|
||||
|
||||
| Method | Path | 说明 |
|
||||
|--------|------|------|
|
||||
| GET | `/api/tenant/templates` | 模板列表 (scope=platform 或 tenant_id 匹配) |
|
||||
| GET | `/api/tenant/templates/{id}` | 模板详情 (prompt_visibility=sealed 时不返回 prompt_template) |
|
||||
| GET | `/api/tenant/templates/{id}/schema` | 模板表单 schema (JSON) |
|
||||
| POST | `/api/tenant/templates/{id}/generate` | 提交生成任务 |
|
||||
|
||||
**生成流程** (§10.1):
|
||||
1. 校验租户身份、额度、参数
|
||||
2. 在单个 PG 事务内:额度预扣 + generation_tasks 创建 + articles 占位记录
|
||||
3. 返回 task_id 和 article_id
|
||||
4. (V1 第一阶段:任务状态模拟为同步完成或标记为 queued 等待 worker)
|
||||
|
||||
### 4.6 Article API
|
||||
|
||||
| Method | Path | 说明 |
|
||||
|--------|------|------|
|
||||
| GET | `/api/tenant/articles` | 文章列表 (分页、筛选:generate_status, publish_status, source_type, template_id, keyword, date_range) |
|
||||
| GET | `/api/tenant/articles/{id}` | 文章详情 (含当前版本内容) |
|
||||
| GET | `/api/tenant/articles/{id}/versions` | 文章版本列表 |
|
||||
| DELETE | `/api/tenant/articles/{id}` | 删除文章 |
|
||||
|
||||
### 4.7 Brand API
|
||||
|
||||
| Method | Path | 说明 |
|
||||
|--------|------|------|
|
||||
| GET | `/api/tenant/brands` | 品牌列表 |
|
||||
| POST | `/api/tenant/brands` | 创建品牌 |
|
||||
| GET | `/api/tenant/brands/{id}` | 品牌详情 |
|
||||
| PUT | `/api/tenant/brands/{id}` | 更新品牌 |
|
||||
| DELETE | `/api/tenant/brands/{id}` | 删除品牌 |
|
||||
| GET | `/api/tenant/brands/{id}/keywords` | 关键词列表 |
|
||||
| POST | `/api/tenant/brands/{id}/keywords` | 创建关键词 |
|
||||
| PUT | `/api/tenant/brands/{id}/keywords/{kid}` | 更新关键词 |
|
||||
| DELETE | `/api/tenant/brands/{id}/keywords/{kid}` | 删除关键词 |
|
||||
| GET | `/api/tenant/brands/{id}/questions` | 问题列表 (按关键词筛选) |
|
||||
| POST | `/api/tenant/brands/{id}/questions` | 创建问题 (自动创建 v1 版本) |
|
||||
| GET | `/api/tenant/brands/{id}/competitors` | 竞品列表 |
|
||||
| POST | `/api/tenant/brands/{id}/competitors` | 创建竞品 |
|
||||
| PUT | `/api/tenant/brands/{id}/competitors/{cid}` | 更新竞品 |
|
||||
| DELETE | `/api/tenant/brands/{id}/competitors/{cid}` | 删除竞品 |
|
||||
|
||||
### 4.8 Health Check
|
||||
|
||||
| Method | Path | 说明 |
|
||||
|--------|------|------|
|
||||
| GET | `/api/health/live` | 存活检查 |
|
||||
| GET | `/api/health/ready` | 就绪检查 (DB + Redis 可达) |
|
||||
|
||||
## 5. 中间件链
|
||||
|
||||
请求处理顺序 (§6.3):
|
||||
|
||||
1. Recovery (panic 恢复)
|
||||
2. RequestID (生成/复用 request_id)
|
||||
3. Logger (请求日志)
|
||||
4. CORS
|
||||
5. Auth (JWT 校验 + 身份注入 ctx) — `/api/auth/login` 和 `/api/health/*` 除外
|
||||
6. TenantScope (从 JWT 提取 tenant_id 注入 ctx)
|
||||
7. Handler
|
||||
|
||||
### 5.1 TenantScope 实现
|
||||
|
||||
```go
|
||||
// 从 context 中获取 tenant_id,repository 层必须调用
|
||||
func TenantIDFromCtx(ctx context.Context) int64
|
||||
```
|
||||
|
||||
所有 tenant repository 方法的 SQL 必须包含 `WHERE tenant_id = $tenant_id`。sqlc 生成的查询默认带上此参数。
|
||||
|
||||
## 6. 本地开发
|
||||
|
||||
### 6.1 docker-compose.yaml
|
||||
|
||||
仅启动基础设施:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
ports: ["5432:5432"]
|
||||
environment:
|
||||
POSTGRES_DB: geo
|
||||
POSTGRES_USER: geo
|
||||
POSTGRES_PASSWORD: geo_dev
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports: ["6379:6379"]
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
```
|
||||
|
||||
### 6.2 Makefile 命令
|
||||
|
||||
```makefile
|
||||
dev-init: # docker compose up -d + migrate up + seed data
|
||||
dev-api: # go run ./server/cmd/tenant-api
|
||||
migrate-up: # golang-migrate up
|
||||
migrate-down: # golang-migrate down 1
|
||||
migrate-create: # 创建新 migration 文件
|
||||
sqlc-generate: # sqlc generate
|
||||
lint: # golangci-lint run
|
||||
test: # go test ./...
|
||||
```
|
||||
|
||||
### 6.3 配置文件
|
||||
|
||||
```yaml
|
||||
# configs/config.yaml
|
||||
server:
|
||||
port: 8080
|
||||
mode: debug # debug / release
|
||||
|
||||
database:
|
||||
host: localhost
|
||||
port: 5432
|
||||
user: geo
|
||||
password: geo_dev
|
||||
dbname: geo
|
||||
sslmode: disable
|
||||
max_open_conns: 25
|
||||
max_idle_conns: 5
|
||||
|
||||
redis:
|
||||
addr: localhost:6379
|
||||
db: 0
|
||||
|
||||
jwt:
|
||||
secret: "${JWT_SECRET:dev-secret-change-in-production}"
|
||||
access_ttl: 15m
|
||||
refresh_ttl: 168h # 7 days
|
||||
|
||||
log:
|
||||
level: debug
|
||||
format: json
|
||||
```
|
||||
|
||||
## 7. 验证方式
|
||||
|
||||
1. `make dev-init` 启动基础设施并初始化数据库
|
||||
2. `make dev-api` 启动 tenant-api
|
||||
3. `POST /api/auth/login` 登录获取 token
|
||||
4. `GET /api/auth/me` 验证身份
|
||||
5. `GET /api/tenant/workspace/overview` 验证工作台数据
|
||||
6. `GET /api/tenant/templates` 查看预设模板
|
||||
7. `POST /api/tenant/brands` 创建品牌
|
||||
8. `GET /api/tenant/brands/{id}/keywords` 查看关键词
|
||||
9. `make test` 所有测试通过
|
||||
10. `make lint` 无 lint 错误
|
||||
Reference in New Issue
Block a user