topfans/docs/backend-audit-2026-07-21.md
zerosaturation d026102a91 docs: backend audit report 2026-07-21 (findings + corrections)
后端全面审查报告:微服务耦合/边界、配置、各服务正确性与安全问题分级(P0/P1/P2),
含数据库实测复核(§七 结算超发/累计时长)与多处复核更正(peripheral 密钥、social OR)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 01:11:13 +08:00

24 KiB
Raw Blame History

后端全面审查报告2026-07-21

审查范围:gatewayBFF+ 11 个 Go 微服务 + 共享 pkg/ + proto/ 契约 + docker / k8s / env 配置。 审查方法5 路并行审计(微服务耦合与数据库边界 / 网关边界与 proto 契约 / 配置与部署 / 共享 pkg 与消息 / 各服务代码正确性与安全最严重结论已逐条核实git 追踪状态、跨服务 import、事务边界、端口绑定均经实际验证。 审查基线 commitb7f8f1b724e5,分支 feat/uni


一、结论概述TL;DR

这是一个典型的「分布式单体」distributed monolith:名义上 12 个服务,实际共享同一个 PostgreSQL、同一套 migration、同一个 pkg/models,且服务之间直接 import 对方 Go 包。RPC 名存实亡。

在此结构问题之上,叠加了三类高危问题:

  1. 生产密钥已提交进 gitOSS / SMS / OpenAI / Dify
  2. 铸造扣费事务内嵌跨服务 gRPC 调用,可导致扣费成功但无藏品、连接池耗尽。
  3. 多处信任请求体里的 user_id / reporter_id,导致越权与刷量。

问题按严重度分为 P0立即处理/ P1尽快/ P2择机三级详见下文。每条均带 文件:行号 证据。

问题分布速览

维度 P0 P1 P2
安全 / 密钥 密钥泄露、身份伪造 用户枚举、JWT 全局密钥 PII 日志、错误信息泄露、JWT 不失效、周边密钥兜底
微服务耦合 / 边界 共享库、跨服务 import、共享 model 契约混合、网关直连 DB、链式调用 死代码、列语义复用
数据一致性 mint 事务嵌 RPC、双写不一致 序列未重置、material_type 无限增长
配置 / 部署 端口四层不一致、starbook 孤儿 健康探针错配、大二进制入库 命名 / 端口小不一致
消息 / 事件 消费者重启丢消息、无 DLQ、事件无版本 未接线死代码

二、P0 — 必须立即处理

P0-1 生产密钥已提交进 git已验证 git ls-files 追踪中)

文件:行 泄露内容
backend/deploy/envs/asset.env:10-11 真实阿里云 OSS AccessKey LTAI5t6QcdJHpYbCPxM8SXYE + Secret
backend/deploy/envs/user.env:21-22 同一套 key 复用为 SMS 密钥
backend/.env.example:106 完整 OpenAI sk-proj-... 密钥
backend/.env.example:124 第二个 OpenAI keyDify key app-...
docker/.env.prod:18-19 第二套 OSS key
docker/.env.local:21-22 同一套 OSS key
backend/deploy/envs/notification.env:22 推送厂商 URL注释明确要求"不要提交"

