Skip to content

11 · 代码地图与事实索引(Vistack) ​

定位:被问到「这段在哪、怎么实现的」时能 10 秒定位;也是自己复习的目录。 用法:Ctrl+F 搜模块名或文件名。所有路径相对仓库根 /Users/binhy/Binhy-Projects/Vistack;行号是写作时的快照,重构后可能偏移,请以符号名为准。 纪律:本文件只写代码里能验证的事实。需要确认的地方标「需确认」。


目录树速览 ​

Vistack
├── cmd/vistack/main.go            # 唯一入口:解析角色 → 初始化日志/配置 → 分发到 internal/role
├── conf/                          # app.toml(最全)/ app.local.toml(本地覆盖)/ app.docker.toml(容器)
├── internal/
│   ├── role/                      # 5 个角色的启动引导:api.go / worker.go / transcoder.go / auth.go / migrate.go
│   ├── api/v1/                    # HTTP 处理器(Gin handler):Video/File/social/danmaku/comment/sensitive_word/health/web/authclient
│   ├── routers/                   # 路由注册
│   │   ├── router.go              # RegisterRoutes:公开组 / 鉴权+限流组
│   │   ├── health.go              # /ping /health
│   │   └── api/v1/                # 每个领域的路由文件:video.go/file.go/social.go/danmaku.go/comment.go/enter.go
│   ├── core/                      # 基础设施封装
│   │   ├── db.go / redis.go / minio.go / kafka.go / snowflake.go / cache.go
│   │   ├── logger.go / server.go / vipper.go / validate.go
│   │   ├── cache/                 # 通用 Cache-Aside 组件 + Redis 布隆过滤器
│   │   ├── leader/                # etcd 领导选举(单例任务)
│   │   └── message_queue/         # Kafka 消费者:transcode/(worker+retry+watchdog)、video/(删除)、danmaku/、comment/
│   ├── middlewares/               # auth.go / cors.go / requestid.go / ratelimit.go
│   │   └── ratelimit/             # limiter.go(接口)、token_bucket.go(单机)、sliding_window.go(Redis Lua)
│   ├── interaction/               # 点赞/收藏/播放计数 + 榜单 + 异步落库(interaction.go / flusher.go / leaderboard.go / keys.go)
│   ├── danmaku/                   # 弹幕(danmaku.go / sensitive.go AC 自动机 / local_cache.go LRU / keys.go)
│   ├── comment/                   # 评论(comment.go / counter.go / moderation.go / attachment.go / keys.go)
│   ├── auth/                      # auth 服务:handler.go(HTTP)+ service.go(gRPC)+ pb/auth/v1
│   ├── authclient/                # api 侧调用 auth 的用户查询客户端
│   ├── transcoder/                # 转码服务:service.go(流程)/ ffmpeg.go(命令与档位)/ server.go / client.go / registry/ / pb/
│   ├── discovery/etcd.go          # 自研 gRPC resolver(scheme=etcd)
│   ├── config/                    # AppConfig 结构 + 默认路径常量
│   ├── consts/Kafka.go            # 4 个 topic 名
│   ├── global/                    # 全局 AppConfig 实例
│   └── model/entity/              # GORM 实体:video/ file/ user/ social/ danmaku/ tag/ audit/
├── pkg/                           # 可复用库:auth/(JWT+JWKS)hashutil/ snowflake/ storage/ timeutil/ response/(未使用)
├── proto/                         # buf 管理的契约:auth/v1/auth.proto、transcoder/v1/transcoder.proto
├── migrations/migrate.go          # AutoMigrate + 索引 + 默认角色
├── db/init.sql                    # 历史 SQL 脚本(未挂载、与实体已漂移,见「已知缺口」)
├── deploy/
│   ├── k8s/                       # api.yaml / worker.yaml / transcoder.yaml / etcd.yaml / configmap.yaml
│   ├── traefik/                   # traefik.yml(静态)/ dynamic.yml(路由分流)
│   └── cdn-publish.sh             # 前端发 CDN(pages / s3 / dry)
├── docs/                          # specs/(10 个子系统 spec + distributed-architecture.md)、review/、img/
├── web/                           # pnpm monorepo
│   ├── ui/src/api/axios.ts        # 共享 axios 客户端(baseURL=VITE_API_BASE,超时 15s)
│   ├── web-client/src/            # 用户端(Vue3):views/Creator(上传)、views/VideoPlayer(播放)、components/player-dash
│   └── web-admin/src/             # 管理端(仅登录 + 违禁词管理)
├── compose.yml / compose-build.yml / compose.server.yml
├── Dockerfile                     # 三阶段,两个运行 target:vistack / vistack-transcoder
└── build/                         # 前端构建产物输出目录(vite outDir:build/web-client、build/web-admin、build/cdn)

web/ui/dist、node_modules、.cache(gomod 缓存)不是源码目录,复习时忽略。


模块 → 文件 → 关键函数 ​

1. 入口与角色引导 ​

模块文件关键符号一句话职责面试常问点
入口cmd/vistack/main.gomain、resolveRole角色解析(VISTACK_ROLE → 首个位置参数 → api),依次初始化 Viper、zap、配置校验,再 switch 分发 5 个角色为什么单二进制?角色怎么选?compose/k8s 实际用的是位置参数
角色internal/role/api.goRunAPI、defaultJWKSURLDB/MinIO/Redis/Cache/Snowflake/JWKS 验签器/auth user client/Kafka producer → 装配三个异步服务 → 注册路由与静态托管 → 30s 优雅停机为什么 api 不消费 Kafka?JWKS 为什么要自动刷新?
角色internal/role/worker.goRunWorker、runSingletonJobs、instanceID4 个消费者 + etcd 领导选举下发单例任务;停机时 WaitKafkaConsumers(30s)dispatcher/watchdog 为什么必须单例?etcd 不可用怎么降级?
角色internal/role/transcoder.goRunTranscoder只 InitMinioClient,启动 gRPC 服务;不连 DB/Redis/Kafka无状态的好处?参数从哪来?
角色internal/role/auth.goRunAuth、loadOrGeneratePrivateKey、advertiseAuthAddr、localAuthIPgRPC(:50052) + etcd 注册 + HTTP(:8081) + JWKS;私钥来自环境变量/文件,缺失则生成临时 2048 位密钥并告警私钥怎么治理?开发环境临时密钥的风险?
角色internal/role/migrate.goRunMigrate跑一次 migrations.AutoMigrate 后退出,避免多副本 api 竞争迁移为什么迁移要独立角色?幂等靠什么?

