后端全面审查报告:微服务耦合/边界、配置、各服务正确性与安全问题分级(P0/P1/P2), 含数据库实测复核(§七 结算超发/累计时长)与多处复核更正(peripheral 密钥、social OR)。 Co-Authored-By: Claude <noreply@anthropic.com>
24 KiB
后端全面审查报告(2026-07-21)
审查范围:
gateway(BFF)+ 11 个 Go 微服务 + 共享pkg/+proto/契约 + docker / k8s / env 配置。 审查方法:5 路并行审计(微服务耦合与数据库边界 / 网关边界与 proto 契约 / 配置与部署 / 共享 pkg 与消息 / 各服务代码正确性与安全),最严重结论已逐条核实(git 追踪状态、跨服务 import、事务边界、端口绑定均经实际验证)。 审查基线 commit:b7f8f1b724e5,分支feat/uni。
一、结论概述(TL;DR)
这是一个典型的「分布式单体」(distributed monolith):名义上 12 个服务,实际共享同一个 PostgreSQL、同一套 migration、同一个 pkg/models,且服务之间直接 import 对方 Go 包。RPC 名存实亡。
在此结构问题之上,叠加了三类高危问题:
- 生产密钥已提交进 git(OSS / SMS / OpenAI / Dify)。
- 铸造扣费事务内嵌跨服务 gRPC 调用,可导致扣费成功但无藏品、连接池耗尽。
- 多处信任请求体里的
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 key;Dify 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-516):userService 已扣水晶但本地 commit 失败 → 用户被扣费却无藏品;客户端超时重试时水晶已扣光却报错。缺少 outbox / 幂等 token。
动作:把 RPC 移出事务;引入幂等键(如 order_id 唯一约束 + 状态机幂等判断);跨服务用 saga / outbox 保证最终一致。
P0-3 信任请求体里的身份 → 越权 / 刷量
| 文件:行 | 问题 |
|---|---|
backend/services/moderationService/provider/moderation_provider.go:35 |
SubmitReport 直接透传 req,ReporterId 由调用方控制 → 冒用他人 id 举报、绕过去重刷量、读取他人举报详情 |
backend/services/assetService/provider/asset_provider.go:483 |
CheckAssetLike 注释"直接使用请求中的用户信息",用 req.UserId → 点赞隐私预言机 |
backend/services/assetService/service/share_service.go:135-243 |
GetAssetQrcode 用 req.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/assetsbackend/services/socialService/repository/social_repository.go:382,476,492,521读写fan_profilesbackend/services/assetService/repository/share_repository.go:43、ranking_repository.go:103+joinusers/fan_profilesbackend/services/galleryService/repository/gallery_repository.go:609,621joinfan_profiles
users/fan_profiles被 5+ 服务读写;asset_registry.display_status被 gallery 和 asset 两个服务并发写(gallery_repository.go:395,426,450vsasset_repository.go:127,145,160)。- 单一
backend/migrations/(22 个文件)承载所有域;pkg/models(22 个 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 | 20001(galleryService/main.go:36) |
| activity | 20005 | 20004 |
| starbook | 20007 | 20005 |
另:galleryService/main.go:44 taskService 默认写成 20002(实为 20006);userService/main.go:283 硬编码 WithPort(20000) 使 -port/PORT 成为死代码。生产靠 env 覆盖侥幸能连,本地跑默认值会连错服务。
动作:统一端口默认值来源;加一个配置测试断言"网关 Dubbo URL 默认端口 == 对应服务绑定端口"。
P0-7 starbookService 孤儿代码却仍在部署链路
- 无
go.mod、不在go.work(第 1-16 行)。 - 却仍被引用:
assetServiceimport(见 P0-4)、docker-compose.local.yml:231-262/docker-compose.prod.yml:365-401/ helmstarbookservice/deployment.yaml/dev.sh:24,432,438/ 网关config.go:165。 docker/Dockerfile.services:50会go build它 → 干净环境 CI/helm 构建会失败。- 目录里躺着 79MB 已编译二进制;
asset_registry表无明确 owner。
动作:二选一——补 go.mod 并加入 go.work 正式化,或彻底删除目录 + 所有部署引用 + 把其代码并入 assetService。
三、P1 — 尽快处理
代码正确性 / 安全
| 文件:行 | 问题 |
|---|---|
backend/services/userService/service/auth_service.go:161 |
bcrypt(cost≈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.ID,persona 关联错乱 |
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_type 用 CONCAT 无限追加逗号,永不收敛 |
backend/services/socialService/service/friend_service.go:685 |
CheckFriendship 硬编码 starID=0(TODO 未做)→ 结果错误 + 好友关系隐私预言机 |
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 限流 TOCTOU:count 检查与 insert 不在同一事务,并发可绕过 |
backend/services/socialService/repository/social_repository.go:648,672 |
: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 与内部 RPC(UpdateCrystalBalance/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",无锁无校验;SetSecret 与 ParseToken 并发 data race |
backend/pkg/mq/streams/adapter.go:177 |
consumer 名带 UnixNano,重启后新消费者不认领旧 PEL → 消费者重启即永久丢消息 |
backend/pkg/mq/streams/adapter.go:212-234 |
失败事件留在 PEL,无 XAUTOCLAIM / 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-194parseRPCError靠字符串解析错误码,应用status.FromError。- 多处
c.JSON(500, gin.H{"message": err.Error()})原样返回内部错误(auth_controller.go:222、user_controller.go:139等)。 backend/services/userService/service/user_service.go:681ResetPassword不失效已签发 JWT(TODO 挂起)。backend/services/userService/service/user_service.go:1023UpdateAvatar不校验头像 URL 同源。- PII(手机号 / 聊天内容 / JWT)打进 INFO 日志(
ai_chat_provider.go:107、mint_service.go/user_service.go多处)。 backend/pkg/mq/大量未接线死代码:EventProducer/EventConsumer接口、Stream*常量 0 引用;asynq/adapter.go:111GetInfo/Delete是假实现。backend/services/galleryService/mq/consumer.go:207is_processed列被复用为settled(三处写入语义冲突,可能导致结算漏单)。backend/services/activityService/service/activity_service.go:217,1579直接用redisClient.Publish(Redis Pub/Sub),绕过 broker 抽象。backend/pkg/peripheral/sign.go:33-44周边 HMAC 密钥优先级为SECRET_KEY > JWT_SECRET > 硬编码开发默认值。正常配置下(backend/.env:75、docker/.env.prod:66均有SECRET_KEY)取SECRET_KEY,与 JWT 无耦合。残留隐患:backend/deploy/envs/asset.env+common.env(systemd 部署路径)既无SECRET_KEY也无JWT_SECRET,若走该路径会回落到硬编码"default-dev-secret-change-me"(可预测密钥)。建议:去掉JWT_SECRET兜底与 dev 默认值,SECRET_KEY缺失时直接 fail-fast。更正说明:初版报告曾将此列为 P1「轮换 JWT 会连带作废周边验证码」,经复核
SECRET_KEY为首选且生产已配置,该结论不成立,已降级并修正。
五、建议修复顺序
- 轮换全部泄露密钥 + 清 git 历史 + 补
.gitignore(P0-1) - mint 事务拆掉内嵌 RPC + 加幂等 token(P0-2)
- 所有 provider 从 attachment 取身份,禁用
req里的user_id/reporter_id(P0-3) - 决断
starbookService:正式化或彻底删除(P0-7) - 端口默认值对齐 + 加配置断言测试(P0-6)
- bcrypt/序列/personaID/CheckFriendship 等 P1 正确性 bug
- 中长期:拆库或用 DTO 替换
pkg/models跨服务共享;proto/event.proto加版本 envelope;MQ 补 DLQ/重试(或删掉未用的 streams adapter,二选一);拆分UserSocialService为公开/内部两个契约。
六、附录:审查维度与方法
| 维度 | 主要工具 |
|---|---|
| 微服务耦合 & DB 边界 | code-review-graph(imports_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 层耦合:public 有 49 条 FK 跨域绑定(fan_profiles→users、assets→users/stars、mint_orders→users/assets/stars、asset_likes→assets/users/stars、booth_slots→fan_profiles…),拆库前必须全部拆除 |
P0-5 |
| 序列健康 | ✅ 当前无 last_value < max(id) 的序列;assets_id_seq=88100008=max(id)、asset_registry_id_seq=308=max(id)。序列隐患为潜在(仅跑缺 setval 的脚本才触发),现网未坏 |
P1(create_gallery_test_users.go) |
material_type 增长 |
⚠️ 234 行资产中 134 行含逗号,但最多 3 个值 hot,new,potential(comma_count=2),未观测到失控增长。代码 CONCAT 追加 bug 存在但尚未爆 |
P1(asset_like_service.go:267) |
is_processed 复用 |
⚠️ 实证 P2:exhibitions 无独立 settled 列;45845 行中 is_processed=false 有 40847 行,无法区分"未清理"还是"未结算收益"——语义冲突有真实数据佐证 |
P2(consumer.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_records共 51742 条,但只对应 46241 个不同exhibition_id→ 4053 个展品被重复结算(最多 3 次)。- 多余重复记录 5501 条,超发水晶累计 2,525,254。
- 这些重复记录
status全部为claimed(9554 条已领取) → 超发水晶已被用户领走,是已实现资损(本数据集内)。 exhibition_revenue_records表无exhibition_id唯一约束,无任何幂等键阻止重复写入。
根因(backend/services/galleryService/service/cleanup_worker.go + taskService/service/revenue_service.go):
exhibition_revenue_records表无(exhibition_id, cycle_start_time)唯一约束,CreateRevenueRecord(revenue_service.go:476)无条件插入。- 多个结算入口对同一展品-周期重复触发:cleanup_worker 的 ZSET 路径(
cleanup_worker.go:137)、DB 兜底路径(cleanup_worker.go:239)、MQ 消费路径三者可对同一 exhibition 重复调用OnExhibitionCompleted。 - 数据实证:4053 个重复展品中 3850 个是"同周期重复"(同一
cycle_start_time被结算 ≥2 次),确认是同一周期的重复结算而非合法的多周期结算。
更正(较早版本的错误归因):本节初稿曾把重复归因为"
like_bet_revenue_records仅 50 条 → 点赞押注失败 →is_processed永不置 true → worker 重跑"。经数据核实此链条不成立:live+expired+unprocessed积压=0(活跃展品都已正确标记 processed);like_bet仅 50 条是因为测试数据点赞极稀疏(45952 展品仅 164 个有赞、仅 9 个有 ≥2 赞),而点赞押注模型只奖励"后续还有点赞"的押注者,故绝大多数展品合法产出 0 条记录、RecordLikeBetRevenue返回 OK 而非报错。重复的真正根因是上述"多路径 + 无唯一约束"。
修复方向:
exhibition_revenue_records加UNIQUE(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),故累计值不可信:
backend/services/userService/repository/fan_profile_repository.go:535-558:AddExhibitionHours收sourceID参数却未用于幂等判断,无条件total_exhibition_hours += hours。backend/services/taskService/service/revenue_service.go:513:资产级assetLevelService.AddExhibitionHours(assetId, hours)完全不传 sourceID。revenue_service.go:436:actualHours=(expireAt-startTime)/3600000基于展品固定时间戳 → 同一展品重复结算每次加相同小时数(非 0)。- 两条结算路径并存:
galleryService/service/cleanup_worker.go:137,239仍调用已标注"本期内废弃"的直连 RPCOnExhibitionCompleted(收益+时长均无去重);galleryService/mq/consumer.go:133走带isAlreadyProcessed去重的 MQ 路径。仅 MQ 路径有幂等,直连路径没有。 - 已证 5501 次重复结算 → 经直连路径的部分会把累计时长多加。
后果:累计时长驱动等级 CalculateLevelFromExhibitionHours → 时长虚高触发提前升级 + 多发升级奖励水晶,与 §七.1 收益重复同根因(结算非幂等 + 双路径并存)。
修复方向:统一到单一带幂等的结算路径(删除 cleanup_worker 的直连 RPC 调用,或给直连路径补 isAlreadyProcessed);AddExhibitionHours 真正实现 sourceID 幂等(加 exhibition_hours_log(source_id UNIQUE) 或复用 crystal_transaction_records 的 source_id 判重);资产级累加补 sourceID;存量数据需按 distinct exhibition 重算校正等级与已发奖励。