backend/deploy/envs/*.env 未被 .gitignore 覆盖(git check-ignore 返回 exit 1 = 未忽略)。

动作:立即轮换所有泄露的 OSS/SMS/OpenAI/Dify 密钥;清理 git 历史(git filter-repo);将 deploy/envs/*.env 加入 .gitignore,密钥改由 KMS / 部署时注入。

P0-2 铸造扣费DB 事务内嵌套跨服务 gRPC

backend/services/assetService/service/mint_service.go:309-318 —— s.db.Transaction(...) 内部调用 s.userClient.UpdateCrystalBalance(...)

  • 连接池耗尽RPC 期间 DB 连接被 pin 住userService 变慢会把整个 asset 连接池拖垮。
  • 跨服务无原子性 + 非幂等CreateMintOrder,同文件 209-516userService 已扣水晶但本地 commit 失败 → 用户被扣费却无藏品;客户端超时重试时水晶已扣光却报错。缺少 outbox / 幂等 token。

动作:把 RPC 移出事务;引入幂等键(如 order_id 唯一约束 + 状态机幂等判断);跨服务用 saga / outbox 保证最终一致。

P0-3 信任请求体里的身份 → 越权 / 刷量

文件:行 问题
backend/services/moderationService/provider/moderation_provider.go:35 SubmitReport 直接透传 reqReporterId 由调用方控制 → 冒用他人 id 举报、绕过去重刷量、读取他人举报详情
backend/services/assetService/provider/asset_provider.go:483 CheckAssetLike 注释"直接使用请求中的用户信息",用 req.UserId → 点赞隐私预言机
backend/services/assetService/service/share_service.go:135-243 GetAssetQrcodereq.SharerUserId 不校验调用方 → 分享归因伪造 + 无限 OSS 写入

根因5 个服务里 4 个信任 Dubbo attachment 里的 user_id 而不再校验(castlove_config_provider.go:35 明写"本服务信任 ctx 透传的 user_id"),只有 userService 真正验签,防御纵深断裂。

动作:所有 provider 统一从 attachment 提取身份并覆盖 req 内的身份字段;对内部 RPC 建立可信边界mTLS / 内部网络 + 签名)。

P0-4 服务间直接 import 对方 Go 包(已验证)

文件:行 耦合
backend/services/taskService/main.go:27-28 import assetService/repository + service
backend/services/assetService/main.go:32 import starbookService/repository
backend/services/starbookService/main.go:20 反向 import assetService/repository(循环依赖)
backend/gateway/router/router.go:19-20 import assetService/repository + service
backend/gateway/controller/asset_controller.go:40 import assetService/service
backend/gateway/controller/moderation_controller.go:11 import moderationService/service

后果RPC 名存实亡——改一个服务的内部包,其它服务/网关必须一起重编重部署;一处 panic 连带拖垮调用方。

动作:跨边界只能走 proto/RPC + DTO共享的错误码、常量下沉到 pkg/ 或 proto禁止 import 兄弟服务的 repository/service

P0-5 全部服务共用同一 Postgres + 同一套 migration + 同一 pkg/models

  • 10 个服务 dbName="top-fans"/"topfans",全用 pkg/database 的全局 *gorm.DB无数据所有权边界
  • 跨域直接读写:
    • backend/services/moderationService/repository/moderation_repository.go:268-302 直接改 users/assets
    • backend/services/socialService/repository/social_repository.go:382,476,492,521 读写 fan_profiles
    • backend/services/assetService/repository/share_repository.go:43ranking_repository.go:103+ join users/fan_profiles
    • backend/services/galleryService/repository/gallery_repository.go:609,621 join fan_profiles
  • users/fan_profiles 被 5+ 服务读写;asset_registry.display_status 被 gallery 和 asset 两个服务并发写(gallery_repository.go:395,426,450 vs asset_repository.go:127,145,160)。
  • 单一 backend/migrations/22 个文件)承载所有域;pkg/models22 个 model被 7 个服务 import → 改一个字段要重编 7 个服务。

动作(中长期):按域拆库或至少拆 schema + 明确表 owner跨服务读改为 RPC + service-local DTO加 lint 禁止在非 owner 服务里对 models.X{} 做 GORM 操作。

P0-6 服务地址端口四层不一致

backend/gateway/config/config.go:162-165 的默认值与各服务实际绑定端口不符:

服务 网关默认 实际绑定
gallery 20004 20001galleryService/main.go:36
activity 20005 20004
starbook 20007 20005

另:galleryService/main.go:44 taskService 默认写成 20002实为 20006userService/main.go:283 硬编码 WithPort(20000) 使 -port/PORT 成为死代码。生产靠 env 覆盖侥幸能连,本地跑默认值会连错服务。

动作:统一端口默认值来源;加一个配置测试断言"网关 Dubbo URL 默认端口 == 对应服务绑定端口"。

P0-7 starbookService 孤儿代码却仍在部署链路

  • go.mod、不在 go.work(第 1-16 行)。
  • 却仍被引用:assetService import见 P0-4docker-compose.local.yml:231-262 / docker-compose.prod.yml:365-401 / helm starbookservice/deployment.yaml / dev.sh:24,432,438 / 网关 config.go:165
  • docker/Dockerfile.services:50go build 它 → 干净环境 CI/helm 构建会失败。
  • 目录里躺着 79MB 已编译二进制;asset_registry 表无明确 owner。

动作:二选一——补 go.mod 并加入 go.work 正式化,或彻底删除目录 + 所有部署引用 + 把其代码并入 assetService。


三、P1 — 尽快处理

代码正确性 / 安全

文件:行 问题
backend/services/userService/service/auth_service.go:161 bcryptcost≈100ms在事务内执行:566 改密同样 → 注册高峰连接池耗尽
backend/services/userService/service/auth_service.go:300 vs 315 Login 用户枚举("用户不存在"与"密码错"错误码不同),且 Login 无限流
backend/services/aiChatService/provider/ai_chat_provider.go:250 保存上下文用请求里的 personaID 而非解析后的 persona.IDpersona 关联错乱
backend/services/aiChatService/provider/ai_chat_provider.go:138,141 Redis 出错静默吞掉(记忆/历史丢失无日志)
backend/services/aiChatService/main.go:190-211 Dify/LLM provider 启动时二选一,无 fallback / 熔断 / 重试Dify 挂了即硬故障,且 provider:188 原样回传 err.Error()
backend/services/assetService/service/asset_like_service.go:267 material_typeCONCAT 无限追加逗号,永不收敛
backend/services/socialService/service/friend_service.go:685 CheckFriendship 硬编码 starID=0TODO 未做)→ 结果错误 + 好友关系隐私预言机
backend/services/socialService/repository/social_repository.go:461 "随机用户"实为 OFFSET rand + LIMIT 取连续段,非随机且可预测
backend/services/assetService/service/mint_service.go:323 保底概率用 time.Now().UnixNano()%100,并发同纳秒结果相同,可被脚本操纵
backend/services/assetService/service/mint_service.go:349 mockTxHash 输入全可观测,可被预测伪造"已上链"
backend/services/assetService/service/peripheral_service.go:217-319 doMint 限流 TOCTOUcount 检查与 insert 不在同一事务,并发可绕过
backend/services/socialService/repository/social_repository.go:648,672 OR 子句括号优先级问题 复核后降级/更正:903,923 是 AND 拼接无此问题;:648,672 经 GORM v1.31.1 实测——GORM 自动给裸 OR 加外层括号(clause/where.go buildExprs),当前版本不触发已删除资产漏出,属假阳性。已做防御性显式分组以消除对 GORM 内部行为的隐式依赖
backend/services/galleryService/service/cleanup_worker.go:137-185 【财务·已实证资损】展出收益结算非幂等 → 重复发放(详见 §七.1

微服务耦合 / 边界

文件:行 问题
backend/proto/user.proto:393-553 UserSocialService 把公开 user API 与内部 RPCUpdateCrystalBalance/UpdateAssetsCount/AddExhibitionHours)混在一个契约
backend/proto/social.proto:367-513 social 把好友 + 资产点赞 + 用户发现混在一起
(对照正确范式)backend/proto/task.proto:276-327 TaskMobileService(公开)与 TaskInternalService(内部)干净拆分
backend/gateway/controller/user_controller.go:647-748 网关 DeleteAccount 自开事务直接改 users+fan_profiles,绕过 userService
backend/gateway/repository/laser_card_repository.go / app_download_repository.go 网关持有 DB repo非纯 BFF
backend/gateway/controller/asset_controller.go:414-436 铸造后本地写激光卡与 assetService 无事务串联 → 双写不一致,实例状态卡在 mining
backend/gateway/controller/auth_controller.go:69-97 等 6 处 Register/Login/Me/Profile 链式调 GetFanIdentities(每次拉全量明星目录只为查一个),下游失败但用户已创建,无补偿

消息 / JWT / 共享库

文件:行 问题
backend/pkg/jwt/jwt.go:18 包级可变全局 jwtSecret,默认弱值 "your-secret-key-change-in-production",无锁无校验;SetSecretParseToken 并发 data race
backend/pkg/mq/streams/adapter.go:177 consumer 名带 UnixNano,重启后新消费者不认领旧 PEL → 消费者重启即永久丢消息
backend/pkg/mq/streams/adapter.go:212-234 失败事件留在 PELXAUTOCLAIM / DLQ / 重试
backend/pkg/statistic/client.go:53-70 TrackEvent fire-and-forget失败只 log 不重试 → 统计数据静默漂移
backend/proto/event.proto:12-20 事件无 envelope / version 字段,被 7 服务 import字段改动即破坏性
producer/consumer 多处 队列名("gallery""default")硬编码在生产者和消费者两侧,改名易漂移丢消息

配置 / 部署

文件:行 问题
backend/scripts/create_gallery_test_users.go 硬编码 id 但未 setval 重置序列,违反 CLAUDE.md 强制规则 → 跑完必报 duplicate key
k8s/helm/topfans/templates/notificationservice/deployment.yaml:50,58 探针用 /healthz 但服务注册 /health → 存活探针永远失败
k8s/helm/topfans/templates/moderationservice/deployment.yaml:50 探针打 /K8s 缺 compose 的 HEALTHCHECK NONE 覆盖
顶层大二进制入库 backend/assetService(80MB) / gateway-fixed(98MB) / test(71MB) / cleanup-orphan-avatars(22MB) 均被 git 追踪
backend/.env.env.example 漂移 WS_AI_CHAT_PATH 未文档化;.example 有一批 .env 缺失的必需键

四、P2 — 择机清理

  • backend/gateway/controller/asset_controller.go:160-194 parseRPCError 靠字符串解析错误码,应用 status.FromError
  • 多处 c.JSON(500, gin.H{"message": err.Error()}) 原样返回内部错误(auth_controller.go:222user_controller.go:139 等)。
  • backend/services/userService/service/user_service.go:681 ResetPassword 不失效已签发 JWTTODO 挂起)。
  • backend/services/userService/service/user_service.go:1023 UpdateAvatar 不校验头像 URL 同源。
  • PII手机号 / 聊天内容 / JWT打进 INFO 日志(ai_chat_provider.go:107mint_service.go / user_service.go 多处)。
  • backend/pkg/mq/ 大量未接线死代码:EventProducer/EventConsumer 接口、Stream* 常量 0 引用;asynq/adapter.go:111 GetInfo/Delete 是假实现。
  • backend/services/galleryService/mq/consumer.go:207 is_processed 列被复用为 settled(三处写入语义冲突,可能导致结算漏单)。
  • backend/services/activityService/service/activity_service.go:217,1579 直接用 redisClient.PublishRedis Pub/Sub绕过 broker 抽象。
  • backend/pkg/peripheral/sign.go:33-44 周边 HMAC 密钥优先级为 SECRET_KEY > JWT_SECRET > 硬编码开发默认值。正常配置下(backend/.env:75docker/.env.prod:66 均有 SECRET_KEY)取 SECRET_KEY,与 JWT 无耦合。残留隐患backend/deploy/envs/asset.env + common.envsystemd 部署路径)既无 SECRET_KEY 也无 JWT_SECRET,若走该路径会回落到硬编码 "default-dev-secret-change-me"(可预测密钥)。建议:去掉 JWT_SECRET 兜底与 dev 默认值,SECRET_KEY 缺失时直接 fail-fast。

    更正说明:初版报告曾将此列为 P1「轮换 JWT 会连带作废周边验证码」,经复核 SECRET_KEY 为首选且生产已配置,该结论不成立,已降级并修正。


五、建议修复顺序

  1. 轮换全部泄露密钥 + 清 git 历史 + 补 .gitignoreP0-1
  2. mint 事务拆掉内嵌 RPC + 加幂等 tokenP0-2
  3. 所有 provider 从 attachment 取身份,禁用 req 里的 user_id/reporter_idP0-3
  4. 决断 starbookService:正式化或彻底删除P0-7
  5. 端口默认值对齐 + 加配置断言测试P0-6
  6. bcrypt/序列/personaID/CheckFriendship 等 P1 正确性 bug
  7. 中长期:拆库或用 DTO 替换 pkg/models 跨服务共享;proto/event.proto 加版本 envelopeMQ 补 DLQ/重试(或删掉未用的 streams adapter二选一拆分 UserSocialService 为公开/内部两个契约。

六、附录:审查维度与方法

维度 主要工具
微服务耦合 & DB 边界 code-review-graphimports_of/importers_of+ grep 验证
网关边界 & proto 契约 通读 controller/service/router + proto
配置 & 部署 git ls-files / git check-ignore + env/compose/helm 对比
共享 pkg & 消息 importers_of 爆炸半径 + mq adapter 通读
各服务正确性 & 安全 service/repository/provider 热点通读

本报告仅识别问题,不含完整修复方案。针对任意一条可另起工程化设计文档(遵循 CLAUDE.md 的 MVP 先行与文档结构规范)。


七、数据库实测复核2026-07-21本地 top-fans 全新库)

对照实际数据库(localhost:15432 / 容器 postgresql-database-1 / 库 top-fans)复核关键 finding结论如下

检查项 结果 对应 finding
单库结构 实证 P0-5单库单实例public schema 一把装 89 张表users/assets/fan_profiles/asset_registry/moderation_/reports/activities/exhibitions/ai_/peripheral_*/tasks 全在一起) P0-5
跨域外键 ⚠️ 实证 P0-5 DB 层耦合:public49 条 FK 跨域绑定(fan_profiles→usersassets→users/starsmint_orders→users/assets/starsasset_likes→assets/users/starsbooth_slots→fan_profiles…),拆库前必须全部拆除 P0-5
序列健康 当前 last_value < max(id) 的序列;assets_id_seq=88100008=max(id)asset_registry_id_seq=308=max(id)。序列隐患为潜在(仅跑缺 setval 的脚本才触发),现网未坏 P1create_gallery_test_users.go
material_type 增长 ⚠️ 234 行资产中 134 行含逗号,但最多 3 个值 hot,new,potentialcomma_count=2未观测到失控增长。代码 CONCAT 追加 bug 存在但尚未爆 P1asset_like_service.go:267
is_processed 复用 ⚠️ 实证 P2exhibitions 无独立 settled45845 行中 is_processed=false40847 行,无法区分"未清理"还是"未结算收益"——语义冲突有真实数据佐证 P2consumer.go:207
statistic schema 更正statistic 事件隔离在独立 statistic schema按天分区 events_YYYY_MM_DD + metric 视图),并非"全落在 public"。statisticService 有部分 schema 隔离 更正子审计说法

