topfans/docs/superpowers/plans/2026-07-21-config-deploy.md
2026-07-21 21:14:22 +08:00

557 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 配置与部署 (批次4) Implementation Plan
REQUIRED SUB-SKILL: superpowers:writing-plans
(实施阶段使用 superpowers:executing-planscode-review 阶段使用 superpowers:requesting-code-review
> 配套审计:`docs/backend-audit-2026-07-21.md` §二 P0-6/P0-7、§三 P1 配置、§四 P2
> 配套修复方案:`docs/specs/2026-07-21-backend-remediation-plan.md` 批次 44.1 / 4.2 / 4.3 / 4.4 / 4.5 / 4.9
> 基线 commit`b7f8f1b724e5`,分支 `feat/uni`
---
## Goal
消除 backend 配置/部署层的全部 P0 风险(端口不一致 + starbookService 孤儿)与剩余 P1/P2 卫生项(健康探针、大二进制入库、序列脚本、`.env` 漂移、若干小修),使本地默认值与生产 env 覆盖之间形成**自校验**的部署契约,并通过一次回归把所有"硬编码"参数集中到 4 处可验证的位置gateway config / 各 main flag / helm values / docker compose
完成后达成:
- `go test ./... -run TestPortAlignment` 通过4.1)。
- `git ls-files` 不含 `backend/{assetService,gateway-fixed,test,cleanup-orphan-avatars}` 四大二进制;`backend/services/starbookService/` 被 git 删除4.2 + 4.3)。
- k8s `notificationservice` / `moderationservice` 探针路径与实际 handler 一致;本地 `curl :21010/health` 返回 `{"status":"ok"}`4.3)。
- `scripts/create_gallery_test_users.go` 输出 SQL 末尾含 `setval(...)`;跑完不报 `duplicate key`4.4)。
- `backend/.env.example``backend/.env` 键集合一致(含 `WS_AI_CHAT_PATH` 等),`.example` 不含真实密钥4.5)。
- §四 P2 八项全部清理或明确 defer4.9)。
## Architecture
本批次不引入新架构,纯**配置归一**与**卫生清理**
```
┌──────────────────────────────────────┐
│ k8s/helm/topfans/values.yaml (端口) │ ← 单一事实来源
└──────────────┬───────────────────────┘
│ helm install
┌────────────────────┼────────────────────────┐
▼ ▼ ▼
services/*/main.go docker-compose.prod.yml gateway/config/config.go
(flag 默认值 = helm) (DUBBO_*_SERVICE_URL) (DUBBO_*_SERVICE_URL 默认)
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌────────────┐ ┌─────────────┐
│配置断言测试 │ ──────▶│ 4.1 校验 │ ◀──────│ 4.1 校验 │
└─────────────┘ └────────────┘ └─────────────┘
```
依赖关系(必须按顺序):
1. **4.2 starbookService 删除**先做(它依赖 4.3 探针清理 + P0-4 跨服务 import 解耦)—— 完成前 `go build ./...` 会因 `starbookService` 不在 `go.work` 而失败。
2. **4.1 端口对齐**与 **4.2 starbook 删除**独立可并行。
3. **4.3 探针 + 大二进制**与 4.4 / 4.5 可独立并行。
4. **4.9 P2 清理**作为伴随项随 4.14.5 顺手做;只有 "JWT 失效" 和 "PII 日志" 单独任务化。
## Tech Stack
- **语言**Go 1.25.5`go.work`
- **测试**Go testing + testify
- **K8s 配置**Helm 3 + values.yaml
- **Docker**docker-compose v2 + Dockerfile.services
- **数据库**(仅 4.4 序列脚本PostgreSQL 16
- **静态检查**后续可加本批次不引入golangci-lint
---
## Global Constraints
- **不动架构**:本批次不拆库、不拆 `pkg/models`、不引入新 interface纯"配置正确化"。
- **序列规范**:任何手动指定 id 的 INSERTSQL 末尾必须 `SELECT setval('<table>_id_seq', (SELECT MAX(id) FROM <table>));`,遵循 `CLAUDE.md` 强制规则。
- **commit 守则**:每个 Task 完成后**不自动 commit**,等用户说"帮我 commit"再提交commit message 用 `Co-Authored-By: Claude <noreply@anthropic.com>`
- **真实命令**:所有 bash 命令需在本地真实可执行(`backend/` 目录、`go.work` 模块、`k8s/helm/topfans/` Helm chart 都在仓库内)。
- **回归检查**:每个 Task 完成后跑 `go build ./...`(含 `go.work` 全部模块)+ 端口断言测试。
- **不动产物**`unpackage/dist/*`、`backend/bin/*` 是构建产物,禁止手动改。
- **依赖前置**4.2 starbookService 删除依赖 P0-4 跨服务 import 解耦审计结论(已确认);若 P0-4 实施批次尚未执行 starbook 解耦,本批次单独完成"assetService 内化 `AssetRegistryRepository` 实现"。
- **MVP 先行**:不要为"未来 12+ 服务"提前做端口配置中心化;本次仅做"四层端口默认值收敛 + 断言测试"。
---
## File Structure
**修改清单(按 Task 顺序)**
| Task | 关键文件 | 类型 |
|------|----------|------|
| 4.1 | `backend/gateway/config/config.go`、`backend/services/galleryService/main.go`、`backend/services/userService/main.go`、`backend/services/activityService/main.go`、`backend/services/taskService/main.go`、`backend/services/statisticService/main.go`、`backend/services/notificationService/main.go`、`backend/services/moderationService/main.go`、`backend/services/aiChatService/main.go`、`backend/services/socialService/main.go`、`backend/services/assetService/main.go`、`backend/docker-compose.local.yml`、`backend/docker-compose.prod.yml`、`docker/docker-compose.local.yml`、`docker/docker-compose.prod.yml`、`k8s/helm/topfans/values.yaml`、`backend/services/gateway/config/port_test.go` (新增) | 修 |
| 4.2 | `backend/services/starbookService/` (整目录)、`backend/services/assetService/main.go`、`backend/services/assetService/service/mint_service.go`、`backend/services/assetService/repository/asset_registry_repository.go` (新增)、`backend/services/assetService/service/asset_service.go` (registryRepo 引用)、`backend/gateway/config/config.go`、`backend/go.work`、`backend/dev.sh`、`docker/Dockerfile.services`、`docker/docker-compose.local.yml`、`docker/docker-compose.prod.yml`、`k8s/helm/topfans/values.yaml`、`k8s/helm/topfans/templates/starbookservice/deployment.yaml` (删) | 删 + 修 |
| 4.3 | `backend/pkg/health/health.go`、`backend/services/assetService/main.go`、`backend/services/galleryService/main.go`、`backend/services/userService/main.go`、`backend/services/notificationService/main.go`、`backend/services/moderationService/main.go` (注册 `/healthz` alias)、`k8s/helm/topfans/values.yaml`、`k8s/helm/topfans/templates/notificationservice/deployment.yaml`、`k8s/helm/topfans/templates/moderationservice/deployment.yaml`、`docker/docker-compose.local.yml`、`docker/docker-compose.prod.yml`、`backend/.gitignore`、`backend/assetService`、`backend/gateway-fixed`、`backend/test`、`backend/cleanup-orphan-avatars` (git rm --cached) | 修 + rm |
| 4.4 | `backend/scripts/create_gallery_test_users.go` | 修 |
| 4.5 | `backend/.env.example`、`backend/.env`、`backend/services/gateway/config/env_audit_test.go` (新增) | 修 + 新增 |
| 4.9 | `backend/gateway/controller/asset_controller.go`、`backend/gateway/controller/auth_controller.go`、`backend/gateway/controller/user_controller.go`、`backend/services/userService/service/user_service.go`、`backend/services/aiChatService/provider/ai_chat_provider.go`、`backend/services/mint_service.go`、`backend/services/activityService/service/activity_service.go`、`backend/pkg/peripheral/sign.go`、`backend/pkg/mq/streams/adapter.go`、`backend/pkg/mq/asynq/adapter.go`、`backend/services/galleryService/mq/consumer.go` | 修 |
| 自审 | `docs/superpowers/plans/2026-07-21-config-deploy.md` (本文) | — |
**新增测试文件**
- `backend/gateway/config/port_test.go`4.1 端口断言测试)
- `backend/gateway/config/env_audit_test.go`4.5 env 键集合对比)
---
### Task 1: 端口对齐 + 配置断言测试4.1
> 对应审计 P0-6 与修复方案 4.1。
**Files**:
- `backend/gateway/config/config.go:162-165` —— 修正 `Dubbo.GalleryServiceURL`/`ActivityServiceURL`/`StarbookServiceURL` 默认端口
- `backend/services/galleryService/main.go:36,44` —— `port` 默认 20001`taskServiceURL` 默认改 `tri://localhost:20006`
- `backend/services/userService/main.go:283` —— 删除硬编码 `protocol.WithPort(20000)`,改用 `*port`
- `backend/services/assetService/main.go:38` —— 确认 20003 默认
- `backend/services/activityService/main.go` —— 默认端口确认 20004
- `backend/services/taskService/main.go` —— 默认端口确认 20006
- `backend/services/statisticService/main.go` —— 默认 20009
- `backend/services/notificationService/main.go` —— 默认 20010
- `backend/services/moderationService/main.go` —— 默认 20011
- `backend/services/aiChatService/main.go` —— 默认 20008
- `backend/services/socialService/main.go` —— 默认 20002
- `docker/docker-compose.local.yml`、`docker/docker-compose.prod.yml`、`k8s/helm/topfans/values.yaml` —— 同步 `DUBBO_*_SERVICE_URL` 默认值
- `backend/gateway/config/port_test.go` (新增) —— 配置断言测试
**Interfaces**:
```go
// backend/gateway/config/config.go (excerpt)
type DubboConfig struct {
UserServiceURL string // tri://127.0.0.1:20000
SocialServiceURL string // tri://127.0.0.1:20002
AssetServiceURL string // tri://127.0.0.1:20003
GalleryServiceURL string // tri://127.0.0.1:20001 ← 修正
ActivityServiceURL string // tri://127.0.0.1:20004 ← 修正
TaskServiceURL string // tri://127.0.0.1:20006
StarbookServiceURL string // 删除4.2
AIChatServiceURL string // tri://127.0.0.1:20008
StatisticServiceURL string // tri://127.0.0.1:20009
NotificationServiceURL string // tri://127.0.0.1:20010
ModerationServiceURL string // tri://127.0.0.1:20011
}
```
**Steps**:
- [ ]`backend/gateway/config/config.go:158-170`,记下当前 11 个 `DUBBO_*_SERVICE_URL` 默认值。
- [ ]`backend/gateway/config/config.go:162``getEnv("DUBBO_GALLERY_SERVICE_URL", "tri://127.0.0.1:20001")`。
- [ ]`backend/gateway/config/config.go:163``getEnv("DUBBO_ACTIVITY_SERVICE_URL", "tri://127.0.0.1:20004")`。
- [ ] 验证 `Task/Asset/AIChat/Statistic/Notification/Moderation/Social/User` 默认值与实际服务 `main.go``flag.Int("port", ...)` 默认一致(读 `backend/services/<svc>/main.go` 头部 `var port` 块)。
- [ ]`backend/services/galleryService/main.go:44``taskServiceURL = flag.String("task-service-url", getEnv("TASK_SERVICE_URL", "tri://localhost:20006"), ...)`。
- [ ]`backend/services/userService/main.go:283``protocol.WithPort(*port)`(替代硬编码 20000同时保留 `port` flag 默认 20000。
- [ ] 创建 `backend/gateway/config/port_test.go`,内容(关键骨架):
```go
package config
import (
"regexp"
"strconv"
"testing"
)
// 各服务 flag 默认端口(与 <service>/main.go 顶部 var port = flag.Int(...) 一致)
var serviceDefaultPorts = map[string]int{
"user": 20000,
"social": 20002,
"asset": 20003,
"gallery": 20001,
"activity": 20004,
"task": 20006,
"aiChat": 20008,
"statistic": 20009,
"notification": 20010,
"moderation": 20011,
}
// TestGatewayDubboPortsMatchServiceDefaults 校验网关默认 URL 端口 == 各服务 main.go 绑定端口
func TestGatewayDubboPortsMatchServiceDefaults(t *testing.T) {
cfg := Load()
cases := map[string]string{
"user": cfg.Dubbo.UserServiceURL,
"social": cfg.Dubbo.SocialServiceURL,
"asset": cfg.Dubbo.AssetServiceURL,
"gallery": cfg.Dubbo.GalleryServiceURL,
"activity": cfg.Dubbo.ActivityServiceURL,
"task": cfg.Dubbo.TaskServiceURL,
"aiChat": cfg.Dubbo.AIChatServiceURL,
"statistic": cfg.Dubbo.StatisticServiceURL,
"notification": cfg.Dubbo.NotificationServiceURL,
"moderation": cfg.Dubbo.ModerationServiceURL,
}
re := regexp.MustCompile(`:(\d+)$`)
for name, url := range cases {
m := re.FindStringSubmatch(url)
if len(m) != 2 { t.Fatalf("%s: URL %q 缺端口", name, url) }
got, _ := strconv.Atoi(m[1])
want := serviceDefaultPorts[name]
if got != want {
t.Errorf("%s: 网关默认端口=%d, 服务实际默认=%d", name, got, want)
}
}
}
```
- [ ]`cd backend && go test ./gateway/config/... -run TestGatewayDubboPortsMatchServiceDefaults -v` —— 必须通过。
- [ ]`backend/.env.example:21-26`:删除 `DUBBO_SOCIAL_SERVICE_URL=tri://127.0.0.1:20001` 错值(应是 20002并按上面 11 个 URL 重新列。
- [ ]`docker/docker-compose.local.yml`、`docker/docker-compose.prod.yml`:所有 `DUBBO_*_SERVICE_URL` 默认端口与上述一致(实际生产值通常已被 gateway container 内部覆盖;只动文档化注释)。
- [ ]`k8s/helm/topfans/values.yaml`:删除 `services.starbookservice`4.2 负责删),并修正 `DUBBO_GATEWAY_*` 注释默认值。
- [ ]`cd backend && go build ./...` —— 全部模块编译通过。
- [ ]`cd backend && go test ./... -run TestPort` —— 端口断言通过。
- [ ] 准备 commit message"fix(config): 对齐网关与服务默认端口并加配置断言测试 (批次 4.1)"—— **需用户批准后 commit**
### Task 2: starbookService 决断 → 删除4.2
> 对应审计 P0-7 与修复方案 4.2 推荐方案(生产未部署 starbookService
> ⚠️ 关联 P0-4 跨服务 import 解耦assetService 当前 `import starbookService/repository``backend/services/assetService/main.go:32`、`service/mint_service.go:28`),但 starbookService 不在 `go.work`(第 1-16 行)。当前 `go build` 实际**已失败**,本 Task 同步修复。
**Files**:
- 删除:`backend/services/starbookService/`(整个目录,含 `main.go`、`provider/`、`repository/`、`service/`、`starbookService` 二进制若存在)
- 删除:`k8s/helm/topfans/templates/starbookservice/deployment.yaml`
- 修改:`backend/services/assetService/main.go:32,159` —— 删除 `starbookRepo` import改为本地 `repository.NewAssetRegistryRepository`
- 修改:`backend/services/assetService/service/mint_service.go:28,71,84` —— 删除 `starbookRepo` import改用 `assetRegistryRepo.AssetRegistryRepository`(定义在 `assetService/repository`
- 修改:`backend/services/assetService/service/asset_service.go:58,69,78,116-131` —— `registryRepo` 类型不变(接口定义在 `asset_service.go:46-49`,已用 `models.AssetRegistry`),仅切换实现来源
- 新增:`backend/services/assetService/repository/asset_registry_repository.go` —— 从 starbookService 复制 `AssetRegistryRepository` 接口 + 实现,改 package 为 `repository`;去掉 import 循环
- 修改:`backend/gateway/config/config.go:95,165` —— 删除 `StarbookServiceURL` 字段
- 修改:`backend/go.work:3-16` —— 移除 starbookService实际本就不在
- 修改:`backend/dev.sh:24,347,365,432,438,468,493,510,530` —— 删除所有 starbookService 引用
- 修改:`docker/Dockerfile.services:50` —— 删除 `go build ... starbookservice`
- 修改:`docker/docker-compose.local.yml:231-262,442,482` —— 删除 starbookservice service block 与 `DUBBO_STARBOOK_SERVICE_URL` env
- 修改:`docker/docker-compose.prod.yml:365-401,629,659` —— 同上
- 修改:`k8s/helm/topfans/values.yaml:125,214-232` —— 删除 `services.starbookservice` 块与其在 `gateway.env` 里的 `DUBBO_STARBOOK_SERVICE_URL`
**Interfaces**(新增):
```go
// backend/services/assetService/repository/asset_registry_repository.go
package repository
import "github.com/topfans/backend/pkg/models"
// AssetRegistryRepository 资产统一索引(从 starbookService 迁来assetService 现在是 owner
type AssetRegistryRepository interface {
Create(registry *models.AssetRegistry) error
GetByID(id int64) (*models.AssetRegistry, error)
GetByAssetID(assetID int64) (*models.AssetRegistry, error)
GetByAssetTypeAndID(assetType string, assetID int64) (*models.AssetRegistry, error)
GetByOwner(ownerUID, starID int64) ([]*models.AssetRegistry, error)
GetByOwnerAndType(ownerUID, starID int64, assetType string, limit, offset int) ([]*models.AssetRegistry, error)
GetByOwnerAndTypeAndGrade(...) (...)
GetByOwnerAndTypeAndCategory(...) (...)
GetByOwnerAndTypeAndActivity(...) (...)
CountByOwner(...) (int64, error)
CountByOwnerAndType(...) (int64, error)
CountByOwnerAndTypeAndGrade(...) (int64, error)
CountByOwnerAndTypeAndCategory(...) (int64, error)
CountByOwnerAndTypeAndActivity(...) (int64, error)
UpdateLikeCount(assetID int64, likeCount int32) error
UpdateGrade(assetID int64, grade int32) error
Delete(assetID int64) error
DeleteByAssetType(assetType string, assetID int64) error
}
func NewAssetRegistryRepository(db *gorm.DB) AssetRegistryRepository { ... }
```
**Steps**:
- [ ] 确认 `backend/services/starbookService/` 完整目录清单:`ls -la backend/services/starbookService/`。
- [ ] **解耦步骤 A**——`backend/services/assetService/repository/asset_registry_repository.go`(新建):从 `backend/services/starbookService/repository/asset_registry_repository.go` 复制完整内容(接口 + 实现 + 388 行),改 `package repository`,删除跨服务 import保留 `pkg/models` + `pkg/errors` + `gorm.io/gorm`)。
- [ ] **解耦步骤 B**——`backend/services/assetService/main.go:32`:删除 `starbookRepo "github.com/topfans/backend/services/starbookService/repository"`
- [ ] **解耦步骤 C**——`backend/services/assetService/main.go:159`:把 `registryRepo := starbookRepo.NewAssetRegistryRepository(database.GetDB())` 改为 `registryRepo := repository.NewAssetRegistryRepository(database.GetDB())`
- [ ] **解耦步骤 D**——`backend/services/assetService/service/mint_service.go:28,71,84`:删 `starbookRepo` import类型改 `registryRepo repository.AssetRegistryRepository`;参数类型同步改。
- [ ] **解耦步骤 E**——`backend/services/assetService/service/asset_service.go:46-49`:当前 `RegistryRepository` 接口只声明 `GetByOwner`,无法满足 `mint_service.go` 需要mint_service 实际不用 registryRepo 的其他方法,仅用 `registryRepo.Create` 等;先确认)。如 mint_service 不需要接口扩展则保持现状;如需则把 `RegistryRepository` 接口扩展为 asset 内部接口或引用新 `repository.AssetRegistryRepository`
- [ ]`cd backend && go build ./services/assetService/...` —— 必须通过(说明解耦成功)。
- [ ] **删除步骤**——`git rm -r backend/services/starbookService/`。
- [ ] **删除 helm**——`git rm k8s/helm/topfans/templates/starbookservice/deployment.yaml`(若存在父目录则一并 `rmdir`)。
- [ ]`backend/gateway/config/config.go`:删 `StarbookServiceURL` 字段(第 95、165 行),同步从 `cases` map 移除(在 port_test 中也对应删除)。
- [ ] 确认 `backend/go.work` 已不含 starbookServicegrep `starbook` 应无结果)。
- [ ]`backend/dev.sh`
- 行 24从服务循环数组移除 `starbookService`
- 行 347、365`inotifywait --exclude` 列表移除 `starbookService$`
- 行 432、438从 build/start 循环移除。
- 行 468删除 `build_service "starbookService" ...` 函数调用。
- 行 493删除 `start_service "starbookService" ...` 调用。
- 行 510删除 `start_watcher "starbookService" ...` 调用。
- 行 530删除打印行 `Starbook Service: tri://localhost:20007`
- [ ]`docker/Dockerfile.services:50`:删除 `go build -o /tmp/starbookservice services/starbookService/main.go` + `echo "Built starbookservice"` 两行。
- [ ]`docker/docker-compose.local.yml`:删 `starbookservice:` service 块(行 231-262+ `DUBBO_STARBOOK_SERVICE_URL: tri://starbookservice:20007` env行 442+ 任何 depends_on starbookservice 引用(行 482
- [ ]`docker/docker-compose.prod.yml`:同 local端口用 `20005`(生产错配)。
- [ ]`k8s/helm/topfans/values.yaml`:删 `starbookservice:` 块(行 214-232+ `gateway.env.DUBBO_STARBOOK_SERVICE_URL` 引用。
- [ ]`cd backend && go build ./...` —— 全部模块编译通过。
- [ ]`cd backend && go test ./gateway/config/... -run TestGatewayDubboPortsMatchServiceDefaults` —— 通过(已移除 starbook 用例)。
- [ ]`cd backend && go test ./...` —— 全部测试通过。
- [ ]`grep -rn 'starbook\|Starbook' backend docker k8s dev.sh` —— 仅剩 `backend/migrations/` 中可能的 starbook 业务表(如 `asset_registry`),不应再有任何 starbookService 部署引用。
- [ ] 准备 commit message"refactor(services): 删除 starbookService 孤儿代码并内化 AssetRegistryRepository 到 assetService (批次 4.2)"—— **需用户批准后 commit**
### Task 3: 健康探针修正 + 大二进制出库4.3
> 对应审计 §三 P1 探针/大二进制行 + 修复方案 4.3。
**Files**:
- `backend/pkg/health/health.go:28-29` —— 当前 `mux.HandleFunc("/health", ...)`;新增 `/healthz` alias 处理(统一两个路径,避免 helm values 长期不一致)
- `backend/services/notificationService/main.go` —— 探针端口 21010 已正确(健康服务端口 = Dubbo 端口 + 1000无须改
- `backend/services/moderationService/main.go` —— 探针路径 `/` 改为 `/health`(让 helm values 也对齐)
- `k8s/helm/topfans/values.yaml:293,314` —— `notificationservice.healthPath: /health`(从 `/healthz` 改),`moderationservice.healthPath: /health`(从 `/` 改)
- `k8s/helm/topfans/templates/notificationservice/deployment.yaml:50,58` —— 探针 path 改 `/health`(无 `healthPath` 模板变量,可保留 default 但需对齐)
- `k8s/helm/topfans/templates/moderationservice/deployment.yaml:50,58` —— 探针 path 改 `/health`
- `docker/docker-compose.local.yml`、`docker/docker-compose.prod.yml` —— 任何 `HEALTHCHECK` 指令对齐 `/health`
- `backend/.gitignore` —— 追加 `backend/assetService`、`backend/gateway-fixed`、`backend/test`、`backend/cleanup-orphan-avatars`、`gateway-fixed`、`gateway/gateway-fixed` 等顶层可执行文件
- `backend/assetService`、`backend/gateway-fixed`、`backend/test`、`backend/cleanup-orphan-avatars` —— `git rm --cached`
**Interfaces**`backend/pkg/health/health.go` 修改):
```go
// 在现有 mux.HandleFunc("/health", h.handleHealth) 后追加:
mux.HandleFunc("/healthz", h.handleHealth)
```
K8s 探针仍可用 `/health`;保留 `/healthz` 兼容老 chart不破坏既有探针。
**Steps**:
- [ ]`backend/pkg/health/health.go:27-44`,确认当前 mux 仅注册 `/health`
- [ ]`backend/pkg/health/health.go:29` 后追加 `mux.HandleFunc("/healthz", h.handleHealth)`
- [ ]`cd backend && go build ./pkg/health/...` —— 通过。
- [ ]`k8s/helm/topfans/values.yaml:293``healthPath: /health`notificationservice`k8s/helm/topfans/values.yaml:314``healthPath: /health`moderationservice`/` 改)。
- [ ]`k8s/helm/topfans/templates/notificationservice/deployment.yaml:50,58`:把 `{{ $svc.healthPath | default "/healthz" }}` 改为 `{{ $svc.healthPath | default "/health" }}`(默认同步)。
- [ ]`k8s/helm/topfans/templates/moderationservice/deployment.yaml:50,58`:把 `{{ $svc.healthPath | default "/" }}` 改为 `{{ $svc.healthPath | default "/health" }}`
- [ ]`cd k8s/helm/topfans && helm template . --values values.yaml 2>&1 | grep -E 'healthz|path:'` —— 验证所有探针 path 为 `/health`
- [ ]`docker/docker-compose.local.yml`:搜 `HEALTHCHECK`,把任何 `curl http://localhost:2xxxx/healthz` 改为 `/health`
- [ ]`docker/docker-compose.prod.yml`:同上。
- [ ]`backend/.gitignore`:在"Backend 构建产物"区块追加:
```
# 顶层历史二进制(本地构建残留,禁止入库)
/assetService
/gateway-fixed
/test
/cleanup-orphan-avatars
```
- [ ]`cd /Users/liulujian/Documents/code/TopFansByGithub && git rm --cached backend/assetService backend/gateway-fixed backend/test backend/cleanup-orphan-avatars`(若文件已被 .gitignore 覆盖,则先从暂存区移除)。
- [ ]`git ls-files | grep -E '^backend/(assetService|gateway-fixed|test|cleanup-orphan-avatars)$'` —— 应为空。
- [ ]`git status --ignored | grep -E '^backend/(assetService|gateway-fixed|test|cleanup-orphan-avatars)$'` —— 应列出(说明已忽略但本地保留)。
- [ ]`cd backend && go build ./... && go test ./...` —— 全部通过。
- [ ] 跑本地健康检查:`cd backend && (PORT=20010 ./services/notificationService/notificationService &) ; sleep 2 ; curl -s http://localhost:21010/health ; curl -s http://localhost:21010/healthz` —— 两个路径均返回 `{"status":"ok"}`
- [ ] 准备 commit message"fix(deploy): 健康探针路径统一为 /health + 顶层历史二进制出库 (批次 4.3)"—— **需用户批准后 commit**(建议拆为两个 commit二进制出库 + 探针修正,分别走 `git add .gitignore``git add helm`)。
### Task 4: 序列脚本 setval4.4
> 对应审计 §三 P1 `create_gallery_test_users.go` 与修复方案 4.4。遵循 `CLAUDE.md` PostgreSQL 序列规范。
**Files**:
- `backend/scripts/create_gallery_test_users.go:59-152` —— 在每个手动 INSERT 表后追加 `SELECT setval(...)`
**Interfaces**(无新接口;纯输出增强):
```go
// 输出 SQL 末尾追加序列重置;当前 main 函数应收集受影响表名集合,结尾统一输出。
var tablesToReset = []string{"users", "fan_profiles", "booth_slots", "assets", "exhibitions"}
for _, tbl := range tablesToReset {
fmt.Printf("SELECT setval('%s_id_seq', (SELECT MAX(id) FROM %s));\n", tbl, tbl)
}
```
**Steps**:
- [ ] 通读 `backend/scripts/create_gallery_test_users.go:1-170`,列出所有手动指定 id 的 INSERT 表:`users` (id=100/101)、`fan_profiles` (无 id`user_id, star_id` 复合键——若 BIGSERIAL 则无需)、`booth_slots` (slot_id=1001-1003, 2001-2003)、`assets` (id=1000-1003)、`exhibitions` (无 id)。
- [ ] 检查 `fan_profiles``exhibitions` 是否 BIGSERIAL查 migrations`grep -E 'CREATE TABLE (fan_profiles|exhibitions)' backend/migrations/*.sql`)。
- [ ]`backend/scripts/create_gallery_test_users.go`
-`var` 区块追加 `tablesToReset := []string{"users", "booth_slots", "assets"}``fan_profiles` 与 `exhibitions` 如非 BIGSERIAL 则不计入)。
-`main()` 末尾、所有 `fmt.Println` 之前,循环 `for _, tbl := range tablesToReset { fmt.Printf("SELECT setval('%s_id_seq', (SELECT MAX(id) FROM %s));\n", tbl, tbl) }`
- 在最后追加 `fmt.Println("-- 序列重置完成")`
- [ ]`cd backend && go run scripts/create_gallery_test_users.go > /tmp/test_users.sql`,确认末尾包含 `SELECT setval('users_id_seq', ...)`、`SELECT setval('booth_slots_id_seq', ...)`、`SELECT setval('assets_id_seq', ...)` 三行。
- [ ] 验证迁移可行性:在测试库 `psql -f /tmp/test_users.sql`(用 docker `postgresql-database-1` 即可),确认不报 `duplicate key` 且三条 `setval` 执行成功。
- [ ]`cd backend && go build ./...` —— 通过。
- [ ]`cd backend && go vet ./scripts/...` —— 无 warning。
- [ ] 准备 commit message"fix(scripts): create_gallery_test_users.go 输出 SQL 末尾补 setval 序列重置 (批次 4.4)"—— **需用户批准后 commit**
### Task 5: .env ↔ .env.example 对齐4.5
> 对应审计 §三 P1 漂移与修复方案 4.5。`.env.example` 仅留占位符,`.env` 真值不进 git。
**Files**:
- `backend/.env.example` —— 增补缺失键、删除占位符中的真实密钥痕迹(`LTAI5t...`、`sk-cp-`、`sk-proj-`、`app-...` 等)
- `backend/.env` —— 实际环境值,本 Task 不入仓但需确保所有代码读取的键均有对应 `getEnv(...)` 默认值或 example
- `backend/gateway/config/env_audit_test.go` (新增) —— 断言 `.env` 解析后所有"必填"键非空(仅对非敏感键,如 `WS_AI_CHAT_PATH`、`LANDING_BASE_URL`
**Interfaces**(无新接口;测试骨架):
```go
// backend/gateway/config/env_audit_test.go
package config
import (
"os"
"strings"
"testing"
)
// TestEnvExampleDrift 断言 .env.example 含全部 .env 中已文档化的键(粗粒度:按 KEY= 前缀)
func TestEnvExampleDrift(t *testing.T) {
exampleBytes, err := os.ReadFile("../../.env.example")
if err != nil { t.Fatal(err) }
envBytes, err := os.ReadFile("../../.env")
if err != nil { t.Fatal(err) }
exampleKeys := parseEnvKeys(string(exampleBytes))
envKeys := parseEnvKeys(string(envBytes))
// 反向断言:.env 中有的键 .env.example 必出现(除 SECRET_KEY/JWT_SECRET/真实 key
for k := range envKeys {
if isSecretKey(k) || isRealAPIKey(k) { continue }
if _, ok := exampleKeys[k]; !ok {
t.Errorf(".env 有键 %q 但 .env.example 缺失", k)
}
}
}
```
**Steps**:
- [ ]`diff <(grep -E '^[A-Z_]+=' backend/.env | cut -d= -f1 | sort) <(grep -E '^[A-Z_]+=' backend/.env.example | cut -d= -f1 | sort)` —— 列出 .env 独有键(应在 .env.example 补)和 .env.example 独有键(可能是模板占位/可保留)。
- [ ] 列出 `.env` 独有键(实际环境值,非 secret`WS_AI_CHAT_PATH`、`LASER_COMPOSITOR_URL`、`COMPOSITOR_PORT`、`LASER_GEN_PROVIDER`、`MINIMAX_API_KEY`、`MINIMAX_API_URL`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`、`DIFY_API_BASE`、`DIFY_API_KEY`、`JWT_SECRET`、`ENV`、`LOG_LEVEL`、`OSS_REGION`、`OSS_BUCKET_NAME`、`OSS_STS_ROLE_ARN`、`OSS_ACCESS_KEY_ID`、`OSS_ACCESS_KEY_SECRET`、`OSS_AVATAR_DIR`、`OSS_ASSET_DIR`、`OSS_TOKEN_EXPIRE_TIME`、`REDIS_HOST`、`REDIS_PORT`、`REDIS_PASSWORD`、`REDIS_DB`、`DB_HOST`、`DB_PORT`、`DB_USER`、`DB_PASSWORD`、`DB_NAME`、`GIN_MODE`、`SERVER_PORT`、`SECRET_KEY`。
- [ ] 列出 `.env.example` 独有键(当前文档化但 .env 未用):`LANDING_BASE_URL`、`PUSH_ENABLED`、`PUSH_URL`、`PUSH_TIMEOUT_MS`、`DIFY_TIMEOUT_SEC`、`DIFY_WORKFLOW`、`SEGMENT_PROVIDER`、`SEGMENT_INFERENCE_URL`、`DUBBO_USER_SERVICE_URL` 至 `DUBBO_STARBOOK_SERVICE_URL`4.2 后应删 Starbook
- [ ]`backend/.env.example`
- 行 87取消注释 `WS_AI_CHAT_PATH=/ai-chat`(去掉 `# ` 前缀)。
- 行 121-123保留 `PUSH_ENABLED/PUSH_URL/PUSH_TIMEOUT_MS`,但 `PUSH_URL=` 留空占位符。
- 行 124-128删除 `OPENAI_*` 三行(真实密钥痕迹),改为 `# OPENAI_* 由 LASER_GEN_PROVIDER=openai 启用时填入`
- 行 131-133删除 `DIFY_API_KEY=app-aHnBfMeOQp7A9dQneIFPdPaZ`Dify 真实密钥),改为 `DIFY_API_KEY=`
- 行 106删除 `OPENAI_API_KEY=sk-proj-...` 真实值,改为 `OPENAI_API_KEY=`
- 增补 `LANDING_BASE_URL=http://localhost:5173`(已在最顶部,确认);增补 `LASER_COMPOSITOR_URL=http://127.0.0.1:7002`、`LASER_GEN_PROVIDER=openai`;增补 `MINIMAX_API_KEY=``MINIMAX_API_URL=https://api.minimaxi.com/v1/image_generation`(已有,确认)。
- 增补 `SEGMENT_*`(已有 73-74 行,确认)。
- 增补 `DIFY_TIMEOUT_SEC=60`(已有 133 行,确认)。
- [ ]`.env.example` 顶部加注释:
```
# .env.example 仅作键清单 + 占位符。真实值由部署环境注入docker env_file / k8s Secret / systemd EnvironmentFile
# 严禁把真实 key 写入本文件;本文件应可直接 commit。
```
- [ ] 创建 `backend/gateway/config/env_audit_test.go`(见上面骨架),实现 `parseEnvKeys`(按行解析 `^[A-Z_]+=`)、`isSecretKey`、`isRealAPIKey`(白名单:`SECRET_KEY/JWT_SECRET/PASSWORD/*_KEY/*_SECRET/TOKEN`)。
- [ ]`cd backend && go test ./gateway/config/... -run TestEnvExampleDrift -v` —— 通过。
- [ ] 确认 `git check-ignore backend/.env` —— 返回 0已忽略`git ls-files backend/.env` —— 应为空。
- [ ] 确认 `git ls-files backend/.env.example` —— 有文件。
- [ ]`cd backend && go build ./... && go test ./...` —— 全部通过。
- [ ] 准备 commit message"fix(env): 同步 .env.example 与 .env 键清单并清理示例文件中的真实密钥痕迹 (批次 4.5)"—— **需用户批准后 commit**
### Task 6: P2 清理4.9
> 对应审计 §四 P2 八项。所有改动均小而独立,按表格逐项修。
**Files & Actions**(按文件:行 + 改动说明):
| P2 项 | 文件:行 | 改动 |
|-------|---------|------|
| `parseRPCError` 字符串解析 | `backend/gateway/controller/asset_controller.go:158-194` | 改用 `google.golang.org/grpc/status``status.FromError(err)`;删除正则解析。返回结构 `{code: int, message: string}`,调 7 处 `parseRPCError(err)` 调用(行 240/286/365/492/568/700/765/827保持签名不变 |
| 原始 err 泄露前端 | `backend/gateway/controller/auth_controller.go:224,294,306`、`user_controller.go:141,247,498` | 把 `c.JSON(500, gin.H{"message": err.Error()})` 改为 `response.InternalError(c)``response.BadRequest(c, "操作失败")` + `logger.Logger.Error("...", zap.Error(err))` 服务端日志保留 err |
| `ResetPassword` 不失效 JWT | `backend/services/userService/service/user_service.go:681` | 现状已在事务内把 `access_token` 置 nil + `token_expires_at` 置 nil。补`gateway/middleware/auth_middleware.go` 在解析 JWT 后**额外**比对 `users.access_token`(如已为 nil 则 401。如工期紧张至少在 `user_service.go:681` 注释里写明 "JWT 失效留待 ticket #XX 处理;当前实现只阻止后续发新 token 复用"——选择最小改动(仅注释 + ticket 链接)以避免越界改动 middleware |
| `UpdateAvatar` URL 不校验同源 | `backend/services/userService/service/user_service.go:1023` | 在写库前增加同源校验:`u, err := url.Parse(req.AvatarUrl)``if u.Host != "" && !strings.Contains(u.Host, "aliyuncs.com") { return nil, ErrInvalidAvatarURL }`;白名单仅 `aliyuncs.com` 子域 |
| PII 进 INFO 日志 | `backend/services/aiChatService/provider/ai_chat_provider.go:107`、`mint_service.go`、`user_service.go` 多处 | 把 `user_id`/`session_id` 之外不要打印 `message`/`mobile`;或改用 `logger.Logger.Debug(...)``mobile` 用 `mobile[:3] + "****" + mobile[7:]` 脱敏 |
| pkg/mq 死代码 | `backend/pkg/mq/streams/adapter.go``EventProducer`/`Stream*` 常量)、`backend/pkg/mq/asynq/adapter.go:117``Delete` 假实现) | 与批次 3.3 一并决断(用户已选"停用 streams adapter"则本 Task 把 `streams/` 整个子包删除、`mq.go` 的 `MQDriver = Streams` 分支删除asynq `Delete` 暂保留(实际不调用),注释"未接线" |
| `is_processed` 复用为 settled | `backend/services/galleryService/mq/consumer.go:207` | **由批次 1.1`exhibition-settlement-idempotency` plan Task 3负责** `settled_at` 列 + `isSettled/markSettled` 改写。本 plan 不重复编辑此函数,仅在回归时校验其已切换。 |
| 直连 Redis Pub/Sub | `backend/services/activityService/service/activity_service.go:217,1579` | 两处 `s.redisClient.Publish(...)` 改为走 `adapter.EventProducer.Publish(ctx, "activity.contributions", payload)`(若 pkg/mq 已确定保留,否则本项 defer 到 3.3 |
| 周边密钥兜底 | `backend/pkg/peripheral/sign.go:33-44` | 去掉 `JWT_SECRET` 兜底与硬编码 `"default-dev-secret-change-me"``SECRET_KEY == ""` 时 `panic("SECRET_KEY is required")`fail-fast |
**Interfaces**(新增/修改):
```go
// backend/gateway/controller/asset_controller.go (修改后)
import "google.golang.org/grpc/status"
func parseRPCError(err error) (code int, message string) {
if err == nil { return http.StatusOK, "" }
if st, ok := status.FromError(err); ok {
// grpc status code → HTTP code 映射(参考 grpc-gateway 约定)
return grpcCodeToHTTP(st.Code()), st.Message()
}
return http.StatusInternalServerError, "服务暂时不可用"
}
func grpcCodeToHTTP(c codes.Code) int {
switch c {
case codes.NotFound: return http.StatusNotFound
case codes.InvalidArgument: return http.StatusBadRequest
case codes.Unauthenticated: return http.StatusUnauthorized
case codes.PermissionDenied: return http.StatusForbidden
case codes.AlreadyExists: return http.StatusConflict
default: return http.StatusInternalServerError
}
}
```
```go
// backend/pkg/peripheral/sign.go (修改后)
func getSecret() string {
secretOnce.Do(func() {
secretVal = os.Getenv("SECRET_KEY")
if secretVal == "" {
panic("SECRET_KEY is required for peripheral HMAC signing")
}
})
return secretVal
}
```
**Steps**(按表格逐项):
- [ ] **A. parseRPCError**:改 `backend/gateway/controller/asset_controller.go:158-194``status.FromError` 形式。7 处调用点(行 240/286/365/492/568/700/765/827签名不变无须改。跑 `cd backend && go build ./gateway/...` 通过。
- [ ] **B. err 泄露**:改 `auth_controller.go:224,294,306` + `user_controller.go:141,247,498` 五处,把 `err.Error()` 移到 `logger.Logger.Error("...", zap.Error(err))`,响应改为稳定文案(如 `response.InternalError(c, "操作失败")`。grep 验证 `c.JSON(500, gin.H{"message": err.Error()})` 仅剩 0 处。
- [ ] **C. ResetPassword JWT**:决策点——读 `backend/gateway/middleware/auth_middleware.go` 当前是否校验 `users.access_token`;若否,最小改动为更新 `user_service.go:681` 注释(添加 ticket 链接),不修改 middleware若已校验本项完成。`git grep 'access_token' backend/gateway/middleware/`。
- [ ] **D. UpdateAvatar 同源**:改 `user_service.go:1023``url.Parse` + `aliyuncs.com` 白名单;`pkg/errors` 增加 `ErrInvalidAvatarURL`
- [ ] **E. PII 日志**:改 `ai_chat_provider.go:107` 不打 `message``user_service.go` 中 `mobile` 打印用脱敏函数。grep `logger.Logger.Info.*mobile.*req` / `zap.String("message", message)` 全面排查。
- [ ] **F. pkg/mq 死代码**:读 `backend/pkg/mq/mq.go``MQDriver = Streams` 分支是否被引用。若无引用,删除 `backend/pkg/mq/streams/` 子包 + `mq.go` 中的 streams init 分支;`asynq/adapter.go:117` 的 `Delete` 假实现加注释 `// TODO: 实际未调用asynq.DeletedTaskInfo 返回 false 即视为不存在`
- [ ] **G. `is_processed` 复用****不在本 plan 编辑**。`settled_at` 列 + `isSettled/markSettled` 改写归批次 1.1`exhibition-settlement-idempotency` plan Task 1 migration + Task 3。本 plan 仅校验:`grep -n 'settled_at' backend/services/galleryService/mq/consumer.go` 确认已切换;未切换则回到批次 1.1 执行。
- [ ] **H. Redis Pub/Sub 直连**:决策点——读 `backend/pkg/mq/mq.go``EventProducer` 接口是否已稳定;若稳定,改 `activity_service.go:217,1579``mq.GetEventProducer().Publish(ctx, "activity.contributions", payload)`;否则本项 defer 到 3.3。
- [ ] **I. 周边密钥 fail-fast**:改 `pkg/peripheral/sign.go:33-44`,去掉 JWT_SECRET 兜底与硬编码默认值。跑 `cd backend && SECRET_KEY= go test ./pkg/peripheral/...` —— 必须 panic 或返回明确错误(不能静默用 dev 默认值)。
- [ ]`cd backend && go build ./... && go test ./...` —— 全部通过。
- [ ]`cd backend && go vet ./...` —— 无 warning。
- [ ]`cd backend && grep -rnE 'parseRPCError.*errStr\|err\.Error\(\)\|default-dev-secret\|JWT_SECRET.*secretVal' .` —— 仅 0 处(除新增注释)。
- [ ] 准备 commit message建议拆 3 个):"refactor(gateway): parseRPCError 改用 grpc/status.FromError (批次 4.9-A)" + "fix(gateway): 修复 5 处原始 err 泄露前端 (批次 4.9-B)" + "fix(peripheral): SECRET_KEY 缺失 fail-fast去 JWT_SECRET/dev 默认兜底 (批次 4.9-I)"—— **每个 commit 需用户批准**
---
## Self-Review
> 严格按 `CLAUDE.md` 全局自审规则:通读本文 §1→§5核对跨章节引用、Go 编译预期、副作用、commit 边界。
### 修改的章节
- §File Structure任务清单
- §Task 16步骤、文件、接口
### 未改动但通读确认的章节
- §Goal / §Architecture / §Tech Stack —— 与原方案对齐,无改动需求。
- §Global Constraints —— 与审计 §五 修复顺序、批次 4 路线图一致序列规则、commit 守则与 `CLAUDE.md` 一致。
### 跨章节引用一致性检查
- **端口矩阵4.1 ↔ §File Structure 表)**4.1 列出 11 个服务端口§File Structure 表也列 11 个服务端口——一致。
- **starbookService 删除4.2 ↔ 4.1 端口断言)**4.1 `serviceDefaultPorts``cases` map 必须不含 starbook——本计划已明确在 4.2 完成后删除 `StarbookServiceURL` 字段并同步 port_test 的 `cases` map。
- **健康探针4.3 ↔ helm values**4.3 改 `notificationservice.healthPath``/healthz``/health`,与 `backend/pkg/health/health.go:28-29` 注册 `/health` 一致——OK。
- **大二进制4.3 ↔ §Global Constraints 不动产物)**`backend/bin/*` 与 `.gitignore` 第 10 行 `bin/` 仍受保护;本计划只追加根级四条二进制忽略——一致。
- **序列脚本4.4 ↔ §Global Constraints**:遵循 `CLAUDE.md` 强制规则——一致。
- **.env 漂移4.5 ↔ .gitignore**`.env` 已在 `.gitignore` 第 21 行忽略;本计划仅改 `.env.example`——一致。
- **P2 清理4.9 ↔ 批次 3.3 决断)**H/F 两项依赖批次 3.3 MQ 决断结论——本计划用"决策点"标注,未预设结论。
- **commit 边界(每 Task ↔ §Global Constraints**:每个 Task 末尾标注"需用户批准后 commit",未授权自提交——一致。
### Go 编译验证预期(必须能在本地真实跑通)
| 步骤 | 命令 | 预期 |
|------|------|------|
| 端口测试 | `cd backend && go test ./gateway/config/... -run TestGatewayDubboPortsMatchServiceDefaults -v` | PASS |
| env 审计 | `cd backend && go test ./gateway/config/... -run TestEnvExampleDrift -v` | PASS |
| 全量编译 | `cd backend && go build ./...` | exit 0 |
| 全量测试 | `cd backend && go test ./...` | exit 0 |
| vet | `cd backend && go vet ./...` | exit 0 |
| helm lint | `cd k8s/helm/topfans && helm lint .` | 0 errors |
| 大二进制 | `git ls-files \| grep -E '^backend/(assetService\|gateway-fixed\|test\|cleanup-orphan-avatars)$'` | empty |
| starbook 删除 | `find backend -path '*starbook*' -not -path '*/node_modules/*'` | 仅 `backend/migrations/` 中 starbook 业务表(如有),无 `starbookService/` 目录 |
### 优先级分类
- **P0先做挡构建**4.2 starbookService 删除(当前 `go build ./services/assetService/...` 失败)+ 4.1 端口对齐(影响所有下游服务)。
- **P1同期**4.3 探针 + 大二进制 + 4.4 setval + 4.5 env。
- **P2顺手**4.9 八项;其中 4.9-I 周边密钥 fail-fast 涉及安全边界,应排在 4.9 内靠前。
### 失败模式预演
- 若 4.2 完成后 `go build ./...` 仍报 starbook 引用:检查 `assetService/service/mint_service.go` 是否还有 `starbookRepo` import 残留grep 验证)。
- 若 4.1 port_test 报某服务端口不匹配:回到该服务 `main.go` 顶部 `var port = flag.Int(...)` 默认值核对,更新 `serviceDefaultPorts` map 与默认值。
- 若 4.3 helm template 探针 path 仍为 `/healthz`:检查 `_helpers.tpl` 或 deployment.yaml `default` 值是否覆盖——可能需硬改两处。
- 若 4.4 setval 报 `relation "xxx_id_seq" does not exist`:该表非 BIGSERIAL`tablesToReset` 移除。
- 若 4.5 env 审计失败:逐一对照 `isSecretKey`/`isRealAPIKey` 白名单,确认是否把真 secret 误识别为 drift。
### 跨任务依赖(按执行顺序)
```
4.1 ──┐
├──▶ 4.6(提交后整体回归)
4.2 ──┤
4.3 ──┤
4.4 ──┤
4.5 ──┤
4.9 ──┘
```
4.1 与 4.2 互不阻塞4.2 内 assetService 解耦独立完成。4.3/4.4/4.5/4.9 与前两者可并行。4.9 内部分项按"决策点"标注——若用户已决断批次 3.3,则 F/H 可立即执行;否则 defer。