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

44 KiB
Raw Blame History

配置与部署 (批次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 基线 commitb7f8f1b724e5,分支 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 key4.4)。
  • backend/.env.examplebackend/.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.5go.work
  • 测试Go testing + testify
  • K8s 配置Helm 3 + values.yaml
  • Dockerdocker-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.gobackend/services/galleryService/main.gobackend/services/userService/main.gobackend/services/activityService/main.gobackend/services/taskService/main.gobackend/services/statisticService/main.gobackend/services/notificationService/main.gobackend/services/moderationService/main.gobackend/services/aiChatService/main.gobackend/services/socialService/main.gobackend/services/assetService/main.gobackend/docker-compose.local.ymlbackend/docker-compose.prod.ymldocker/docker-compose.local.ymldocker/docker-compose.prod.ymlk8s/helm/topfans/values.yamlbackend/services/gateway/config/port_test.go (新增)
4.2 backend/services/starbookService/ (整目录)、backend/services/assetService/main.gobackend/services/assetService/service/mint_service.gobackend/services/assetService/repository/asset_registry_repository.go (新增)、backend/services/assetService/service/asset_service.go (registryRepo 引用)、backend/gateway/config/config.gobackend/go.workbackend/dev.shdocker/Dockerfile.servicesdocker/docker-compose.local.ymldocker/docker-compose.prod.ymlk8s/helm/topfans/values.yamlk8s/helm/topfans/templates/starbookservice/deployment.yaml (删) 删 + 修
4.3 backend/pkg/health/health.gobackend/services/assetService/main.gobackend/services/galleryService/main.gobackend/services/userService/main.gobackend/services/notificationService/main.gobackend/services/moderationService/main.go (注册 /healthz alias)、k8s/helm/topfans/values.yamlk8s/helm/topfans/templates/notificationservice/deployment.yamlk8s/helm/topfans/templates/moderationservice/deployment.yamldocker/docker-compose.local.ymldocker/docker-compose.prod.ymlbackend/.gitignorebackend/assetServicebackend/gateway-fixedbackend/testbackend/cleanup-orphan-avatars (git rm --cached) 修 + rm
4.4 backend/scripts/create_gallery_test_users.go
4.5 backend/.env.examplebackend/.envbackend/services/gateway/config/env_audit_test.go (新增) 修 + 新增
4.9 backend/gateway/controller/asset_controller.gobackend/gateway/controller/auth_controller.gobackend/gateway/controller/user_controller.gobackend/services/userService/service/user_service.gobackend/services/aiChatService/provider/ai_chat_provider.gobackend/services/mint_service.gobackend/services/activityService/service/activity_service.gobackend/pkg/peripheral/sign.gobackend/pkg/mq/streams/adapter.gobackend/pkg/mq/asynq/adapter.gobackend/services/galleryService/mq/consumer.go
自审 docs/superpowers/plans/2026-07-21-config-deploy.md (本文)

新增测试文件

  • backend/gateway/config/port_test.go4.1 端口断言测试)
  • backend/gateway/config/env_audit_test.go4.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 默认 20001taskServiceURL 默认改 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.ymldocker/docker-compose.prod.ymlk8s/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:162getEnv("DUBBO_GALLERY_SERVICE_URL", "tri://127.0.0.1:20001")
  • backend/gateway/config/config.go:163getEnv("DUBBO_ACTIVITY_SERVICE_URL", "tri://127.0.0.1:20004")
  • 验证 Task/Asset/AIChat/Statistic/Notification/Moderation/Social/User 默认值与实际服务 main.goflag.Int("port", ...) 默认一致(读 backend/services/<svc>/main.go 头部 var port 块)。
  • backend/services/galleryService/main.go:44taskServiceURL = flag.String("task-service-url", getEnv("TASK_SERVICE_URL", "tri://localhost:20006"), ...)
  • backend/services/userService/main.go:283protocol.WithPort(*port)(替代硬编码 20000同时保留 port flag 默认 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.ymldocker/docker-compose.prod.yml:所有 DUBBO_*_SERVICE_URL 默认端口与上述一致(实际生产值通常已被 gateway container 内部覆盖;只动文档化注释)。
  • k8s/helm/topfans/values.yaml:删除 services.starbookservice4.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/repositorybackend/services/assetService/main.go:32service/mint_service.go:28),但 starbookService 不在 go.work(第 1-16 行)。当前 go build 实际已失败,本 Task 同步修复。