说明:"全新库"含实测数据users 94 / fan_profiles 96 / assets 234 / asset_registry 239 / mint_orders 281 / exhibitions 45845 / reports 4 / stars 6非空库。exhibitions 4.5 万行相对 234 资产偏高,结合 is_processed 语义冲突,建议排查是否存在结算/清理积压。

七.1 【重大发现·P1 财务】展出收益结算非幂等,已实测超发并被领取

对 exhibitions 的 is_processed 深挖时,从数据里挖出一个已实现资损的 bug。

数据证据(当前 top-fans 库)

  • exhibition_revenue_records51742 条,但只对应 46241 个不同 exhibition_id4053 个展品被重复结算(最多 3 次)。
  • 多余重复记录 5501 条,超发水晶累计 2,525,254
  • 这些重复记录 status 全部为 claimed9554 条已领取) → 超发水晶已被用户领走,是已实现资损(本数据集内)。
  • exhibition_revenue_recordsexhibition_id 唯一约束,无任何幂等键阻止重复写入。

根因backend/services/galleryService/service/cleanup_worker.go + taskService/service/revenue_service.go

  1. exhibition_revenue_records(exhibition_id, cycle_start_time) 唯一约束CreateRevenueRecordrevenue_service.go:476)无条件插入。
  2. 多个结算入口对同一展品-周期重复触发cleanup_worker 的 ZSET 路径(cleanup_worker.go:137、DB 兜底路径(cleanup_worker.go:239、MQ 消费路径三者可对同一 exhibition 重复调用 OnExhibitionCompleted
  3. 数据实证4053 个重复展品中 3850 个是"同周期重复"(同一 cycle_start_time 被结算 ≥2 次),确认是同一周期的重复结算而非合法的多周期结算。

更正(较早版本的错误归因):本节初稿曾把重复归因为"like_bet_revenue_records 仅 50 条 → 点赞押注失败 → is_processed 永不置 true → worker 重跑"。经数据核实此链条不成立live+expired+unprocessed 积压=0活跃展品都已正确标记 processedlike_bet 仅 50 条是因为测试数据点赞极稀疏45952 展品仅 164 个有赞、仅 9 个有 ≥2 赞),而点赞押注模型只奖励"后续还有点赞"的押注者,故绝大多数展品合法产出 0 条记录、RecordLikeBetRevenue 返回 OK 而非报错。重复的真正根因是上述"多路径 + 无唯一约束"。

修复方向

  • exhibition_revenue_recordsUNIQUE(exhibition_id, cycle_start_time) 幂等约束,写入用 INSERT ... ON CONFLICT DO NOTHING
  • 收敛结算入口到单一带幂等的路径(保留 MQ 消费路径,删除 cleanup_worker 的 ZSET/DB 双直连 RPC或让三者共用同一 isAlreadyProcessed 判重);
  • 用独立 settled_at 审计列替代复用 is_processed(原 §四 P2 项);
  • 存量:按 (exhibition_id, cycle_start_time) 去重,回收已发放的重复水晶(本库超发 2,525,254 且已 claimed需评估回滚口径
  • 附带发现exhibition_revenue_records.created_at 单位混乱——51849 条中 6013 条是10 位、45836 条是毫秒13 位)。同列混用两种时间单位,任何按 created_at 排序/区间过滤/展示的逻辑都会错乱。应统一为毫秒并回填历史数据。

关于 like_bet_revenue_records 仅 50 条:非 bug。测试数据点赞极稀疏45952 展品仅 164 有赞、仅 9 有 ≥2 赞),点赞押注模型只对"后续仍有点赞"的押注者发放,故绝大多数展品合法产出 0 条。