2. 路由与中间件 ​

模块文件关键符号一句话职责面试常问点
路由internal/routers/router.goRegisterRoutes/(health)、/api/v1 公开组(video 播放、social、danmaku 读、comment 读)、/api/v1 鉴权组(挂 AuthMiddleware + RateLimit,含 file/video 写/social 写/danmaku 写/comment 写/敏感词)哪些接口需要登录?限流挂在哪一层?
路由internal/routers/api/v1/{video,file,social,danmaku,comment}.goInitXxxPublicRouter / InitXxxPrivatesRouter按领域把 handler 注册到公开组/鉴权组能背出 /videos/upload/* 与播放相关的路径
路由internal/routers/health.goHealthRouter.InitHealthRouter/ping、/health与 k8s 探针的关系(没接)
中间件internal/middlewares/auth.goAuthMiddleware取 Authorization → 兼容 Bearer → TokenValidator.ValidateToken → claims 写入 gin 上下文为什么注入接口而不是直接调 verifier?
中间件internal/middlewares/ratelimit.goBuildLimiter、RateLimit、SetLogger按配置构造限流器(token_bucket / sliding_window),按 userID 限流,Redis 故障 fail-open,写标准限流头为什么按用户 ID 而不是 IP?fail-open 的取舍
中间件internal/middlewares/ratelimit/{limiter,token_bucket,sliding_window}.goLimiter/Result、NewTokenBucket/Allow、NewSlidingWindow/Allow两种算法的实现;滑动窗口是一段 Lua 脚本Lua 里为什么用 uuid 当 member?
中间件internal/middlewares/{cors,requestid}.goCORS、RequestID白名单 CORS;每请求注入 X-Request-IDAllowCredentials 与 * 的处理

3. HTTP 处理器(internal/api/v1) ​

模块文件关键符号一句话职责面试常问点
上传Video.goInitVideoUpload秒传查重 → 续传会话 → NewMultipartUpload,返回 uploadId/objectKey秒传事务里建了几张表?ref_count 怎么加
上传Video.goGetUploadPartURL、ListUploadedParts用公网 Core client 签 PUT(1h);列已传分片(maxParts 10000)为什么用 Presign 而不是 PresignedPutObject
上传Video.goCompleteVideoUpload合并分片 + 事务建 4 张表 + 投 Kafka(失败则标 failed 并入重试队列)一致性边界在哪?失败如何兜底
视频Video.goDeleteVideo软删 + 失效缓存 + 投 delete_file(不动 ref_count,交给 worker)为什么 API 不扣引用计数
视频Video.goGetVideoInfo、GetSelfVideoPage、PutVideoInfo、GetVideoRecommend详情(带布隆的读缓存)、创作中心分页、改元信息(删缓存)、推荐(固定 TTL)缓存 key 是什么?写路径怎么失效
播放Video.goGetVideoMdp、GetVideoSegmentsSignatureMPD 代理下发(max-age=3600);STS 临时凭证 + 受限 policy + Redis 缓存 25minSTS 为什么用 root 凭证(已知问题);policy 怎么限资源
文件File.goAvatarUpload、CoverUpload、CommentImageUpload5MB 限制 + 前缀分类 + 建 files 记录只校验大小的风险
互动social.goLikeVideo/FavoriteVideo/PlayVideo/GetVideoStats/GetVideoInteraction/GetHotVideos/enrichCountstoggle 点赞收藏、播放上报、三计数读取(Redis→DB 回退)、ZSet 榜单计数以谁为准?榜单怎么过滤已删视频
弹幕danmaku.go、sensitive_word.goSendDanmaku/GetDanmaku、List/Add/DeleteSensitiveWord按时间区间拉取、发送(鉴权+限流)、敏感词 CRUDCache-Control 为什么是 5s;cache_control_max_age 配置其实没被用
评论comment.goListComments/ListReplies/CreateComment/ToggleLike/DeleteComment/CommentCount/buildItems/toItem游标分页 + 首屏内嵌 2 条回复 + 批量补作者与附件 URL游标分页为什么不用 offset;N+1 在哪
缓存video_cache.gogetOrLoad/deleteCache/addVideoBloom/BuildVideoBloom/videoInfoCacheKey/recommendCacheTTL封装 core.Cache,core.Cache == nil 时直接回源兜底关掉缓存时行为是否一致
静态web.goRegisterWebStatic、spaFileSystem.Open/ 与 /admin/ 托管构建产物,SPA 回退,/api/ 保持 404SPA 回退为什么排除 /api/
健康health.goPing、HealthCheck探测 DB(1s)/MinIO(2s)/Redis(1s),恒返回 200为什么不能当 readiness 探针
作者authclient.goSetUserClient、resolveAuthor、resolveAuthors、toAuthor经 gRPC 批量查作者信息(去重后一次 RPC)为什么不让 api 直接 join 用户表

4. 基础设施(internal/core) ​

模块文件关键符号一句话职责面试常问点
配置vipper.goViper、GetConfigPath加载 toml,VISTACK_ 前缀自动映射,WatchConfig 只打日志-c / VISTACK_CONFIG_PATH 优先级
配置校验validate.goValidateConfig空实现(_ = cfg)诚实承认:配置校验如今是空壳
日志logger.goInitLogger、SyncLoggerzap Production + ISO8601 时间 + level 可配结构化日志字段有哪些
服务器server.goNewServergin.New + ginzap 访问日志 + RecoveryWithZap + RequestID + CORS中间件顺序
DBdb.goInitDBpostgres DSN 或字段拼装;连接池与 Ping连接池三参数
Redisredis.goInitRedishost 为空则跳过;池大小与超时未配 Redis 时的降级行为
MinIOminio.goInitMinioClient、GetPublicBaseURL、GetInternalBaseURL、GetMinioObjectPublicURL三个客户端(内网/Core/公网 Core),建桶、写 policy、配生命周期为什么要区分内网与公网 endpoint
Kafkakafka.goInitKafka、SendKafkaMessage、EnsureTopic、StartKafkaConsumer、runConsumer、WaitKafkaConsumers同步 Producer;建 topic(1 分区 1 副本);同组多 reader;手动提交并发上限怎么算;commit 时机
主键snowflake.goInitSnowflake、deriveNodeIDnode_id 显式优先,否则 FNV32a%1024 派生碰撞风险(已知)
缓存装配cache.goInitCache默认值兜底 + 构造 cache.Cache 与全局布隆关掉缓存会发生什么
缓存组件cache/cache.goNew、GetOrLoad、Delete、loadAndCache、acquireLock、releaseLock、randomTTL、waitAndRead空值缓存 + singleflight + Redis 互斥锁 + 随机 TTL锁的释放为什么用 Lua;等锁失败为什么直接回源
布隆cache/bloom.goNewBloom、positions、Build、Add、ExistsRedis bitmap + FNV-1a 双哈希;:ready 未就绪时降级为「可能存在」假阳性/删除的影响
选举leader/leader.goNew、Run、waitRetry、DefaultLeaderKeyetcd 会话 + Election;失去领导权时 cancel leadCtx 并重选无主窗口多大;为什么优于 Redis 锁
转码消费message_queue/transcode/worker.goStartTranscodeWorker、handleTranscodeMessage、markFailed、persistTranscodeResult幂等 → lease → gRPC(25min) → 事务落库三层时间常数为什么逐级放大
重试message_queue/transcode/retry.goscheduleDelay、AddTranscodeRetry、StartTranscodeRetryDispatcher指数退避写 ZSet;每 5s 取到期最多 100 条回投为什么用 ZSet 而不是延迟 topic
看门狗message_queue/transcode/watchdog.goStartTranscodeWatchdog每分钟扫 processing>15min(且无 lease)与 pending>10minpending 分支为什么要先碰 updated_at
删除message_queue/video/delete_video_worker.goStartVideoDeleteWorker、handleVideoDeleteMessage、processFileTX1 引用计数 −1 并标 deleting → TX2 二次校验真实引用数并删行 → 提交后删 MinIO 对象与 DASH 前缀两段事务为什么分开;重投幂等性(缺陷)
弹幕落库message_queue/danmaku/worker.goStartDanmakuWorkerOnConflict DoNothing 按主键幂等落库幂等键是什么
审核message_queue/comment/worker.goSetModerator、StartCommentModerationWorker、handleModerationMessage消费 comment_moderation 并调审核器审核器目前是桩实现

5. 领域服务 ​

模块文件关键符号一句话职责面试常问点
互动interaction/interaction.goNewService、ToggleLike、ToggleFavorite、RecordPlay、Counts、IsLiked、IsFavorited三段 Lua 脚本把「状态+计数+榜单+事件」做成原子操作为什么用 Set 而不是计数器去重
互动interaction/flusher.gopopEvents、applyEvents、syncCounts、syncOneCount、FlushPending、StartFlusher批量弹出事件 → 净效果落库 → Redis 计数回写 videos净效果去重解决了什么;丢批风险
互动interaction/leaderboard.goHotZRevRange 取 top N榜单无时间窗(已知)
弹幕danmaku/danmaku.goNewService、Send、Fetch、LoadSensitiveWords、AddSensitiveWord、DeleteSensitiveWord敏感词 → ZSet 实时 → Kafka 异步;读取三级缓存为什么发完立刻可见(先写 Redis)
弹幕danmaku/sensitive.goSensitiveFilter、Reload、buildFail、ContainsAC 自动机,atomic.Pointer 原子替换根节点支持热更新为什么能无锁并发读
弹幕danmaku/local_cache.goNewLocalCache、Get、Set进程内 LRU + TTL(懒过期)为什么 TTL 只有 2 秒
评论comment/comment.goNewService、Create、List、queryRoots、ListReplies、CommentCount、Delete、cacheList楼中楼推导(root/parent/reply_to)+ 事务插入 + 计数 + 审核投递;首屏列表缓存 60s三级指针分别是什么;游标分页实现
评论comment/counter.goToggleLike、IsLiked、LikeCount、StartFlusher、flushLikes评论点赞 Lua toggle + 异步落库与视频点赞的差异
评论comment/moderation.goModerator、PassthroughModerator、EnqueueModeration、ProcessModeration、ApplyModerationResult可插拔审核;状态机 pending→visible/hidden,拒绝时回滚计数为什么是幂等的(状态守卫)
评论comment/attachment.goParseAttachments、marshalAttachments、validateAttachments附件 JSON 编解码 + 上限 9 + ref_type 校验为什么要校验 ref_type
认证pkg/auth/token_manager.goNewTokenManager、GenerateToken、ValidateToken、PublicJWKS、parseRSAPrivateKeyRS256 签发 + 私钥验签 + 输出 JWKS(支持 PKCS1/8)kid/issuer 从哪里来
认证pkg/auth/verifier.goNewTokenVerifier、StartAutoRefresh、ValidateToken、refreshKeys、getKeyJWKS 公钥缓存 + kid 未命中强制刷新 + 每小时定时刷新为什么消费方不持私钥
认证pkg/auth/claim.goClaims、NewClaims、GetClaim、GetUserIDclaims 结构(只有 user_id)与上下文读取没有角色位导致无法做授权
认证internal/auth/handler.goNewHandler、RegisterRoutes、Register、Login、GetUserInfo、UpdateProfileDirect、UpdateUserPassword、JWKS注册/登录/资料/改密/JWKS 端点昵称唯一性靠 Count(竞态);登录不校验 banned
认证internal/auth/service.goNewService、GetUserInfos、toProtoUserInfogRPC 批量返回用户公开信息(昵称/头像/角色)头像 URL 怎么拼
认证internal/authclient/client.goNewUserClient、GetUserInfos、Closeapi 侧客户端(etcd 发现或静态地址)与 transcoder client 的对称性
杂项pkg/hashutil/bcrypt.goHashPassword、CheckPasswordHashbcrypt 封装默认 cost
杂项pkg/timeutil/rand.goRandomRangeExpire[min,max) 随机时长,用于防雪崩为什么随机 TTL 有效

6. 转码服务与发现(internal/transcoder、internal/discovery) ​

模块文件关键符号一句话职责面试常问点
服务transcoder/service.goService、ProcessVideo下载 → 探时长 → 抽帧 → DASH 转码 → 全量上传 → 返回元数据临时目录怎么清理;封面失败是否致命
ffmpegtranscoder/ffmpeg.goGetVideoResolution、GetVideoDuration、ExtractVideoFrame、SelectAdaptiveQualities、filterQualities、ResolveQualities、TranscodeToDASH档位表与选择策略、单条 ffmpeg 命令生成为什么单条命令多档;GOP 与分片对齐
服务端transcoder/server.goRunServer、advertiseAddr、localIPgRPC 监听 + etcd 注册 + GracefulStopPOD_IP 为什么必须注入
客户端transcoder/client.goNewClient、ProcessVideo、Closeetcd 发现(etcd:///prefix + round_robin)或静态地址两种模式的切换条件
注册transcoder/registry/etcd.goRegister、keepAlive、Close租约 TTL 10s + 3s KeepAliveOnce,失败重 Grant+Put为什么建议换 KeepAlive 通道
发现discovery/etcd.goNewEtcdBuilder、Scheme、Build、watch、update、ResolveNow、Close实现 resolver.Builder,Watch 前缀变化刷新地址列表为什么不直接用 k8s DNS

7. 数据模型与迁移 ​

模块文件关键符号一句话职责面试常问点
迁移migrations/migrate.goAutoMigrate、initDefaultRoles20 个模型按依赖顺序迁移;补列长度、两个索引、attachments 回填;种 4 个默认角色为什么 resolution 要手工 ALTER
实体model/entity/video/{video,transcode,manifest}.goVideo/VideoSource/VideoTranscode/VideoManifest + 状态枚举视频主表/源/转码任务/播放清单状态机怎么流转
实体model/entity/file/file.goFile、PublicURL、FileStatus、FileRefType物理文件的统一登记表 + 引用计数ref_count 语义(易错)
实体model/entity/user/*.goUser/UserProfile/Role/Authority/RoleAuthority/UserAuthorityRBAC 数据模型(有表无执行点)诚实承认未做授权判定
实体model/entity/social/*.goVideoComment、CommentLike、VideoLike、VideoFavorite、VideoPlayLog评论与互动明细复合主键的作用
实体model/entity/danmaku/*.goDanmaku、SensitiveWord弹幕与敏感词time_offset 无索引(已知)
实体model/entity/tag/tag.go、audit/audit.goTag、VideoTag、AuditLog标签(未接 API)、审计日志(迁移被注释,未建表)audit 是死代码

8. 前端(web/) ​

模块文件关键符号一句话职责面试常问点
上传web-client/src/views/Creator/index.vuecalculateHash、startUpload、chunkSize、concurrency2MB 分块算 MD5 → init → 列分片 → 6 并发签名直传 → complete8MB/6 并发/2MB 三个数各自的理由
上传 APIweb-client/src/views/Creator/api/api.tsinitVideoUpload、getUploadPartUrl、listUploadedParts、completeVideoUpload、getMyVideos、deleteVideo4 个上传接口 + 列表/删除接口路径与后端绑定字段要对得上
播放web-client/src/views/VideoPlayer/index.vueloadSignature、scheduleRefresh、loadDanmaku、reportPlay拉 STS 与 MPD、提前 2 分钟刷新、拉弹幕、上报播放为什么播放上报是公开接口
播放器web-client/src/components/player-dash/useDashPlayer.tssetupPlayer、updateStsConfig、buildSegmentUrl、setQuality、addRequestInterceptordash.js 初始化 + 请求拦截签名 + 清晰度控制拦截器只改 .m4s;无错误处理
签名web-client/src/lib/s3-signer.tssignS3Request、getSignatureKey、buildCanonicalQuery手写 AWS SigV4(js-sha256)UNSIGNED-PAYLOAD 的含义
弹幕渲染web-client/src/components/player-dash/danmaku/useDanmaku.ts、DanmakuBar.vueuseDanmaku、canvas 渲染参数弹幕引擎(滚动/固定/队列上限 400)为什么用 canvas 不用 DOM
HTTPweb/ui/src/api/axios.tsbaseURL、get/post/put/del、token 拦截器统一客户端(15s 超时、Bearer、401 清 token)VITE_API_BASE 缺省 /api

三条核心链路逐跳追踪 ​

链路 ① 上传(浏览器 → api → MinIO → Kafka) ​

  1. 用户选文件 → onFileChange(Creator/index.vue:52)重置 fileHash。
  2. calculateHash(index.vue:83-119):FileReader.readAsArrayBuffer 按 2MB 逐块喂 SparkMD5.ArrayBuffer,spark.end() 得整文件 MD5;进度映射到 0–10%。
  3. initVideoUpload → POST /api/v1/videos/upload/init → VideoApi.InitVideoUpload(Video.go:81):
    • 3a. 秒传:files WHERE hash=? AND status='active'(Video.go:100)命中 → 事务内 ref_count+1、建 Video(processing)、建 VideoSource(复用同一 file_id)、建 VideoTranscode(pending)(Video.go:102-149)→ addVideoBloom(:152)→ SendKafkaMessage(transcode)(:161)→ 返回 {uploaded:true, video_id},前端直接结束(index.vue:152-159)。
    • 3b. 续传:Redis upload_session:<userID>:<fileHash> 命中则原样回放(Video.go:184-193)。
    • 3c. 新建:objectKey = raw/<uuid><ext>(:196-198)→ core.MinioCore.NewMultipartUpload(:202)→ 会话写 Redis 24h(:220)→ 返回 {upload_id, object_key, bucket}。
  4. listUploadedParts → GET /videos/upload/parts → ListObjectParts(maxParts=10000)(Video.go:298)→ 前端构造 uploadedMap,算出缺失的 partsToUpload(index.vue:161-183)。
  5. 每个分片(最多 6 个 worker 并行,index.vue:192-250):
    • getUploadPartUrl → GET /videos/upload/sign → MinioCorePublic.Presign("PUT", bucket, objectKey, 1h, {uploadId, partNumber})(Video.go:264);
    • fetch(url, {method:'PUT', body: chunk}) 直传 MinIO(index.vue:215-218),ETag 取自响应头(:225);
    • 失败 → 把 partNumber 塞回队列、setTimeout 2000ms 重试(无上限,:237-244)。
  6. completeVideoUpload → POST /videos/upload/complete → CompleteVideoUpload(Video.go:329):
    1. CompleteMultipartUpload 合并对象(:343);
    2. 开事务:Video(processing) → File(video_source, ref_count=1, hash, mime=video/mp4) → VideoSource → VideoTranscode(pending)(:351-408)→ Commit;
    3. addVideoBloom(:411);
    4. SendKafkaMessage("transcode", key=videoID)(:423)——失败则标 failed + AddTranscodeRetry(attempt=1) + 返回 500(:426-434);
    5. 删除 Redis 上传会话(:440)→ 返回 video_id。
  7. 前端展示 已提交转码,视频 ID: xxx(index.vue:699);没有转码状态轮询,只有重新拉列表才更新状态。

涉及文件:函数:views/Creator/index.vue:calculateHash/startUpload、views/Creator/api/api.ts:*、api/v1/Video.go:InitVideoUpload/GetUploadPartURL/ListUploadedParts/CompleteVideoUpload、core/minio.go:InitMinioClient、core/kafka.go:SendKafkaMessage、pkg/storage/minio.go:UploadFile(仅小文件)。

链路 ② 转码(Kafka → worker → transcoder → 事务落库) ​

  1. worker 启动 StartTranscodeWorker → core.StartKafkaConsumer("transcode", handleTranscodeMessage)(mq/transcode/worker.go:37-39),实例内起 4 个同组 reader(受 1 分区限制实际并发 1)。
  2. handleTranscodeMessage(:41)解 JSON 得 {video_id, transcode_id, object_key, attempt}。
  3. 幂等 ①:First(&VideoTranscode, transcodeID),status == completed 直接 return(:49-55)。
  4. 幂等 ②:Redis.SetNX("lease:transcode:<id>", "1", 30min),抢不到就跳过;defer Del(:57-62)。
  5. 置 processing(:64)。
  6. 组装 ProcessVideoRequest:bucket、object_key、output_prefix=dash/<videoID>、cover_object_key=covers/<videoID>.jpg、cover_time_seconds=0(:72-78)。
  7. context.WithTimeout(25min)(:80)→ transcoderClient.ProcessVideo(:83);gRPC 经自研 resolver + round_robin 打到某个 transcoder 实例。
  8. transcoder Service.ProcessVideo(transcoder/service.go:25):
    • os.MkdirTemp 建临时目录(:30,defer 清理);
    • Minio.FGetObject 下载原片为 input.mp4(:37);
    • GetVideoDuration(:42,ffprobe format=duration);
    • 抽帧时间:请求值优先,否则 >10s→5s,>2s→duration/2,其余 1.0s(:51-59);
    • ExtractVideoFrame + FPutObject 上传封面(:65-78,失败只告警);
    • ResolveQualities(0,0,req.QualityHeights) → TranscodeToDASH(:87-88,单条 ffmpeg 命令多档输出到本地 output/);
    • filepath.Walk 遍历产物,按扩展名设 Content-Type(.mpd→application/dash+xml、.m4s→video/iso.segment)逐个上传(:98-136);
    • 返回 duration/manifest(key,size)/cover(key,size)/profiles(:146-153)。
  9. 失败路径:markFailed(:92)→ 标 failed + INCR attempts:transcode:<id>(TTL 24h)+ cnt<=7 就 AddTranscodeRetry(:94-102),handler 返回 nil(不重投 Kafka)。
  10. 成功路径:persistTranscodeResult(:106)单事务:
    • 建 manifest File(video_manifest, application/dash+xml)(:110-121);
    • 更新 VideoTranscode:completed、manifest_file_id、resolution(逗号拼接)、codec="h264,aac"(:130-140);
    • 建 VideoManifest(protocol=dash, profiles=JSONB, status=ready)(:143-153);
    • 有封面则建 cover File(:155-170);
    • 更新 Video:status=published、duration、cover_file_id(:172-184);Commit(:186)。
  11. Redis.Del("attempts:transcode:<id>")(:190)。
  12. 兜底链:StartTranscodeRetryDispatcher 每 5s 从 transcode:retry:zset 取到期 ≤100 条回投 Kafka,成功才 ZRem(retry.go:46-81);StartTranscodeWatchdog 每 1 分钟扫 processing 超 15min(且 lease 已不存在)与 pending 超 10min 的任务重新入队,attempts>7 则放弃(watchdog.go:14-72)。两者都由 etcd 领导选举保证单例(role/worker.go:79-122)。

涉及文件:函数:core/kafka.go:StartKafkaConsumer/runConsumer、mq/transcode/{worker,retry,watchdog}.go、core/leader/leader.go:Run、transcoder/{client,server,service,ffmpeg}.go、transcoder/registry/etcd.go:Register、discovery/etcd.go:Build。

链路 ③ 播放(MPD 代理 → STS → dash.js 拦截签名 → MinIO 直连) ​

  1. 进入播放页 onMounted 并发发起 6 个请求(VideoPlayer/index.vue:223-232):loadSignature、loadVideoInfo、loadRelated、loadStats、loadInteraction、reportPlay。
  2. loadSignature → GET /api/v1/videos/:id/segments/signature → GetVideoSegmentsSignature(Video.go:772):
    • 查视频存在(:780-784);
    • Redis video:sts:<id> 命中则直接用(:786-793);
    • 未命中:构 policy 只允许 s3:GetObject 于 arn:aws:s3:::<bucket>/dash/<id>/*(:798-807)→ credentials.NewSTSAssumeRole(内网 endpoint, {root AK/SK, policy})(:811-828)→ expiration = now+30min(:837)→ 写 Redis 25min(:846-853);
    • 返回 {base_url: "<publicBase>/<bucket>/dash/<id>/", credentials:{accessKey,secretKey,sessionToken,expiration}}(:865-870)。
  3. <DashPlayer> 只在 manifestUrl && segmentsBaseUrl && segmentsCredentials 三者都有时渲染(index.vue:264),manifest 地址是 axios baseURL 拼接的绝对 URL(:37-40),并由 dash.js 直接 fetch(不走 axios 拦截器)。
  4. GET /api/v1/videos/:id/manifest.mpd → GetVideoMdp(Video.go:721):查 VideoManifest(video_id, protocol=dash, status=ready, Preload File)(:737)→ Minio.GetObject(:748)→ 设 Content-Type: application/dash+xml + Cache-Control: public, max-age=3600(:757-759)→ DataFromReader 流式返回(:768)。
  5. dash.js 初始化:MediaPlayer().create() → updateSettings(abr.autoSwitchBitrate.video=true) → addRequestInterceptor(...)(useDashPlayer.ts:67-81)→ initialize(video, url, autoplay)(:97)。
  6. 每个分片请求进入拦截器(useDashPlayer.ts:69-81):
    • buildSegmentUrl:只对 以 .m4s 结尾 的请求改写成 base_url + filename(:48-61);
    • signS3Request(finalUrl,'GET','us-east-1',creds,new Date()) 生成 Authorization / x-amz-date / x-amz-content-sha256(+x-amz-security-token)(s3-signer.ts:68-111);
    • 写回 req.url 与 req.headers,浏览器直连 MinIO(生产经 Traefik /vistack → minio:9000)。
  7. streamInitialized 事件:从 getRepresentationsByType('video') 生成清晰度列表(height/bandwidth),QualitySelector 切档走 setQuality(关掉 autoSwitchBitrate + setRepresentationForTypeByIndex)(useDashPlayer.ts:82-96,103-113)。
  8. 凭证续期:scheduleRefresh(expiration, id) 在过期前 2 分钟重新调 signature 接口(时间无效则 10s 后重试)(index.vue:206-221);segmentsCredentials 变化触发 updateStsConfig,拦截器用最新凭证(useDashPlayer.ts:175-187)。
  9. 旁路请求:弹幕 GET /videos/:id/danmaku?start=0&end=<max(60,duration)>(index.vue:109-117)、评论与点赞各自独立接口,与播放链路解耦。

涉及文件:函数:VideoPlayer/index.vue:loadSignature/scheduleRefresh/loadVideoInfo、VideoPlayer/index.ts:getVideoSegmentsSignature、player-dash/useDashPlayer.ts:setupPlayer/updateStsConfig/buildSegmentUrl/setQuality、lib/s3-signer.ts:signS3Request、api/v1/Video.go:GetVideoSegmentsSignature/GetVideoMdp、core/minio.go:GetPublicBaseURL/GetInternalBaseURL、deploy/traefik/dynamic.yml(media-router)。


数据库表结构 ​

真实 schema 由 migrations/migrate.go:AutoMigrate 生成(20 个模型);db/init.sql 是历史脚本、已与实体漂移(见「已知缺口」)。audit_logs 实体存在但迁移被注释掉,表不会创建。

表实体关键字段用途 / 注意点
rolesuser.Roleid PK、name unique、description默认种 4 个:superadmin/admin/user/vipuser(initDefaultRoles)
usersuser.Userid PK、username unique、email、password_hash、role_id、status(active/banned)、created_atpassword_hash 带 json:"-";登录不校验 banned(已知)
user_profilesuser.UserProfileid PK、user_id、nickname(unique)、avatar_file_id昵称唯一靠 unique 约束;注册时另用 Count 预检(有竞态)
authoritiesuser.Authorityid PK、resource_method、resource_uriRBAC 权限定义(无执行点)
role_authorityuser.RoleAuthorityid PK、role_id、authority_id角色-权限关联
user_authorityuser.UserAuthorityid PK、user_id、authority_id、grand_status、remark列名拼写是 grand_status(历史遗留)
filesfile.Fileid PK、bucket、object_key、status(active/deleting/deleted)、ref_type、mime_type、hash(index)、size、ref_count、created_at/updated_at物理文件登记 + 引用计数;hash 是秒传依据;ref_type 六种(avatar/video_source/transcode_artifact/video_manifest/video_cover/comment_image)
videosvideo.Videoid PK、user_id、title、description、cover_file_id、duration、status(uploaded/processing/published/failed/deleted)、visibility(public/private/unlisted)、like_count/favorite_count/play_count 冗余列、created_at/updated_at状态机主表;三计数由 Redis 权威异步回写
video_sourcesvideo.VideoSourceid PK、video_id、file_id、uploaded_at原始视频与 files 的关联(一个视频可多源)
video_transcodesvideo.VideoTranscodeid PK、video_id、status(pending/processing/completed/failed)、resolution(100)、codec、manifest_file_id、created_at/updated_at转码任务;idx_transcode_status_update_at(status,updated_at,video_id,id) 支撑 watchdog
video_manifestvideo.VideoManifestid PK、video_id、protocol(dash/hls)、file_id、profiles(jsonb)、status(ready/failed)、created_at/updated_at播放清单;表名单数,与 video_sources/video_transcodes 不一致
tags / video_tagstag.Tag / tag.VideoTagtags(id, name unique);video_tags(video_id+tag_id 复合主键)标签体系(目前没有任何 API 使用)
video_commentssocial.VideoCommentid PK、video_id、user_id、root_id、parent_id、reply_to_id、reply_to_uid、content、attachments(jsonb)、status(visible/pending/hidden/deleted)、like_count、reply_count、created_at/updated_at、deleted_at楼中楼三级指针;idx_comments_video_root(video_id,root_id,id)
comment_likessocial.CommentLikecomment_id+user_id 复合主键、created_at评论点赞明细(异步落库)
video_likessocial.VideoLikevideo_id+user_id 复合主键、created_at视频点赞明细(复合主键天然去重)
video_favoritessocial.VideoFavoritevideo_id+user_id 复合主键、created_at视频收藏明细
video_play_logssocial.VideoPlayLogid PK、video_id、user_id、played_at、ip_address(inet)、user_agent播放流水;当前只写 id/video_id/played_at,user/ip/ua 留空
danmakusdanmaku.Danmakuid PK、video_id(index)、user_id、content、time_offset(float,秒)、color、mode(0滚动/1顶部/2底部)、created_attime_offset 没有索引;Kafka 消费按主键幂等
sensitive_wordsdanmaku.SensitiveWordid PK、word unique、created_atAC 自动机词表来源;增删触发全表重载

配置项速查 ​

加载顺序:-c <path> > 环境变量 VISTACK_CONFIG_PATH > 默认 conf/app.local.toml(core/vipper.go:56-73、config/config.go:139)。容器部署用 VISTACK_CONFIG_PATH=/app/conf/app.docker.toml。

配置段关键键含义 / 默认值
[server]host port mode web_dir admin_web_dirHTTP 监听(0.0.0.0:8080)、gin 模式、静态托管目录(docker 下为 /app/web、/app/web-admin)
[logging]leveldebug/info/warn/error(core/logger.go:14-26)。注意三份 conf 里都没有这个段,所以实际生效是 info
[database]host/port/user/password/name 或 dsn;max_idle_conns=10、max_open_conns=100、conn_max_lifetime=3600DSN 优先,否则按字段拼装(core/db.go:17-60)
[redis]host port db password pool_size=10host 为空则跳过初始化
[minio]endpoint public_endpoint access_key secret_key secure bucket内网 endpoint 用于服务端读写,public_endpoint 用于浏览器直连与预签名
[auth]kid=vistack-rs256 issuer=vistack jwt_expiration=3600 jwks_path=/.well-known/jwks.json签发方参数(auth 角色用)
[auth_service]http_addr=:8081 grpc_addr=:50052 jwks_urlapi 侧拉 JWKS 与调 gRPC 用;缺省时 defaultJWKSURL 兜底成 http://127.0.0.1:8081/...
[cache]enabled default_ttl_min=300 default_ttl_max=600 null_ttl=60 lock_ttl=5 lock_wait_ms=2000 recommend_ttl=300 bloom_enabled bloom_bits=10000000 bloom_hashes=7缓存三件套参数;enabled=false 时 core.Cache=nil,读路径直接回源
[ratelimit]enabled algorithm=sliding_window token_rate=10 token_burst=20 window=60 limit=100登录后接口限流
[social]enabled flush_interval=5 flush_batch=200 leaderboard_size=50点赞/收藏/播放计数
[danmaku]enabled local_cache_size=1024 local_cache_ttl=2 cache_control_max_age=5弹幕(注意 cache_control_max_age 目前未被使用,handler 硬编码 5s)
[comment]enabled flush_interval=5 flush_batch=200评论点赞异步落库
[snowflake]node_id=00 表示自动派生(app.local.toml 里仍是 1,本地单实例可接受)
[kafka]brokers group_id=vistack-consumer-group concurrency=4每实例并发 reader 数(代码兜底为 1;实际受分区数限制)
[cors]enable allow_origins allow_methods allow_headers allow_credentials白名单式 CORS(middlewares/cors.go)
[etcd]endpoints prefix=/vistack/transcoders leader_ttl=10服务注册 + 领导选举
[transcoder]listen_addr=:50051 addr=localhost:50051 use_etcd=trueworker 发现模式与静态兜底地址

环境变量

变量作用
VISTACK_CONFIG_PATH指定配置文件(容器内 /app/conf/app.docker.toml)
VISTACK_<SECTION>_<KEY>Viper AutomaticEnv + .→_:如 VISTACK_DATABASE_HOST、VISTACK_MINIO_BUCKET(core/vipper.go:23-25)
VISTACK_AUTH_RSA_PRIVATE_KEY内联 PEM 私钥(\n 会被还原)
VISTACK_AUTH_RSA_PRIVATE_KEY_FILEPEM 私钥文件路径
POD_IPk8s 注入,用于 etcd 注册地址与 snowflake node_id 派生
-c <path> / 首个位置参数命令行指定配置路径 / 指定角色

⚠️ 两个需要「需确认」的细节:① Viper 的 AutomaticEnv 配合 Unmarshal 时,配置文件里不存在的键不一定能被环境变量覆盖(Viper 已知行为),要用前最好实测;② web/*/vite.config.ts 里打印的是 VITE_API_URL,但客户端实际读的是 VITE_API_BASE(web/ui/src/api/axios.ts:8),不要被日志误导。