Files:

  • 删除:backend/services/starbookService/(整个目录,含 main.goprovider/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(新增):

// 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、365inotifywait --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.ymldocker/docker-compose.prod.yml —— 任何 HEALTHCHECK 指令对齐 /health
  • backend/.gitignore —— 追加 backend/assetServicebackend/gateway-fixedbackend/testbackend/cleanup-orphan-avatarsgateway-fixedgateway/gateway-fixed 等顶层可执行文件
  • backend/assetServicebackend/gateway-fixedbackend/testbackend/cleanup-orphan-avatars —— git rm --cached

Interfacesbackend/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:293healthPath: /healthnotificationservicek8s/helm/topfans/values.yaml:314healthPath: /healthmoderationservice/ 改)。
  • 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 .gitignoregit 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(无新接口;纯输出增强):

// 输出 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 (无 iduser_id, star_id 复合键——若 BIGSERIAL 则无需)、booth_slots (slot_id=1001-1003, 2001-2003)、assets (id=1000-1003)、exhibitions (无 id)。
  • 检查 fan_profilesexhibitions 是否 BIGSERIAL查 migrationsgrep -E 'CREATE TABLE (fan_profiles|exhibitions)' backend/migrations/*.sql)。
  • backend/scripts/create_gallery_test_users.go
    • var 区块追加 tablesToReset := []string{"users", "booth_slots", "assets"}fan_profilesexhibitions 如非 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_PATHLANDING_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 独有键(实际环境值,非 secretWS_AI_CHAT_PATHLASER_COMPOSITOR_URLCOMPOSITOR_PORTLASER_GEN_PROVIDERMINIMAX_API_KEYMINIMAX_API_URLOPENAI_API_KEYOPENAI_BASE_URLOPENAI_MODELDIFY_API_BASEDIFY_API_KEYJWT_SECRETENVLOG_LEVELOSS_REGIONOSS_BUCKET_NAMEOSS_STS_ROLE_ARNOSS_ACCESS_KEY_IDOSS_ACCESS_KEY_SECRETOSS_AVATAR_DIROSS_ASSET_DIROSS_TOKEN_EXPIRE_TIMEREDIS_HOSTREDIS_PORTREDIS_PASSWORDREDIS_DBDB_HOSTDB_PORTDB_USERDB_PASSWORDDB_NAMEGIN_MODESERVER_PORTSECRET_KEY
  • 列出 .env.example 独有键(当前文档化但 .env 未用):LANDING_BASE_URLPUSH_ENABLEDPUSH_URLPUSH_TIMEOUT_MSDIFY_TIMEOUT_SECDIFY_WORKFLOWSEGMENT_PROVIDERSEGMENT_INFERENCE_URLDUBBO_USER_SERVICE_URLDUBBO_STARBOOK_SERVICE_URL4.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-aHnBfMeOQp7A9dQneIFPdPaZDify 真实密钥),改为 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:7002LASER_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_]+=)、isSecretKeyisRealAPIKey(白名单: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/statusstatus.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,306user_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:107mint_service.gouser_service.go 多处 user_id/session_id 之外不要打印 message/mobile;或改用 logger.Logger.Debug(...)mobilemobile[:3] + "****" + mobile[7:] 脱敏
pkg/mq 死代码 backend/pkg/mq/streams/adapter.goEventProducer/Stream* 常量)、backend/pkg/mq/asynq/adapter.go:117Delete 假实现) 与批次 3.3 一并决断(用户已选"停用 streams adapter"则本 Task 把 streams/ 整个子包删除、mq.goMQDriver = Streams 分支删除asynq Delete 暂保留(实际不调用),注释"未接线"
is_processed 复用为 settled backend/services/galleryService/mq/consumer.go:207 由批次 1.1exhibition-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-194status.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:1023url.Parse + aliyuncs.com 白名单;pkg/errors 增加 ErrInvalidAvatarURL
  • E. PII 日志:改 ai_chat_provider.go:107 不打 messageuser_service.gomobile 打印用脱敏函数。grep logger.Logger.Info.*mobile.*req / zap.String("message", message) 全面排查。
  • F. pkg/mq 死代码:读 backend/pkg/mq/mq.goMQDriver = Streams 分支是否被引用。若无引用,删除 backend/pkg/mq/streams/ 子包 + mq.go 中的 streams init 分支;asynq/adapter.go:117Delete 假实现加注释 // TODO: 实际未调用asynq.DeletedTaskInfo 返回 false 即视为不存在
  • G. is_processed 复用不在本 plan 编辑settled_at 列 + isSettled/markSettled 改写归批次 1.1exhibition-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.goEventProducer 接口是否已稳定;若稳定,改 activity_service.go:217,1579mq.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 serviceDefaultPortscases map 必须不含 starbook——本计划已明确在 4.2 完成后删除 StarbookServiceURL 字段并同步 port_test 的 cases map。
  • 健康探针4.3 ↔ helm values4.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:该表非 BIGSERIALtablesToReset 移除。
  • 若 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。