备注worker 查询带 deleted_at IS NULL,故 40740 行"已软删+未处理"不会被再次结算(当前 live+expired+unprocessed 积压=0重复来自展品在软删之前被 worker 多轮处理。

七.2 展品累计时长(user_exhibition_hours / 资产 season_exhibition_hours

数据层面无明显错误user_exhibition_hours 1075 行无负值、无溢出max 1727h≈72 天)、无重复 (user_id,star_id) 行,唯一约束 uk_exhibition_user_star 健康。

但累计逻辑存在幂等缺口,且数据来源(结算流)已含重复(见 §七.1),故累计值不可信

  1. backend/services/userService/repository/fan_profile_repository.go:535-558AddExhibitionHourssourceID 参数却未用于幂等判断,无条件 total_exhibition_hours += hours
  2. backend/services/taskService/service/revenue_service.go:513:资产级 assetLevelService.AddExhibitionHours(assetId, hours) 完全不传 sourceID
  3. revenue_service.go:436actualHours=(expireAt-startTime)/3600000 基于展品固定时间戳 → 同一展品重复结算每次加相同小时数(非 0
  4. 两条结算路径并存galleryService/service/cleanup_worker.go:137,239 仍调用已标注"本期内废弃"的直连 RPC OnExhibitionCompleted(收益+时长均无去重);galleryService/mq/consumer.go:133 走带 isAlreadyProcessed 去重的 MQ 路径。仅 MQ 路径有幂等,直连路径没有。
  5. 已证 5501 次重复结算 → 经直连路径的部分会把累计时长多加。

后果:累计时长驱动等级 CalculateLevelFromExhibitionHours → 时长虚高触发提前升级 + 多发升级奖励水晶,与 §七.1 收益重复同根因(结算非幂等 + 双路径并存)。

修复方向:统一到单一带幂等的结算路径(删除 cleanup_worker 的直连 RPC 调用,或给直连路径补 isAlreadyProcessedAddExhibitionHours 真正实现 sourceID 幂等(加 exhibition_hours_log(source_id UNIQUE) 或复用 crystal_transaction_records 的 source_id 判重);资产级累加补 sourceID存量数据需按 distinct exhibition 重算校正等级与已发奖励。