44 KiB
配置与部署 (批次4) Implementation Plan
REQUIRED SUB-SKILL: superpowers:writing-plans (实施阶段使用 superpowers:executing-plans;code-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批次 4(4.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 八项全部清理或明确 defer(4.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 校验 │
└─────────────┘ └────────────┘ └─────────────┘
依赖关系(必须按顺序):
- 4.2 starbookService 删除先做(它依赖 4.3 探针清理 + P0-4 跨服务 import 解耦)—— 完成前
go build ./...会因starbookService不在go.work而失败。 - 4.1 端口对齐与 4.2 starbook 删除独立可并行。
- 4.3 探针 + 大二进制与 4.4 / 4.5 可独立并行。
- 4.9 P2 清理作为伴随项随 4.1–4.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 的 INSERT,SQL 末尾必须
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:20006backend/services/userService/main.go:283—— 删除硬编码protocol.WithPort(20000),改用*portbackend/services/assetService/main.go:38—— 确认 20003 默认backend/services/activityService/main.go—— 默认端口确认 20004backend/services/taskService/main.go—— 默认端口确认 20006backend/services/statisticService/main.go—— 默认 20009backend/services/notificationService/main.go—— 默认 20010backend/services/moderationService/main.go—— 默认 20011backend/services/aiChatService/main.go—— 默认 20008backend/services/socialService/main.go—— 默认 20002docker/docker-compose.local.yml、docker/docker-compose.prod.yml、k8s/helm/topfans/values.yaml—— 同步DUBBO_*_SERVICE_URL默认值backend/gateway/config/port_test.go(新增) —— 配置断言测试
Interfaces:
// 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),同时保留portflag 默认 20000。 - 创建
backend/gateway/config/port_test.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—— 删除starbookRepoimport,改为本地repository.NewAssetRegistryRepository - 修改:
backend/services/assetService/service/mint_service.go:28,71,84—— 删除starbookRepoimport,改用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_URLenv - 修改:
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(新增):
// 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:删starbookRepoimport;类型改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 行),同步从casesmap 移除(在 port_test 中也对应删除)。 - 确认
backend/go.work已不含 starbookService(grepstarbook应无结果)。 - 改
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。
- 行 24:从服务循环数组移除
- 改
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:20007env(行 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", ...);新增/healthzalias 处理(统一两个路径,避免 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 改/healthdocker/docker-compose.local.yml、docker/docker-compose.prod.yml—— 任何HEALTHCHECK指令对齐/healthbackend/.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 修改):
// 在现有 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: 序列脚本 setval(4.4)
对应审计 §三 P1
create_gallery_test_users.go与修复方案 4.4。遵循CLAUDE.mdPostgreSQL 序列规范。
Files:
backend/scripts/create_gallery_test_users.go:59-152—— 在每个手动 INSERT 表后追加SELECT setval(...)
Interfaces(无新接口;纯输出增强):
// 输出 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(用 dockerpostgresql-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(...)默认值或 examplebackend/gateway/config/env_audit_test.go(新增) —— 断言.env解析后所有"必填"键非空(仅对非敏感键,如WS_AI_CHAT_PATH、LANDING_BASE_URL)
Interfaces(无新接口;测试骨架):
// 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 行,确认)。
- 行 87:取消注释
- 在
.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(新增/修改):
// 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
}
}
// 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打印用脱敏函数。greplogger.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-idempotencyplan 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 1–6(步骤、文件、接口)
未改动但通读确认的章节
- §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与casesmap 必须不含 starbook——本计划已明确在 4.2 完成后删除StarbookServiceURL字段并同步 port_test 的casesmap。 - 健康探针(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是否还有starbookRepoimport 残留(grep 验证)。 - 若 4.1 port_test 报某服务端口不匹配:回到该服务
main.go顶部var port = flag.Int(...)默认值核对,更新serviceDefaultPortsmap 与默认值。 - 若 4.3 helm template 探针 path 仍为
/healthz:检查_helpers.tpl或 deployment.yamldefault值是否覆盖——可能需硬改两处。 - 若 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。