已知缺口与可诚实承认的点 ​

每条给一句「怎么答」。主动说缺口比被问出来强,但要说得具体、有改进方向、不贬低整体设计。

#缺口(代码事实)怎么答
1直播完全没实现:README 和弹幕 spec 都写了 live777(Rust SFU)/OBS 推流/WebRTC,但全仓库 grep -rni 'live777|webrtc|rtmp|whip' 在代码里零命中,没有推流密钥校验「直播是我文档里规划的下一阶段,代码还没落地,目前交付的是完整的点播链路。我不想把没做的说成做了。」
2无 Prometheus / OpenTelemetry:只有 zap 日志,没有 metrics、没有 trace、没有日志聚合「排障时最痛苦就是看不到 Kafka 积压和转码成功率。这是我们自己 review 里排在 P1 的项(docs/specs/distributed-architecture.md:73-76)。」
3/health 恒返回 200,只报告依赖状态不做判定,且不分 readiness/liveness;k8s 清单里也没有探针「它现在只能用来人肉看状态,不能当探针——因为失败也返回 200。正确拆法是 /healthz 永远 200 + /readyz 依赖不通返回 503。」
4无 DLQ:重试 7 次后 markFailed/watchdog 直接 continue,静默丢弃且无告警「这是明确的缺口。理想做法是超限消息进死信 topic,配告警和可重放工具。」
5无 Outbox:CompleteVideoUpload 是「事务提交后再发 Kafka」,中间进程被杀会丢消息「现在靠 watchdog 扫 pending 超 10 分钟兜底,用户最多等 10 分钟;严格场景应该上 outbox 表。」
6transcoder gRPC 无鉴权:grpc.NewServer() 无任何 interceptor,内网任意服务都能发起转码「内网信任模型的简化,改进就是 mTLS(我们 roadmap 里已列),至少也要加一个 token interceptor。」
7etcd 单节点:compose 一个容器、k8s replicas: 1 且没有挂 volume(--data-dir=/etcd-data 是临时存储)「单节点只够开发;生产要 3~5 节点 + TLS。k8s 清单里 etcd 还缺 PVC,重启会丢注册数据。」
8Kafka 1 分区 1 副本:EnsureTopic 写死 NumPartitions:1, ReplicationFactor:1,导致 concurrency=4 实际只跑得起 1 个消费者「这是我最想先改的一处:topic 提到 8~16 分区、副本因子 ≥2,消费并发才真正生效。」
9无压测、无 QPS/延迟数据:README 与 specs 里没有任何 benchmark 数字「我没有压测数据,所以不会给任何 QPS 结论。要说性能的话,我只能讲清架构上的并发点在哪(分片直传绕开 api、DASH 分片走对象存储)。」
10测试覆盖很低:6 个测试文件、26 个 Test(cache / interaction / danmaku / ratelimit / web),没有集成测试、没有 CI 覆盖率门禁「核心算法层(缓存、限流、AC 自动机、互动计数)有单测,链路级没有。要补的话优先补上传→转码的状态机测试。」
11snowflake node_id 用 FNV32a % 1024 派生,实例多时理论上会碰撞;pkg/snowflake 懒加载兜底还是 Init(1)「1024 个槽位对当前规模够用,但派生哈希不保证唯一。规模化应该改成 etcd CAS 分配。」
12没有 AI 相关代码:全仓库没有任何 LLM/推理/向量相关实现「没有就是没有。项目里跟内容相关的智能处理只有 AC 自动机关键词过滤,那不算 AI。」
13RBAC 只有表没有执行点:roles/authorities/role_authority/user_authority 都建了、注册时分默认角色,但代码里没有任何授权判定「README 说的细粒度权限目前是目标。JWT 里也没有角色位,所以 api 侧现在只能做「登录与否」的判定。」
14/admin/sensitive-words 没有角色校验:只挂了 AuthMiddleware + 限流「任意登录用户都能改敏感词表,根因同上(RBAC 没执行)。这是我会立刻补的一处。」
15k8s 清单不完整:没有 auth 的 Deployment/Service,没有探针、没有 resource limits、没有 HPA/Ingress/Secret,凭证明文写在 ConfigMap,且 ConfigMap 里还留着已废弃的 jwt_secret = "secret"「deploy/k8s 是脚手架级示例,不是生产可用清单。auth 那块是拆分后没同步补上的。」
16k8s 期望的 conf/app.k8s.toml 不存在(ConfigMap 现场生成,且缺 [auth_service] 段)「这会导致 api 在集群里退化去连 127.0.0.1:8081 拉 JWKS,实际跑不通——是清单与拆分改动脱节的典型例子。」
17评论敏感词不生效:管理端加词只重建 danmaku 的自动机,评论服务的 filter 启动加载一次后再不刷新,审核 worker 里那个 service 词表还是空的「这是真 bug,不是设计取舍。修法很简单:加词后同时通知两个 filter,或者把词表放到 Redis/pubsub 统一刷新。」
18图片审核是桩实现:PassthroughModerator.Review 永远返回 true,SetModerator 全仓库无调用点「带图评论会 pending → Kafka → 立即自动通过。留了 Moderator 接口就是为接第三方内容安全,但还没接。」
19Redis 队列弹出即丢:LPopCount 后若写库失败,这批事件已经没了(无回塞、无 DLQ)「『幂等可重试』只对重放同一批成立,对丢批不成立。改用 LMOVE 到 processing 列表或直接换 Kafka 就解决了。」
20删除 worker 重投不幂等:没有已处理标记、不检查 video.status,TX2 里 3 条 Delete 的错误也没检查;ref_count - 1 无下限钳制「TX1 提交后若 TX2/MinIO 失败,消息重投会把引用计数再减一遍。要加状态守卫(status=deleted 直接返回)和 GREATEST(ref_count-1,0)。」
21大量 Redis key 没有 TTL:点赞/收藏/播放集合、两个榜单 ZSet、待落库 List、vistack:danmaku:* 都不会过期「榜单没有时间窗、弹幕 ZSet 会随视频长度无限增长。需要加滑动窗口或定期归档裁剪。」
22前端上传的重试无上限、无进度事件(fetch 无 Content-MD5/无 abort)、uploadId 不持久化(刷新即丢)、没有转码状态轮询「体验层面的欠账:刷新页面就要重传,用户也看不到转码进度。改进点是 localStorage 存 uploadId + 轮询或 SSE 状态。」
23pkg/response 整包未使用(无任何 import);storage.UploadLocalFile、snowflake.GenStringID、core.GetMinioObjectPublicURL、auth.NewClaims、comment.SetModerator、UiDashPlayer 也都无调用点「这些是重构后的死代码,应该删掉。被问到我会直接承认没做清理。」
24db/init.sql 不可用:没被任何 compose 服务挂载,且不是合法 PostgreSQL(MySQL 风格 COMMENT、缺分号、FK 指向 authority 而表叫 authorities),也没有索引 DDL,内容已落后于实体「真实 schema 来自 GORM AutoMigrate,init.sql 是历史遗留,我该删掉或重写。」
25compose-build.yml + Dockerfile.build 已失效:编译的是仓库根本不存在的 ./main.go,且用 Go 1.25 镜像而模块要求 1.26「这是最早期的交叉编译脚本,入口改名后没同步更新。我会删掉它,交叉编译改用 Makefile 或 goreleaser。」
26core.ValidateConfig 是空实现(_ = cfg);配置校验只剩 auth 角色启动时的私钥检查「backend-hardening spec 里的 H1 是针对旧的 HS256 secret;换成 RS256 后这段校验被搬走了,函数留下成了空壳,应该删或补上真实校验。」
27上传只校验大小不校验类型:File.go 三个上传口和视频上传都不看 MIME/魔数,SVG 可能造成存储型 XSS「已知问题,修法是 http.DetectContentType + 白名单 jpeg/png/webp,并在响应里加 Content-Disposition。」
28Traefik 无 TLS、dashboard insecure: true、没有定义任何 middleware「compose 里的 Traefik 面向本地开发;生产应由外部网关或加 443 entrypoint + 证书。dashboard 的 insecure 也要关掉。」
29无版本化迁移与回滚:全部 AutoMigrate,只有 3 条手写 DDL(migrations/migrate.go:66-82)「AutoMigrate 不删列、不改类型,也没有 down 脚本。生产应该换 golang-migrate 之类带版本号的方案。」
30Kafka 消息是裸 JSON、无版本字段「字段演进靠兼容不删改,跨版本灰度发布期可能解析失败。加 "v":1 或上 Schema Registry 都能解决。」
31internal/model/entity/audit 与 tag 是孤儿:audit_logs 迁移被注释(表不会建),tags/video_tags 建了表但没有任何 API 使用「设计时留的扩展位,实际没接。被问到我会说是未完成而不是已完成。」
32compose.yml 里 VISTACK_ROLE 从未设置,全靠位置参数;另外 .env.example 里的 JWT_SECRET 是 HS256 时代的遗留键「角色选择是双通道(环境变量或位置参数),清单用的是位置参数,功能正常但文档和实现有点脱节;.env.example 的 JWT_SECRET 现在没有任何代码读它,应该删。」

一句话自查清单(面试前扫一眼) ​

  • 能立刻说出:5 个角色 / 4 个 topic / 3 条链路 / 2 个 proto。
  • 能立刻画出:浏览器 ↔ api ↔ (Kafka, Redis, MinIO) ↔ worker ↔ (etcd) ↔ transcoder。
  • 能立刻报数:8MB 分片、6 并发、2MB 哈希块、4s DASH 分片、120 帧 GOP、25min gRPC 超时、30min lease、15/10min watchdog、7 次重试、8h 退避上限、10s 租约 / 3s 保活、300–600s 缓存、60s 空值、5s 锁、100 次/60 秒限流、50 榜单、200/5s 事件批、30min STS。
  • 能立刻承认:直播没做、RBAC 没执行、无 metrics/DLQ/Outbox、Kafka 单分区、测试与压测都不足。

持续学习,持续构建。