DOMAIN QUESTIONS / VERTICAL ARCHITECTURE PARADIGMS

垂直领域架构范式与场景实战

基于最新工程能力要求 · 72 道覆盖金融、电商、SaaS、本地生活、物联网、内容社区、物流、广告、游戏与强合规行业的领域范式与端到端场景设计题

领域题把 S 篇技术范式装进行业骨架;框在 方法论挂网。

题目数:72 | 高级 ×34 | 架构 ×29 | 专家 ×9 | 十二大垂直领域
领域题破解之道法术 · 先读这一页

本页 72 道领域题,与 S 篇按技术域分组不同,这里按垂直行业重组:先建立领域概念与模型,再谈架构取舍。破解路径是「复述业务闭环 → 立领域模型 → 讲关键范式 → 端到端场景 + 合规兜底」:道法术劝说

道 · 先懂业务再谈架构——领域题的第一失分点不是技术,是说错业务语言:把「清结算」答成转账、把「复式记账」答成记流水。每张卡的「场景背景」都给了硬指标(TPS、成功率、时效、资损红线),先复述并量化,再指出该领域的核心不变量——支付是幂等与对账、账务是借贷平衡、库存是三态守恒、券是库存与限领。

法 · 范式复用——领域方案不是从零发明:秒杀防超卖(S01.01)、分布式事务(S02.02)、热点治理(S01.05)、长连接(S10.01)这些 S 篇技术范式是共用积木;领域题考的是把积木装进行业骨架——状态机定义、领域事件、合规红线、资损兜底。与 S 篇技术范式同构的卡片已标注「← 技术范式引用」回链到对应 S 篇技术范式,答题时显式关联可证知识迁移能力。

术 · 合规与代码落地——收尾必须补三件事:① 合规红线(资金零资损、分录不可变、监管报送时效、审计留痕)② 对账兜底(渠道不可信、缓存与 DB 不一致靠补偿)③ 代码落地(状态机转移表、Lua 原子扣减、双分录写入)——证明你能把领域模型执行到代码级,而不只会画领域模型图。

  • 通用作答技巧(适用全部题):开场先澄清业务口径(「T+1 是自然日还是工作日?可退余额含不含在途?」),展现领域严谨度;答技术前先用一句话给出该领域的第一性原理(如「支付的核心是一笔钱全程可证」);讲方案按领域模型 → 关键范式 → 端到端链路分层;收尾主动补合规与对账。切勿只背技术名词而无领域判断。

每张卡末尾的「破题点拨」用三点(一眼定位 / 破题主线 / 易错点)帮你速记该题的答法要点;「领域模型层给出聚合根、实体、值对象与不变量,是领域题的答题骨架。

一 金融 · 支付与清结算 6 题

本组知识链

支付系统的骨架是「一笔钱从发起到落账全程可证」:状态机管流程、幂等管重复、账务管余额、对账真相。覆盖支付核心、账务、清结算对账、热点账户、渠道路由与跨境。

  1. 支付核心:支付单是唯一事实源,收单/退款/关单全链路走状态机,渠道交互带幂等键。
  2. 账务:复式记账,有借必有贷借贷必相等,分录不可变、余额可重算。
  3. 清结算:日切定界 → 三方对账 → 差异挂账 → T+1 清分结算。
  4. 账户与渠道:热点扣款防超扣、渠道路由成功率成本双目标、跨境多币种。
工程化要点
  • 一切写操作带幂等键(biz_no 唯一索引),重试与重复通知不产生第二笔资金变动。
  • 资金变动必须落账务分录;Redis/DB 不一致靠对账补偿,不靠「应该没问题」。
  • 外部渠道一律不可信:回执以异步通知 + 主动查询双通道校准,超时状态主动巡查。
  • 限流降级的返回码对客一致,避免用户重复支付;监管报文与审计日志可回溯。
本组思维模式
  • 先账后款:先记分录后动渠道,任何时刻账实可对上。
  • 对账兜底:不一致是常态而非异常,日终对账是最后防线。
  • 状态机思维:支付/退款/关单都是状态迁移,拒绝散落的 if-else。
  • 资损红线优先于可用性:可疑时宁可拦截不可放行,拦截可申诉。
合规与资损红线
  • 资金安全零资损:重复支付率、超扣率、错发结算款均为零容忍指标。
  • 分录 append-only:历史凭证不可改,修正只能红冲;审计日志不可删。
  • 日切对账 T+1 完成,长短款当日定位;监管报送按时效提交。
  • 敏感字段(卡号、证件号)加密存储,展示脱敏;跨境收付符合外汇申报要求。
D01.01
支付核心系统:收单、退款、关单全链路与幂等设计架构P0
高频 领域模型 架构 状态机 幂等 渠道路由
场景背景

某聚合支付平台日交易 800 万笔,峰值 3000 TPS,对接微信、支付宝、银联、数字人民币四类渠道。硬指标:支付成功率 ≥ 99.95%、重复支付率 0、资金损失 0、支付单全程状态可追溯(监管要求)。业务形态含担保交易(确认收货后结算给商家)与即时到账两类。技术栈:MySQL 8.0 分库分表 + Redis 7 + RocketMQ 5.x。S02.02 讲分布式事务原理、S07.06 讲支付链路一致性——本题考支付域本身的模型与全链路。

核心挑战

① 渠道超时与状态未知:请求已扣款但响应超时,本地状态停在「支付中」,后续轮询与通知谁为准;② 重复支付:用户重复提交、前端重试、渠道重复通知,三路来源都要幂等;③ 逆向流程:退款(含部分退款、多次退款)、关单的状态回退与可退金额守恒;④ 三方一致性:支付单、订单、账务分录在任一时刻可对上;⑤ 渠道差异:四类渠道报文体、异常码、通知时序各不相同。

分析框架

一句话原理:支付单是唯一事实源,一切状态迁移走状态机,一切渠道交互带幂等键,一切最终一致性靠对账兜底。收单四步:创建支付单(幂等)→ 渠道下单 → 等待回执/轮询 → 终态落账。渠道回执双通道校准:异步通知 + 定时主动查询,通知先到以通知为准、均未到以查询为准。退款是逆向状态机,可退金额守恒(累计退款 ≤ 原交易额)。

领域模型

聚合根:支付单(PaymentOrder)——支付单号、商户号、渠道、金额、币种、状态、幂等键、渠道交互记录列表。实体:渠道交互记录(ChannelInteraction,请求/响应/异常码/耗时)。值对象:金额(分)、币种、渠道账号。不变量:① 同一幂等键至多一笔成功支付;② 渠道成功与内部 SUCCESS 必须可对账,对不上不得对外报成功;③ 退款累计不超过原支付金额。状态机:CREATED → PAYING → SUCCESS / FAILED / CLOSED;SUCCESS → REFUNDING → REFUNDED(可多次部分退款)。领域事件:PaymentSucceeded、RefundSucceeded、OrderClosed——驱动订单状态与账务分录,支付单自己不直接改别人。

解决方案

1. 收单:接入层按(商户号 + 商户请求号)建唯一索引,重复提交直接返回原支付单;创建后置 PAYING 并调渠道路由(D01.05)下发。

2. 回执校准:渠道异步通知(HTTP 回调)先到先得;通知缺失由巡查任务按(渠道 + 创建时间)批量主动查询;查询与通知都幂等(按支付单号 + 渠道流水号去重)。

3. 终态落账:支付成功发 PaymentSucceeded 事件,订单服务改单、账务服务写分录(D01.02),本地消息表保证至少一次。

4. 逆向:退款创建退款单(关联原单),校验可退余额后调渠道退款,渠道回执驱动 REFUNDING → REFUNDED;关单只允许 CREATED/PAYING 单,关单前先查渠道避免「关了但款到了」。

graph LR A[商户发起收单] --> B[创建支付单 幂等键] B --> C{状态机 PAYING} C --> D[渠道路由下发] D --> E[渠道异步通知] D --> F[巡查主动查询] E --> G{双通道校准} F --> G G -->|成功| H[SUCCESS 发事件] G -->|失败| I[FAILED 通知商户] H --> J[订单改单] H --> K[账务写分录] H --> L[退款逆向状态机]
支付核心全链路与双通道校准
方案 优点 缺点
同步等渠道返回即终态 链路短、实现简单 渠道超时即状态未知,重复通知难处理
异步通知 + 定时轮询校准 终态可靠、可追溯、抗通知丢失 链路长,需巡查任务与对账兜底
用订单状态推导支付状态 少存一张表 违反单一事实源,退款/关单无法表达
关键代码
// 幂等创建:唯一索引(merchant_no, out_trade_no)兜底,重复提交返回原单
public PaymentOrder createPayment(CreatePaymentCmd cmd) {
    try {
        PaymentOrder po = PaymentOrder.newCreated(cmd); // 状态 CREATED,带 idemKey
        paymentRepo.insert(po);            // 唯一索引冲突即重复提交
        return po;
    } catch (DuplicateKeyException e) {
        return paymentRepo.findByMerchantAndOutNo(cmd.merchantNo(), cmd.outTradeNo());
    }
}

// 状态机迁移:白名单转移表,非法迁移直接拒绝(禁散落 if-else)
private static final Map<String, Set<String>> TRANSITIONS = Map.of(
    "CREATED", Set.of("PAYING", "CLOSED"),
    "PAYING",   Set.of("SUCCESS", "FAILED", "CLOSED"),
    "SUCCESS",  Set.of("REFUNDING"));

public void transit(String payNo, String target, String reason) {
    int n = paymentRepo.transitIfAllowed(payNo, target); // UPDATE ... WHERE status IN (允许的前置)
    if (n == 0) throw new IllegalStateException("非法状态迁移: " + target);
    outbox.save(new DomainEvent("PaymentSucceeded", payNo)); // 同事务落事件表
}
深度追问
  • 渠道一直返回「处理中」怎么办?
    支付单停留 PAYING 并进入轮询队列(指数退避,上限约三十次);超过关单时限(如三十分钟)先关单,但关单前必须查一次渠道终态,避免「关了但款已到」——真出现了走「关单后退款」原路退回。
  • 重复支付如何做到零?
    三层防线:接入层(商户请求号唯一索引)挡重复提交;应用层(用户维度待支付单检查)挡用户重点;渠道层(渠道流水号幂等)挡渠道重发。任一层命中都返回已有支付单,不产生第二笔扣款。
  • 部分退款的金额怎么守住?
    退款单创建时校验(累计已退 + 本次退 ≤ 原交易额),用条件 UPDATE 占用可退额度;退款成功再累加。可退额度本身也是状态机的一部分,不能先查后判。
  • 支付单表越来越大怎么查?
    冷热分离:近三个月热库按商户分库分表,历史归档冷库 + ES 供运营检索;查询接口默认带商户号与时间范围,禁止全表扫描。归档任务在低峰期执行,归档后原单号可定位。
常见陷阱
  • 用订单状态推导支付状态:订单「已支付」不等于支付单 SUCCESS,退款/关单/部分退无法表达,必须支付单自治。
  • 幂等键用自增 ID:重试生成新 ID 即新单,幂等失效;必须用业务自然键(商户号 + 商户请求号)建唯一索引。
  • 关单前不查渠道:超时关单后渠道才回调成功,形成「已关单但已扣款」的资金悬案,只能事后人工处理。
  • 渠道回调直接信:不校验签名、不校验金额、不校验重复通知,被伪造回调或重复入账攻破。
破题点拨
一眼定位:支付核心 = 单一事实源 + 状态机 + 幂等键 + 双通道校准,四件套缺一不可。
破题主线:收单四步(幂等创建→下发→双通道校准→终态落账)→ 逆向状态机(退款/关单)→ 对账兜底。
易错点:订单推导支付;自增 ID 当幂等键;关单前不查渠道;回调不验签不判重。
D01.02
账务系统:复式记账与分录一致性设计专家P0
高频 领域模型 架构 复式记账 不可变流水 试算平衡
场景背景

支付平台账务系统日记账分录 5000 万条、月增千万级;科目涵盖用户钱包、平台收入、在途资金、手续费、汇兑损益等。硬指标:任一时刻全部分录借贷合计相等(试算平衡)、账户余额可随时重算验证、历史分录不可篡改(审计要求)、日终对账差异为零。技术栈:MySQL 分库分表 + 批量归档。

核心挑战

① 分录与余额的原子性:余额物化更新与流水写入必须同事务,否则中间态可被读出;② 热点账户:平台收入户分录量巨大,单行更新成为瓶颈;③ 幂等重放:上游事件重发导致重复记账,必须有业务单号唯一约束;④ 历史修正:记错了不能改,只能红冲(反向凭证),保证不可变性;⑤ 余额与流水漂移:物化余额长期运行后与流水重算值出现分位级差异。

分析框架

第一性原理:复式记账的铁律是「有借必有贷、借贷必相等」,余额永远是流水的函数。因此:分录表 append-only(只 INSERT),余额是物化缓存而非事实源;任何修正走红冲凭证(一笔新的反向分录);用「不变量」替代「信任」——全局借贷合计恒等、单账户余额=Σ该户分录,二者每日机检。

领域模型

聚合根:账户(Account)——科目号、余额方向(借/贷)、当前余额、版本号。实体:分录(Entry)——分录号、账户、方向、金额、业务单号、幂等键、记账时间。值对象:金额(分,BigDecimal)、科目、币种。不变量:① 每笔记账生成成对分录且借贷合计为零;② 账户余额 = Σ该户分录(按方向加减);③ 历史分录不可更新删除。红冲凭证是一笔携带「原分录号」引用的普通分录,不是特权操作。

解决方案

1. 写路径:业务方提交记账请求(含业务单号),账务服务在同事务内写成分录 + 更新双方余额;唯一索引(账户、业务单号、序号)兜底幂等。

2. 热点分片:平台收入户按商户维度拆子户(收入户-A/B/C…),全局收入为各子户之和;子户内仍守记账铁律,全局平衡校验在日终执行。

3. 余额更新:条件 UPDATE(WHERE version = ?)乐观锁防并发覆盖;冲突重试由上游幂等保证安全。

4. 校验体系:实时——单笔记账借贷校验(入库前断言);日终——全量试算平衡(Σ借=Σ贷)、抽样账户流水重算比对;差异自动定位到分录号进人工队列。

graph TD A[业务事件 带业务单号] --> B[记账服务] B --> C[同事务写双分录] B --> D[同事务更新双方余额] C --> E[(分录表 append-only)] D --> F[(账户余额 物化)] G[日终试算平衡] --> H{Σ借=Σ贷} H -->|不平| I[定位分录号 人工队列] H -->|平| J[生成对账报告] K[修正请求] --> L[红冲凭证 非 UPDATE]
复式记账写路径与校验体系
余额方案 优点 缺点
余额直接 UPDATE 无流水 简单、快 不可审计、错账无法追溯、禁不起合规检查
只存流水实时 SUM 绝对一致 热点账户查询不可行
流水 append-only + 物化余额 + 日终重算校验 可审计、高性能、差异可发现 需维护校验与红冲机制
关键代码
// 一笔转账 = 借方分录 + 贷方分录 + 两个余额更新,全部同事务
public void transfer(TransferCmd cmd) {
    if (cmd.from().equals(cmd.to())) throw new BizException("借贷方不能相同");
    tx.run(() -> {
        // ① 幂等:唯一索引(account_id, biz_no, seq)冲突即已记账
        entryRepo.insert(Entry.debit(cmd.from(), cmd.amount(), cmd.bizNo(), 1));
        entryRepo.insert(Entry.credit(cmd.to(),  cmd.amount(), cmd.bizNo(), 2));
        // ② 乐观锁更新余额,失败重试(幂等保证重试安全)
        balanceRepo.applyDelta(cmd.from(), cmd.amount().negate(), cmd.bizNo());
        balanceRepo.applyDelta(cmd.to(),  cmd.amount(),          cmd.bizNo());
    });
}

// 日终试算平衡:一条 SQL 验全局,不平即告警
// SELECT ABS(SUM(debit_amt) - SUM(credit_amt)) FROM entry WHERE acct_date = ?
深度追问
  • 为什么历史分录一条都不能改?
    审计要求「任何时点的账都可重放验证」。改历史分录会破坏「余额=Σ流水」的可重算性,也让监管抽查无法对齐。修正只有一条路:红冲凭证——新写一笔引用原分录号的反向分录,业务含义留痕。
  • 物化余额和流水重算不一致了怎么修?
    以流水为准(流水是事实源)。先冻结该账户写入,跑重算任务校准余额,差额生成一笔「差额调整」分录留痕;同时回溯是哪条写入路径漏了校验——修数据更要修代码,否则每周复发。
  • 热点账户拆子户后,全局平衡还成立吗?
    成立。子户是记账维度不是物理表——每个子户内部仍满足双分录;全局试算平衡在日终对全部分录执行,与拆不拆无关。拆子户只缓解单行更新热点,不改变记账语义。
  • 跨币种怎么记账?
    分录按原币种记账,同时记录折算本位币金额与汇率;平衡校验按折算后的本位币执行(详见 D01.06 跨境)。汇兑损益由日终重估单独生成分录,不混入交易分录。
常见陷阱
  • 单边分录:只记付出方或只记收入方,短期看不出问题,日终试算不平衡时已积累百万级差异。
  • 用余额反推流水:查余额就报账,审计季一问三不知;余额是缓存,流水才是事实。
  • 修正用 UPDATE:直接改历史分录金额,账面「平」了但审计链断了,属合规事故。
  • 无幂等约束:上游 MQ 重发导致重复记账,发现时资金已错;唯一索引要在数据库层兜底,不能只靠应用判断。
破题点拨
一眼定位:账务 = 分录成对 + append-only + 余额可重算,三条铁律答全即专家线。
破题主线:双分录同事务 → 乐观锁余额 → 热点拆子户 → 日终试算平衡 → 修正只走红冲。
易错点:单边记账;改历史分录;无唯一索引兜底;余额与流水漂移无校验。
D01.03
清结算与对账:日切、T+1 清分与长短款差异处理专家P1
高频 领域模型 架构 对账 清结算 日切
场景背景

平台日交易 800 万笔,服务 200 家商户、四类收单渠道。硬指标:T+1 上午九点前完成全部商户清分结算、长款短款当日定位到单笔、结算款不错发(错发即资金事故)。商户结算金额 = 交易净额 − 手续费 − 退款,涉及在途资金(担保交易未确认收货部分)的暂缓结算。G06.02 讲对账工程化、S07.06 讲支付链路一致性——本题考清结算域的模型与批量作业。

核心挑战

① 日切定界:跨日交易归属哪一日,全系统必须同一个日切时刻;② 账单差异:渠道账单延迟、格式不一、渠道侧状态与我方不一致;③ 长短款定位:长款(我方少记/渠道多给)、短款(我方多记/渠道少给)要定位到单笔并给处理结论;④ 结算金额计算:交易额、手续费、退款、在途的组合计算精度;⑤ 差异与结算的解耦:差异未清不能无限期阻塞商户结算,但又不能带着错账结算。

分析框架

主线:日切定界 → 三方对账(我方支付单 / 渠道流水 / 账务分录)→ 差异分类挂账 → 清分结算。对账分三层:金额对账(总额)、状态对账(终态一致)、明细对账(逐笔)。核心取舍:差异挂「待处理账项」不阻塞结算——已明差异调账、未明差异挂账并限定资金敞口;结算单一旦生成即不可改,错了走冲正。

领域模型

聚合根:结算批次(SettlementBatch,日切日 + 状态)、结算单(SettlementOrder,商户、应结金额、手续费、状态)。实体:差异项(DifferenceItem,类型:长款/短款/单边账/状态不一致,处理状态)、渠道账单文件(ChannelBill)。值对象:金额、账期。不变量:结算单金额 = 交易净额 − 手续费 − 退款,四项从账务分录聚合而来,不允许手填;单边账必须挂账而非强行平账。

解决方案

1. 日切:全局统一日切时刻(如凌晨一点),日切任务写入账期标记;跨日交易以支付单成功时刻归属,全系统只认这一个口径。

2. 账单拉取:各渠道适配器按约定时间拉取 T+1 账单文件,解析入库(格式差异在适配层消化);账单延迟超过阈值自动告警并升级渠道。

3. 三方对账:以渠道流水号为主键,关联我方支付单与账务分录;输出一致明细 + 差异明细。差异四类处理:渠道长款挂「待查收入」、我方少记补分录、手续费差异走调账、单边账挂「在途科目」待渠道确认。

4. 清分结算:按商户聚合账期分录生成结算单,走资金出账(付款到商户账户);结算单幂等(商户 + 账期唯一);出账结果回写并再次对账。

sequenceDiagram participant J as 日切任务 participant P as 我方支付单 participant C as 渠道账单 participant A as 账务分录 participant S as 清分结算 J->>P: 定界账期 J->>C: 拉取T+1账单 P->>A: 账期分录聚合 C->>A: 三方逐笔比对 A->>A: 差异分类 长款短款单边 A->>S: 明差异调账 未明挂账 S->>S: 生成结算单 幂等 S->>S: 资金出账 回写对账
T+1 清结算时序:定界 → 对账 → 挂账 → 结算
策略 优点 缺点
差异清零才结算 账绝对干净 一笔单边账卡住全商户结算,不可用
差异挂账 + 结算照常 时效可达、敞口可控 需账项台账与限额管理
总额对账为主 计算量小 总额平了明细错(对冲差错)发现不了
关键代码
// 三方对账:渠道流水 JOIN 我方支付单,输出差异分类
public DiffReport reconcile(LocalDate acctDate) {
    Map<String, ChannelTxn> ch = channelBillRepo.loadByDate(acctDate); // key=渠道流水号
    List<PaymentOrder> mine = paymentRepo.findSuccessByDate(acctDate);
    List<DifferenceItem> diffs = new ArrayList<>();
    for (PaymentOrder po : mine) {
        ChannelTxn ct = ch.remove(po.channelSn());
        if (ct == null) { diffs.add(Diff.missing(po)); continue; }      // 单边:我方有渠道无
        if (!ct.amount().equals(po.amount())) diffs.add(Diff.amount(po, ct)); // 金额差
        if (!ct.status().match(po.status())) diffs.add(Diff.status(po, ct)); // 状态差
    }
    ch.values().forEach(ct -> diffs.add(Diff.extra(ct)));                    // 单边:渠道有我方无(长款嫌疑)
    return new DiffReport(diffs);
}

// 结算金额:只从账务分录聚合,禁手填;分位计算用 BigDecimal
BigDecimal net = entryRepo.sumByMerchantAndType(mch, acctDate, TRADE_NET);
BigDecimal fee = entryRepo.sumByMerchantAndType(mch, acctDate, FEE);
BigDecimal refund = entryRepo.sumByMerchantAndType(mch, acctDate, REFUND);
BigDecimal settle = net.subtract(fee).subtract(refund);
深度追问
  • 渠道账单迟迟不来,结算还做吗?
    做——以我方分录先行结算(商户时效优先),渠道账单到达后补对账,差异走冲正(补差/退差一张新的结算调整单)。前提是我方分录可信(D01.02 的校验体系),且冲正通道事先演练过。
  • 日切时刻交易跨天怎么归属?
    只认一个口径:支付单成功时刻(落账时刻)。多渠道账单按各自文件日期对齐到该口径,渠道侧归属差异体现在「状态/金额差」里处理,绝不搞两套账期。
  • 防止重复结算?
    结算单唯一键(商户 + 账期),出账指令带幂等键;出账通道返回未知时主动查而非重发;结算批次整体只允许一次「确认」动作,确认后任何修改都是冲正单。
  • 长短款的资金敞口怎么控?
    长款(渠道多给)挂「待查收入」科目不计入可结算余额,短款先行挂账但设定商户级/平台级限额,超限触发资金部人工介入;每日输出敞口报表,超阈值自动暂停该商户结算。
常见陷阱
  • 日切时刻各系统不一致:支付按成功时刻、账务按入账时刻、渠道按文件日期,永远对不平——必须全局一个口径并写进文档。
  • 只对总额不对明细:两笔方向相反的错账总额相抵,月底才发现,定位成本百倍。
  • 差异阻塞结算:把「对账平」设为结算前置,一笔单边账卡全量商户,属可用性事故。
  • 结算金额手填可改:运营能改结算单金额,错发与舞弊风险;金额必须从分录聚合且改只有冲正。
破题点拨
一眼定位:清结算 = 日切定界 + 三方对账 + 差异挂账 + 幂等结算,答全挂账与冲正即专家线。
破题主线:统一账期口径 → 明细对账 → 差异分类(长款/短款/单边)→ 挂账不阻塞 → 结算幂等 + 冲正通道。
易错点:多账期口径;总额对账;差异阻塞;结算单可改。
D01.04
热点账户扣款:余额防超扣与串行化设计架构P0
高频 领域模型 架构 并发扣减 乐观锁 资金三态
场景背景

钱包余额支撑打赏、转账、理财赎回、红包等场景,日扣款 50 亿;直播场景下头部主播账户同时收到十万笔打赏入账,头部用户账户同时发起十万笔扣款。硬指标:余额绝不为负(超扣即资损)、扣款不丢、P99 < 50ms、日终账实相符。与 S01.05 同场景但视角不同:S01.05 讲热点技术治理,本题讲资金模型下的扣款正确性。

核心挑战

① 超扣:先查余额再扣的并发窗口,十个并发请求同时看到余额一千、各扣一百、实际只够扣十次;② 热点竞争:单账户行锁/热 key 竞争,P99 劣化甚至超时;③ 资金三态:可用、冻结、总额的关系,扣冻结中的余额怎么办;④ 扣款与账务的原子边界:余额变了流水没记,或反之;⑤ 冲正闭环:扣款成功但下游失败(订单取消、理财撤单),钱怎么原路回。

分析框架

第一性原理:扣减的正确性由「原子条件扣减」保证,不由「先查后扣」保证——UPDATE account SET available = available - ? WHERE available >= ?,影响行数为零即余额不足,从根上消除超扣。热点账户(平台收入户、头部主播)拆子账户哈希路由打散单行;资金三态模型:可用 + 冻结 = 总额,扣款先冻结后实扣,取消则解冻,任何一条路径都有反向闭环。

领域模型

聚合根:钱包账户(WalletAccount)——可用余额、冻结余额、版本号。实体:资金流水(FundFlow,类型:冻结/实扣/解冻/入账/冲正)、扣款请求(DebitRequest,幂等键)。值对象:金额。不变量:① 可用 ≥ 0(原子扣减保证);② 可用 + 冻结 = 总额(每次变动成对更新);③ 每笔扣款有且仅有一次实扣或零次(幂等键唯一)。状态机:扣款请求 INIT → FROZEN → DEBITED / RELEASED,无其他出口。

解决方案

1. 接入:网关按用户限流;扣款请求携带业务幂等键(订单号),重复请求直接返回首次结果。

2. 预扣(可选):极热账户走 Redis Lua 预扣快速失败,DB 侧仍以条件 UPDATE 为最终防线(Redis 与 DB 差异由对账校准,参考 S01.01)。

3. 实扣:同事务内条件 UPDATE 可用余额 + 写资金流水 + 写账务分录(D01.02 双分录),三者原子。

4. 热点分片:收入户/主播账户按尾号哈希拆子账户(子账户-A…),扣款路由到子账户,展示层聚合;子账户间调拨走内部转账。

5. 冲正:下游失败发解冻/冲正事件,走反向流水;冲正幂等,重复事件不产生二次回滚。

graph TD A[扣款请求 带幂等键] --> B{重复请求} B -->|是| C[返回首次结果] B -->|否| D[冻结 available 条件UPDATE] D -->|余额不足| E[快速失败 不落库] D -->|成功| F[同事务写流水+账务分录] F --> G[下游业务执行] G -->|失败| H[解冻或冲正 反向流水] G -->|成功| I[DEBITED 终态] J[热点账户] --> K[哈希拆子账户]
原子扣减与冲正闭环
防超扣方案 优点 缺点
先 SELECT 再 UPDATE 直观 并发窗口超扣,资损事故源头
条件 UPDATE(available >= ?) 数据库层原子,零超扣 热点单行竞争需分片配合
Redis Lua 预扣 + DB 兜底 抗峰值、快速失败 双写不一致需对账补偿
关键代码
// 原子扣减:影响行数为 0 即余额不足,从根上无超扣
int n = jdbc.update(
    "UPDATE wallet SET available = available - ?, frozen = frozen + ?, version = version + 1 " +
    "WHERE account_id = ? AND available >= ? AND version = ?",
    amt, amt, acctId, amt, expectVer);
if (n == 0) throw new InsufficientBalanceException();

// 同事务写流水(幂等键唯一索引兜底重复扣款)
flowRepo.insert(FundFlow.freeze(acctId, amt, idemKey)); // DuplicateKey → 重复请求

// 冲正:反向流水 + 余额回加,同一幂等键保证只回滚一次
public void compensate(String idemKey) {
    tx.run(() -> {
        flowRepo.insert(FundFlow.release(acctId, amt, idemKey)); // 重复事件撞唯一索引即忽略
        jdbc.update("UPDATE wallet SET available = available + ?, frozen = frozen - ? WHERE account_id = ?", amt, amt, acctId);
    });
}
深度追问
  • 和 S01.05 的热点扣款题差异在哪?
    S01.05 考热点治理技术(队列化、分桶、异步);本题考资金正确性模型——三态资金、原子扣减、冲正闭环。知识点答法:先讲资金模型守住底线,再讲热点技术保性能,两层都答显深度。
  • 扣款成功但账务写入失败怎么办?
    同事务回滚(余额、流水、分录三者原子),不存在「扣了没记」;若用的是 Redis 预扣 + 异步落库,则本地消息表补偿 + 超时回补 Redis,日终对账兜底。
  • 冻结一直不解除怎么办?
    冻结带业务超时(如三十分钟),超时扫描任务自动解冻并通知业务方;业务方后续再来走新请求。解冻是常态路径不是异常路径,必须常驻设计。
  • 子账户拆分会让用户余额「分裂」吗?
    不会——子账户是内部路由细节,对用户呈现一个聚合余额;扣款路由到具体子账户,入账默认进基准子账户,子账户间不均衡时后台调拨。分片只改物理分布,不改资金语义。
常见陷阱
  • 先查后扣:SELECT 余额判断后 UPDATE,并发窗口必超扣;条件 UPDATE 是唯一正确答案。
  • 只管可用不管冻结:总额 = 可用 + 冻结,只更新一边导致总额漂移,日终对账爆雷。
  • 无冲正机制:下游失败钱冻死了,客诉爆炸;解冻/冲正必须是设计的一部分而非补丁。
  • 幂等只做在应用层:重试并发到达时应用判断双双通过;唯一索引要在数据库层兜底。
破题点拨
一眼定位:防超扣 = 条件 UPDATE 原子扣减 + 资金三态 + 冲正闭环,性能问题用分片解。
破题主线:幂等接入 → 原子扣减 → 同事务落流水与分录 → 热点拆子账户 → 超时解冻与冲正。
易错点:先查后扣;总额只更一边;无解冻超时;幂等无 DB 兜底。
← 技术范式引用 S01.05 热点账户高并发扣款
D01.05
渠道路由与熔断:成功率与成本双目标设计架构P1
高频 架构 规则引擎 熔断降级 多目标优化
场景背景

聚合支付路由层,四类渠道费率与成功率各异(成本从低到高、成功率从高到低错位分布),日路由 800 万笔。硬指标:整体支付成功率 ≥ 99.9%、综合成本率 ≤ 0.45%、单渠道故障 30 秒内流量切走、路由决策耗时 < 5ms。渠道能力还有维度差异(单笔限额、支持的银行、是否支持退款)。

核心挑战

① 双目标冲突:最便宜的成功率低,最稳的成本高,多目标如何量化权衡;② 故障快速探测:渠道抖动从「成功率跌」到「全挂」只有分钟级窗口,探测不能太慢也不能太灵敏(误切);③ 规则复杂度:按商户、金额段、银行、渠道能力组合路由,规则经常变;④ 变更安全:路由规则改错=批量支付失败,必须灰度与回滚;⑤ 决策性能:5ms 内完成匹配与打分,规则不能全量求值。

分析框架

三层决策:静态规则粗筛 → 动态权重精选 → 熔断实时剔除。粗筛用规则引擎处理硬约束(限额/银行支持/黑白名单,不可协商);精选用打分模型(成功率得分 × 权重 + 成本得分 × 权重,商户分级可调权重);熔断器按渠道维度独立运行,摘除的渠道不参与打分。规则与权重配置化(DSL + 版本号),变更走影子比对 → 灰度 → 全量,可一键回滚。

领域模型

聚合根:渠道(Channel)——能力矩阵、费率、状态(健康/熔断/人工关闭)。实体:路由规则(RoutingRule,条件 DSL + 优先级 + 动作)、熔断器(CircuitBreaker,统计窗口、失败率阈值、半开放量比例)。值对象:费率、成功率统计。不变量:被熔断或人工关闭的渠道绝不进入候选集;任一决策可重放解释(命中了哪条规则、各候选得分)。

解决方案

1. 规则层:Aviator/Groovy DSL 预编译,按(商户标签 → 金额段 → 银行)索引组织,避免全量扫描;硬约束规则(限额、能力)输出候选集。

2. 权重层:候选渠道打分 = 成功率得分 × w1 + 成本得分 × w2,w 按商户分级配置(大商户保成功率、小商户保成本);得分相同按历史成功率微扰打散。

3. 熔断层:滑动窗口(如三十秒)统计失败率,超阈值(如 20%)进入半开,半开按 10% 放量探测,恢复后关闭;熔断状态本地缓存 + 广播同步。

4. 变更安全:新规则先影子运行(只记录不生效),比对影子与现网决策差异率 < 0.1% 再灰度 5% → 50% → 全量;配置版本化,异常一键回退上一版。

graph TD A[支付请求] --> B[规则粗筛 硬约束] B --> C[候选渠道集] C --> D[剔除熔断中渠道] D --> E[权重打分 成功率+成本] E --> F[选择最高分渠道] F --> G[下发并异步统计] G --> H[滑动窗口失败率] H -->|超阈值| I[半开放量探测] I -->|恢复| J[关闭熔断] I -->|持续失败| K[维持打开]
渠道路由三层决策与熔断状态机
路由策略 优点 缺点
固定优先级依次尝试 简单可预期 主渠道故障恢复后流量不回摆;成本不可控
纯权重轮询 负载均匀 无视渠道实时健康,把流量发给挂掉的渠道
规则粗筛 + 权重精选 + 熔断剔除 双目标、故障自愈、可解释 系统复杂度高,需影子与回滚配套
关键代码
// 熔断器:滑动窗口 + 半开放量(Sentinel/HRes 同款思想)
public enum State { CLOSED, OPEN, HALF_OPEN }

public boolean allowRequest(String channel) {
    State s = stateOf(channel);
    if (s == State.OPEN) return false;
    if (s == State.HALF_OPEN) return rand.nextDouble() < 0.1; // 一成放量探测
    return true;
}

public void onResult(String channel, boolean ok) {
    window.record(channel, ok);
    double fr = window.failRate(channel, 30); // 三十秒窗口
    if (fr > 0.2 && window.count(channel) >= 100) trip(channel, State.OPEN);
}

// 打分:w 按商户分级配置,双目标归一化后加权
double score = w1 * successRateNorm(c) + w2 * (1 - costRateNorm(c));
深度追问
  • 成功率与成本冲突时听谁的?
    商户分级决定权重:战略商户(GMV 占比高)w1 拉满保成功率,长尾商户保成本;同一商户不同金额段也可不同——大额走稳渠道、小额走便宜渠道。权重是配置不是代码。
  • 新渠道怎么冷启动?
    先影子运行收集真实成功率与成本,再按保底流量(如 1%)灰度接入;观察期(如两周)内成功率低于门槛自动降权。禁止新渠道直接进高分候选集。
  • 路由决策本身挂了怎么办?
    路由服务无状态多活 + 本地规则缓存兜底:配置中心不可用时用本地快照继续决策(分钟级陈旧可接受);决策服务整体故障时降级为静态优先级表——宁可少赚不可不收款。
  • 熔断阈值怎么定?
    按渠道历史成功率分布定基线(如均值 − 三倍标准差),叠加最小样本量(避免十个请求挂两个就误切);半开放量比例与观察期靠演练校准,不靠拍脑袋。
常见陷阱
  • 只成功率不顾成本:全走最稳渠道,成本率超标,财务季度复盘才发现。
  • 熔断阈值一刀切:所有渠道同一阈值,低成功率渠道天天误切、高成功率渠道从不触发。
  • 规则硬编码发版:每次调路由走发版流程,故障时改不动;规则必须 DSL 化 + 版本化。
  • 无影子直接全量:规则改错批量支付失败;影子比对 + 灰度是底线。
破题点拨
一眼定位:路由 = 规则粗筛 + 权重精选 + 熔断剔除,双目标用商户分级权重调和。
破题主线:硬约束候选集 → 双目标打分 → 熔断自愈 → 影子灰度发布 → 本地缓存兜底。
易错点:阈值一刀切;规则硬编码;无影子灰度;路由服务无兜底。
D01.06
跨境支付:多币种记账与汇兑合规设计专家P2
架构 领域模型 多币种 汇兑损益 外汇合规
场景背景

跨境电商收单平台,支持十余种币种收款,日流水约两亿等值美元。硬指标:多币种账务平衡(折算本位币后借贷相等)、汇兑损益准确、符合外汇管理局跨境收支申报要求、退款可跨币种原路退回。涉及持牌合作(支付机构跨境人民币资质)与汇率服务。

核心挑战

① 多币种平衡:分录按原币记,如何验证全局平衡;② 汇率口径:交易汇率、重估汇率、结算汇率三套口径,用错即错账;③ 汇兑损益:日终重估产生的损益如何正确入账;④ 合规报送:跨境收支申报的时效与格式;⑤ 跨币种退款:退款时汇率变了,差额谁承担。

分析框架

主线:原币种记账 + 本位币折算校验 + 汇差单列 + 合规数据从账务产出。分录永远带「原币金额 + 折算本位币金额 + 汇率快照」三要素;平衡校验按折算后的本位币执行;日终按重估汇率生成汇兑损益分录(走独立科目,不混交易分录);申报文件的数据源就是账务,不另起炉灶。

领域模型

聚合根:多币种账户(Account,币种维度独立余额)。实体:分录(Entry,扩展三币种字段)、汇兑损益单(FxRevaluation,日终批次)。值对象:汇率快照(RateSnapshot,来源 + 时间 + 值)。不变量:任一记账日,全部分录折算本位币后借贷合计相等;汇兑损益只由日终重估生成,交易分录不得直接产生损益。

解决方案

1. 汇率服务:多源拉取(央行中间价 + 合作行报价)交叉校验,熔断降级用缓存价;报价带时间戳与来源,入账时快照固化。

2. 记账:交易分录按原币金额入账,同时写折算本位币(交易汇率快照);平衡校验(D01.02 试算平衡)按本位币执行。

3. 日终重估:按重估汇率对外币余额计算损益,生成汇兑损益分录(借/贷「汇兑损益」科目);损益可逐日追溯。

4. 合规报送:从账务按监管口径聚合生成国际收支申报文件(收付口径 + 交易编码),按时效提交;报送记录留档。

5. 跨币种退款:退款按「原路原币」原则,汇率差走汇兑损益科目,不让商户或用户承担系统汇率风险(除非产品规则明确约定)。

graph TD A[原币种收款] --> B[交易分录 原币+本位币+汇率快照] B --> C[本位币试算平衡校验] D[汇率服务 多源+熔断] --> B C --> E[日终重估] E --> F[汇兑损益分录 独立科目] B --> G[跨境收支申报 与账务同源] H[原路原币退款] --> I[汇率差走损益科目]
多币种记账、重估与合规链路
方案 优点 缺点
全部折算单币记账 模型简单 原币信息丢失,无法原路退与申报
原币记账 + 本位币折算校验 可审计、可申报、可原路退 模型复杂,需汇率与重估体系
关键代码
// 多币种分录:原币 + 折算本位币 + 汇率快照三要素
public class MultiCurrencyEntry {
    private String acctNo;
    private Currency origCcy;      // USD / EUR / CNY ...
    private BigDecimal origAmt;    // 原币金额
    private BigDecimal baseAmt;    // 折算本位币金额
    private RateSnapshot rate;    // 交易汇率快照(来源+时间+值)
}

// 平衡校验按本位币:任一日 Σ借(本位币) == Σ贷(本位币)
// SELECT ABS(SUM(debit_base) - SUM(credit_base)) FROM entry WHERE acct_date = ?

// 日终重估:外币余额 × 重估汇率 − 账面本位币 = 损益,走独立科目
BigDecimal pl = bal.origAmt().multiply(revalRate).subtract(bal.baseAmt());
深度追问
  • 汇率精度怎么管?
    汇率保留足够小数位(如八位),金额与折算计算全程 BigDecimal 指定舍入模式(银行家舍入),折算差额一分以上必须挂「 rounding 」尾差科目,禁止悄悄抹平。
  • 退款时汇率已变怎么办?
    原路原币退回,汇率差产生汇兑损益走科目;若产品承诺「退款按原额」,则平台承担损益——这是产品决策,账务只负责准确记录,不承担定价。
  • 合规申报的时效与口径?
    跨境收支申报有明确时限(如业务发生日后一定工作日内),数据从账务按监管口径自动聚合生成,人工只复核不录入;申报记录与原始分录可双向追溯,监管抽查能逐笔还原。
  • 没有牌照能自建跨境清算吗?
    不能。跨境清算必须持牌(支付机构跨境人民币业务资格/外汇局批复),自建只能做「境内发卡、境外收单」的合作链路;架构上把合规边界当一等约束——链路设计前先确认资质覆盖范围。
常见陷阱
  • 实时汇率入账:用入账时刻的市场价而非快照汇率,事后无法重放验证,审计季直接翻车。
  • 折算不平当笔硬凑:为了平账把差额塞进某笔分录,破坏双分录语义;尾差必须走独立尾差科目。
  • 忽略申报义务:只做了账没做报送,监管处罚;申报数据源必须与账务同源。
  • 单币思维设计:先按人民币建账后期加币种,改造等于重写;多币种要从第一天进模型。
破题点拨
一眼定位:跨境 = 原币记账 + 本位币折算平衡 + 汇差单列 + 申报同源,四层答全即专家线。
破题主线:汇率快照服务 → 三币种分录 → 本位币平衡校验 → 日终重估损益 → 申报从账务产出。
易错点:实时汇率入账;尾差硬抹;漏报送;单币思维后期改造。

二 金融 · 风控与合规 6 题

本组知识链

风控系统的骨架是「特征 → 规则/模型 → 决策 → 处置 → 反馈」的闭环,合规是把红线织进流水线。覆盖实时风控、反欺诈、AML、监管数据治理、信贷额度与贷后催收。

  1. 实时风控:交易事件百毫秒决策,规则与模型融合,处置分级。
  2. 反欺诈:设备指纹 + 环境检测 + 关系图谱,团伙视角识别黑产。
  3. AML:KYC 分级 → 实时名单筛查 → 批量可疑监测 → 监管上报。
  4. 合规与额度:数据分级分域分权留痕;额度三态与逾期冻结联动。
  5. 贷后催收:DPD 推进案件状态机,动作策略化,通信合规前置,还款分配透明。
工程化要点
  • 决策链路 P99 < 100ms:特征并行获取、规则预编译、模型批量+缓存。
  • 名单与规则热更新:发布即生效,带版本与回滚;名单误删可恢复。
  • 决策全程留痕:命中规则、模型分、特征快照可回溯复盘,监管抽查可还原。
  • 处置分级(放行/验证/限额/拦截),误杀有申诉通道,申诉数据回流模型。
本组思维模式
  • 实时与批量分离:交易反欺诈走实时链路,AML 走批量链路,不混架构。
  • 召回优先、精确后置:宁可多拦不可漏过,最终精度靠人工复核兜。
  • 合规是设计输入:等保、个保法、外汇申报从方案第一天进设计,不是上线前补丁。
  • 对抗是常态:黑产会演进,规则与模型必须可持续迭代,一次上线不是终点。
合规与资损红线
  • 监管报送按时效提交,迟报漏报属合规事故;报送数据与账务同源。
  • 敏感数据最小化采集,脱敏展示,导出审批;审计日志不可删。
  • 名单匹配用模糊+别名,制裁名单漏匹配风险零容忍。
  • 风控处置影响用户资金(拦截/限额)时必须可解释、可申诉、留痕。
D02.01
实时风控引擎:规则 + 模型 + 名单的百毫秒决策链路专家P0
高频 领域模型 架构 规则引擎 特征服务 分级处置
场景背景

支付/转账/提现实时风控,日决策一亿次、峰值五千 QPS。硬指标:决策 P99 < 80ms、误杀率 < 0.1%(拦了正常用户)、漏过率 < 0.01%(放了欺诈);规则支持热更新(发布到生效秒级)、名单实时同步(新增黑名单分钟级生效)、每个决策可解释(为什么拦/放)。与 S07.02 金融风控系统同域:S07.02 讲整体架构,本题深挖决策引擎本身。

核心挑战

① 特征延迟:设备、行为、关系特征散在多个服务,串行拉取必超时;② 混合决策编排:名单(硬)、规则(快)、模型(准但慢)三类信号如何融合,动作冲突听谁的;③ 大名单查询:千万级黑名单不能全量驻内存逐个扫;④ 可解释:模型给分还要给理由,监管与申诉都要;⑤ 降级策略:特征服务或模型挂了,放行还是拦截——这是业务决策不是技术决策。

分析框架

链路:特征快照 → 并行决策(名单 / 规则 / 模型)→ 融合定级 → 分级处置。特征侧:本地缓存 + 变更订阅,把「拉」变「推」;决策侧:名单先行(硬命中直接拦截)、规则与模型并行求值,融合策略按优先级取最高动作;处置分级:放行 / 加强验证 / 限额 / 拦截,每一级对应不同文案与申诉入口。降级矩阵事先与业务约定:特征缺失按「保守」处理(提级验证而非直接放行)。

领域模型

聚合根:决策请求(DecisionRequest,事件 + 主体 + 上下文 + 超时预算)。实体:规则(Rule,DSL + 优先级 + 版本)、名单(List,类型 + 过期 + 来源)、模型(Model,版本 + 阈值)。值对象:特征快照(FeatureSnapshot,取值 + 时间 + 来源)。决策结果(DecisionResult):动作、命中规则列表、模型分、耗时、traceId。不变量:名单命中优先级最高;任何结果必须带可解释载荷;决策流水 append-only。

解决方案

1. 接入层:风控网关限流(超预算直接进异步通道或默认处置);请求带 traceId 贯穿全链路。

2. 特征服务:设备/账户特征本地缓存(秒级 TTL)+ 变更消息订阅刷新;跨服务特征并行 RPC,任一超时用缓存旧值并标记「陈旧」。

3. 决策编排:名单查询走布隆过滤器 + 精确集合两级(千万级名单内存占用可控);规则引擎预编译 AST 并行求值;模型服务批量推理 + 结果短缓存(同特征窗口内复用)。

4. 融合与处置:动作优先级 拦截 > 限额 > 验证 > 放行;命中即止,无需等全部信号(省时);决策流水进 Kafka → 数仓,供复盘与模型训练。

5. 降级矩阵:特征服务挂 → 用陈旧特征 + 提级验证;模型挂 → 纯规则决策 + 告警;决策引擎整体挂 → 按业务配置的默认动作(支付类默认提级验证而非放行)。

graph TD A[风控请求 traceId] --> B[特征快照 缓存+订阅] B --> C[名单查询 布隆+精确] B --> D[规则引擎 预编译并行] B --> E[模型服务 批量+缓存] C -->|命中| F[直接拦截] D --> G[融合定级] E --> G G --> H[分级处置 放行/验证/限额/拦截] H --> I[决策流水 Kafka] I --> J[复盘与模型迭代]
实时风控决策链路
决策架构 优点 缺点
纯规则引擎 可解释、热更新、低延迟 对抗性弱,漏过新型欺诈
纯模型打分 泛化强 不可解释、延迟不稳、难热更新
名单硬命中 + 规则并行 + 模型辅助 + 分级处置 兼顾时效、可解释、可迭代 链路复杂,需治理与降级矩阵
关键代码
// 并行求值 + 优先级融合(命中即止,省预算)
public DecisionResult decide(DecisionRequest req, Duration budget) {
    FeatureSnapshot fs = featureService.snapshot(req, budget);          // 缓存+订阅,陈旧有标记
    if (listService.hit(fs.deviceId()) || listService.hit(fs.userId()))
        return DecisionResult.block("名单命中");                       // 硬信号优先
    List<RuleHit> hits = ruleEngine.evalParallel(fs);                // 预编译 AST 并行
    double score = modelService.scoreCached(fs, Duration.ofMillis(20));
    Action act = Action.merge(hits, score);                              // 拦截>限额>验证>放行
    return new DecisionResult(act, hits, score, fs.stale(), req.traceId());
}

// 名单两级查询:布隆判否定,精确集合判肯定(防击穿)
public boolean hit(String key) {
    if (!bloom.mightContain(key)) return false;
    return exactSet.contains(key) && !expiredBlack.contains(key);
深度追问
  • 特征拉取超时怎么办?
    超时用本地缓存旧值并打「陈旧」标记,陈旧特征走保守策略(提级验证);同时特征服务侧对慢依赖加熔断。绝不让一个慢特征拖垮整个决策——P99 是硬指标。
  • 模型和规则结论相反听谁的?
    按「安全优先」:规则命中拦截类动作优先于模型放行;模型高分但规则未命中时提级验证而非拦截(模型要可解释才配拦钱)。具体优先级是业务策略,写成配置不写死代码。
  • 名单误删了怎么恢复?
    名单操作全部走变更流水(谁、何时、删了谁),删除是「标记过期」不是物理删;恢复即取消过期标记并广播。物理删除要双人审批 + 延迟生效窗口。
  • 规则热更新怎么保证不出错?
    规则 DSL 变更走:语法校验 → 影子求值(与现网规则并行比对命中差异)→ 按商户灰度 → 全量;版本号随决策流水落库,任何历史决策可用当时版本重放。
常见陷阱
  • 串行拉特征:三个特征各 30ms 串行即 90ms 起步,峰值直接击穿 P99;必须并行 + 缓存。
  • 决策不落库:被申诉时无法复盘「当时为什么拦」,监管抽查交不出证据链。
  • 无降级矩阵:模型挂了默认放行,欺诈潮时系统裸奔;默认动作必须与业务事先约定。
  • 名单全量驻内存硬扫:千万级名单每次全扫,CPU 打满;布隆 + 精确集合两级是底线。
破题点拨
一眼定位:实时风控 = 特征快照 + 三类信号并行融合 + 分级处置 + 降级矩阵。
破题主线:并行特征 → 名单硬命中优先 → 规则模型并行 → 优先级融合 → 流水留痕可重放。
易错点:串行特征;无留痕;默认放行式降级;名单硬扫内存。
D02.02
反欺诈与设备指纹:团伙识别设计架构P1
高频 架构 设备指纹 关系图谱 团伙识别
场景背景

营销反欺诈场景(刷券、薅补贴、虚假注册),黑产武器库含模拟器、改机工具、群控、接码平台与代理 IP。硬指标:模拟器/云手机识别准确率 > 95%;已知 TOP 团伙的关联账户识别覆盖率 > 90%;正常用户误伤率 < 0.05%(家庭共享设备不能误杀)。

核心挑战

① 指纹稳定性:同一设备多次上报要收敛到同一 ID,黑产改机要能识别出「变过」;② 环境检测:模拟器特征(传感器缺失、CPU 架构异常、电池伪装)与 hooks 框架的识别;③ 图谱规模:亿级节点十亿级边的实时查询;④ 团伙判定阈值:共享资源不等于同伙(合租、家庭 WiFi 是正常共享);⑤ 对抗演进:规则命中即过时,模型要持续迭代。

分析框架

四层:设备指纹 → 环境检测 → 关系图谱 → 团伙聚类。指纹多信号融合(硬件常量 + 行为特征 + 网络特征)加权生成设备 ID,带置信度;环境检测用规则集 + 小模型判「虚拟化/自动化」;图谱把设备、账户、IP、WiFi、收款账户作节点,交互作边,离线算连通分量与社区发现,线上实时查「这个账户的邻居风险分」;团伙判定用「共享资源密度 + 行为同步性」双条件,避免误伤正常共享。

领域模型

聚合根:设备(Device,指纹 ID + 风险分 + 首末次见时间)。实体:环境信号(EnvironmentSignal)、关系边(Edge,类型:同设备/同 IP/同 WiFi/同收款账户/同时段行为)、团伙(Group,标签 + 置信度)。值对象:风险分。不变量:设备 ID 由信号集合确定性生成(同信号同 ID);团伙标签必须带证据链(边列表),禁「黑箱判定」。

解决方案

1. 指纹 SDK:采集硬件常量(IMEI/OAID/序列号哈希)、系统属性、传感器完备性、电池与充电行为、网络特征;多信号缺失时降级为「低置信度指纹」并联动验证码。

2. 环境检测:规则集判模拟器(无传感器、分辨率异常、改机工具包名)+ 行为模型判自动化(点击间隔方差、滑动轨迹线性度);命中进「高风险设备池」联动 D02.01 实时风控。

3. 图谱构建:实时流(注册/登录/支付)生成边进图存储;离线 Spark GraphFrames 算社区发现与团伙分;团伙名单回流名单服务(D02.01)。

4. 在线查询:二度邻居风险聚合(邻居风险分 × 衰减),输出「关联风险」供决策融合;查询走图数据库二级索引,P99 < 30ms。

5. 误伤治理:家庭共享场景白名单规则(同 WiFi + 同住宅网关 + 无资金往来 = 正常共享);误伤申诉通道,申诉样本进训练集闭环。

graph TD A[终端 SDK 信号] --> B[设备指纹 多信号融合] B --> C[环境检测 规则+模型] C -->|高风险| D[高风险设备池] A --> E[关系边 设备/账户/IP/WiFi] E --> F[图存储] F --> G[离线社区发现] G --> H[团伙名单] H --> I[实时风控决策] F --> J[在线二度邻居查询] J --> I
反欺诈:指纹、环境检测与图谱联动
关键代码
// 指纹确定性生成:同信号集合 → 同 ID;缺信号时降级
public DeviceId fingerprint(Signals s) {
    int filled = s.filledCount();
    if (filled < 4) return DeviceId.lowConfidence(s.networkHash());  // 降级:联动验证码
    return DeviceId.of(Hash.sha256(s.hardwareConstants() + "|" + s.behaviorVector()));
}

// 团伙判定:共享资源密度 + 行为同步性 双条件(防误伤)
public boolean isGroup(Set<Account> accounts) {
    long shared = accounts.stream().flatMap(a -> graph.resourcesOf(a).stream())
        .collect(Collectors.groupingBy(r -> r, Collectors.counting()))
        .values().stream().filter(c -> c >= 3).count();        // 三账户以上共享同一资源
    boolean sync = behavior.syncScore(accounts) > 0.8;          // 行为时间同步性
    return shared > 0 && sync;                                   // 双条件,缺一不可
}
深度追问
  • 指纹被篡改(改机工具)怎么办?
    单一硬件信号都可伪造,靠多信号交叉 + 行为生物特征(打字节奏、滑动习惯)提高伪造成本;检测到「信号突变 + 行为不一致」即判高风险。指纹是概率武器不是确定防线。
  • 十亿级边怎么做到实时查询?
    热数据(近七天边)驻图数据库供实时二度查询;全量社区发现走离线批算,产出团伙标签供名单服务;实时查不到就返回「未知」走保守策略,不用全图遍历。
  • 家庭共享 WiFi 怎么不误杀?
    团伙判定要求「共享资源密度 + 行为同步性」双条件——家人各自登录不同账户、无资金往来,同步性低,天然不满足;再加白名单规则(住宅网关段)双保险。单条件共享即判团伙必然误伤。
  • 黑产换新对抗怎么破?
    规则命中会过时,所以检测能力分层:易变的放规则(周更),稳定的放行为模型(月更);每次对抗事件(漏过/误伤)都进样本库,模型迭代节奏跟黑产节奏赛跑。
常见陷阱
  • 单一信号判定:一个「模拟器包名」即拦截,黑产换个包名全量绕过;必须多信号融合。
  • 图谱只建不查:离线跑出团伙名单却不回流实时决策,图谱沦为报表。
  • 共享即团伙:同 WiFi 即拦截,家庭与合租场景误伤爆炸,客诉与监管双输。
  • 无申诉闭环:误伤用户无处申诉,样本不回流,模型永远学不会正常行为。
破题点拨
一眼定位:反欺诈 = 多信号指纹 + 环境检测 + 图谱聚类,双条件判定防误伤。
破题主线:SDK 信号 → 确定性指纹 → 虚拟化检测 → 实时边流 + 离线社区 → 名单回流决策 → 申诉闭环。
易错点:单信号判定;图谱不回流;共享即团伙;无申诉样本闭环。
D02.03
AML 可疑交易监测:KYC、名单筛查与监管上报专家P1
高频 领域模型 架构 反洗钱 名单筛查 监管报送
场景背景

持牌支付机构 AML 系统,客户五千万、日监测交易八百万笔。硬指标:制裁名单/政治公众人物(PEP)名单实时筛查零漏匹配;可疑交易模型 T+1 检出;大额与可疑交易按监管时限(如五个工作日内)上报;全部筛查与调查记录留痕备查。

核心挑战

① 名单匹配精度:中文名别称、拼音、曾用名、证件号多格式,精确匹配大量漏;② 客户分级:五千万客户不能同一套模型,KYC 等级决定监测强度;③ 可疑模型:规则阈值低了误报炸、高了漏报,需要场景化规则(小额多笔、快进快出、夜间集中);④ 上报时效与质量:监管报文格式严格,迟报错报都是事故;⑤ 名单更新延迟:新名单到达前的交易怎么处理。

分析框架

五段链路:KYC 分级 → 实时筛查 → 批量监测 → 人工调查 → 监管上报。名单匹配用「规范化(拼音/去噪声)+ 近似匹配(编辑距离 + 别名库)」两级召回,宁可多报候选不漏;可疑监测按客户风险等级跑差异化规则集;调查案例进人工工作流(可暂停交易/可排除);上报报文由系统按监管模板生成,人工只复核。名单更新走发布订阅,新名单生效前的批量交易补筛。

领域模型

聚合根:客户(Customer,KYC 等级 + 风险评分 + 证件信息)。实体:名单条目(ListEntry,类型:制裁/PEP/负面媒体,别名库)、预警(Alert,规则 + 分数 + 状态)、案例(Case,调查结论 + 上报编号)。值对象:姓名规范化结果。不变量:制裁名单命中必须阻断(非「待查」);案例结论必须留痕(谁、何时、依据);上报记录与原始交易双向可追溯。

解决方案

1. 名单服务:多名单源统一接入(OFAC/EU/联合国/国内),规范化引擎(简繁、拼音、去标点、别称映射)生成检索键;近似匹配(编辑距离 + 拼音相似)产出候选分,阈值分以上转人工。

2. 实时筛查:开户、交易、变更三类事件过名单服务;制裁名单命中直接阻断并告警,PEP 命中转人工尽调。

3. 批量监测:T+1 跑可疑规则集(小额测试后大额、分散转入集中转出、夜间高频、对手方集中),按客户等级差异化阈值;产出预警进调查队列。

4. 调查工作流:调查员看交易网络、历史行为、备注;结论(排除/上报/持续监测)必须填依据;上报案例生成监管报文(XML/JSON 规范模板)按时限提交。

5. 补筛机制:名单更新后对「生效日至今」的交易批量补筛,命中走案例流程——名单延迟不成为漏匹配借口。

graph TD A[开户/交易/变更事件] --> B[名单实时筛查] B -->|制裁命中| C[阻断交易 立即告警] B -->|PEP命中| D[人工尽调] B -->|无命中| E[T+1批量监测] E --> F[可疑规则集 按客户分级] F --> G[预警队列] G --> H[人工调查工作流] H -->|上报| I[监管报文 按时限] H -->|排除| J[留档并反馈规则] K[名单更新] --> L[批量补筛]
AML 五段链路:分级、筛查、监测、调查、上报
名单匹配 优点 缺点
证件号精确匹配 零误报 别称/拼音/格式差异大量漏匹配
规范化 + 近似匹配两级 召回高,漏匹配可控 有误报,需人工复核队列
关键代码
// 姓名规范化:简繁统一 + 拼音 + 去噪声,生成检索键
public List<String> normalize(String rawName) {
    String t2s = ChineseConverter.toSimplified(rawName);
    String cleaned = t2s.replaceAll("[\\s·.]", "");
    return List.of(cleaned, Pinyin.toPinyin(cleaned), Pinyin.toPinyinInitials(cleaned));
}

// 两级匹配:精确键命中或近似分超阈值
public List<MatchResult> screen(Party party) {
    return normalize(party.name()).stream()
        .flatMap(key -> listIndex.search(key).stream())     // 倒排:拼音键 → 条目
        .map(hit -> new MatchResult(hit, similarity(party.name(), hit.name())))
        .filter(r -> r.score() >= thresholdFor(hit.listType()))            // 制裁名单阈值更严
        .toList();
}
深度追问
  • 误报太多调查员瘫痪怎么办?
    三级降噪:规范化键精确命中直接过;近似匹配按分数分档(高分自动转案例、低分抽样人工);「排除结论」沉淀白名单(同证件号同结论不再重复告警)。误报率是 SLA 指标,要周度复盘。
  • 名单更新到达前的交易怎么处理?
    名单生效时刻之后的所有交易补筛(批量回溯),命中按案例流程处理;涉及资金未离场的可临时管控。补筛覆盖率与时效本身是合规检查项。
  • 客户不配合 KYC 会怎样?
    按监管要求限制功能(禁止出金/限额交易)并持续催告,超期冻结;全过程留痕。KYC 不是营销流程是合规义务,产品不能为转化率让路。
  • 上报错了能撤回吗?
    可以更正——按监管流程提交更正报文并注明原上报编号;系统保留原始上报与更正记录。关键是更正也走系统流程留痕,禁止私下联系监管了事。
常见陷阱
  • 精确匹配了事:证件号对得上但姓名用别称的漏匹配,监管检查一查一个准。
  • 全客户同一套阈值:低风险客户被规则炸出大量误报,高风险客户又不够灵敏;必须 KYC 分级差异化。
  • 上报靠人工填表:格式错误、迟报、口径漂移;报文必须系统按模板生成。
  • 案例无留痕:调查结论查不到依据,监管抽查时系统不可信。
破题点拨
一眼定位:AML = KYC 分级 + 两级名单匹配 + 差异化可疑监测 + 案例上报闭环。
破题主线:规范化检索键 → 制裁阻断/PEP 尽调 → 分级规则集 → 调查工作流 → 报文按时限 → 补筛兜底。
易错点:精确匹配漏别名;一刀切阈值;人工填表上报;案例无留痕。
D02.04
监管合规与数据治理:等保、分级、脱敏与审计架构P1
高频 架构 数据分级 脱敏 审计
场景背景

金融科技平台需过等保三级并满足《个人信息保护法》要求。硬指标:数据按核心/重要/一般三级分类分级打标;敏感字段按角色脱敏(客服看尾号、风控看全量需授权);操作审计全覆盖且日志不可删改;数据导出走审批流。系统含面向客户端的 App、运营后台、风控引擎与数仓。

核心挑战

① 分级落地:五千万客户、上百张表,打标不能靠人肉 excel;② 脱敏与业务平衡:客服核身需要部分可见,风控需要全量,一刀切脱敏业务瘫、不脱敏不合规;③ 审计量与性能:全操作审计数据量大,查询还要快;④ 权限最小化:谁能看什么,静态角色不够细(要行级数据权限);⑤ 持续合规:法规会变,一次性达标不等于持续达标。

分析框架

四字诀:分级 → 分域 → 分权 → 留痕。分级是纲:先建数据资产地图自动打标(表/字段级),驱动后续所有控制;分域:核心数据(密钥、证件)独立加密域存储,与一般数据物理/逻辑隔离;分权:RBAC + 数据行级权限 + 导出审批,权限模型带「目的限定」(客服角色只能用于核身场景);留痕:全操作审计进不可篡改存储(WORM + 哈希链),查询走 OLAP。持续合规靠自动化扫描(分级漂移、越权访问、未脱敏接口)周度巡检。

领域模型

聚合根:数据资产(DataAsset,表/字段 + 分级 + 责任人 + 打标来源)。实体:脱敏规则(MaskingRule,角色 + 算法 + 适用范围)、审计事件(AuditEvent,操作人 + 对象 + 前后值摘要 + 时间)。值对象:分级标签。不变量:核心级数据出加密域必须走授权与解密审计;审计事件只增不改;导出必须关联审批单号。

解决方案

1. 数据地图:扫描库表元数据 + 敏感词/正则识别(手机号、证件号、卡号)自动打标建议,责任人确认后生效;新表上线强制走打标流水线(没分级不准上线)。

2. 分域存储:核心数据(密钥材料、完整证件号)进独立加密域(KMS 托管密钥,字段级加密);一般数据明文域;域间流转走授权服务并记审计。

3. 动态脱敏:脱敏中间件按「角色 + 场景 + 数据分级」决策——客服角色默认脱敏,核身场景经授权码短暂全量;脱敏在数据出口统一做(应用层 AOP 或 DB 代理),不依赖各服务自觉。

4. 审计与权限:操作审计(读/写/导/批操)全量进 Kafka → OLAP,日志 WORM 存储 + 哈希链防篡改;权限 RBAC + 行级(数据范围按组织/商户),导出必须审批单,审批与导出记录关联。

5. 持续合规:周度自动扫描:未打标新表、脱敏规则失效接口、越权访问尝试、导出未审批;报告进合规看板,问题单跟踪闭环。

graph TD A[库表元数据扫描] --> B[敏感识别 自动打标] B --> C[数据资产地图 三级分级] C --> D[分域存储 KMS加密域] C --> E[动态脱敏 角色+场景] C --> F[权限模型 RBAC+行级] D --> G[出口统一管控] E --> G F --> G G --> H[操作审计 WORM+哈希链] I[周度合规扫描] --> J[问题单闭环]
合规数据治理四层控制
脱敏位置 优点 缺点
各服务自己脱敏 就近处理 漏一个接口全盘皆输,无法统一审计
出口统一脱敏(中间件/DB 代理) 策略集中、漏不出去 需改造数据访问链路
关键代码
// 出口统一脱敏:按角色 + 场景 + 分级决策(AOP 拦截查询结果)
@Around("@annotation(Sensitive)")
public Object mask(ProceedingJoinPoint pj) throws Throwable {
    Object r = pj.proceed();
    MaskCtx ctx = MaskCtx.current();                       // 角色 + 场景 + 授权码
    masker.apply(r, ctx);                                   // 核心级无授权码 → 全掩码
    audit.record(ctx, r.getClass(), touchedFields(r)); // 读了哪些敏感字段
    return r;
}

// 审计哈希链:防篡改——每条审计带 prev_hash,改历史即断链
// hash = SHA256(prev_hash + actor + action + target + ts + payload_digest)
深度追问
  • 客服核身要看到完整证件号怎么办?
    默认脱敏 + 「核身授权码」机制:客户在 App 侧确认授权(短信/人脸),生成短时效授权码,客服凭码在核身场景短暂查看全量,查看行为本身被强审计。授权码限时限次,超时自动失效。
  • 审计日志本身被删了怎么办?
    WORM(一次写多次读)存储 + 哈希链:每条审计含上一条哈希,删一条即断链可检测;备份异地保留满足等保留存期要求。删除入口在系统侧就不开放。
  • 分级打错了数据会怎样?
    高等级标低=敏感数据裸奔(事故),低等级标高=业务被过度管控(效率损失)。所以打标要「自动识别建议 + 责任人确认 + 抽查复核」三层,且周度扫描分级漂移。
  • 个保法的「删除权」怎么实现?
    用户行使删除权时走「账户注销 + 数据匿名化」:交易记录按法规保留但去标识化(不可关联到个人),其余个人数据删除;匿名化需覆盖主库、备份、数仓、日志四类载体,有专门的清理流水线与完成凭证。
常见陷阱
  • 全站统一脱敏策略:要么全员裸奔要么全员打码;策略必须按角色 × 场景 × 分级三维决策。
  • 各服务自行脱敏:新增接口忘了配即泄漏;脱敏必须在数据出口统一收口。
  • 审计日志可删:给了运营「清理日志」按钮,等于审计作废;WORM + 哈希链是底线。
  • 导出无审批:一个 SELECT 导出五百万用户信息;导出必须审批单关联 + 全量审计。
破题点拨
一眼定位:合规 = 分级(纲)+ 分域 + 分权 + 留痕,四层控制全部落到数据出口。
破题主线:自动打标 → 加密域分存 → 出口统一脱敏 → 行级权限 + 导出审批 → WORM 审计 + 周度扫描。
易错点:一刀切脱敏;服务自行脱敏;审计可删;导出无审批。
D02.05
信贷额度中心:授信、额度冻结与贷中管理专家P2
领域模型 架构 额度三态 授信审批 逾期冻结
场景背景

消费信贷额度中心,客户两千万,日额度查询五千万次、授信申请五十万。硬指标:授信审批秒级(自动审批为主)、额度占用实时准确(多产品共用总额度)、逾期自动冻结、额度账实相符(日终与放款系统对账)。产品含信用支付(免息期)与现金借据两类。

核心挑战

① 并发占用:多产品同时占用同一额度池,超占即超放;② 审批时效:授信要过风控模型(D02.01)与规则,秒级要求下异步链路易超时;③ 调整时效:提额/降额要生效时间点可控;④ 逾期联动:逾期事件驱动冻结,冻结范围(全冻结/仅冻结新增);⑤ 额度与债权对账:额度占用与真实放款金额要逐日对平。

分析框架

额度三态模型:总额度 = 可用 + 已占用 + 已冻结,所有变动走占用/释放/调整/冻结四类动作,每类动作生成流水。占用随订单/借款生命周期(下单占、放款转实、取消还);调整走「调整单 + 生效时间」,不直接改数;逾期事件(外部系统发布)驱动冻结动作,冻结只挡新增不动存量。日终额度与放款对账:占用流水 Σ = 未结清债权。

领域模型

聚合根:客户额度(CustomerLimit,总额、可用、占用、冻结、版本)。实体:占用记录(Occupancy,业务单号 + 金额 + 状态)、调整单(Adjustment,类型 + 生效时间)、冻结单(Freeze,原因 + 范围)。值对象:额度分。不变量:可用 ≥ 0(原子占用保证);总额 = 可用 + 占用 + 冻结;占用必须有对应释放路径(业务终态驱动)。

解决方案

1. 授信:申请进授信流水线(反欺诈 → 征信 → 评分卡 → 额度定价),自动审批为主、边缘案例转人工;审批结果写调整单按生效时间生效。

2. 占用:下单/借款发起时原子占用(条件 UPDATE 可用额度),Redis 预扣抗峰值、DB 兜底;业务终态(取消/拒付/结清)释放,释放幂等。

3. 调整:提额降额生成调整单,带生效时间(立即/定时);调整只改「总额」,不动已占用;降额时若可用不足只压缩可用部分。

4. 冻结:逾期事件(信贷系统发布)驱动冻结单:全冻结(可用清零 + 禁新增)或部分冻结;还款后按规则解除,解除也走流水。

5. 对账:日终额度占用流水与放款系统的债权明细核对,差异定位到占用记录;额度查询走客户维度缓存(热点客户本地缓存 + 变更失效)。

graph TD A[授信申请] --> B[审批流水线] B --> C[调整单 生效时间] C --> D[额度池 总额/可用/占用/冻结] E[下单/借款] -->|原子占用| D D -->|业务终态| F[释放 幂等] G[逾期事件] -->|驱动| H[冻结单 范围可控] H --> D D --> I[日终与放款对账]
额度三态与生命周期动作
关键代码
// 原子占用:可用不足即失败;占用记录幂等键兜底
int n = jdbc.update(
    "UPDATE credit_limit SET available = available - ?, occupied = occupied + ?, version = version + 1 " +
    "WHERE customer_id = ? AND available >= ? AND version = ?", amt, amt, custId, amt, ver);
if (n == 0) throw new LimitExceededException();
occupancyRepo.insert(Occupancy.newOne(custId, bizNo, amt));   // 唯一索引兜底重复占用

// 日终对账:占用流水合计 vs 未结清债权,差异即告警
BigDecimal occSum = occupancyRepo.sumActive(custId);
BigDecimal debtSum = loanClient.outstanding(custId);
if (occSum.compareTo(debtSum) != 0) alerting.fire("LIMIT_DEBT_MISMATCH", custId);
深度追问
  • 占用了但订单取消,额度怎么回?
    业务终态事件驱动释放(占用记录置「已释放」+ 可用额度加回),释放带原占用幂等键防重复回滚;取消事件延迟到达也没关系——释放接口幂等,重复调用返回首次结果。
  • 提额灰度怎么控风险?
    提额是风险决策不是运营决策:必须过风控复评(D02.01),且新额度带生效时间与观察期;大额提额抽样人工复核。运营侧只能发起申请,不能直接改额度。
  • 五千万次/日查询怎么抗?
    读多写少:客户维度额度缓存(Redis + 本地二级),占用/释放/调整都发失效消息;查询走缓存,余额校验的强一致路径(占用时)才落库。缓存与 DB 差异由日终对账兜底。
  • 冻结误判了怎么恢复?
    冻结单带「冻结原因 + 解除条件」,还款/申诉成功即触发解除动作走流水;冻结与解除全程留痕(合规要求,见 D02.04)。误判恢复时效是 SLA——客诉与合规双指标。
常见陷阱
  • 只加不减:占用了没有释放路径,客户额度越用越少,客诉爆炸;释放必须是设计的一部分。
  • 占用无幂等:重复下单重复占用,可用被多扣;唯一索引兜底。
  • 冻结无解除机制:逾期冻结后还清款仍冻结,属于合规与服务双重事故。
  • 额度与放款各算各的:占用流水与债权不对账,坏账暴露时才发现超额放款。
破题点拨
一眼定位:额度中心 = 三态额度 + 四类动作(占/释/调/冻)+ 与债权对账。
破题主线:授信流水线 → 原子占用 → 终态释放 → 调整单生效时间 → 逾期冻结可解 → 日终对账。
易错点:只占不放;占用不幂等;冻结不可解;与放款不对账。
← 技术范式引用 S02.02 跨服务分布式事务方案
D02.06
贷后催收管理:案件分案、催收策略与通信合规架构P1
高频 领域模型 架构 账龄分案 催收策略 通信合规
场景背景

消费信贷贷后管理,在贷余额数百亿、月逾期案件数十万。硬指标:M1(逾期一至三十天)自催回收率、委外回收成本、客诉率(通信合规是红线)、还款分配争议率。逾期按账龄滚动分级,催收手段含短信、电话、外访、法诉,逾期三十天以上可委外。

核心挑战

① 案件分案:数十万案件按账龄、金额、客户价值、历史还款表现分池到自催/委外/法诉,分案规则要可调;② 策略执行:每个案件有动作序列(短信提醒→电话→上门),动作间隔与升级路径要策略化;③ 通信合规:拨打时段(晚九点到早八点禁呼)、频次上限、禁呼名单(投诉过)、用语文明——违规即监管处罚;④ 还款分配:部分还款先抵费用还是本金,直接关系客户剩余债务与平台损益;⑤ 委外管理:数据给出去要可控(最小化+脱敏+回收),回收效果要对账。

分析框架

案件状态机驱动一切:逾期日(DPD)推进案件状态(M1→M2→M3→M3+),每级有动作序列与责任人。催收不是「催」而是「恢复履约」:先易后难(短信自动 → 电话人工 → 外访/法诉),每级设置停留时限与升级条件。还款分配走「先费用后本金、先利息后本金」的固定规则(监管认可顺序),分配结果客户可查。合规是前置校验而非事后审计:每次触达前校验时段/频次/禁呼,不合规直接拦截。

领域模型

聚合根:催收案件(CollectionCase,逾期日 + 等级 + 状态 + 责任人 + 池)。实体:催收动作(Action,类型 + 渠道 + 结果 + 合规校验单)、还款分配(Allocation,还款单 × 分项)、委外批次(OutsourceBatch,机构 + 范围 + 回收数据)。值对象:账龄分、触达频次计数。不变量:案件等级随逾期日单调推进(不倒退);每次外呼前有合规校验通过的记录;还款分配总额 = 实收金额;委外数据不外泄(脱敏交付 + 到期回收)。

解决方案

1. 分案引擎:日切跑批按 DPD 滚动推进案件等级,分池规则(金额分层 × 账龄 × 客户标签 × 历史还款)配置化;分池结果生成责任人待办。

2. 策略引擎:每池一套动作序列(间隔天数 + 升级条件 + 渠道),规则外置可热更;动作执行前过合规前置校验(时段/频次/禁呼),不通过自动顺延。

3. 还款管理:客户还款进来先分配(费用→利息→本金),生成分配明细;协商还款(减免/分期)走审批流,协议电子签,新还款计划覆盖原计划。

4. 委外:案件打包脱敏导出,机构侧只能看脱敏数据;回收款原路回平台统一入账再与机构结算佣金;委外数据到期回收并出具销毁记录。

5. 质检与客诉:全量录音 ASR 抽检 + 敏感词告警;客诉工单闭环(申诉→核查→处置→反馈),禁呼名单实时生效。

graph TD A[逾期日切] --> B[案件等级推进 M1-M3+] B --> C[分池 规则可配] C --> D[自催池 短信/电话] C --> E[委外池 脱敏导出] C --> F[法诉池 证据包] D --> G[动作前置合规校验] G -->|通过| H[执行触达] G -->|拦截| I[顺延/禁呼] J[客户还款] --> K[分配 费用/利息/本金] L[协商还款] --> M[审批+电子签] M --> N[新还款计划]
催收案件流转与合规前置
关键代码
// 触达前置合规校验:时段/当日频次/禁呼名单,任一不过即拦截
public boolean canContact(Case c, Channel ch, LocalDateTime now) {
    if (now.getHour() >= 21 || now.getHour() < 8) return false;        // 禁呼时段
    int todayCnt = touchLogRepo.countToday(c.getId(), ch);
    if (todayCnt >= ch.dailyCap()) return false;                        // 频次上限
    return !blockList.contains(c.getCustId(), ch);                       // 禁呼名单(投诉/投诉处理中)
}

// 部分还款分配:费用 → 利息 → 本金(监管认可顺序),逐项冲抵
Money remain = payment.getAmount();
for (AllocItem item : List.of(Fee.first(), Interest.second(), Principal.last())) {
    Money paid = item.writeOff(remain);
    remain = remain.subtract(paid);
    if (remain.isZero()) break;
}
深度追问
  • 客户还了一部分,剩下的债怎么算?
    严格按分配规则(费用→利息→本金)冲抵并生成分配明细,客户可查;协商还款有协议的按协议(如本金打折)执行,但必须留审批与签章记录。争议大多来自「客户以为还的是本金」——明细透明化是解法。
  • 催收动作怎么做到不骚扰又有效?
    双层约束:合规硬线(时段/频次/禁呼)前置拦截;效果软线(同渠道连续无响应即升级或换渠道,避免机械重复)。催收策略按回收率与客诉率双指标迭代,客诉率超阈值的策略自动下线。
  • 委外数据安全怎么保障?
    最小化 + 脱敏 + 可控期:只给案件必要字段(脱敏姓名电话)、机构专号回呼、数据接口白名单、到期自动回收并出具销毁记录;委外期间还款仍回平台入账,机构不经手资金。
  • 逾期冻结和催收什么关系?
    额度冻结(D02.05)在逾期确认时由信贷系统触发,催收系统只做恢复履约;冻结挡新增、催收恢复存量,两条线事件驱动联动,不共用一个状态机。
常见陷阱
  • 合规事后补:先打再审,监管通报就是行政处罚;合规校验必须前置到触达动作之前。
  • 分案规则写死:策略调整要发版;分池规则必须配置化,且变更留痕可回放。
  • 还款分配糊涂账:分配顺序不固定、客户查不到明细,争议与客诉双输。
  • 委外一放了之:数据不回不销、机构骚扰客户平台背锅——委外合同要写数据条款与客诉连带责任。
破题点拨
一眼定位:贷后催收 = 案件状态机(DPD 推进)+ 策略化动作 + 合规前置 + 分配透明 + 委外可控。
破题主线:日切分案 → 池化策略 → 触达前置校验 → 还款分配留痕 → 协商走签 → 委外最小化。
易错点:合规后置;规则写死;分配不透明;委外数据失控。

三 电商 · 交易与履约 6 题

本组知识链

交易系统的骨架是「状态机驱动的正向流程 + 完备的逆向流程」。覆盖订单、库存、商品价格、履约售后、购物车与商家结算。

  1. 订单中心:父子单 + 统一状态机 + 事件驱动,逆向流程与正向对等。
  2. 库存中心:三态库存、预扣实扣分离、仓间调拨、不超卖不漏卖。
  3. 商品价格:SPU/SKU 模型分层、价格版本化、错价防控。
  4. 履约售后:拆合单规则引擎、时效承诺、逆向物流与仲裁。
  5. 商家结算:三户模型、明细级记账、结算单驱动打款、逆向冲抵透明。
工程化要点
  • 订单状态机集中定义(转移表),禁止散落的 if-else 改状态。
  • 库存预扣/实扣/释放全部幂等,日终盘点校准,超时关单自动释放。
  • 价格快照随下单固化,在途订单不随调价变动;错价有监控与熔断。
  • 跨服务最终一致(Saga/事务消息),对账任务兜底;履约时效可监控可告警。
本组思维模式
  • 领域模型先行:先划清聚合根与不变量,再谈表设计与服务拆分。
  • 异常流即主干:取消、退款、超时、部分退货是常态不是边角,正向逆向同设计。
  • 快照一致:下单瞬间的价格、优惠、库存决定固化,后续变动只影响新单。
  • 柔性事务:跨服务不追求强一致,用状态机 + 补偿 + 对账达到最终一致。
合规与资损红线
  • 订单实付金额与支付金额必须一致,价保/退款按实付口径计算。
  • 库存不超卖(超卖=违约赔付);退款原路返回,到账时效可承诺。
  • 售后凭证(照片/物流单)留存,仲裁结论可追溯;禁止价格欺诈(虚构原价)。
  • 用户地址、手机号等个人信息按个保法分级管控(见 D02.04)。
D03.01
订单中心:领域建模、状态机与逆向流程设计架构P0
高频 领域模型 架构 状态机 领域事件 逆向流程
场景背景

电商订单中心日订单五百万、峰值两万 TPS,订单类型含普通、预售、拼团、虚拟商品与跨店拆分。硬指标:状态流转正确率 100%(错态即资损或客诉)、逆向流程(取消/退款/退货/换货)完备、订单查询 P99 < 20ms、大促峰值不丢单。S07.01 讲电商订单全链路一致性,本题讲订单域本身的建模。

核心挑战

① 状态爆炸:订单类型 × 流程阶段组合出几十个状态,散落判断必然漏迁移;② 逆向交叉:已发货订单取消(拦截/拒收)、退款中又退货、部分行售后,正向逆向状态交叉;③ 父子单一致性:父单聚合状态与子单履约状态同步;④ 峰值性能:大促创建洪峰下状态机执行与消息扇出;⑤ 历史查询:三年前订单仍要秒查。

分析框架

三支柱:父子单分层 + 统一状态机 + 事件驱动。父单管「交易视角」(金额、支付、整体状态),子单管「履约视角」(仓、物流、行项目);状态机用「状态 × 事件 → 目标状态 + 动作」转移表集中定义,非法迁移直接拒绝;下游(支付、库存、履约)由领域事件驱动,订单不直接调别人。逆向流程定义为正向的逆操作集合,独立状态机(售后单),与正向同权。

领域模型

聚合根:主订单(Order,买家、实付金额、整体状态)+ 子订单(SubOrder,店铺/仓、行项目、履约状态)。实体:订单行(OrderLine,SKU、数量、行快照价)、售后单(AfterSaleOrder)。值对象:金额、地址快照、商品快照。状态机:待支付 → 已支付 → 待发货 → 已发货 → 已完成;分支:已取消(任意前置可取消,规则不同)、售后中(已支付后任意时点)。领域事件:OrderCreated、OrderPaid、OrderShipped、OrderCompleted、AfterSaleApplied。不变量:实付金额 = Σ行实付 − 优惠 + 运费,任何时刻可重算验证。

解决方案

1. 下单:购物车试算(D03.05)→ 价格快照固化 → 创建父子单(同事务)→ 发 OrderCreated 事件(事务消息)→ 支付超时关单(延迟消息)。

2. 状态机引擎:转移表驱动(见代码),每次迁移记录(from, to, event, operator, ts);非法迁移抛错并告警——错态比拒绝更危险。

3. 事件驱动:OrderPaid → 库存实扣(D03.02)、履约接单;OrderShipped → 通知 + 确认收货倒计时;事件消费者全部幂等(事件 ID 去重)。

4. 逆向:未支付取消(关单即终态);已支付未发货取消(退全款 + 库存释放);已发货取消(物流拦截 / 拒收后退款);售后单独立状态机(申请→审核→退货物流→退款/换货→完成),售后与原单状态双向同步。

5. 存储:热库按买家分库分表(查询主场),商家维度走异构索引(ES);三个月前订单归档冷库,原单号可定位。

stateDiagram-v2 [*] --> 待支付: 下单 待支付 --> 已支付: 支付成功 待支付 --> 已取消: 超时关单 已支付 --> 待发货: 备货 已支付 --> 已取消: 全额退款 待发货 --> 已发货: 发货 已发货 --> 已完成: 确认收货 已发货 --> 售后中: 申请售后 售后中 --> 已完成: 售后完结 已完成 --> [*] 已取消 --> [*]
订单主状态机(逆向分支与正向同权)
建模方案 优点 缺点
单表 + if-else 状态判断 起步快 状态一多必漏迁移,错态资损
父子单 + 转移表状态机 + 事件驱动 迁移可枚举、下游解耦、逆向对等 建模成本高,需要引擎与治理
引入通用流程引擎 可视化灵活 订单状态是高频核心路径,引擎性能与复杂度双输
关键代码
// 转移表:集中定义,非法迁移拒绝(订单状态机的唯一事实源)
private static final Map<String, Map<String, String>> TRANSITIONS = Map.of(
    "待支付", Map.of("PAID", "已支付", "TIMEOUT", "已取消", "CANCEL", "已取消"),
    "已支付", Map.of("SHIP", "待发货", "REFUND_ALL", "已取消"),
    "待发货", Map.of("DELIVER", "已发货", "REFUND_ALL", "已取消"),
    "已发货", Map.of("CONFIRM", "已完成", "AFTERSALE", "售后中"),
    "售后中", Map.of("AS_DONE", "已完成"));

public void fire(Long orderId, String event) {
    Order o = orderRepo.get(orderId);
    String target = TRANSITIONS.getOrDefault(o.status(), Map.of()).get(event);
    if (target == null) throw new IllegalStateTransition(o.status() + " --" + event + "--> ?");
    orderRepo.transit(orderId, o.status(), target);          // CAS:WHERE status = 旧值
    eventBus.publish(new OrderEvent(orderId, event, target));    // 事务消息,消费者幂等
}
深度追问
  • 已发货订单用户不要了怎么办?
    走「退款不退货」或「拒收」两类:未签发走物流拦截(成功率看承运商),已签发引导拒收,包裹回退仓后触发退款。关键是取消申请先建售后单占住流程,避免用户前台点了取消、包裹还在路上两端都没人管。
  • 部分行的售后怎么处理状态?
    售后单挂在子单/行维度,父单整体状态不进「售后中」而是打「部分售后」标记;整单所有行都售后完才整体完结。金额按行实付计算(可退金额守恒),父单金额始终可重算。
  • 预售订单的状态机差异?
    定金段(待付定金→已付定金→待付尾款)与现货段不同,用「订单类型决定初始状态集」实现:同一状态机框架,按类型挂不同的转移表子集。切忌为预售单开一整套平行状态机——那是状态爆炸的起点。
  • 两万 TPS 创建洪峰怎么抗?
    创建路径只做必需事:快照 + 插库 + 发事件,其余(通知、统计)全异步;事件总线分区按订单 ID 保证顺序;关单延迟消息按超时分桶批量处理。大促前全链路压测(S05.05),热点买家分片。
常见陷阱
  • 状态散落 if-else:十几个服务各自判断订单状态,新加一个取消规则漏改三处,错态上线。
  • 逆向流程后补:正向跑通就上线,退款/取消当异常处理,客诉来了才发现逻辑全是洞。
  • 父子单各改各的:子单发货了父单还显示待发货;聚合规则必须集中定义。
  • 事件消费者不幂等:MQ 重放导致重复扣库存、重复通知;事件 ID 去重是底线。
破题点拨
一眼定位:订单中心 = 父子单分层 + 转移表状态机 + 事件驱动 + 逆向对等。
破题主线:快照下单 → 集中状态机 → 事件解耦下游 → 售后单独立状态机 → 冷热分离查询。
易错点:if-else 状态机;逆向后补;父子单不同步;消费者不幂等。
D03.02
库存中心:三态库存、预扣释放与仓间调拨架构P0
高频 领域模型 架构 防超卖 库存三态 多仓
场景背景

库存中心支撑自营 + 商家双模式,SKU 五千万,日库存变动两亿次,大促扣减峰值十万 TPS。硬指标:不超卖(超卖即违约赔付)、不漏卖(有货却显示无货=收入损失)、库存可随时盘点校准、超时未支付订单自动释放。仓网含中心仓、前置仓、门店仓,同 SKU 多仓有货。

核心挑战

① 预扣与实扣的窗口:下单预扣、支付后实扣,窗口内库存「双重身份」易错;② 多仓分配:一单多仓拆分配,总货够但分仓不足;③ 超时释放:未支付订单占着库存,释放必须幂等且及时;④ 冻结与可用转换:盘点冻结、活动锁定占用可用;⑤ 峰值热点:爆品单仓单 SKU 扣减热点(技术层参考 S01.01)。

分析框架

三态库存模型:实物在仓 = 可用 + 预占(预占含已售未发与锁定),在途库存(调拨/采购中)单独维度。扣减分层:Redis Lua 预扣抗峰值(快速失败)→ DB 条件 UPDATE 实扣(最终防线)→ 日终对账校准。多仓拆单遵循「就近 + 可发 + 成本」规则,单仓不足时拆多仓或引导替代仓。超时释放走延迟消息,释放幂等。

领域模型

聚合根:库存(仓 × SKU,可用、预占、在途、冻结)。实体:占用记录(Occupancy,业务单号 + 数量 + 仓)、调拨单(Transfer)、盘点单(Stocktake,差异处理)。值对象:数量。不变量:① 可用 + 预占 ≤ 实物在仓;② 可用 ≥ 0(原子扣减);③ 每笔预占有对应释放或实扣路径(订单终态驱动)。领域事件:StockPreempted、StockDeducted、StockReleased、StockArrived(在途转在仓)。

解决方案

1. 预扣:下单时对目标仓 Redis Lua 预扣(扣可用加预占),失败即引导换仓/无货;预扣记录带业务单号与超时时间。

2. 实扣:支付成功事件驱动 DB 条件 UPDATE(可用减、预占减、已售加),同事务写流水;Redis 与 DB 差异由对账校准。

3. 释放:关单/取消/售后退货事件驱动释放(可用加、预占减),幂等键兜底;延迟消息扫超时预占。

4. 多仓:拆单引擎(规则见 D03.04)选仓;仓间调拨生成调拨单(在途库存),到货确认后转在仓;门店仓支持「店发货」与「仓店一体」。

5. 盘点:盘点单冻结盘点范围库存,差异生成调整流水(可追溯),支持循环盘点不影响全仓。

graph TD A[下单] --> B[选仓 规则引擎] B --> C[Redis Lua 预扣] C -->|失败| D[换仓或无货] C -->|成功| E[预占中] E -->|支付成功| F[DB 条件实扣] E -->|超时关单| G[延迟消息释放 幂等] E -->|取消| G F --> H[已售待发] H -->|发货| I[出库] J[采购/调拨] --> K[在途库存] K -->|到货确认| L[在仓可用]
库存三态与占用生命周期
扣减架构 优点 缺点
纯 DB 扣减 强一致简单 十万 TPS 直接打穿,大促不可用
Redis 预扣 + DB 实扣 + 对账 抗峰值、快速失败 双写窗口需对账与补偿
库存分桶(同 S01.01) 打散热点 key 合并与借调逻辑复杂,仅爆品需要
关键代码
// 预扣 Lua:原子扣可用加预占,防超卖第一层
private static final String PREEMPT =
    "local av = tonumber(redis.call('hget', KEYS[1], 'avail')) " +
    "if not av or av < tonumber(ARGV[1]) then return 0 end " +
    "redis.call('hincrby', KEYS[1], 'avail', -ARGV[1]) " +
    "redis.call('hincrby', KEYS[1], 'preempt', ARGV[1]) " +
    "redis.call('set', KEYS[2], ARGV[2], 'EX', ARGV[3]) return 1";  // 预占单号+TTL 供释放

// 实扣:DB 条件 UPDATE 兜底(防 Redis 与 DB 偏差导致超卖)
int n = jdbc.update(
    "UPDATE stock SET avail = avail - ?, preempt = preempt - ?, sold = sold + ? " +
    "WHERE warehouse_id = ? AND sku_id = ? AND avail >= ? AND preempt >= ?",
    q, q, q, wh, sku, q, q);
if (n == 0) alerting.fire("STOCK_INCONSISTENT", wh + "/" + sku);  // 双写不一致,进对账队列

// 释放:幂等键(业务单号)兜底,重复释放不生效
int m = jdbc.update(
    "UPDATE occupancy SET status = 'RELEASED' WHERE biz_no = ? AND status = 'ACTIVE'", bizNo);
if (m == 1) jdbc.update("UPDATE stock SET avail = avail + ?, preempt = preempt - ? WHERE ...", q, q, wh, sku);
深度追问
  • 和 S01.01 的秒杀库存有什么不同?
    秒杀是「活动库存」——独立池、短周期、无仓概念、峰值极端;本题是「销售库存」——长生命周期、多仓三态、常态流量。技术层可复用 Lua/条件扣减,但模型层完全不同:别把秒杀方案当库存中心答。
  • 总货足够但目标仓不足怎么办?
    拆单引擎按「时效优先/成本优先」策略选仓组合;体验优先时引导用户选「就近仓发货」或拆多仓包裹;成本优先时合并到有货仓。选仓结果是订单快照的一部分,不随后续库存变动改。
  • Redis 与 DB 库存不一致怎么发现和修?
    实时:实扣条件 UPDATE 影响行数为零即告警进对账队列;日终:全量对账(Redis vs DB vs 在途),以 DB 为准校准 Redis。差异必须定位到占用记录,定位不了就冻结该仓 SKU 售卖——宁可停售不可错卖。
  • 盘点出现负数库存怎么处理?
    负数必然意味着「漏释放或超扣」:先冻结该仓 SKU(停售),按占用记录逐笔回溯找未释放/重复扣减,补释放流水后再解冻;盘亏盘盈都走调整流水留痕,绝不直接 UPDATE 抹平。
常见陷阱
  • 先查后扣:SELECT 可用再 UPDATE,并发窗口超卖;条件 UPDATE / Lua 原子是唯一解。
  • 只扣不释放:超时订单、取消订单占着库存,越卖越少;释放必须幂等且常驻。
  • 多仓各扣各的:不校验整单可发量,总货够但拆不出来;拆单前要有可发性预检。
  • 无对账校准:双写差异不清算,月底盘点爆雷;实时告警 + 日终对账双保险。
破题点拨
一眼定位:库存中心 = 三态模型 + 分层扣减 + 释放闭环 + 多仓拆配。
破题主线:预扣(Lua)→ 实扣(条件 UPDATE)→ 释放(幂等 + 延迟扫描)→ 在途调拨 → 盘点校准。
易错点:先查后扣;漏释放;多仓不预检;双写不对账。
← 技术范式引用 S01.01 百万 QPS 秒杀系统设计
D03.03
商品与价格体系:SPU/SKU 模型、价格时效与错价防控高级P1
领域模型 SPU/SKU 价格版本 错价防控
场景背景

商品中心管理 SKU 五千万,日价格变更百万次。价格多来源:挂牌价、活动价、会员价、券后价。硬指标:调价按时生效(含定时生效)、在途订单不随调价变动、错价(如把 999 设成 9.9)分钟内发现并止损、价格全程可追溯(监管要求)。

核心挑战

① 模型设计:SPU/SKU/类目/品牌关系复杂,前后台类目分离;② 生效时效:定时调价、大促价准点切换,迟到一分钟就是资损或客诉;③ 多价格源:优先级与叠加规则,用户最终看哪个价;④ 错价防控:改错一个字段秒级传播全站,必须有监控与熔断;⑤ 快照一致性:在途订单价格固化,不能「下单后变价」。

分析框架

模型分层:SPU(标准化产品)→ SKU(销售单元,规格组合)→ 库存单元(仓 × SKU),前后台类目分离(后台管运营,前台管搜索导购)。价格版本化:每条价格带(类型、渠道、生效区间),查询按「当前时刻 ∩ 渠道」取版本,天然支持定时与历史追溯。错价防控三道:变更前校验(跌幅阈值告警)、变更中灰度(按 SKU 抽样)、变更后监控(价格波动率 + 下单量异常联动熔断下架)。在途订单价格靠下单快照(D03.05)。

领域模型

聚合根:SPU(名称、类目、品牌、属性模板)、SKU(规格组合、条形码、上下架状态)。实体:价格(Price,类型:挂牌/活动/会员,渠道,生效区间,版本)、类目(前后台两棵树)。值对象:规格属性。不变量:同一时刻同一渠道同类型价格至多一个生效版本;SKU 必须挂属有效 SPU;价格版本只增不改(改即新版本)。

解决方案

1. 商品发布:编辑 → 类目属性校验 → 审核(机审 + 人审)→ 发布;发布是版本动作,回滚即回退到上一版本。

2. 价格服务:价格写入带生效区间([start, end));生效任务(延迟消息)到点切换当前版本并广播缓存失效;查询侧多级缓存(本地 + Redis)+ 失效广播。

3. 价格优先级:会员价 > 活动价 > 挂牌价,渠道维度独立(App/小程序/三方);叠加规则(券)在 D04.02 价格引擎处理,商品中心只回答「商品原价是多少」。

4. 错价防控:跌幅超阈值(如七成)强制二次确认 + 告警;新价格先灰度(抽样 SKU 生效)+ 监控(价变率、下单转化异常);异常自动回滚上一版本并下架。

graph TD A[商品编辑] --> B[属性校验] B --> C[审核] C --> D[发布 版本化] E[价格变更 带生效区间] --> F[灰度抽样] F --> G[到点切换当前版本] G --> H[广播缓存失效] H --> I[查询 本地+Redis] J[错价监控 波动+转化] -->|异常| K[自动回滚+下架]
商品发布与价格版本化链路
价格方案 优点 缺点
单价格字段直接覆盖 简单 无历史无定时,错价无法回滚
价格版本 + 生效区间 定时、可追溯、可回滚 查询需按时刻匹配,缓存复杂些
关键代码
// 价格查询:当前时刻 ∩ 渠道 ∩ 类型,命中生效区间版本(天然支持定时与追溯)
public Optional<Price> currentPrice(Long skuId, Channel ch, PriceType type, Instant now) {
    return priceRepo.findEffective(skuId, ch, type, now);   // WHERE sku=? AND ch=? AND type=? AND start<=? AND end>?
}

// 错价护栏:跌幅超阈值强制二次确认 + 告警
public void changePrice(ChangePriceCmd cmd) {
    BigDecimal old = currentPrice(cmd.skuId(), cmd.channel(), cmd.type(), Instant.now())
        .map(Price::amount).orElse(cmd.newAmount());
    if (old.compareTo(cmd.newAmount()) > 0
        && cmd.newAmount().divide(old, 4, RoundingMode.HALF_EVEN).compareTo(new BigDecimal("0.3")) < 0) {
        if (!cmd.forceConfirmed()) throw new NeedSecondConfirmException();
        alerting.fire("PRICE_DROP_ABNORMAL", cmd);
    }
    priceRepo.insertNewVersion(cmd);   // 只增不改:改即新版本
}
深度追问
  • 大促价到点没切换怎么办?
    生效任务双保险:延迟消息到点切换 + 兜底扫描(每分钟扫应生效未生效的版本)补切;切换是幂等操作(按版本号判断当前版本)。切换失败告警到运营,超阈值自动回滚挂牌价——宁可少促销不可错价格。
  • 在途订单会跟着变价吗?
    不会——下单瞬间价格快照固化进行项(D03.05),订单只看快照;后续调价只影响新单。价保(D04.04)是显式的售后动作,不是自动跟随。
  • 会员价和活动价能同时存在吗?
    按优先级取一:会员价 > 活动价 > 挂牌价;是否可叠加是产品决策,配置在优先级表里。叠加逻辑(如会员价再叠加券)在价格引擎(D04.02)统一算,商品中心不掺和。
  • 类目调整影响历史订单吗?
    不影响——订单行存商品快照(类目路径 + 名称 + 规格),类目调整只影响前台导购与搜索。后台类目合并/迁移走类目树版本,历史映射保留,搜索侧按新树聚合。
常见陷阱
  • 价格直接覆盖无版本:错价后无法回滚,历史订单对不上账;版本化是底线。
  • 在途订单跟着变价:用户下单后涨价/降价引发批量客诉;快照固化解决。
  • 类目混乱无治理:前后台类目混用、重复类目,搜索与运营双输;两棵树 + 版本迁移。
  • 错价无监控:9.9 元的手机卖了一晚上才发现;波动率 + 转化异常监控联动熔断。
破题点拨
一眼定位:商品体系 = SPU/SKU 分层 + 价格版本化 + 错价三道防线 + 下单快照。
破题主线:模型分层 → 版本化价格 → 定时生效与失效广播 → 灰度 + 监控熔断 → 快照隔离在途订单。
易错点:无版本;无快照;类目混用;错价无熔断。
D03.04
履约与售后:拆合单、时效承诺与退款仲裁架构P1
高频 架构 拆单 履约时效 售后仲裁
场景背景

平台日订单五百万,多仓多商家混合履约。硬指标:拆单规则(仓/商家/商品类型/重量/时效产品)配置化且准确率 100%;合单降本(同仓同时段单合并发货);履约时效承诺达成率 ≥ 95%(超时赔付);售后仲裁(仅退款/退货退款/换货/维修)有据可依、处理时效可承诺。

核心挑战

① 拆单规则复杂:多维规则组合还经常调,硬编码必烂;② 合单与体验平衡:合单省运费但拉长用户等待,等多久、哪些单等;③ 时效计算:发货时效 + 物流时效 + 仓作业时段,还要算截单时间与节假日;④ 售后类型多:仅退款(未发货/已发货)、退货退款、换货、维修,凭证审核与防薅;⑤ 逆向物流:上门取件/自行寄回,退款与退货入库联动。

分析框架

拆单 = 规则链引擎:每条规则输出「必拆/可合/目标仓」,链式执行(成本优先:时效优先的规则前置);规则 DSL 配置化 + 版本化,变更可灰度。合单 = 等待窗口:下单后进入合单池,窗口期(如两小时)内同仓单合并,窗口结束或达批量上限即打包;时效敏感商品(生鲜)不进合单池。时效承诺 = 计算引擎(仓时段 + 承运商时效 + 截单规则),承诺时间在下单时给出并随履约单跟踪。售后 = 独立状态机 + 凭证审核 + 风控(防薅),与 D03.01 订单状态双向同步。

领域模型

聚合根:履约单(FulfillmentOrder,仓、物流、时效承诺、节点轨迹)。实体:拆单规则(SplitRule,条件 + 动作 + 优先级)、售后单(AfterSaleOrder,类型、凭证、状态)、仲裁记录(Arbitration)。值对象:时效承诺(发货时限 + 预计到达)。不变量:一行商品只能归属一个履约单(拆后不相交);售后单金额 ≤ 原行实付;仲裁结论必须引用凭证与规则条款。

解决方案

1. 拆单:规则链(商家必拆 → 仓可发性 → 商品类型(大件/冷链分离)→ 重量段 → 时效产品优先);规则引擎预编译,变更影子比对后灰度。

2. 合单:合单池按「仓 + 收件人 + 品类」聚合,窗口期合并;生鲜/贵重/时效品直发;合单结果通知用户(等多久、省多少)。

3. 时效:下单时计算承诺(仓作业时段 + 承运商线路时效 + 截单点);履约单按节点跟踪(揽收/运输/派送),超时自动赔付或安抚。

4. 售后:售后单状态机(申请 → 审核 → 退货物流 → 入库 → 退款/换货 → 完结);凭证审核(图片 + 物流单号规则校验);仅退款走风控(信用分 + 频次,防薅);争议进人工仲裁,结论引用规则条款留痕。

graph TD A[已支付订单] --> B[拆单规则链] B --> C[履约单生成] C --> D{合单池} D -->|时效敏感| E[直发] D -->|可合| F[窗口期合并] F --> G[仓作业 WMS] E --> G G --> H[物流跟踪] H --> I{超时} I -->|是| J[自动赔付/安抚] K[售后申请] --> L[凭证审核+风控] L --> M[售后状态机] M -->|退货| N[逆向物流 入库] N --> O[退款/换货]
拆合单履约与售后逆向链路
关键代码
// 拆单规则链:规则按优先级排序,任一「必拆」即终止合并
public List<FulfillmentGroup> split(Order order, SplitContext ctx) {
    List<FulfillmentGroup> groups = new ArrayList<>();
    for (SplitRule r : rules.sortedByPriority()) {
        if (r.matches(order, ctx)) {
            if (r.action() == MUST_SPLIT) { groups.addAll(r.apply(order)); break; }
            if (r.action() == PREFER_MERGE) { /* 记录偏好继续 */ }
        }
    }
    return groups;
}

// 时效承诺:仓时段 + 承运商时效 + 截单点,下单即算出可承诺值
public Promise calcPromise(Warehouse wh, Carrier carrier, LocalDateTime now) {
    LocalDateTime cutOff = wh.todayCutoff(now);
    LocalDateTime shipBy = now.isAfter(cutOff) ? wh.nextWorkdayStart(now) : now;
    return new Promise(shipBy.plusHours(wh.pickHours()),
            carrier.eta(wh, shipBy).orElseThrow());
}
深度追问
  • 合单把用户等生气了怎么办?
    合单是「用时间换运费」的交易,必须透明:下单时告知「合并发货预计省 X 元、可能延迟 Y 小时」,给用户「不想等」的一键直发选项。时效敏感品类默认不合单。暗着合单是客诉与投诉双雷。
  • 仅退款怎么防薅?
    风控前置:信用分低/高频售后/物流未签收即申请的进人工;已发货仅退款要物流拦截或拒收凭证;金额阈值以上强制人工。防薅与体验平衡——把「可信用户秒退、可疑用户细查」做成策略,不是一刀切全人工。
  • 部分发货后剩余行取消?
    按履约单拆分处理:已发履约单走售后(拒收/退货),未发部分正常关单退实付;金额按行分摊计算(见 D04.02 分摊规则),父单状态机同步部分完结标记。
  • 逆向物流怎么跟踪?
    逆向运单号与售后单绑定,回传物流节点;仓库收货时按售后单预录入快速入库;用户自行寄回的,凭证(运单号)审核通过才启动退款,防止「假退货真退款」。
常见陷阱
  • 拆单规则硬编码:运营每调一次走发版,大促前规则全改直接崩;规则 DSL + 灰度。
  • 售后无凭证审核:仅退款被批量薅,黑产公开教程;凭证 + 风控 + 阈值人工。
  • 时效承诺无监控:承诺了不跟踪,超时不赔付,监管投诉上门;承诺→跟踪→赔付闭环。
  • 逆向物流黑盒:用户寄回了但系统不知道,退款卡住;逆向运单节点必须回传。
破题点拨
一眼定位:履约 = 拆单规则链 + 合单窗口 + 时效承诺闭环 + 售后独立状态机。
破题主线:规则链拆单 → 合单池(透明告知)→ 时效计算与跟踪 → 售后凭证风控 → 逆向物流回传。
易错点:规则硬编码;仅退款无风控;承诺不跟踪;逆向黑盒。
D03.05
购物车与下单试算:价格计算与事务边界高级P1
领域模型 购物车 价格试算 快照锁定
场景背景

购物车日活一亿,平均每车十余件商品,跨店混合常见。硬指标:试算(商品价 + 运费 + 优惠 + 积分抵扣)< 100ms 且与最终实付一致;多端(App/PC/小程序)购物车合并;失效商品(下架/无货/禁售)自动清理或提示;大促试算量十倍峰值。

核心挑战

① 存储模型:存 SKU 引用还是存价格——存价格则变价不同步,存引用则每次要重算;② 试算与实付一致:试算后十分钟才支付,价格/优惠可能已变;③ 优惠叠加顺序:店铺券、平台券、满减的先后影响结果;④ 多端合并:同一用户多设备加购,合并策略(并集/以新为准);⑤ 失效处理:失效商品不能阻断整单结算。

分析框架

购物车存引用(SKU + 数量 + 选中态)不存价格,价格实时算——保证看到的价格永远是最新的,同时避免「购物车里价格三个月不变」的虚假感。下单时做两件事:试算锁定(生成报价单,价格 + 优惠 + 运费快照,带短时效)→ 订单快照(支付成功后行项目固化,在途订单此后与市场价无关)。优惠计算顺序固定为:商品级 → 店铺级 → 平台级(D04.02 价格引擎),同层内按优先级,保证「同输入同输出」可复现。

领域模型

聚合根:购物车(Cart,用户维度,条目列表)。实体:购物车条目(CartItem,SKU、数量、选中态、加入时间)、报价单(Quotation,快照 + 过期时间)。值对象:SKU 引用。不变量:单 SKU 在购物车至多一条(数量合并);报价单金额构成可重算复现(同快照同结果)。

解决方案

1. 存储:Redis Hash(用户 → 条目)+ DB 兜底持久化;条目存 SKU/数量/选中/时间戳,不存价格。

2. 多端合并:按加入时间戳取并集,同 SKU 数量相加(或以后端策略为准);本地未登录车在登录时合并。

3. 试算:批量取价(商品服务)+ 库存可发校验 + 价格引擎(D04.02)+ 运费计算;失效商品分区展示(可选/失效/无货),不阻断结算。

4. 锁定:下单生成报价单(快照 + 分钟级 TTL),支付按报价单建单;报价过期则重新试算并提示用户确认——价格变了要说清楚,不能偷偷按新价扣款。

graph TD A[加购 SKU引用] --> B[购物车 Hash 多端合并] B --> C[结算页试算] C --> D[批量取价+可发校验] D --> E[价格引擎 顺序固定] E --> F[运费计算] F --> G[报价单 快照TTL] G -->|未过期| H[按报价建单] G -->|过期| I[重试算 用户确认] H --> J[订单行快照固化]
购物车引用模型与试算锁定
关键代码
// 试算:批量取价 + 价格引擎(顺序固定:商品→店铺→平台),同输入同输出
public Quotation quote(Long userId, List<CartItem> items) {
    Map<Long, Price> prices = priceClient.batchCurrent(items.keySet());   // 实时取价,不读车内存价
    List<Line> lines = items.stream()
        .map(i -> Line.of(i.skuId(), i.qty(), prices.get(i.skuId())))
        .filter(l -> l.available())                                            // 失效/无货分区
        .toList();
    PriceResult pr = priceEngine.calc(lines, ctxOf(userId));                   // 商品→店铺→平台 固定顺序
    Money freight = freightEngine.calc(lines, ctxOf(userId));
    return Quotation.snapshot(lines, pr, freight, Duration.ofMinutes(15));
}

// 建单只认报价单:过期即拒,让前端重试算(价格变了必须用户确认)
public Order createFromQuotation(String quoteId, Long userId) {
    Quotation q = quotationRepo.get(quoteId);
    if (q == null || q.expired() || !q.ownerIs(userId))
        throw new QuoteExpiredException();
    return orderService.create(q.toOrderCmd());   // 行项目用快照价,与市场价脱钩
}
深度追问
  • 购物车存价格会出什么问题?
    用户三天前加购看到九十九,结算时实际五十九——体验割裂且投诉「虚假价格」;促销价过期后还显示旧价更麻烦。引用模型 + 实时取价 + 报价单快照三层配合,才是「看得新、付得明」。
  • 优惠叠加顺序错了会怎样?
    同样商品,先平台券再店铺券和反过来差几块钱——用户比价后觉得「杀熟」,且财务对账说不清。顺序必须固定且公示(商品→店铺→平台),结果可复现,客诉有据可查。
  • 购物车上限怎么设?
    按 SKU 数(如一百)+ 店铺数(如五十)双限,防「把全站加进购物车」的性能攻击;超限引导去收藏夹。大促期间可临时放宽但需限流保护。
  • 试算和实付不一致怎么兜?
    报价单机制保证「建单价 = 报价快照价」;若支付成功后价格引擎补款差异(如运费到付),走显式「补差价」流程而非静默扣款。任何资金变动都要用户可见——静默扣款是客诉与合规双雷。
常见陷阱
  • 购物车存价格:变价不同步、过期价误导;存引用实时算。
  • 试算无锁定:十分钟后支付按新价扣款,用户被「偷袭」;报价单快照 + 过期重确认。
  • 优惠顺序不定:同车不同结果,对账与客诉双输;固定顺序 + 可复现。
  • 失效商品阻断结算:一件无货卡住整单;分区展示 + 一键清失效。
破题点拨
一眼定位:购物车 = 引用存储 + 实时试算 + 报价单锁定 + 下单快照。
破题主线:Hash 存储多端合并 → 批量取价 → 固定顺序价格引擎 → 报价单 TTL → 快照建单。
易错点:存价格;无锁定;顺序不定;失效阻断。
← 技术范式引用 S02.02 跨服务分布式事务方案
D03.06
商家结算中心:多商户分账、结算周期与保证金架构P0
高频 领域模型 架构 三户模型 账期结算 保证金
场景背景

平台型电商,自营加三方数十万商家。硬指标:结算准确率(分到分不能错)、结算时效(确认收货后按账期出结算单)、打款成功率、逆向结算(退款/赔付冲抵)处理时长。商家资金涉及待结算、可提现、保证金三个口袋,任何差错都是资金事故。

核心挑战

① 分账计算:订单实付要在平台佣金、支付通道费、商家货款、积分抵扣、运费险之间拆分,一分钱误差都会引发商家纠纷;② 账期与确认:确认收货时点、售后期结束、账期(月结/半月结)多版本并存;③ 逆向冲抵:结算单已出又发生退款,冲抵本期还是下期,规则要对商家透明;④ 资金安全:平台不得挪用商家待结算资金,待结算与平台自有资金严格隔离;⑤ 打款与失败:批量打款失败(卡号错/户名错)重试与退票处理。

分析框架

商家的钱分三个口袋:待结算(未过账期/售后期)、可提现(可打款)、保证金(冻结担保),所有资金变动走不可变流水。应收明细按订单维度逐笔记账(复式记账,与 D01.02 同源),日切聚合生成结算单;结算单是打款的唯一依据,先结后打、一笔一清。逆向交易(退款/赔付/违约扣款)生成负向明细冲抵对应口袋。打款走银企直连批量文件,失败原路退回可提现并通知商家。

领域模型

聚合根:商家账户(MerchantAccount,待结算/可提现/保证金/版本)。实体:应收明细(Receivable,订单维度 + 分账项 + 状态)、结算单(Settlement,周期 + 明细汇总 + 金额)、打款单(Payout,批次 + 银行流水)、保证金单(Deposit,缴纳/扣除/解冻)。值对象:分账比例、账期。不变量:三户之和 = 商家权益总额;结算单金额 = 明细汇总(逐明细对账);打款单必须引用结算单;待结算资金不得用于平台其他用途(监管红线)。

解决方案

1. 分账引擎:订单确认收货时按商家签约的费率模型(佣金 + 通道费 + 增值服务)拆分生成应收明细;费率模型版本化(按订单时间取生效版本),保证历史可重放。

2. 账期与日切:日切把「已过售后期」的明细从未结算池移入待结算池;账期(月结/半月结)到点按商家聚合生成结算单,试算平衡后生效。

3. 打款:结算单驱动打款批次,银企直连文件提交;成功回单核销、失败退票回滚可提现并告警;打款状态机(待打款→已提交→成功/失败)全程可查。

4. 逆向处理:退款按原路冲抵——未结算冲明细、已结算生成负向结算单(下期抵扣);赔付与违约扣款同样生成负向明细,商家侧可见每一笔。

5. 保证金:入驻缴纳、违规扣除(走处罚单)、关店解冻(售后争议期满后),全流水留痕。

graph TD A[订单确认收货] --> B[分账引擎 费率版本化] B --> C[应收明细 复式记账] C --> D[日切 售后期结束] D --> E[待结算池] E -->|账期到点| F[结算单 试算平衡] F --> G[打款批次 银企直连] G -->|成功| H[回单核销] G -->|失败| I[退票回滚+告警] J[退款/赔付] --> K[负向明细 冲抵] K --> C L[保证金 缴纳/扣除/解冻] --> M[三户模型]
商家三户与结算打款主链路
关键代码
// 日切:售后期已过的明细从未结算池移入待结算池(幂等,按明细状态推进)
List<Receivable> ready = receivableRepo.findMatured(today, State.UNSETTLED);
for (Receivable r : ready) {
    int n = receivableRepo.advance(r.getId(), State.UNSETTLED, State.PENDING_SETTLE);
    if (n > 0) ledger.post(r.toEntry());   // 状态推进成功才入账
}

// 结算单生成:聚合待结算明细,试算平衡后才允许生效
Settlement st = Settlement.aggregate(merchantId, period, pendingDetails);
st.verify();                     // 汇总金额 = 逐明细之和,不平抛异常
settlementRepo.save(st);         // 生效后即打款唯一依据

// 逆向冲抵:已结算的退款生成负向结算单,下期抵扣
if (detail.isSettled()) {
    settlementRepo.save(Settlement.negativeOf(detail, refundAmount));
} else {
    detail.writeOff(refundAmount);   // 未结算直接冲明细
}
深度追问
  • 分账差一分钱怎么查?
    分账明细按订单逐笔记账,汇总与结算单必须逐明细核对(不是只对总额);差额定位到明细后按冲正流程处理,并回放费率版本确认是不是费率取错。日常跑「明细-汇总-结算单」三方对账,当天差异当天清。
  • 结算单已出又退款,钱怎么回来?
    已结算部分生成负向结算单计入下期,未结算部分直接冲原明细;规则写进商家协议,商家后台每笔冲抵可见。切忌「从下期总额里悄悄扣」——不透明就是纠纷源。
  • 怎么保证平台不动商家钱?
    待结算资金进监管账户/银行存管,平台侧只有指令权;系统层面待结算余额与打款指令一一对应,日终与银行存管账户对账。合规上这是支付机构与平台的红线,架构上从账户隔离开始设计。
  • 打款失败怎么兜底?
    失败原路退回可提现(不加收费用),自动重试有限次数(防卡号临时冻结误判),同时通知商家修改卡号;退票资金不得滞留中间账户,当天回滚。批量打款要对银行回执逐笔核销,不是「提交了就算成功」。
常见陷阱
  • 总额对齐就行:只对汇总不对明细,差一分钱查一周;明细级对账是底线。
  • 费率不版本化:签约后调费率影响历史订单重放;费率按订单时间取生效版本。
  • 逆向无冲抵规则:退款发生时结算单已出,没规则就靠人工补,规模一大就崩。
  • 资金混同:待结算资金进平台自有账户,挪用即重大合规风险;隔离从账户层做起。
破题点拨
一眼定位:商家结算 = 三户模型 + 明细级复式记账 + 结算单驱动打款 + 逆向冲抵透明。
破题主线:分账费率版本化 → 明细逐笔记账 → 日切+账期出结算单 → 先结后打 → 失败退票 → 逆向冲抵可见。
易错点:只对总额;费率不版本化;逆向靠人工;资金混同。

四 电商 · 营销与定价 6 题

本组知识链

营销系统的骨架是「预算约束下的规则引擎」。覆盖优惠券、促销价格、积分、价保、投放治理与消息触达。

  1. 优惠券:模板-批次-实例三级,领取防超领、核销幂等。
  2. 价格引擎:分层叠加、确定顺序、分摊快照、最优价。
  3. 积分:积分账务化,流水不可变、过期成流水。
  4. 价保与投放:价差重算原路退;投放限速、预算熔断、ROI 归因。
  5. 消息触达:通道路由按成本/到达率选路,疲劳度前置,归因口径统一。
工程化要点
  • 券的发放/领取/核销全部幂等;库存与限领原子扣减,防超发防超领。
  • 价格计算确定性:同输入同输出,顺序固定、分摊可复现、结果快照。
  • 预算是一等公民:实时扣减、超限熔断、多渠道分摊可对账。
  • 营销规则配置化 + 版本化,变更影子比对后灰度,可一键回滚。
本组思维模式
  • 规则可配置优先于硬编码:营销规则一周三变,代码要追着业务跑必死。
  • 预算优先于效果:没有预算熔断的营销系统,出事就是一晚上。
  • 叠加有序、结果可复现:优惠计算是资金计算,必须像账务一样严谨。
  • 营销效果可度量:发放→领取→核销→GMV 全链路归因,没有度量的投放是撒钱。
合规与资损红线
  • 券不可超发、核销不可重复、预算不可透支——三者都是真金白银。
  • 活动规则必须公示且可追溯,禁止虚构原价等价格欺诈行为。
  • 积分抵现比例、券使用范围变更不影响已发放未使用券的既有权益。
  • 投放圈人使用个人信息需符合个保法(告知同意、最小必要,见 D02.04)。
D04.01
优惠券系统:模板、批次、核销与防超领设计架构P0
高频 领域模型 架构 状态机 原子扣减 幂等核销
场景背景

券平台日发券五千万张,大促峰值领取百万 QPS。券类型含满减、折扣、兑换码。硬指标:不超发(发了没有的券=资损)、不超领(一人一券/活动限领)、核销准确(重复核销=资金漏洞)、券全程可追溯(领取-使用-退款回退-过期)。券还参与价格引擎叠加(D04.02)。

核心挑战

① 高并发领取:百万 QPS 抢券,库存扣减热点;② 限领规则:一人一券、活动期间限领 N 张,判断与扣减要原子;③ 核销一致性:券与订单绑定,订单取消/部分退款要回退;④ 过期:过期自动失效 + 到期前提醒,扫描任务量大;⑤ 券与在途订单:下单锁定券,超时未付释放。

分析框架

三级模型:模板(规则)→ 批次(库存 + 发放窗口)→ 实例(用户券 + 状态机)。领取走 Redis Lua 原子扣批次库存 + 写限领标记,DB 异步落实例(消息补偿保证不丢);核销与订单同事务(或事务消息),券实例状态机(未用 → 已用 → 已退/已过期);过期用延迟消息 + 定时扫描双通道。所有动作幂等,批次库存是唯一发放阀门。

领域模型

聚合根:券批次(Batch,模板 ID、总库存、剩余、发放窗口、限领规则)。实体:用户券(UserCoupon,批次、状态、绑定订单)、券模板(CouponTemplate,面额、门槛、适用范围、叠加规则)。值对象:面额、门槛。不变量:已领 + 剩余 = 总库存(超发即违反);单用户限领数由批次规则约束;已核销券不可二次使用;退款按券规则回退(原路退回券 or 不退)。

解决方案

1. 领取:Lua 脚本原子完成「批次库存扣减 + 用户限领计数」,失败即已领完/已超限;成功发 MQ 异步创建用户券实例(本地消息表保证不丢不发重)。

2. 核销:下单时锁券(未用 → 锁定中,带订单号);支付成功转已用并记录订单;取消/超时释放回未用;核销幂等(券 ID + 订单号唯一)。

3. 退款回退:整单退按券规则原路退回券(返还到账户、可能重置有效期);部分退按分摊比例处理(见 D04.02 分摊);不退规则要在券模板显式声明。

4. 过期:过期时间进延迟消息队列,到期置过期;兜底扫描每日对「应过期未过期」补处理;到期前(如三天)推送提醒。

stateDiagram-v2 [*] --> 未使用: 领取成功 未使用 --> 锁定中: 下单锁券 锁定中 --> 未使用: 取消/超时释放 锁定中 --> 已使用: 支付成功核销 未使用 --> 已过期: 到期 锁定中 --> 已过期: 到期强制释放 已使用 --> 未使用: 退款按规则退回 已使用 --> [*] 已过期 --> [*]
用户券状态机(锁定-核销-回退-过期)
领取架构 优点 缺点
直接扣 DB 库存 强一致 百万 QPS 打穿,行锁热点
Redis Lua 扣库存 + MQ 异步建实例 抗峰值、快失败 需消息补偿与库存对账兜底
关键代码
// 领取 Lua:批次库存 + 用户限领 同一脚本原子完成(禁拆成两次调用)
private static final String CLAIM =
    "local left = tonumber(redis.call('get', KEYS[1])) " +
    "if not left or left <= 0 then return 0 end " +                          // 已领完
    "local got = tonumber(redis.call('get', KEYS[2]) or '0') " +
    "if got >= tonumber(ARGV[1]) then return -1 end " +                     // 超个人限领
    "redis.call('decr', KEYS[1]) redis.call('incr', KEYS[2]) return 1";

public ClaimResult claim(Long batchId, Long userId) {
    long r = redis.execute(CLAIM, List.of("batch:left:" + batchId, "user:got:" + batchId + ":" + userId),
        String.valueOf(batchRules.perUserLimit(batchId)));
    if (r != 1) return ClaimResult.reject(r);                       // 0=领完 -1=超限
    outbox.save(new CouponClaimedMsg(batchId, userId));                 // 同事务:MQ 异步建实例,不丢不发重
    return ClaimResult.ok();
}

// 核销幂等:唯一索引(coupon_id, order_no),重复核销撞索引即忽略
int n = jdbc.update("UPDATE user_coupon SET status='USED', order_no=? WHERE id=? AND status='LOCKED' AND order_no=?",
    orderNo, couponId, orderNo);
深度追问
  • 部分退款时券怎么退?
    按行分摊比例退:券抵扣金额按「该行实付占比」分摊,退对应部分;整单退才整券退回。券退回时重置有效期(按退回时刻 + 模板有效期),并通知用户。规则必须在券模板显式声明——「最终解释权归平台」在法律上站不住。
  • Redis 库存与 DB 实例数不一致?
    Redis 是发放阀门(性能),DB 实例是事实源(正确性)。日终对账:DB 已领实例数 + Redis 剩余 = 总库存;不一致以 DB 为准修 Redis 阀门。极端情况(Redis 被清)用 DB 已领数重建。
  • 批量发券(圈定用户群)怎么做?
    走发放任务:用户群列表 → 批量写实例(分片 + 限速,避免打爆 DB)→ 到账通知。与「用户主动领取」是两条路径,但实例模型一致;批量发不受批次库存限(按名单发),但要有总量熔断防误操作发全军。
  • 券被黑产批量扫了怎么办?
    三层:领取接口限流 + 设备风控(D02.02 反欺诈接入)+ 异常批次熔断(领取速率/设备集中度异常自动暂停批次)。事后按设备指纹与关系图谱回溯已发券批量作废(作废留痕并通知)。
常见陷阱
  • 库存扣减非原子:先查剩余再扣,并发下超发;Lua 原子是底线。
  • 核销不幂等:重复通知/重复请求导致一券多用;唯一索引兜底。
  • 退款不回退券:用户退货了券也没了,客诉 + 合规风险;回退规则进模板。
  • 过期不处理:过期券还能用,资金漏洞;延迟消息 + 扫描双通道。
破题点拨
一眼定位:券系统 = 三级模型 + 实例状态机 + 原子领取 + 幂等核销。
破题主线:Lua 原子领 → MQ 建实例 → 锁券核销幂等 → 退款按规则回退 → 过期双通道。
易错点:非原子扣库存;核销不幂等;退款不退券;过期不清。
← 技术范式引用 S01.01 百万 QPS 秒杀系统设计
D04.02
促销价格引擎:满减折扣叠加与最优价计算架构P1
高频 架构 规则引擎 优惠分摊 可复现
场景背景

价格引擎日计算十亿次。促销类型含满减、满折、直降、多买优惠(第二件半价)、N 元 N 件。硬指标:同购物车计算结果确定可复现(同输入同输出)、叠加顺序符合商家与平台约定、计算 < 50ms(大促 < 100ms)、优惠分摊到行可支持部分退款。

核心挑战

① 叠加顺序:商品级/店铺级/平台级促销如何排列,同层多活动如何取舍;② 互斥:同一商品多个活动互斥组配置,选错活动用户觉得「被套路」;③ 最优价:多活动组合爆炸,穷举不可行;④ 分摊:订单级优惠如何分摊到行,直接决定部分退款退多少;⑤ 一致性:试算价(D03.05)与实付价必须一致,优惠在途失效怎么办。

分析框架

分层计算:商品级 → 店铺级 → 平台级逐层应用,同层内按优先级(可配互斥组);每层输入输出都是「行价 + 行优惠」结构,天然支持分摊(订单级优惠按行实付占比分摊,余数分给最后一档保证总和守恒)。最优价不穷举:按「层内取最优单活动 + 层间固定顺序」近似(精确最优限定组合规模,超限降级近似)——营销是商业策略不是数学竞赛。结果带快照(用了哪些活动、每层优惠额),保证可复现可解释。

领域模型

聚合根:促销(Promotion,类型、规则 DSL、优先级、互斥组、适用维度)。实体:价格计算上下文(CalcContext,行列表 + 用户 + 渠道 + 券)。值对象:计算结果(PriceResult,行实付 + 分摊明细 + 命中活动快照)。不变量:① Σ行优惠 = Σ层优惠(分摊守恒,余数进调节行);② 同互斥组至多一个活动生效;③ 相同上下文重算结果逐字节一致(禁随机/禁时间依赖——除活动生效区间本身)。

解决方案

1. 规则组织:促销按维度(商品/店铺/平台)分索引,查询按购物车批量取适用活动;互斥组预检,冲突活动按优先级胜出。

2. 分层计算:商品级(直降/多买优惠)→ 店铺级(满减/满折)→ 平台级(跨店满减 + 券,D04.01 接入);每层输出更新后的行价。

3. 分摊:订单级优惠按「行实付占比」分摊到行,余数(分位差)调节到指定行;分摊结果随订单快照固化——部分退款按行分摊退,口径唯一。

4. 最优价(可选能力):对「可叠加候选」做有界搜索(候选 ≤ K 个时精确,否则层内取最优);搜索超时(如 20ms)降级为优先级方案,保时效。

5. 一致性:报价单快照含「命中活动 + 各行优惠」,支付按快照建单;活动中途失效不影响已报价订单(快照隔离),只影响新报价。

graph TD A[购物车行列表] --> B[取适用活动 分维度索引] B --> C[互斥组预检] C --> D[商品级计算] D --> E[店铺级计算] E --> F[平台级+券] F --> G[行优惠分摊 守恒] G --> H[报价快照 命中活动] H --> I[订单行固化] J[最优价搜索 有界] -.->|超时降级| D
价格引擎分层计算与分摊
叠加策略 优点 缺点
并行全组合择优 理论最优 组合爆炸,大促必超时
固定分层顺序 可复现、可解释、性能稳 非数学最优,需商业上接受
分层 + 有界搜索 平衡最优与时效 实现复杂,需降级策略
关键代码
// 分层计算:每层输入出行列表,输出更新后行价;顺序固定保证可复现
public PriceResult calc(List<Line> lines, CalcContext ctx) {
    List<Line> cur = lines;
    List<Applied> applied = new ArrayList<>();
    for (Dimension dim : List.of(ITEM, SHOP, PLATFORM)) {           // 固定三层顺序
        List<Promotion> acts = promoIndex.findApplicable(dim, cur, ctx);
        acts = exclusivity.resolve(acts);                          // 互斥组:同组取优先级最高
        for (Promotion p : acts) { cur = p.apply(cur); applied.add(new Applied(p.id(), p.discount(cur))); }
    }
    return PriceResult.of(cur, applied, ctx.coupons());
}

// 订单级优惠分摊:按行实付占比,余数进调节行,保证 Σ守恒
public List<LineShare> allocate(Money orderDiscount, List<Line> lines) {
    Money total = lines.stream().map(Line::payable).reduce(Money.ZERO, Money::add);
    List<LineShare> shares = new ArrayList<>();
    Money allocated = Money.ZERO;
    for (int i = 0; i < lines.size(); i++) {
        Money share = (i == lines.size() - 1)
            ? orderDiscount.subtract(allocated)                    // 末行兜余数,总和精确守恒
            : orderDiscount.multiply(lines.get(i).payable()).divide(total);
        allocated = allocated.add(share);
        shares.add(new LineShare(lines.get(i).id(), share));
    }
    return shares;
}
深度追问
  • 为什么不让用户「手动选最优组合」?
    组合爆炸大促必超时,且手动选择把比价成本转嫁给用户,平台还会被「套路感」投诉。业界通行做法是固定分层顺序 + 层内智能选,并在结算页明示「已享最优」——可解释比可穷举重要。
  • 分摊对售后有什么影响?
    分摊决定部分退款退多少:行实付 = 行原价 − 行分摊优惠,退款按行实付比例走;没有分摊明细,部分退款就只能拍脑袋,客诉与资损双输。分摊快照必须随订单固化。
  • 活动进行中改规则怎么办?
    促销配置版本化:新版本带生效时间,已报价订单用旧版本快照(隔离在途);新报价用新版本。绝不允许「进行中热改且追溯生效」——那是价格事故标准姿势。
  • 跨境多币种促销怎么算?
    每币种独立一套价格引擎上下文(币种维度隔离),满减门槛按币种配置,不做跨币种凑单;结算汇率在支付层处理(D01.06)。促销计算只认单币种——混算必错。
常见陷阱
  • 顺序不确定:并发/遍历顺序影响结果,同车两次算两个价;固定分层 + 可复现。
  • 分摊不守恒:各行优惠之和 ≠ 订单优惠,退款时对不上;末行兜余数。
  • 最优价穷举:大促十种活动组合超时,结算页卡死;有界搜索 + 降级。
  • 无快照:活动改了,已下单订单跟着变;报价快照隔离在途。
破题点拨
一眼定位:价格引擎 = 分层固定顺序 + 互斥预检 + 分摊守恒 + 快照可复现。
破题主线:分维度索引取活动 → 三层顺序计算 → 互斥组取舍 → 占比分摊(余数兜底)→ 快照固化。
易错点:顺序不定;分摊不守恒;穷举超时;无快照隔离。
D04.03
会员积分体系:积分账务、过期与冻结高级P1
领域模型 积分账务 过期策略 冻结解冻
场景背景

积分体系日变动五千万笔,来源含购物返积分、活动奖励、签到;消耗含抵现、兑换、加钱购。硬指标:积分账实相符(余额可重算)、过期策略准确(过期即失效、到期可提醒)、冻结解冻闭环(下单占用、取消释放)、日终与账务对账一致。

核心挑战

① 账实一致:余额是物化缓存,必须能从流水重算验证;② 过期策略:固定过期(年度清零)vs 滚动过期(到账后 N 天),先过哪批积分影响用户体验;③ 抵现换算:积分与钱的兑换比例、抵扣上限、是否可组合券;④ 冻结:下单锁积分、取消释放、超时自动解冻;⑤ 退款回收:退款时积分按比例回收,已过期部分怎么算。

分析框架

核心思想:积分当钱管——账务化(同 D01.02)。流水 append-only,每笔带批次与过期时间;余额 = Σ有效流水;过期任务生成「过期扣减」流水而非直接改余额;冻结解冻成对;退款回收生成反向流水。日终对账:积分流水与账务积分科目核对。

领域模型

聚合根:积分账户(PointsAccount,可用余额、冻结、版本)。实体:积分流水(PointsEntry,类型:获取/消耗/过期/冻结/解冻/回收,批次,过期时间)。值对象:积分数量。不变量:可用 = Σ未过期未冻结流水;每笔消耗有对应获取来源(可追溯);冻结必有解冻或转消耗路径。

解决方案

1. 记账:获取/消耗都写流水(唯一索引幂等),余额物化同事务更新;抵现比例与上限配置化。

2. 过期:流水带过期时间,日终扫描生成过期扣减流水(先过最早到期批次——FIFO,对用户最公平);到期前三天推送提醒。

3. 冻结:下单锁积分(可用减、冻结加),取消/超时释放(幂等);抵现支付成功转消耗。

4. 回收:退款按原消耗流水比例生成回收流水;对应积分已过期的,按规则(退回过期批次 or 折算补偿)显式处理并告知用户。

graph TD A[购物/活动返积分] --> B[获取流水 带批次过期时间] B --> C[积分账户 可用+冻结] D[下单抵现] -->|冻结| C C -->|取消/超时| E[解冻流水] C -->|支付成功| F[消耗流水] G[日终过期扫描] -->|FIFO| H[过期扣减流水] I[退款] --> J[回收流水 按比例] C --> K[日终与账务对账]
积分账务化:流水、冻结与过期
关键代码
// 消耗:扣最早到期批次(FIFO),每批生成一条消耗流水,可追溯
public void consume(Long acctId, int amount, String bizNo) {
    List<BatchBalance> batches = batchRepo.findAvailable(acctId, Order.byExpireAsc());
    int rest = amount;
    for (BatchBalance b : batches) {
        int take = Math.min(rest, b.remaining());
        entryRepo.insert(PointsEntry.consume(acctId, take, b.batchId(), bizNo));  // 幂等键兜底
        rest -= take;
        if (rest == 0) break;
    }
    if (rest > 0) throw new InsufficientPointsException();
    balanceRepo.applyDelta(acctId, -amount, bizNo);
}

// 过期扫描:只生成扣减流水,禁直接改余额(同账务铁律)
for (BatchBalance b : batchRepo.findExpiredBefore(today)) {
    entryRepo.insert(PointsEntry.expire(acctId, b.remaining(), b.batchId()));
    balanceRepo.applyDelta(acctId, -b.remaining(), "EXPIRED:" + b.batchId());
}
深度追问
  • 为什么过期不能直接改余额?
    与账务同理(D01.02):余额是缓存、流水是事实源。直接改余额会让「余额 ≠ Σ流水」且无法向用户解释「为什么我的积分少了」——过期流水本身就是给用户看的凭证,客服工单直接引用。
  • 积分过期策略怎么选?
    固定过期(年度清零)利于平台负债管理但体验差;滚动过期(到账后 N 天)体验好但实现复杂。多数平台用滚动 + 临近提醒;策略必须在用户协议写清,且过期前多触点提醒——悄悄清零是投诉重灾区。
  • 积分套利怎么防?
    大额积分变动(获取/消耗)过风控(D02.01);消耗与退款回收联动,退货运费险式「套积分」走比例回收 + 频次限制;兑换商品发货前冻结,取消释放。
  • 积分负债(未消耗部分)怎么管?
    积分是平台负债,财务上要计提:日终输出「各过期批次未消耗积分」报表给财务做负债评估;大促前做积分负债压力测算——发出去的每一分都是要还的。
常见陷阱
  • 余额直接改:无流水不可解释不可审计;账务化是唯一解。
  • 过期策略混乱:同一账户多套策略,用户问「我的积分为什么没了」答不上来。
  • 冻结无释放:下单锁了分、取消没还,客诉爆炸;释放幂等且常驻。
  • 无对账:积分与账务科目长期不平,财务审计才发现。
破题点拨
一眼定位:积分 = 账务化流水 + 批次过期 + 冻结闭环 + 负债视角。
破题主线:流水 append-only → FIFO 消耗/过期 → 冻结释放成对 → 退款按比例回收 → 日终对账。
易错点:直接改余额;策略不清;只冻不放;与账务不平。
D04.04
价保与差价:规则匹配、差价计算与退款一致性高级P2
领域模型 价保 差价计算 防套利
场景背景

价保服务日申请五十万。规则:同店同品 N 天内降价退差价(排除秒杀、限购等特殊活动)。硬指标:差价计算准确(含用券订单)、退款原路一致、防套利(先买再退再买)、申请处理时效可承诺。用券订单按行实付重算差价;部分退只覆盖剩余行;跨店不并单。防套利:同一用户同一 SKU 在价保窗口内限制「退了再买」。

核心挑战

① 规则匹配:同 SKU/同店铺/时间窗/排除项(活动类型、 coupons);② 差价计算:现价 vs 实付的差,含券订单怎么算(券是否可比);③ 防套利:频繁价保、价保后立刻重下;④ 与原单关联:部分退货后剩余部分的价保;⑤ 与原路退出一致性:差价退到哪里、券差价怎么退。

分析框架

价保单独立成单:规则匹配(SKU + 时间窗 + 排除项)→ 差价重算(当前价 vs 原实付,走价格引擎 D04.02 口径)→ 风控审查(频次/金额/行为)→ 原路退差。差价一律按「实付口径」计算(把原单快照拉出来重算,不拍脑袋);退款走原支付渠道,券差价退券或按比例退。防套利用频次与行为规则,不搞一刀切拒绝。

领域模型

聚合根:价保单(PriceProtectionOrder,原单、SKU、价差、状态)。实体:价保规则(PPRule,时间窗、排除活动类型、适用范围)。值对象:价差。不变量:价差 = 原实付 − 现应付(同商品同数量同优惠口径);一单同 SKU 价保次数受限(规则控制);价保单必须引用原单与价格快照。

解决方案

1. 匹配:按原单行项目取 SKU + 店铺,查当前价;时间窗(如七天)与排除项(活动类型黑名单)规则化配置。

2. 重算:用价格引擎以「原优惠口径」重算当前应付(含券是否可复用),价差 = 原实付 − 重算值;结果带快照。

3. 风控:价保频次(同用户同期上限)、大额价保人工抽检;价保成功后短期内重复购买同品进观察名单。

4. 退差:现金部分原路退回;券差价按规则退券(同面额新券)并通知;退差订单与售后单隔离记账。

graph TD A[价保申请 引用原单] --> B[规则匹配 SKU+时间窗+排除] B -->|不适用| C[拒绝 明示原因] B -->|适用| D[价格引擎重算 原优惠口径] D --> E[价差 = 原实付-现应付] E --> F[风控审查 频次金额] F -->|可疑| G[人工复核] F -->|可信| H[原路退差 现金+券] H --> I[价保单完结 留痕]
价保流程:匹配、重算、风控、退差
关键代码
// 差价重算:用原单快照的优惠口径,拉当前价重算(禁直接两数相减)
public Money calcDiff(OrderLineSnapshot orig, Long userId) {
    PriceResult now = priceEngine.calc(
        List.of(Line.of(orig.skuId(), orig.qty(), orig.unitPriceSnapshot())),
        CalcContext.of(userId, orig.channel()));
    return orig.paidAmount().subtract(now.linePayable(orig.skuId()));
}

// 规则匹配:时间窗 + 排除活动类型,配置化
public boolean match(PPRule rule, OrderLineSnapshot orig, Price now) {
    return orig.skuId().equals(now.skuId())
        && orig.shopId().equals(now.shopId())
        && Duration.between(orig.paidAt(), Instant.now()).toDays() <= rule.windowDays()
        && !rule.excludedPromos().contains(now.promoType());
深度追问
  • 用券买的订单价差怎么算?
    按「实付口径」:原单实付 vs 当前价用同等优惠口径重算的应付。券已过期不能复用时,差价只算现金部分或按规则补券——口径必须在规则里写死,不能每次客诉现场解释。
  • 部分退货后还能价保吗?
    能——价保按「剩余行项目」计算:先扣掉已退部分,剩余数量按原实付均价与现价比重算。原单快照(D03.05)+ 分摊明细(D04.02)让这一切可算,没有快照的订单价保就是糊涂账。
  • 怎么识别「价保套利」?
    行为特征:价保成功后短时间重购同品、多账户同收货信息、价保频次超阈值。识别不是目的——正常价保是用户权益,风控只拦异常模式,且拦截要可申诉。
  • 价保和退差谁付钱?
    平台活动价价保由平台承担,商家自发降价走商家承担(或按协议分担)——价保单必须带「责任方」字段,否则财务说不清成本归属;这是商务条款不是技术问题,但系统要能表达。
常见陷阱
  • 差价直接两数相减:忽略优惠口径,算出来的差价对不上,客诉升级。
  • 规则漏排除项:秒杀价也价保,被批量套利;排除项必须配置化。
  • 无防套利:黑产公开「价保撸羊毛」教程;频次 + 行为风控前置。
  • 与原单脱节:价保单不引用原单快照,部分退货后全乱;快照是唯一依据。
破题点拨
一眼定位:价保 = 规则匹配 + 原口径重算 + 风控防套利 + 原路退差。
破题主线:引用原单快照 → 规则匹配(时间窗 + 排除)→ 价格引擎重算 → 风控审查 → 退差留痕。
易错点:两数相减;漏排除项;无风控;与原单脱节。
D04.05
优惠投放与券生命周期治理:限速、预算与归因高级P2
领域模型 投放 预算熔断 ROI 归因
场景背景

营销投放平台日曝光十亿次。运营圈选人群 + 选权益(券/积分/红包)+ 选渠道(Push/短信/站内)发起投放。硬指标:发放限速(不打爆下游券系统)、预算实时管控(超支即停)、发放→领取→核销→GMV 全链路可归因、失败可重试可对账。

核心挑战

① 发放峰值:千万级人群一次性发,券系统与通知通道双爆;② 预算:权益成本实时累计,超预算必须能停;③ 归因:领了券的用户后续下单,怎么归因到这次投放;④ 失败处理:发放失败(用户注销/券批次关闭)的重试与跳过;⑤ 券沉睡:发出去不领不用,成本沉没——需要治理而非只发不管。

分析框架

投放四件套:人群 → 权益 → 渠道 → 度量。人群走标签平台圈选(内容画像能力,见 D08;D09 是物流供应链,不是内容域);权益调券系统(D04.01)按用户逐个发,走限速队列;渠道发送同样限速 + 模板审核;预算引擎对「权益成本 + 渠道成本」实时累计,超限自动停投。归因用「投放批次 → 券批次 → 核销订单」链路:券的唯一批次 ID 是天然归因键,核销时带上即可算 ROI。治理看沉睡率(未领取)、核销率,低效投放自动降权。

领域模型

聚合根:投放活动(Campaign,人群包、权益、渠道、预算、状态)。实体:发放任务(DispatchTask,限速、进度、失败重试)、效果报表(Report,发放/领取/核销/GMV)。值对象:预算。不变量:已发放权益成本 + 渠道成本 ≤ 预算(超限即停);每笔发放可追溯到(活动 + 用户 + 券批次)。

解决方案

1. 人群与权益:标签平台圈选导出人群包;权益选券批次(或动态发券);配置预算(权益 + 渠道)与限速(如每秒五千)。

2. 发放调度:任务分片 + 令牌桶限速;单用户失败(注销/黑名单)跳过并记录;失败重试带退避;发放进度实时可观测(进度条 + 失败率)。

3. 预算熔断:每笔发放前原子扣预算(Redis + DB 双记录),预算耗尽自动停投并通知运营;超支告警到运营 + 财务。

4. 归因看板:券批次 ID 贯穿「投放 → 发放 → 领取 → 核销订单 → GMV」,日级报表;末次归因口径写清(多触点冲突规则)。

5. 沉睡治理:到期未领取自动释放预算回收;核销率低于阈值的投放模板降权;重复领取同一权益的用户进疲劳度控制(同一用户多渠道触达频次上限)。

graph TD A[圈选人群] --> B[选权益 券批次] B --> C[预算+限速配置] C --> D[发放调度 分片+令牌桶] D -->|预算耗尽| E[自动停投 告警] D --> F[券到账 通知渠道] F --> G[领取] G --> H[核销带批次ID] H --> I[归因报表 ROI] J[到期未领] --> K[释放预算]
投放链路:限速、预算熔断与归因
关键代码
// 发放前原子扣预算:超限即停(防一晚上超支)
private static final String DEDUCT_BUDGET =
    "local left = tonumber(redis.call('get', KEYS[1])) " +
    "if not left or left < tonumber(ARGV[1]) then return 0 end " +
    "redis.call('decrby', KEYS[1], ARGV[1]) return 1";

public boolean trySpend(String campaignId, Money cost) {
    long ok = redis.execute(DEDUCT_BUDGET, List.of("campaign:budget:" + campaignId),
        String.valueOf(cost.cents()));
    if (ok != 1) { campaignService.pause(campaignId, "BUDGET_EXCEEDED"); return false; }
    budgetLogRepo.append(campaignId, cost);   // DB 流水兜底,日终对账 Redis
    return true;
}

// 限速发放:令牌桶 + 分片,单用户失败跳过不阻塞整批
rateLimiter.acquire(batchSize);
dispatchExecutor.submit(users.stream()
    .filter(u -> !blacklist.contains(u))
    .map(u -> couponService.issue(u, batchId))
    .toList());
深度追问
  • 发放把券系统打爆了怎么办?
    限速是第一道(令牌桶按券系统容量配置);第二道是投放侧分级(大促投放提前报备容量,券系统预留);第三道是熔断(发放失败率飙升自动暂停任务)。容量要按大促峰值预留,不能按日均设计。
  • 归因口径争吵怎么解?
    系统只保证「数据可归因」(券批次 ID 贯穿链路),口径(末次/首次/线性)是业务约定——在看板上明示口径定义,让运营用同一套语言吵架,而不是各算各的。
  • 人群圈选不准怎么办?
    小流量灰度(如 1% 人群)看核销率再放量;标签平台提供标签覆盖率与准确度指标;圈选条件变更留版本,效果对比可回溯。投放是迭代过程,不是一锤子买卖。
  • 券沉睡率太高怎么治理?
    到期未领自动回收预算;按模板维度统计「发放→领取→核销」漏斗,核销率持续低于阈值的权益组合降权或停用;对用户侧做触达疲劳度控制(同一用户周期内触达上限),别把不发券的用户也归为沉睡。
常见陷阱
  • 无限速全量发:千万人群瞬间打爆券与通知系统;令牌桶 + 分片是底线。
  • 无预算熔断:配置错误一晚上超支百万;原子扣预算 + 自动停投。
  • 无归因:发了券不知道带来多少 GMV,ROI 说不清;批次 ID 贯穿链路。
  • 只发不管:沉睡券堆积,预算浪费;到期回收 + 沉睡率治理看板。
破题点拨
一眼定位:投放 = 人群权益渠道 + 限速预算熔断 + 批次 ID 归因 + 沉睡治理。
破题主线:圈选 → 权益批次 → 限速发放(失败跳过)→ 预算原子扣减 → 核销归因 → 到期回收。
易错点:无限速;无熔断;无归因;只发不管。
D04.06
营销消息触达:通道路由、频控疲劳度与转化归因高级P1
高频 领域模型 通道路由 疲劳度 归因窗口
场景背景

电商大促与日常营销,日消息触达千万级:App Push、短信、微信模板消息、站内信四类通道。硬指标:触达成本(短信按条计费,Push 近零成本)、到达率(Push 到达率受厂商通道影响大)、退订/取关率(过度打扰的代价)、转化归因准确率(投放效果怎么算)。

核心挑战

① 通道路由:同一条消息选哪个通道发,成本/到达率/时效三维权衡,Push 失败要降级短信但短信有成本;② 频控与疲劳度:用户一天被多少条营销消息轰炸会流失,全局与分通道都要有上限;③ 个性化渲染:模板 + 用户变量 + 商品变量的拼装,渲染失败要有兜底文案;④ 归因:用户下单前的最后一次点击算谁的功劳,窗口期与归因模型影响投放决策;⑤ 发送峰值:大促集中推送,瞬时百万级,要平滑削峰。

分析框架

触达 = 「谁 × 什么内容 × 哪个通道 × 什么时机」。通道选路按优先级矩阵:能 Push 不短信(成本)、必须实时才短信(时效)、用户偏好优先(订阅关系)。疲劳度模型:用户近 N 天营销触达次数超阈值即压制(全局 + 分通道两级),阈值可按用户价值分层——高价值用户容忍度略高但也要设上限。归因用「末次点击 + 窗口期」(如七天),渠道间只认一次触点防止自夸。发送侧:任务拆批 + 限速 + 错峰,大促前预热扩容。

领域模型

聚合根:触达任务(Campaign,人群 + 模板 + 通道策略 + 计划时间)。实体:触达记录(TouchLog,用户 × 任务 × 通道 × 状态)、频控计数(FatigueCounter,用户 × 窗口)、归因记录(Attribution,触点 × 订单 × 渠道)。值对象:渲染结果、模板变量。不变量:同一用户同一任务只发一次(幂等);疲劳度超限必须拦截;退订用户不进营销任务;归因触点唯一(末次触点)。

解决方案

1. 消息中心:模板管理(审核 + 版本)、变量渲染(用户属性 + 商品数据),渲染失败降级为通用文案;多语言与个性化兜底。

2. 通道路由:按通道优先级(Push → 微信 → 短信)+ 可达性(Push Token 有效性)+ 场景时效(验证码类直发短信,营销类优先 Push)选路;Push 回执失败自动降级短信。

3. 频控:发送前查疲劳度(Redis 计数,用户 × 全局/通道 × 日/周窗口),超限进压制池次日再评估;退订与免打扰时段(晚十点后不发营销)硬拦截。

4. 归因:触点埋点(任务 × 通道 × 点击),订单成交时按末次点击 + 窗口期归因;同一渠道内多次触点只算最近一次,防自夸。

5. 发送引擎:任务拆微批 + 令牌桶限速 + 失败重试与死信告警;大促前通道侧容量报备与压测。

graph TD A[营销任务 人群+模板] --> B[渲染 变量+兜底文案] B --> C[频控检查 疲劳度] C -->|超限| D[压制池 次日再评] C -->|通过| E[通道路由] E --> F[Push 优先] F -->|Token失效/未达| G[降级短信] E --> H[微信模板] E --> I[站内信 免费] F --> J[触点埋点] G --> J H --> J J --> K[订单归因 末次点击+窗口]
通道路由与归因链路
关键代码
// 疲劳度检查:全局 + 分通道两个窗口,任一超限即压制
public boolean passFatigue(String userId, Channel ch) {
    if (fatigue.get(userId, "global", Window.DAY) >= Limit.GLOBAL_DAILY) return false;
    if (fatigue.get(userId, ch.name(), Window.DAY) >= ch.dailyCap()) return false;
    return true;
}

// 通道选路:Push 优先(成本近零),失败降级短信(按条计费)
SendResult r = pushClient.send(userId, rendered);
if (!r.isDelivered() && campaign.allowSmsFallback()) {
    r = smsClient.send(userId, rendered.toPlain());   // 降级前查成本预算
}

// 末次点击归因:同渠道内只保留最近触点,窗口七天
attribution.record(Touch.of(userId, campaignId, ch, now));
if (order.createdAfter(touch, Duration.ofDays(7))) {
    attribution.attribute(orderId, touch.channel());   // 新触点覆盖同渠道旧触点
}
深度追问
  • Push 到达率低怎么办?
    厂商通道分级(自建 + 厂商 push + 厂商融合链路),Token 有效性治理(无效 Token 清理)、到达回执闭环监控;到达率按厂商分维度看板,低于阈值的消息类型走降级或换厂商。营销 Push 允许丢,交易通知 Push 不允许丢——两类消息分通道管理。
  • 频控阈值怎么定?
    按「触达-取关率」曲线定:监控不同频次下用户的退订/卸载率变化,找到拐点设阈值;分层运营(高价值用户上限略高)但全局硬顶不能破。阈值是实验出来的,不是拍脑袋。
  • 归因口径不一致怎么办?
    归因模型(末次点击/线性/时间衰减)与窗口期是分析口径,一旦确定全渠道统一写死,禁止各团队自定口径自夸;触点埋点链路(曝光→点击→落地页→下单)全链路打点,口径可复现。
  • 大促发送峰值怎么扛?
    任务提前拆批 + 错峰(按用户时区/活跃时段)+ 限速发送;渲染与人群计算前置预跑,发送时只读结果;通道侧提前报备容量,压测验证;失败重试进死信队列,人工兜底,绝不无限重试。
常见陷阱
  • 没有疲劳度:全通道轰炸,退订率飙升,通道被封(短信投诉率高会关通道)。
  • 营销打扰交易消息:两类消息混在一个发送通道,营销洪峰把验证码挤延迟。
  • 归因自夸:同渠道多次点击算多次转化,投放 ROI 虚高;末次触点口径统一。
  • 渲染失败发裸文案:「亲爱的用户,您购买的{商品名}已发货」——兜底文案要预置。
破题点拨
一眼定位:消息触达 = 渲染 + 疲劳度前置 + 通道成本路由 + 降级兜底 + 统一归因口径。
破题主线:模板渲染 → 频控拦截 → Push 优先、失败降级短信 → 触点埋点 → 末次点击归因。
易错点:无疲劳度;营销挤交易;归因自夸;兜底文案缺失。

五 企业级 · SaaS 多租户 6 题

本组知识链

B 端 SaaS 的主线是「一个租户是一个边界,一切设计先回答租户是谁」。覆盖隔离、计费、权限、配置、私有化治理与数据生命周期。

  1. 隔离:共享库 / 独立 Schema / 独立库三级模型,路由与迁移通道。
  2. 计费:计量事件 → 预聚合 → 定价 → 账单 → 发票,账期幂等。
  3. 权限:SSO 统一身份,RBAC + 数据范围,行级隔离。
  4. 配置与部署:配置分层 + 插件扩展点;私有化版本矩阵治理。
  5. 数据生命周期:迁移双写可回切、PITR 靠演练、销毁四步留证。
工程化要点
  • 隔离是默认假设:每张业务表带 tenant_id,连接池、缓存 key、索引、消息 topic 全部租户化。
  • 计量不丢不重:事件至少一次 + 窗口去重,明细与聚合双轨可对账。
  • 账单生成幂等:账期 + 租户唯一键兜底,错账走红冲不走改数。
  • 配置分层治理:系统默认 → 租户覆盖 → 用户偏好,变更留痕可回滚。
本组思维模式
  • 隔离是默认假设:先假设租户会互相踩,再讨论要不要优化。
  • 一切租户可配置:大客户的定制诉求是常态,分支合并不了的就是配置化没做够。
  • 计量即资损:少计=平台亏,多计=客户投诉,账必须对得上。
  • 私有化是架构约束不是交付补丁:外网依赖、License、遥测都要在设计期回答。
合规与资损红线
  • 租户数据隔离失效(串租户)属重大事故:查询必须带租户条件,跨租户导出需显式授权与审计。
  • 计费错误涉及资金:账单修正须红冲留痕,禁止直接改历史账单金额。
  • 私有化场景数据不出客户域:遥测、License 校验、外网依赖均需可关闭。
  • 个人信息处理遵循最小必要(告知同意、用途限定),见 D02.04 / D12.04。
D05.01
多租户架构:隔离模型选型与租户路由架构P0
高频 领域模型 架构 租户隔离 数据路由 在线迁移
场景背景

协同办公 SaaS:起步三个大客户,五年后五千中小租户 + 数十个超大租户(单租户数据百 GB 级、要求独立数据库与专属备份)。硬指标:租户间数据零串扰、单租户故障不影响他人(防噪声邻居)、大租户可平滑升级隔离级别、连接池不能随租户数线性膨胀。

核心挑战

① 隔离模型选型:共享库省成本但怕热点与串数据,独立库隔离好但成本高、 schema 演进要刷全量租户;② 租户路由:请求入口就要确定租户与数据源,还要防「忘带租户条件」;③ 大租户升级:从共享库迁到独立库要在线、可回滚;④ 连接池治理:五千租户不能开五千个池;⑤ 全局索引:登录名、手机号这类全局唯一如何与租户内唯一共存。

分析框架

隔离分三级:L1 共享库共享表(tenant_id 列,成本最低,适合长尾中小租户)、L2 独立 Schema(同库不同 schema,平衡型)、L3 独立库/实例(大客户专属,含专属备份与限流)。运行时统一走「路由层 → 数据源分组」:请求上下文解析租户 → 查租户目录(缓存)→ 绑定数据源;ORM 拦截器自动给所有 SQL 追加租户条件;大租户升级走「双写 + 校验 + 切流 + 收尾」的在线迁移管道。选型看三个因子:数据敏感度、单租户体量、运维成本承受力。

领域模型

聚合根:租户(Tenant:隔离级别、数据源定位、订阅计划、生命周期)。实体:租户目录项、迁移任务(MigrationJob)。值对象:隔离级别(L1/L2/L3)、数据源引用。不变量:任一业务行必归属唯一租户;跨租户读取必须经显式授权通道(内部服务账号 + 审计);迁移期间租户数据唯一事实源不 dual-truth(双写期旧库为准,切流后新库为准,切换点由迁移任务状态机锁定)。

解决方案

1. 路由:网关注入租户标识(域名/Header/JWT claim)→ TenantContext(ThreadLocal + 异步透传)→ MyBatis 拦截器为所有业务表 SQL 自动追加 tenant_id;租户目录本地缓存 + 短 TTL,避免每次查库。

2. 隔离落地:L1 用 tenant_id + 复合索引(tenant_id, biz_id);L2 用 schema 级连接切换;L3 独立数据源分组,连接池按组限额,防止大租户打满共享池。全局唯一(如登录邮箱)走「全局唯一索引表」单独治理,不与租户内唯一混淆。

3. 大租户在线迁移:迁移任务状态机(准备 → 全量拷贝 → 增量追平 → 双写校验 → 切流 → 旧库只读观察 → 下线),每步可中止可回滚;校验用行数 + 采样哈希 + 关键业务查询比对。

flowchart LR Req[请求入口] --> GW[租户解析
域名/Header/JWT] GW --> Ctx[TenantContext
ThreadLocal + 异步透传] Ctx --> Dir[租户目录缓存
隔离级别+数据源] Dir -->|L1| Shared[(共享库 tenant_id)] Dir -->|L2| Schema[(独立 Schema)] Dir -->|L3| Dedicated[(独立库/实例)] Ctx --> ORM[ORM 拦截器
自动注入租户条件] ORM --> Shared
租户路由与三级隔离落地
隔离级别 单租户成本 隔离强度 适用
L1 共享库共享表 最低 逻辑隔离 长尾中小租户
L2 独立 Schema 中 物理隔离(同实例) 中等付费客户
L3 独立库/实例 高 完全隔离 超大客户 / 强合规
ORM 拦截器:给所有业务 SQL 追加租户条件(防漏写)
public class TenantLineInterceptor implements Interceptor {
    // 只拦截业务表(白名单来自租户目录元数据),漏配即报错而非静默放过
    private static final Set<String> TENANT_TABLES = TenantMetaRegistry.tables();

    public String intercept(Invocation inv) {
        BoundSql bs = (BoundSql) inv.getArgs()[0];
        String sql = bs.getSql();
        if (!TENANT_TABLES.contains(extractTable(sql))) return sql;
        if (sql.contains("tenant_id")) return sql;
        throw new IllegalStateException("业务 SQL 缺少租户条件,已被拦截");
    }
}
关键代码
// 租户上下文 + 数据源路由:共享库隔离模式按 tenant_id 强制注入
public final class TenantContext {
    private static final ThreadLocal<String> CURRENT = new ThreadLocal<>();

    public static void set(String tenantId) { CURRENT.set(tenantId); }
    public static String require() {
        String t = CURRENT.get();
        if (t == null) throw new NoTenantException();   // 禁无租户查询(防串租户)
        return t;
    }
    public static void clear() { CURRENT.remove(); }
}

// 路由:独立库租户走物理分库,共享库租户走行级隔离
public DataSource route(String tenantId) {
    TenantPlan plan = planRepo.findByTenant(tenantId);
    return switch (plan.mode()) {
        case POOL_DB   -> sharedDs;                    // 共享库:SQL 层自动追加 tenant_id
        case SCHEMA    -> schemaDs.forSchema(plan.schema());
        case DATABASE  -> dbs.get(plan.dsKey());  // 大租户/合规租户独立库
    };
}
深度追问
  • 共享库下如何做单租户备份恢复?
    按 tenant_id 逻辑导出(mysqldump where 条件或 SELECT INTO OUTFILE 分片),恢复进临时 schema 校验后再并回。频率与保留期按租户 SLA 分级;超大租户应引导升级 L3,物理备份才是正解。
  • 租户目录缓存与数据源变更的 Race?
    目录变更走版本号 + 短 TTL(秒级)双保险:切流完成才改目录版本,旧连接自然耗尽回收;迁移期间目录返回「只读」标记,请求走旧源,避免切流窗口的双写不一致。
  • 跨租户统计报表(运营视角)怎么做?
    不走业务库跨租户 JOIN(禁令),走数仓 ETL:各库 binlog 统一入湖,按租户维度建模,指标口径 SSOT 化。实时性要求高的用 Flink 多流合并 + 维表关联。
  • 连接池随租户数膨胀怎么办?
    L1 全部共享一个池(天然不膨胀);L2/L3 按组限额 + 空闲快速回收 + 慢查询隔离;单租户峰值超限时路由到备用组或触发限流,绝不允许一个租户吃光共享资源。
常见陷阱
  • 租户条件漏写:新表新查询忘了带 tenant_id,一次事故串全租户数据;拦截器白名单 + 缺条件即抛错是底线。
  • 缓存/消息不带租户:Redis key、Kafka topic 缺租户前缀,串数据比 DB 更隐形;key 规范进代码评审清单。
  • 全局唯一与租户唯一混用:邮箱既要全局登录唯一又要租户内资料唯一,混用会互相破坏;拆两张索引表。
  • 迁移双写无校验:切流后才发现丢行;全量 + 增量 + 行数哈希 + 采样比对四道关。
破题点拨
一眼定位:多租户 = 隔离三级 + 路由拦截 + 在线迁移管道。
破题主线:先定隔离级别(敏感度×体量×成本)→ 路由与拦截器兜底 → 大租户升级走状态机迁移 → 连接池分组限额。
易错点:租户条件漏写;缓存不带租户;全局/租户唯一混用;双写不校验。
D05.02
计量计费与订阅:账单、发票与账期一致性架构P1
领域模型 架构 计量事件 订阅状态机 账单幂等
场景背景

PaaS 平台按量计费:API 调用次数、存储容量、消息条数、席位数四类计量维度,五千租户月账单百万行明细。订阅含免费版/专业版/旗舰版,支持月付年付、随时升级降级、中途增购席位。硬指标:计量不丢不重(少计平台亏、多计客诉)、账单可复现(同账期同口径重算结果一致)、错账可修正(红冲留痕)、发票与账单一致。

核心挑战

① 计量准确性:调用日志海量,采集链路(客户端 SDK/网关/服务端)可能重复或丢失;② 账期边界:跨月调用、订阅中途变更、按量阶梯价的时间归属;③ 账单可复现:重算要能复现历史口径(价格版本生效区间);④ 幂等与修正:账单重复生成、事后发现错账如何红冲;⑤ 发票:抬头、税号、拆分、作废重开与税务合规。

分析框架

链路分五段:计量事件 → 预聚合 → 定价 → 账单 → 发票。计量事件是唯一事实源(至少一次投递,窗口内按事件 ID 去重);预聚合按「租户 × 维度 × 小时」落汇总表,明细进对象存储供账单钻取;定价读价格版本(按生效区间寻址,保证历史可复现);账单按「账期 + 租户」幂等生成,行项 = 用量 × 单价 − 减免;修正一律红冲(冲正单 + 重开单),禁止原地改数;发票是账单的税务投影,独立状态机但金额锚定账单。

领域模型

聚合根:订阅(Subscription:计划、周期、席位、状态)、账单(Invoice:账期、行项、金额、状态)。实体:计量事件(不可变)、价格版本。值对象:金额、账期、用量。不变量:计量事件不可变更(修正以新事件表达);账单行项金额 = 单价版本 × 用量 − 减免;红冲单与被冲单金额镜像;发票总额 = 账单应付额(或经确认的调整额)。

解决方案

1. 计量:网关埋点 + 服务埋点双路,事件带全局事件 ID;Kafka 至少一次 → Flink 按事件 ID 去重 + 小时窗口预聚合 → 汇总表 + 明细 OSS。日终对账:服务端原始计数与预聚合总量比对,差异进差异表人工/自动修。

2. 定价:价格是一等公民版本化(计划价目 + 阶梯价 + 生效区间),账单生成时按用量发生时间寻址价格版本,而不是按生成时刻——这是可复现的关键。

3. 账单:账期关闭后跑账(幂等键 = 账期 + 租户),生成行项快照(含价格版本号);未关闭可重跑,关闭后只能红冲。升级降级按天比例折算,生成「补差」行项。

4. 发票:账单确认后申请,税务系统对接;作废重开走红冲 + 新票,票面信息快照留存。

sequenceDiagram participant GW as 网关/服务埋点 participant K as Kafka participant F as Flink 去重预聚合 participant S as 汇总表+明细OSS participant B as 账单引擎 participant I as 发票系统 GW->>K: 计量事件(至少一次) K->>F: 按事件ID去重 F->>S: 小时窗口聚合/明细归档 B->>S: 账期用量 B->>B: 按发生时间寻址价格版本 B->>B: 幂等生成(账期+租户) B->>I: 账单确认 I->>I: 开票/作废重开(红冲)
计量计费五段链路(去重、价格版本、幂等账单、红冲)
修正场景 错误做法 正确做法
多计量重复 手工删汇总行 事件 ID 去重 + 修窗口重算
价格用错版本 改历史账单金额 红冲旧账单 + 按新版本重开
中途升降级 整月按新价 按天折算 + 补差行项
账单幂等:账期 + 租户唯一键兜底
-- 重复生成直接撞唯一索引,返回既有账单而非重复出账
INSERT INTO invoice (tenant_id, period, amount, status, price_snapshot)
VALUES (?, ?, ?, 'DRAFT', ?)
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id);
-- 唯一索引:uk_tenant_period (tenant_id, period)
关键代码
// 计量事件:带窗口聚合 + 幂等去重(usageId 唯一)
public UsageAgg ingest(UsageEvent e) {
    if (!dedup.tryAdd(e.usageId())) return agg.current(e.tenantId(), e.metric()); // 重复上报直接返回现值
    return agg.add(e.tenantId(), e.metric(), e.qty(), e.windowKey());  // 按小时窗口累加
}

// 订阅状态机:试用 → 生效 → 到期/欠费 → 冻结 → 恢复
private static final Map<SubState, Set<SubState>> TRANS = Map.of(
    SubState.TRIAL,   Set.of(SubState.ACTIVE, SubState.CANCELLED),
    SubState.ACTIVE,  Set.of(SubState.EXPIRED, SubState.PAST_DUE, SubState.CANCELLED),
    SubState.PAST_DUE, Set.of(SubState.SUSPENDED, SubState.ACTIVE),
    SubState.SUSPENDED, Set.of(SubState.ACTIVE, SubState.CANCELLED));

public void transit(SubId id, SubState target) {
    Sub s = subRepo.find(id);
    if (!TRANS.get(s.state()).contains(target))
        throw new IllegalStateException("非法订阅迁移: " + target);
    subRepo.updateState(id, target);        // 欠费冻结:停服不停数据,补款即恢复
    outbox.save(new SubStateChanged(id, target));
}
深度追问
  • 计量事件迟到(跨账期才到)怎么处理?
    迟到事件进「补账窗口」:若账期未关闭,直接并入重算;已关闭则生成补充计费事件落入下一账期或触发红冲重开。水位线 + 允许迟到时长在 Flink 侧配置,超时进死信队列。
  • 客户质疑账单数字,怎么自证?
    三层证据:汇总表(小时粒度)、明细钻取(OSS 原始事件,带事件 ID 与时间戳)、价格快照(生成时的版本号)。提供「用量明细导出」是 B 端计费的基本信任工具。
  • 订阅与计费状态不一致(用户以为降级了但照原价扣)?
    订阅变更是领域事件,计费侧订阅该事件并写入「生效计划」;账期生成读取的是生效计划快照而非当前订阅。任何即时生效诉求都要回答「已产生用量按哪个价」——默认按天比例。
  • 发票红冲后税务怎么衔接?
    红冲单(负数票)与原票号关联,重开票用新号码;全电票模式下对接税务数字账户自动开具/红冲。关键是发票状态机与账单状态解耦但金额锚定,杜绝「票账不符」。
常见陷阱
  • 按生成时刻取价格:价格调整后重算历史账单全变;必须按用量发生时间寻址版本。
  • 账单可改数:支持 UPDATE amount,一出错就是资金事故;关闭后只准红冲。
  • 升降级整月切换:客户投诉乱扣费;按天折算写进产品规则。
  • 明细不留存:只存汇总,客户质疑时无法自证;明细归档 + 保留期策略。
破题点拨
一眼定位:计费 = 事件去重 + 价格版本 + 幂等账单 + 红冲修正。
破题主线:双路埋点 → 窗口去重预聚合 → 版本化定价 → 账期幂等 → 红冲修错 → 发票锚定。
易错点:价格按生成时刻取;账单可改数;升降级不折算;明细不留。
D05.03
IAM 与数据权限:组织、角色、行级隔离与 SSO高级P1
领域模型 RBAC 行级权限 SSO 越权防护
场景背景

租户内部的组织权限诉求:多部门树(集团-事业部-小组)、角色(管理员/部门经理/普通成员/审计员)、数据范围(仅自己/本部门/本部门及以下/全部)、以及企业客户要求的 SSO(对接客户 IdP,OIDC/SAML 单点登录 + 离职自动禁用)。硬指标:越权零发生(水平越权看他人数据比功能越权更常见)、权限变更分钟级生效、SSO 对接不逐个客户定制开发、全程可审计。上述是观察口径,具体阈值须按本业务标定,禁止把本卡文字直接当成门禁数字。

核心挑战

① RBAC 不够用:同角色不同部门要看到不同数据,需要数据范围维度;② 行级隔离:把「数据范围」翻译成 SQL 条件且不漏(列表/详情/导出/报表全路径);③ 组织树继承:部门调整、人员调岗的权限重算;④ SSO 多协议:OIDC/SAML/CAS 都要接,断言验签与账号映射(JIT 开通);⑤ 权限与性能:每次请求鉴权的开销。

分析框架

授权分两层:功能权限(能不能做)用 RBAC(角色 → 权限点),数据权限(能看到谁)用数据范围(角色/成员 → 范围表达式)。两层在网关统一鉴权:功能权限查权限点缓存,数据权限生成「行级条件」注入查询(与 D05.01 的租户拦截器同机位——租户条件 + 数据范围条件一起注入)。SSO 侧统一认证网关适配多协议,断言验签 → 账号映射(邮箱/工号)→ JIT 开通或更新 → 签发本地会话。权限决策 PBAC 化:策略 = 角色 × 数据范围 × 资源类型,可解释可审计。

领域模型

聚合根:成员(Member:账号、组织节点、状态)、角色(Role:权限点集 + 数据范围模板)。实体:组织节点、授权记录。值对象:数据范围(枚举 + 自定义部门集)、权限点。不变量:任何数据查询必带租户条件 + 数据范围条件;禁用/离职成员所有会话即时失效;权限变更产生审计事件;SSO 侧账号唯一映射一个本地成员。

解决方案

1. 模型:功能权限 = 角色 → 权限点(树);数据权限 = 成员 → 数据范围(部门树子集 + 规则)。授权结果编译成「策略缓存」(租户 + 成员版本号),变更即失效。

2. 行级注入:拦截器在租户条件之后追加数据范围条件(如 dept_id IN (...) 或 dept_path LIKE 'xxx%'),列表/详情/导出/搜索全部同路径,杜绝「列表过滤了详情没过滤」型越权。

3. SSO:认证网关支持 OIDC/SAML;验签 → 属性映射(邮箱为主键)→ JIT:首次登录按租户映射表开通(防任意人闯入:先校验邮箱域属于该租户)→ 本地会话(短 TTL + 刷新)。IdP 侧离职禁用通过「会话短周期 + 登出回调」双兜底。

4. 审计:权限变更、越权拦截、导出行为全量审计(不可篡改存储,见 D12.05)。

flowchart TD Login[登录请求] --> Mode{SSO or 本地} Mode -->|企业客户| IdP[IdP OIDC/SAML] IdP --> Verify[验签+属性映射] Mode -->|普通| Local[本地口令/验证码] Verify --> JIT{JIT开通?} Local --> JIT JIT -->|是| Create[按邮箱域匹配租户开通] JIT -->|否| Session[签发本地会话] Create --> Session Session --> GW[网关鉴权] GW --> Func[功能权限:角色→权限点] GW --> Data[数据权限:范围→行级条件] Data --> Inject[与租户条件一起注入SQL]
SSO 接入与两层授权决策链
数据范围条件编译(与租户拦截器同机位注入)
// DataScope 编译为 SQL 片段;NOT_ALL 必须产生可执行条件,否则拒绝查询
public String compile(DataScope scope, Long memberId) {
    switch (scope.getType()) {
        case SELF:      return "owner_id = " + memberId;
        case DEPT:      return "dept_id = " + scope.getDeptId();
        case DEPT_BELOW:return "dept_path LIKE '" + scope.getDeptPath() + "%'";
        case ALL:       return "1 = 1";
        default:         throw new AccessDeniedException("未定义数据范围");
    }
}
关键代码
// 行级权限:查询强制追加数据范围条件,禁绕过
public void applyRowScope(Query q, Operator op) {
    DataScope scope = policy.resolve(op.role(), q.resource());
    switch (scope.type()) {
        case ALL      -> {}                                          // 平台超管
        case ORG      -> q.where("org_id IN (:orgs)", scope.orgIds());
        case SELF     -> q.where("owner_id = :uid", op.userId());
        case CUSTOM   -> q.where(scope.predicate());            // ABAC 自定义谓词
    }
    audit.record(op, "ROW_SCOPE", q.resource(), scope.type());
}

// SSO:断言验签 + 账号绑定(先绑定后授权,禁自动建号即授权)
public Session ssoLogin(String idpAssertion) {
    Assertion a = idp.verify(idpAssertion);               // 验签 + 时效 + 接收方校验
    User u = userRepo.findByExternal(a.idp(), a.subject());
    if (u == null) throw new UnboundException(a.subject()); // 走绑定流程
    return session.create(u, a.groups());            // 组映射为角色,权限实时解析
}
深度追问
  • 水平越权(改个 ID 看别人数据)怎么系统性防?
    越权防线是「注入式」而非「判断式」:所有数据查询必经租户 + 数据范围注入,没有旁路;IDOR 测试进 CI(自动化改 ID 断言 403)。前端隐藏按钮只是体验,不是安全。
  • 组织树调整后权限什么时候生效?
    成员版本号 + 策略缓存失效,秒级生效;涉及部门合并的要跑「权限重算任务」(异步、可观测、可回放),重算期间用旧策略 + 变更日志补审。
  • SSO 用户离职了但会话还有效?
    三层:本地会话短 TTL(分钟级)+ IdP 登出回调(OIDC Logout)强制失效 + 敏感操作二次鉴权。绝不依赖「客户会去 IdP 点注销」。
  • 客户要求「临时授权外部协作者」?
    外部成员独立账号类型(非组织树节点),授权 = 显式资源白名单 + 过期时间,范围最小化;到期自动回收,审计单独标记。这是常见的外部顾问/审计场景。
常见陷阱
  • 前端鉴权当安全:菜单藏了但接口裸奔;后端每接口都要鉴权,无一例外。
  • 详情/导出旁路:列表过滤了,详情和导出没走同一注入路径;统一拦截器机位。
  • JIT 不校验邮箱域:任何邮箱都能开通进租户;先验域归属再开通。
  • 离职不失效:SSO 依赖客户 IdP,本地无兜底;短会话 + 登出回调 + 二次鉴权。
破题点拨
一眼定位:IAM = 功能 RBAC + 数据范围两层 + SSO 网关 + 注入式行级隔离。
破题主线:两层授权模型 → 策略缓存 → 统一注入(租户 + 范围)→ SSO 多协议 + JIT → 审计兜底。
易错点:前端鉴权;详情/导出旁路;JIT 不验域;离职不失效。
D05.04
租户级配置与插件化扩展:从定制分支到配置驱动高级P1
领域模型 配置分层 功能开关 插件机制 扩展点
场景背景

二十个大客户各要定制:A 要审批流多一级、B 要自定义字段、C 要对接内部审批系统、D 要特殊报表。最初给每个客户开代码分支,两年后十几个分支合并不回、发版全靠人肉。硬指标:定制能力配置化(不靠分支)、租户间配置互不影响、插件故障不影响主流程、定制能力可灰度可回滚、租户配置可导出可迁移(私有化交付)。

核心挑战

① 配置隔离:系统默认值、租户覆盖、用户偏好三层叠加与寻址;② 变更安全:配置改错影响一个租户的核心流程,要灰度 + 快照 + 回滚;③ 插件隔离:第三方插件崩溃/死循环/拖慢主线程;④ 扩展点设计:哪些能力值得开放成扩展点,契约怎么定;⑤ 升级兼容:插件 API 演进,旧插件怎么办。

分析框架

分而治之:可变行为配置化(字段、流程节点、规则参数走配置 + 低代码),不可配行为扩展点化(对接外部系统这类逻辑走插件 SPI)。配置三层模型:系统默认(随版本发布)→ 租户覆盖(运营可改,带版本与审批)→ 用户偏好(终端用户)。插件走「扩展点注册 + 契约接口 + 沙箱隔离(独立类加载/进程 + 超时熔断 + 资源配额)」,插件只能通过扩展点与核心交互,禁止直连内部 API。一切定制能力带 feature flag,灰度发布、异常自动熔断到默认行为。

领域模型

聚合根:租户配置集(ConfigSet:分层键值 + 版本 + 生效状态)、插件安装(PluginInstall:扩展点、版本、授权范围)。实体:配置项(含 schema 校验)、扩展点注册。值对象:配置作用域(系统/租户/用户)、插件版本。不变量:租户配置仅在租户内可见可写;插件只能调用已发布扩展点;配置值必须通过 schema 校验(类型/范围/依赖);灰度中的配置可一键回滚到上一版本。

解决方案

1. 配置中心:键 = 作用域 + 模块 + 名称;读取走本地缓存 + 长轮询推送(秒级生效);写走「申请 → 校验 schema → 灰度(按租户百分比)→ 全量」,每步留版本快照。敏感配置(密钥)进 KMS,配置中心只存引用。

2. 低代码扩展:自定义字段(元数据驱动表单与存储列映射)、流程节点(BPMN 子集 + 租户自定义节点实现)、报表(语义层 + 拖拽)。低代码产物也是配置,纳入同一版本治理。

3. 插件:定义扩展点(如「订单创建后」「审批通过后」),插件实现契约接口;运行时隔离:超时(默认短)、线程池独立、异常计数熔断;插件生命周期(安装/启用/禁用/升级/卸载)由平台管控,权限最小化(只能经 API 网关访问授权数据)。

4. 私有化导出:租户配置集 + 低代码产物 + 插件清单整体导出为「定制包」,私有化部署时一键应用,保证环境一致。

flowchart TD subgraph 配置三层 S[系统默认
随版本发布] --> T[租户覆盖
审批+灰度+版本] T --> U[用户偏好
终端用户] end Write[配置变更申请] --> Schema[schema校验] Schema --> Gray[按租户灰度] Gray --> Full[全量生效] Full --> Snap[版本快照
可回滚] Ext[扩展点调用] --> SPI[插件契约接口] SPI --> Sandbox[沙箱:超时/熔断/资源配额] Sandbox --> Default[异常→默认行为]
配置分层治理与插件沙箱隔离
定制诉求 实现路径 反模式
加字段/改表单 元数据驱动低代码 开分支加列
流程多一级审批 流程引擎配置节点 if tenantId 硬编码
对接外部系统 插件 + 扩展点 核心代码里写死回调 URL
关键代码
// 配置分层:默认值 → 套餐 → 租户覆盖,逐层合并(禁全量覆盖丢默认)
public Config effective(String tenantId, String key) {
    Config base  = defaultRepo.find(key);               // 平台默认
    Config plan  = planRepo.findByTenant(tenantId).override(key);
    Config tenant = tenantConfRepo.find(tenantId, key);
    return base.merge(plan).merge(tenant);       // 深合并,未设置的键继承下层
}

// 插件化:能力声明 + 沙箱加载,插件只经扩展点接入
public void loadPlugin(PluginJar jar, String tenantId) {
    PluginManifest mf = manifest.read(jar);              // 声明扩展点/依赖/所需权限
    manifest.checkCompat(mf, platformVersion);          //// 版本矩阵校验(D05.05)
    ClassLoader cl = sandbox.create(jar, mf.permissions());  // 权限沙箱:禁反射禁文件
    for (ExtensionPoint ep : mf.extensionPoints()) {
        registry.register(tenantId, ep, cl.load(ep.implClass()));
    }
}
深度追问
  • 配置漂移(租户改了没人知道)怎么治理?
    每次变更强制「申请人 + 理由 + 审批单」;配置差异看板(同模块各租户配置对比,异常值高亮);关键配置(计费、权限)双人复核。漂移不可怕,看不见才可怕。
  • 插件把主流程拖慢怎么办?
    同步扩展点默认带超时(百毫秒级)+ 线程隔离;重逻辑引导走异步扩展点(事件投递);连续超时/异常自动熔断到默认行为并告警租户管理员,插件市场下架处理。
  • 插件 API 要演进怎么办?
    契约版本化(接口带版本号,旧版兼容期两个大版本);扩展点注册表声明最低兼容版本;CI 跑插件兼容矩阵测试。破坏性变更必须提前公告 + 迁移工具。
  • 配置化会不会让系统变得不可调试?
    会,所以要有「配置解释器」:任一次业务行为可回答「哪些配置项参与了决策、各自取哪个作用域的值」。这是可观测性的一部分,不是附加题。
常见陷阱
  • 配置散落各表无治理:租户配置存在业务表注释字段里,没人敢动;统一配置中心 + schema。
  • if tenantId 硬编码:定制逻辑进核心代码,分支地狱起点;扩展点 + 插件。
  • 开关无审计:线上问题查不到谁开了什么;变更全留痕。
  • 插件无隔离:一个插件 OOM 拖垮主服务;沙箱 + 配额 + 熔断。
破题点拨
一眼定位:定制治理 = 配置三层 + 低代码 + 插件沙箱 + 全程灰度回滚。
破题主线:诉求分流(配置/低代码/插件)→ schema 校验 → 灰度 → 快照回滚 → 可解释。
易错点:tenantId 硬编码;配置无 schema;插件无隔离;定制不可解释。
D05.05
私有化部署与版本治理:一套代码的多形态交付高级P2
私有化 版本矩阵 License 灰度升级
场景背景

金融与政务客户要求私有化:数据不出域、离线可用、对接客户内网基础设施(K8s 版本老旧、无外网镜像仓库)。同时 SaaS 侧月月发版,一年后存在多个版本组合在线。硬指标:一套代码三种形态(公有云 SaaS / 专有云 / 客户内网私有化)、Schema 演进向前兼容、升级可灰度可回退、License 可管控、故障可在客户现场自证(日志诊断包)。

核心挑战

① 外网依赖:SaaS 默认依赖(短信、对象存储、License 校验、遥测)在离线环境全部失效;② Schema 演进:私有化客户可能一年才升一次级,跨多个大版本升级路径;③ 版本矩阵:组件版本 × 客户环境的组合验证;④ License:授权管控与防破解的平衡;⑤ 现场运维:客户环境无外网,问题定位靠诊断包。

分析框架

核心原则:把「部署形态」变成配置而非分支。所有外部依赖抽象成 SPI(短信/存储/支付/遥测),私有化替换为本地实现;镜像与 Helm chart 离线打包(air-gap 交付物);Schema 演进遵守「只增不改、双写过渡、大版本合并」策略,升级脚本版本化并按序执行(Flyway 式);发布列车:主干月版 + LTS 季版,私有化只交付 LTS;License 服务本地化(签名证书 + 硬件指纹绑定,校验失败降级而非完全不可用——留申诉通道);诊断包一键生成(脱敏日志 + 配置快照 + 健康检查)。

领域模型

聚合根:部署实例(Deployment:形态、版本、License、组件清单)。实体:升级任务(UpgradeJob:源版本、目标版本、步骤状态)、License。值对象:部署形态(SaaS/专有云/私有化)、版本号。不变量:升级路径必经版本矩阵校验(不存在跨版本的脚本跳变);License 与硬件指纹绑定且可续期;私有化实例默认关闭一切外网依赖(显式开启才通)。

解决方案

1. 依赖抽象:通知、存储、鉴权、遥测全部 SPI;私有化提供本地实现(内网 SMTP、MinIO、内网 IdP、日志文件)。启动时探测形态,缺失依赖降级并明确告警,绝不静默失败。

2. 离线交付:Helm chart + 全部镜像 tar 包 + 初始化 SQL + 配置文件模板,一个交付目录;文档含「最低环境要求矩阵」(K8s 版本、存储类、镜像仓库)。

3. 升级治理:版本矩阵(哪到哪合法、必经中间版本);升级 Operator:预检(磁盘/版本/依赖)→ 备份(DB + 配置 + 数据卷快照)→ 执行脚本 → 健康检查 → 失败自动回滚到备份。双轨 Schema:新版本代码兼容旧 schema(读旧写新过渡),跑批回填后再切。

4. License:授权证书(租户、功能清单、席位数、到期)+ 私钥签名,启动/定期校验;硬件指纹变更可申请重签;功能按 License 灰度开启。

5. 可观测:私有化默认本地 Prometheus + 日志落盘;诊断包一键导出(含环境信息、版本、近期错误、配置脱敏快照),支持离线导入 SaaS 侧分析。

flowchart LR subgraph 一套代码 Core[核心服务] --> SPI[外部依赖 SPI] end SPI --> SaaS[公有云实现
短信/OSS/遥测] SPI --> Priv[私有化实现
内网SMTP/MinIO/日志] subgraph 交付物 Chart[Helm chart] --> Tar[镜像 tar 包] Tar --> SQL[版本化 SQL] SQL --> Lic[License 证书] end Op[升级 Operator] --> Pre[预检] Pre --> Bak[备份] Bak --> Run[执行脚本] Run --> Health[健康检查] Health -->|失败| Roll[回滚备份]
多形态交付与升级 Operator 流程
关键代码
// 版本矩阵:租户可升版本集合 = 当前版本 → 目标版本的兼容路径
public Set<Version> upgradePath(Version from, Version to) {
    if (from.compareTo(to) >= 0) return Set.of();
    Set<Version> path = new LinkedHashSet<>();
    Version cur = from;
    while (cur.compareTo(to) < 0) {                          // 逐 minor 升,禁跳大版本
        Version next = compat.nextHop(cur, to);
        if (next == null) throw new NoPathException(cur, to);
        path.add(next); cur = next;
    }
    return path;
}

// 私有化灰度升级:按批次推进 + 健康检查失败即熔断回滚
public void rollingUpgrade(DeploymentId dep, Version target) {
    for (Batch b : plan.batches(dep)) {                    // 首批灰度 5%
        deployer.upgrade(b, target);
        HealthReport r = health.check(b, Duration.ofMinutes(10));
        if (!r.ok()) {
            deployer.rollback(b);                        // 只回滚本批,前批不受影响
            throw new UpgradeHaltException(r.reason());
        }
    }
}
深度追问
  • 私有化发现的 bug 怎么回灌主干?
    修复必须进主干 + cherry-pick 到对应 LTS 维护分支;补丁版本号独立计数(如 3.2.1-fix.1);私有化热修复走同一 CI 出离线包,杜绝「现场改包」——那是最危险的模式。
  • License 被破解怎么办?
    承认无法绝对防破解,目标是提高门槛 + 商业可控:签名 + 指纹 + 服务端续期检查(有网时);关键防护是License 与法务合同组合。过度防护(频繁强校验)伤害正常客户体验,平衡点是「降级提示」而非「直接停机」。
  • 客户要求「数据不出域」但你要采遥测?
    遥测默认关闭且分级:L0 完全关闭 / L1 只出错误码与版本(不出业务数据)/ L2 脱敏指标。合同明确约定;技术上遥测通道独立开关,客户可审计网络出口。
  • 客户现场 Kubernetes 版本很老,镜像跑不起来?
    环境矩阵进 CI 验证(老版本 K8s 定期跑兼容性测试);镜像基础版本保守选型;提供「非 K8s 部署模式」(二进制 + systemd)作为兜底交付形态。
常见陷阱
  • SaaS 假设渗透私有化:默认连外网服务,离线启动即失败;依赖全 SPI + 启动探测。
  • SQL 脚本无版本管理:现场手工执行,升级顺序错乱库就坏;版本化 + Operator 编排。
  • 现场改包:客户现场直接改 jar,后续升级冲突且无迹可查;修复回主干 + 补丁包。
  • License 强校验:网络抖动导致生产停摆;降级策略 + 宽限期。
破题点拨
一眼定位:私有化 = 依赖 SPI + 离线交付物 + 版本矩阵 + 升级 Operator。
破题主线:形态即配置 → 外部依赖全抽象 → LTS 发布列车 → 升级预检备份回滚 → License 与遥测分级。
易错点:外网依赖;SQL 无版本;现场改包;强校验停摆。
D05.06
租户数据迁移与备份:隔离升级、PITR 与数据销毁架构P1
高频 领域模型 架构 双写切换 PITR 数据销毁
场景背景

SaaS 多租户平台演进到中期:大租户要从共享库迁到独立库(隔离与性能)、共享集群要拆库分片、备份要支持任意时间点恢复(PITR)、退订租户数据要合规销毁。硬指标:迁移期间租户业务不中断(或分钟级)、数据零丢失、PITR 精度(分钟级)、销毁可出具证明。上述是观察口径,具体阈值须按本业务标定,禁止把本卡文字直接当成门禁数字。

核心挑战

① 迁移不中断:大租户数据量 TB 级,停机拷贝不可接受,要双写 + 增量追平 + 秒级切流;② 一致性校验:迁移后两库数据一致(行数 + 校验和),灰度期双读比对;③ 回退:切流后发现异常要能回切旧库,双写窗口内可逆;④ PITR:误删数据要恢复到任意时间点,备份频率与存储成本的平衡;⑤ 销毁合规:退订后不是物理删就行——软删保留期、匿名化、备份副本也要过期,销毁要留证。

分析框架

迁移五阶段:全量拷贝 → 增量追平(binlog/WAL 重放)→ 校验比对 → 切流(读写新库)→ 观察期后下线双写。切流点选低峰,切流是配置开关(秒级),回切同理。备份策略分层:全量(日)+ 增量(分钟级 binlog),PITR = 选时间点的全量 + 后续增量重放。销毁三阶段:软删(业务不可见)→ 保留期(法务申诉窗口)→ 物理删除 + 备份过期,全程留销毁证明(谁、何时、删了什么范围)。

领域模型

聚合根:迁移任务(MigrationJob,租户 + 阶段 + 校验状态 + 切流开关)。实体:迁移校验报告(DiffReport,表级行数/校验和)、备份策略(BackupPolicy,全量/增量周期 + 保留期)、销毁工单(DestructionTicket,阶段 + 证明)。值对象:租户标识、时间点。不变量:校验不通过不允许切流;双写期内双库一致(最终);销毁工单完成 = 软删 + 保留期满 + 物理删 + 备份过期四项齐备。

解决方案

1. 迁移引擎:按租户粒度编排(全量拷贝分片并行 → binlog 增量追平 → 校验 → 切流),全程可视化与可暂停;切流开关平台侧秒级生效,切流后新库为准、旧库只读兜底。

2. 双写与回切:切流前双写期(新库写成功算成功),双写差异异步补偿;观察期(如七天)内异常一键回切旧库,回切后反向补偿。

3. 备份与 PITR:每日全量 + 分钟级 binlog 归档到对象存储(跨区),恢复按「目标时间点 = 全量 + 增量重放」,恢复演练每季度一次(不演练的备份等于没有)。

4. 退订销毁:退订 → 软删(立即可见不可用)→ 保留期(如一百八十天)→ 物理删除主库 → 匿名化备份 → 出具销毁证明;存储侧加密密钥同步销毁(加密即销毁的兜底)。

graph TD A[迁移任务 创建] --> B[全量拷贝 分片并行] B --> C[增量追平 binlog重放] C --> D[校验比对 行数+校验和] D -->|不通过| C D -->|通过| E[双写期] E --> F[切流 秒级开关] F --> G[观察期 可回切] G --> H[下线双写] I[退订] --> J[软删] J --> K[保留期] K --> L[物理删除+备份过期] L --> M[销毁证明]
迁移五阶段与销毁四步
关键代码
// 切流开关:租户维度生效,读写按开关路由;秒级生效、可回切
public class TenantRouter {
    public DataSource route(String tenantId) {
        MigrationJob job = jobs.get(tenantId);
        if (job != null && job.isCutover()) return job.newDs();   // 切流后新库为准
        return job != null && job.isDualWrite() ? job.oldDs() : job.newDs(); // 双写期默认旧库
    }
}

// PITR:目标时间点 = 最近全量 + 该点之后 binlog 重放
RestorePlan plan = backup.planPITR(tenantId, targetTime);
plan.restoreBase(fullSnapshot.latestBefore(targetTime));   // 恢复全量
plan.replayBinlog(baseTime, targetTime);                       // 重放增量到目标点
plan.verifyRowCounts();                                     // 行数校验防漏放

// 销毁四步齐备才闭环(缺一不可)
if (ticket.isSoftDeleted() && ticket.retentionExpired() && ticket.isPhysicallyPurged() && ticket.isBackupExpired()) {
    destruction.issueCertificate(ticket);                     // 出具销毁证明
}
深度追问
  • 迁移中租户写入量很大,增量一直追不上怎么办?
    追不上就推迟切流(切流条件是追平 + 校验通过,不是时间表);追平慢的瓶颈在单线程重放,按表并行重放 + 热点表跳过比对先追;同时给双写补偿留 buffer。宁可晚上几天,不能带差异切流。
  • 切流后发现新库有 bug,怎么回切?
    观察期内双写仍保持(旧库也写),一键回切开关秒级生效;回切后把新库多出来的写反向补偿回旧库(补偿按 binlog 时间戳过滤)。观察期结束才停双写——这就是回切窗口,没设计双写窗口就是不可逆赌博。
  • 备份恢复了但数据还是不对,哪里漏了?
    常见三个漏点:binlog 格式(ROW 模式才有行数据)、时区(恢复点时间口径)、漏放热点表。恢复必须演练:每季度抽租户做真实 PITR 演练,比对恢复点前后关键表行数与业务校验和。备份不演练等于没有备份。
  • 退订用户的数据,备份里怎么删?
    备份是快照,物理删只影响主库;两个手段:备份保留期自然过期(写入时加密 + 密钥轮换,过期即不可读)+ 匿名化(下次全量时该租户数据已不存在)。销毁证明里要写明备份过期时间与密钥状态,审计才认。
常见陷阱
  • 带差异切流:增量没追平就切,数据丢失不可逆;切流条件是校验通过,不是排期。
  • 没有回切窗口:切流即停双写,出问题只能跑路;观察期双写是生命线。
  • 备份不演练:恢复时发现 binlog 没开 ROW、时区错、漏表,事故现场才第一次见恢复流程。
  • 销毁只删主库:备份、缓存、日志、导出文件里的副本全是漏网之鱼;销毁清单要覆盖所有副本。
破题点拨
一眼定位:数据迁移 = 五阶段(拷/追/校/切/观)+ 可回切;备份 = 全量+binlog 的 PITR;销毁 = 四步留证。
破题主线:迁移编排(校验不过不切)→ 双写窗口保回切 → PITR 靠演练兜底 → 销毁覆盖副本。
易错点:带差异切流;无回切窗口;备份不演练;销毁漏副本。

六 本地生活 · 派单与 LBS 6 题

本组知识链

本地生活的主线是「供需在空间与时间上的匹配」。覆盖 LBS 检索、派单决策、计价、调度干预与运力预测。

  1. LBS 与 ETA:网格化索引、附近检索、时效预测模型。
  2. 派单:匹配策略、派单与抢单、压力均衡与公平性约束。
  3. 计价:规则引擎、动态计价合规、试算一致。
  4. 调度与风控:热区调度、预约单、轨迹反作弊。
  5. 运力预测:网格预测 + 供需比分层 + 定向引导,激励挂缺口。
工程化要点
  • 位置数据高频写入:网格聚合 + 批量上报,骑手位置走内存网格 + 定期落库。
  • 派单决策要快(百毫秒级)且可解释:候选集召回 → 评分排序 → 约束过滤。
  • 派单结果最终一致:先派后录,重复派单靠状态机 + 幂等收敛。
  • ETA 是概率不是承诺:分布预测 + 分位数承诺,持续校准。
本组思维模式
  • 供需均衡是目标,派单是调控手段:单量最优不等于系统最优。
  • 就近是约束不是目标:还有顺路、负载、公平性、客户等级多个维度。
  • 异常是常态:骑手取消、商家不出餐、用户联系不上,都要有预案路径。
  • 动态计价必须可解释:溢价合规、用户知情、监管报送。
合规与资损红线
  • 动态计价(溢价)须明示且幅度合规,禁止大数据差别定价(同服务同条件同价)。
  • 骑手轨迹属个人信息:采集最小必要、保存限期、脱敏后使用。
  • 派单公平性:禁止对骑手隐性压单(不合理的连派),抽成规则透明。
  • 用户地址、联系方式仅用于履约,禁止挪作营销(见 D02.04)。
D06.01
派单系统:匹配策略、派单与抢单、公平性设计专家P0
高频 领域模型 架构 匹配算法 压力均衡 公平性
场景背景

外卖平台午晚高峰百万级订单/小时,运力(骑手)十万级在线。派单决策要在一秒内完成:候选骑手召回 → 评分 → 派单 → 骑手响应(接单/拒绝)→ 超时转派。硬指标:准时率、骑手满意度(不能连续派烂单)、系统吞吐(峰值不堆积)、决策可解释(骑手问「为什么这单给我」要答得出来)。

核心挑战

① 多目标冲突:用户等得短、骑手跑得顺、平台成本低,三者此消彼长;② 压力均衡:避免单骑手被连续派单、局部区域运力抽干;③ 派单与抢单并存:派单(系统指派)保体验,抢单(骑手选择)保公平与激励;④ 异常路径:骑手拒单/取消、商家出餐慢、用户改址,派单结果要能重算;⑤ 实时性:位置、负载、订单状态都在变,决策依据是快照。

分析框架

派单是约束下的多目标优化,工程上分三步:召回 → 评分 → 仲裁。召回用空间网格(骑手位置 + 取餐距离)快速圈候选集;评分模型综合「顺路度、预计送达增益、骑手负载、客户权重」,输出期望分;仲裁层处理约束与公平性(同骑手最小间隔、区域最低在线数、连派上限、客户等级保底),并对「派单池」做批量配对(一趟多单的并单估算)。派单是「建议 + 响应窗口」:骑手秒级未响应或拒单即转下一候选,全程状态机驱动、幂等收敛(同一订单的派单记录唯一生效)。

领域模型

聚合根:派单单(DispatchOrder:订单、状态、当前指派、历史轨迹)。实体:指派记录(Assignment:骑手、分数、响应、时间)、运力快照。值对象:位置、评分向量。不变量:同一订单同一时刻至多一个生效指派;骑手并发在途单数 ≤ 上限;指派状态机单向流转(待响应 → 已接/已拒 → 已到店 → 已取货 → 已送达/已取消);拒单要记录原因(计入骑手画像与公平性校准)。

解决方案

1. 召回:骑手位置写入内存网格(Redis GEO 或自研网格),取餐点查周边网格骑手 + 扩展环(每轮扩大半径),控制在百毫秒预算内。

2. 评分:特征 = 距离、顺路(与在途单路径夹角)、预计送达增益、骑手当前负载、历史准时率;轻量模型(GBDT 或线性加权)在线推理;分数只用于排序,不对外承诺。

3. 约束与公平性:连派上限(同骑手 N 单后强制间隔)、区域保底(每网格至少留 M 骑手防抽干)、大单/远单轮候(避免总压给新人)、客户等级仅作小权重(禁止过度倾斜)。

4. 派单执行:推送 + 响应窗口(秒级)→ 接单锁定时效 → 拒单/超时进下一候选;并单在取餐后动态评估(同商家顺路单合并)。全部事件入消息队列,派单状态机最终一致。

sequenceDiagram participant O as 订单系统 participant D as 派单引擎 participant G as 运力网格 participant R as 骑手端 O->>D: 新订单(取餐点+期望) D->>G: 网格召回候选骑手 G-->>D: 候选集+位置/负载 D->>D: 评分排序+约束过滤 D->>R: 派单推送(响应窗口) alt 骑手接单 R-->>D: 接单 D->>D: 锁定指派(幂等) else 拒单/超时 R-->>D: 拒单/超时 D->>D: 记录原因+下一候选 end D->>O: 指派结果(状态机)
派单引擎:召回-评分-仲裁-响应闭环
机制 优点 代价
纯派单 体验最优、全局效率高 骑手被动感强、公平性争议
纯抢单 骑手自主、公平感强 远单烂单没人抢、准时率差
派抢结合 体验与公平平衡 系统复杂、需公平性校准
关键代码
// 派单评分:距离 + 顺路度 + 压力 + 公平性权重,取最优骑手
public Optional<RiderId> dispatch(OrderId oid, List<Rider> candidates) {
    return candidates.stream()
        .map(r -> Map.entry(r.id(),
            dist(r.pos(), oid.pickup()) * W_DIST                       // 距离成本
            + detour(r.route(), oid) * W_DETOUR                        // 顺路度(现有任务的绕行增量)
            + (r.activeOrders() >= r.capacity() ? 1e9 : 0)          // 容量硬约束
            - r.fairnessScore() * W_FAIR))                            // 公平性:低分优先(防饿死)
        .min(Comparator.comparing(Map.Entry::getValue))
        .map(Map.Entry::getKey);
}

// 派单冲突:乐观锁抢占,失败即转下一候选(禁双重派单)
public boolean claim(OrderId oid, RiderId rid) {
    int n = orderRepo.claimIfUnassigned(oid, rid, "DISPATCHING");
    if (n == 1) { riderRepo.incActive(rid); return true; }
    return false;                                          // 已被抢:重新评分下一骑手
}
深度追问
  • 评分模型上线后效果怎么评估?
    离线用历史数据回放(模拟派单 vs 实际派单的准时率/成本对比);在线小流量 AB,核心指标(准时率、客诉、骑手时薪)看统计显著性;同时监控「被拒单率」与「连派集中度」两个公平性指标。模型只是排序器,约束层才是安全底线。
  • 并单(一趟送多单)怎么评估该不该并?
    并单评估「增量成本」:合并后各单预计送达时间增量之和 vs 节省的运力成本;超过阈值的组合放弃。取餐后路径重算(动态 VRP 简化版),用户端明示「并单可能延长 X 分钟」。
  • 骑手批量同时拒单(罢工式)怎么办?
    熔断与降级:拒单率突增触发「抢单模式优先 + 放宽距离约束 + 调度奖励杠杆」;同时识别是模型问题(某类单被系统性地派给错误人群)还是运力问题(天气/突发),分别走模型回滚与运力调度。
  • 派单快照不一致(派的时候顺路,接的时候已经取了别的单)?
    指派带「运力版本号」,接单时校验版本;过期则重新评估(可能改派)。本质是乐观并发:基于快照决策,基于最新状态确认,冲突就重算而非将错就错。
常见陷阱
  • 只优化单一指标:准时率上去了骑手时薪下来了,两个月后运力流失;多目标 + 公平性指标同权监控。
  • 派单不幂等:重试导致一单两派;指派唯一键 + 状态机收敛。
  • 连派无上限:老骑手被连续派远单,集体退场;间隔与上限进约束层。
  • 决策黑盒:骑手问凭什么这单给我,客服答不出;评分因子与指派理由可查询。
破题点拨
一眼定位:派单 = 召回(网格)+ 评分(多目标)+ 仲裁(约束公平)+ 状态机闭环。
破题主线:候选召回 → 评分排序 → 约束公平过滤 → 响应窗口 → 拒单转下一候选 → 并单动态评估。
易错点:单指标优化;派单不幂等;连派无上限;决策不可解释。
D06.02
LBS 与 ETA:网格化检索、就近匹配与时效预测高级P0
领域模型 GeoHash 附近检索 ETA 预测 位置校准
场景背景

LBS 是本地生活的地基:用户「附近商家」、骑手「附近订单」、调度「热力分析」都建立在位置检索上。骑手位置秒级上报(十万骑手 × 每数秒),用户与商家位置亿级。ETA(预计送达时间)直接影响派单与用户预期。硬指标:附近检索毫秒级、位置写入不丢、ETA 误差可控(超时报备与补偿的依据)、位置数据合规。

核心挑战

① 网格边界:相邻网格的边缘漏查(查了本格漏了隔壁);② 高频写入:骑手位置直接写库必挂,聚合与批量是关键;③ ETA 不确定性:出餐时间、等电梯、路口红灯都是变量,点估计不可靠;④ 定位漂移:GPS 漂移、室内无信号,位置可信度分层;⑤ 距离口径:直线距离误导派单,路网距离计算成本高。

分析框架

位置检索走多级网格:GeoHash 前缀分级(粗格定位区域、细格精确定位),检索时查「中心格 + 周边八格」消边界;骑手热数据进内存网格(本地缓存 + 分片),批量聚合后异步落库(历史轨迹进时序库)。ETA 用分位数回归输出分布而非单点:特征 = 路网距离、历史同时段耗时、商家出餐均时、天气、骑手类型;承诺取保守分位(如 P80),校准闭环(实际送达回写,模型周更)。路网距离用「预计算 + 缓存」:热门 OD 对预计算,冷门实时算(降级直线距离 × 绕路系数)。

领域模型

聚合根:位置流(PositionStream:实体 ID、位置序列、可信度)。实体:网格单元(GridCell:层级、成员)。值对象:坐标、GeoHash、ETA 分布。不变量:位置带采集时间与可信度(漂移过滤后才进决策);ETA 承诺分位数一旦对用户公布即锁定(变更需显式通知);轨迹保留期合规(到期脱敏归档)。

解决方案

1. 附近商家(静态):商家入库时算 GeoHash(多精度),ES/Redis GEO 双索引;用户检索查中心格 + 八邻格,按距离 + 评分排序;结果短 TTL 缓存(位置不变内容不变)。

2. 附近骑手(动态):骑手位置进分片内存网格(按城市分片),写走批量合并(每数百毫秒一批);派单引擎直读内存网格;落库只服务轨迹回放与热力分析。

3. ETA:两段式(商家出餐预测 + 在途骑行预测),输出 P50/P80;对用户承诺 P80 + 缓冲分钟;实际送达回写训练集;天气/节假日走特征切换。

4. 漂移过滤:速度合理性检查(两帧位移/时长超过上限丢弃)、置信度分层(高精度 GPS/网络定位/基站分别权重)、静止判定防鬼畜。

flowchart TD Report[骑手上报] --> Filter[漂移过滤
速度合理性+置信度] Filter --> MemGrid[分片内存网格
批量合并写入] MemGrid --> Dispatch[派单引擎读取] MemGrid --> Batch[批量落库] Batch --> TSDB[时序库
轨迹/热力] Query[附近检索] --> Center[中心GeoHash] Center --> Neigh[+八邻格消边界] Neigh --> Sort[距离+评分排序] ETA[ETA服务] --> Seg1[出餐预测] ETA --> Seg2[在途预测] Seg1 --> Dist[P50/P80分布] Seg2 --> Dist Dist --> Promise[承诺P80+缓冲] Delivered[实际送达] --> Calib[校准回写]
LBS 数据链路与 ETA 两段式预测
附近检索:中心格 + 八邻格(消 GeoHash 边界漏查)
// GeoHash 精度 6(约 ±0.6km);查 3x3 邻域后按真实距离过滤排序
List<String> cells = geoHash.neighbors(centerHash, 6); // 含中心共九格
List<Merchant> hits = redis.geoSearch(KEY_SHOPS, cells);
List<Merchant> result = hits.stream()
    .filter(m -> haversine(m.getLoc(), userLoc) <= radiusKm)
    .sorted(comparing(m -> haversine(m.getLoc(), userLoc)))
    .limit(50)
    .toList();
关键代码
// GeoHash 网格:精度 6(约 150m),附近检索按 8 邻域展开
public Set<String> nearbyCells(LatLng p, int precision) {
    String center = GeoHash.encode(p.lat(), p.lng(), precision);
    return GeoHash.neighbors(center);                    // 中心 + 8 邻域,查 9 格覆盖边界
}

// ETA 校准:实测时长回流,用中位数衰减修正预测(防历史脏数据拉偏)
public Duration predictEta(Route r) {
    double base = routeModel.estimate(r);                // 距离/路况/时段模型
    List<Long> hist = feedback.recentDurations(r.cellPair(), 200);
    if (hist.size() >= 20) {
        double median = percentile(hist, 50);                // 中位数抗离群
        base = base * ALPHA + median * (1 - ALPHA);               // 衰减融合,新数据权重渐进
    }
    return Duration.ofSeconds((long) (base + SAFETY_MARGIN));
}
深度追问
  • ETA 预测偏差大,怎么系统性校准?
    建「ETA 校准闭环」:每次承诺与实际都入库,按 OD、时段、天气、骑手类型分桶算偏差;P80 承诺如果实际超率超过阈值,说明分位数偏乐观,回调重训。周级模型迭代 + 日级偏差看板是标准配置。
  • 用户地址定位不准(老小区/写字楼)?
    地址库治理:POI 库 + 用户历史纠错沉淀「万能坐标」;下单时推荐常用地址 + 地图打点确认;骑手端可标记「难找地址」进知识库。定位不准是产品问题,不只技术问题。
  • 十万骑手位置全量实时计算热力图,成本扛不住?
    热力图不需要单骑手精度:网格聚合计数即可,内存网格天然就是聚合层;历史热力走离线 T+1;实时热力按分钟窗口抽样。分层精度匹配分层成本。
  • 轨迹数据被爬/内部滥用?
    轨迹是敏感个人信息:访问审批 + 脱敏查询(默认不出真实 ID)+ 导出审计;保留期合规(到期删除/匿名化);技术上库级加密 + 查询白名单。
常见陷阱
  • 只查中心格:边界商家时隐时现;九宫格邻域是标配。
  • 骑手位置直写数据库:高峰写入打垮库;内存网格 + 批量落库。
  • ETA 用点估计:承诺 30 分钟实际 40 分钟天天超;分位数承诺 + 缓冲。
  • 直线距离派单:跨江/单行道场景全错;路网距离预计算 + 绕路系数。
破题点拨
一眼定位:LBS = 多级网格 + 邻域检索 + 批量位置流;ETA = 两段式 + 分位数承诺。
破题主线:静态商家索引 → 动态骑手内存网格 → 漂移过滤 → 路网距离 → ETA 分布承诺 → 校准闭环。
易错点:中心格单查;位置直写库;ETA 点估计;直线距离。
D06.03
计价规则引擎:动态计价、优惠叠加与一致性高级P1
领域模型 规则引擎 动态计价 试算一致
场景背景

配送费 = 起步价 + 距离费 + 时段费 + 天气费 − 优惠 + 动态溢价;不同城市不同规则,运营天天调。下单时试算价、支付时实收价、售后时退价必须一致。硬指标:规则热更新不重启、试算与实收一致(同输入同输出)、溢价可解释(用户问「为什么这单贵」)、审计可回溯(每一单的计价明细与规则版本)。

核心挑战

① 规则复杂度:距离阶梯、时段矩阵、天气联动、会员折扣、优惠叠加,组合爆炸;② 一致性与时效:试算后规则变了,实收按哪个版本算;③ 动态溢价合规:涨幅上限、明示、禁止差别定价;④ 可解释:价格构成要能逐项展示给用户;⑤ 热更新:规则变更要即时生效且不影响在途单。

分析框架

计价是确定性函数:同输入(距离/时段/天气/优惠/规则版本)必同输出。规则建模为「价目表 + 计费组件流水线」:组件按固定顺序执行(起步 → 距离阶梯 → 时段 → 动态系数 → 优惠 → 封顶),每组件输出「金额 + 理由」。试算时记录「规则版本 + 输入快照」,实收按该版本执行——版本快照是一致性的关键。动态系数(溢价)单独成组件,带上限(如不超过基础价若干成)、明示项、以及合规开关(恶劣天气/高峰允许,平时关闭)。规则配置走 DSL + 灰度,与 D05.04 同套治理。

领域模型

聚合根:计价单(PriceOrder:输入快照、规则版本、明细、总额)。实体:价目规则(RuleSet:组件序列、生效区间、灰度比例)。值对象:价格构成项、规则版本。不变量:总额 = 各构成项代数和;溢价幅度 ≤ 上限;试算快照未被消费则下次试算可刷新,已被订单引用则锁定;退款额 = 已付 − 已享受服务对应扣减(规则预声明)。

解决方案

1. 规则引擎:DSL 描述组件(距离阶梯、时段矩阵、天气系数);规则版本化 + 灰度发布;运行时加载器热更新(双 buffer 切换,无重启)。

2. 试算与锁定:下单试算返回「价格 + 明细 + 规则版本 + 有效期」;支付时校验版本未失效(失效则重试算并明示变化);订单锁定价格快照,售后按快照算退款。

3. 动态溢价:供需比 → 系数(封顶);恶劣天气补贴与高峰溢价分开配置;溢价项在价格明细单独展示(合规明示);用户端「为什么贵」页逐项解释。

4. 一致性保障:计价服务无状态 + 纯函数;同一快照重复执行结果逐字节一致(黄金用例集回归)。

flowchart LR In[输入
距离/时段/天气/优惠] --> V[规则版本
灰度热更新] V --> C1[起步价组件] C1 --> C2[距离阶梯组件] C2 --> C3[时段/天气组件] C3 --> C4[动态系数
封顶+明示] C4 --> C5[优惠叠加组件] C5 --> Cap[封顶/保底] Cap --> Out[价格+明细+理由] Out --> Snap[快照锁定
试算↔实收一致]
计价组件流水线与版本快照
机制 做法 风险
试算实收一致 规则版本 + 输入快照锁定 快照过期未刷新引发客诉
动态溢价 供需系数 + 封顶 + 明示 无封顶 → 舆情与监管
规则热更新 双 buffer 切换 + 灰度 半新半旧规则并存
关键代码
// 计价规则:按优先级逐项累加;只有 terminal 规则才停止后续(可解释)
public Price quote(QuoteCtx ctx, List<Rule> rules) {
    Price p = basePrice(ctx);                                 // 起步价 + 里程 + 时长
    for (Rule r : rules.stream().sorted(byPriority).toList()) {
        if (r.matches(ctx)) {                                // 每步记录命中规则,账单可解释
            p = r.apply(p, ctx);
            trace.add(r.id());
            if (r.terminal()) break;                     // 独占规则终止后续
        }
    }
    return p.clamp(ctx.minFare(), ctx.surgeCap());          // 溢价上限合规红线
}

// 试算一致:下单用同一 quote 函数与版本,禁两套实现漂移
public Price quoteAtOrder(QuoteCtx ctx) {
    return quote(ctx, ruleRepo.findActive(ruleVersion.current()));
}
深度追问
  • 试算后用户去支付,规则刚好变了?
    快照带版本与有效期(分钟级):有效期内按快照执行;过期则用新版本重算并弹窗明示「价格已按最新规则更新」,用户确认后下单。绝不让用户在不知情下按新价扣款。
  • 溢价算不算大数据杀熟?
    合规红线:溢价只与供需/天气/时段相关,绝不与用户画像(会员等级除外且需明示)相关;同用户同条件多次询价结果一致(防杀熟抽查进合规测试);审计可按订单回放「当时的供需输入」。
  • 计价出错批量订单怎么补偿?
    计价单留全量快照,可重放;批量订正任务(按规则版本 + 时间窗圈定受影响单)→ 生成退款/补收 → 用户通知。可重放是补救的前提,快照设计时就要想到这一天。
  • 规则引擎选型(自研 vs Drools vs 表达式)?
    计价这种「高频、强一致、要可解释」的场景:组件流水线 + 自研 DSL 往往最合适(Drools 的推理式对确定性函数过重,且可解释性与调试成本高)。原则:可解释 > 表达力,确定性 > 灵活性。
常见陷阱
  • 规则写死在代码里:运营每次调价发版;DSL + 配置化是刚需。
  • 试算实收两套代码:试算一个函数、扣款一个函数,必然分叉;同一纯函数。
  • 溢价无封顶:暴雨天溢价翻三倍,舆情事故;硬顶 + 分级审批。
  • 价格不可解释:用户投诉时客服看不到构成;明细即产品。
破题点拨
一眼定位:计价 = 组件流水线 + 规则版本快照 + 溢价封顶明示。
破题主线:DSL 建模 → 热更新灰度 → 试算锁快照 → 实收同函数 → 溢价合规 → 快照可重放。
易错点:规则硬编码;试算实收分叉;溢价无顶;不可解释。
D06.04
供需均衡与调度干预:热区调度、预约单与奖励杠杆架构P1
架构 供需预测 调度干预 预约单 激励杠杆
场景背景

潮汐效应明显:午高峰写字楼爆单、晚高峰住宅爆单、雨天全城紧张、演唱会散场局部爆炸。被动派单只会追赶缺口,主动调度才能削峰填谷。同时预约单(明天早八配送)要预分配运力。硬指标:热点区域不瘫、预约单履约率、调度成本可控(奖励不能乱撒)、干预效果可度量。

核心挑战

① 预测精度:需求预测(网格 × 时段)误差直接决定调度成败;② 干预手段组合:奖励(钱)、调度(指派人)、定价(溢价)、供给激励(冲单奖)各有副作用;③ 干预时序:提前多久干预、干预多强,过猛浪费、过猛无效;④ 预约单与实时单混跑:预约占运力与实时单抢运力;⑤ 效果归因:怎么证明这波不瘫是调度的功劳。

分析框架

与 D06.06 的边界:本卡是调度杠杆(定价微调、奖励、预约预留);需求预测与听单引导的执行见 D06.06。

调度是预测驱动的干预闭环:需求/供给预测(网格 × 时段,含天气事件特征)→ 缺口计算(需求 − 可运力)→ 干预策略(按缺口等级选杠杆:轻微靠定价微调、中等靠奖励引导、严重靠跨区调度 + 预约限制)→ 执行(奖励触达骑手、派单偏好调整、用户端时效管理)→ 效果回收(实际缺口 vs 预测、干预 ROI)。预约单走「运力预留」:按时段 × 网格预留运力池,预约单占用池容量,池不足时限制新预约或引导时段迁移。一切干预带预算约束与熔断(奖励超支自动停)。

领域模型

聚合根:调度策略(DispatchStrategy:网格、时段、缺口等级、干预组合、预算)。实体:干预执行记录、预约运力池。值对象:缺口等级、预算额度。不变量:干预总花费 ≤ 策略预算(超支即熔断);预约单占用运力不超过池容量;同一网格同时生效的策略唯一(避免多重干预叠加打架)。

解决方案

1. 预测:网格 × 15 分钟粒度,特征含历史同期、天气、节假日、事件(演唱会/球赛日历);输出需求区间而非点值,按区间下沿保守调度。

2. 干预策略:缺口分级(绿黄橙红)→ 杠杆矩阵:绿=不动;黄=轻微溢价 + 定向奖励(给缺口网格周边骑手);橙=跨区调度(邻区运力引流)+ 预约限流;红=全局调度 + 用户端时效拉长明示 + 运力上限保护(只接能送的单)。

3. 预约单:预约运力池(按时段预留),预约单抢占池容量;池水位低时引导用户换时段(给券);预约单提前 N 分钟派给专属运力,冲突时预约优先(用户承诺过)。

4. 归因与熔断:每次干预记录基线(不干预的仿真缺口)vs 实际,算 ROI;奖励预算日清,超支熔断;策略效果月度复盘,淘汰负收益策略。

flowchart TD Pred[需求/供给预测
网格×时段] --> Gap[缺口计算
需求−可运力] Gap --> Level{缺口分级} Level -->|绿| None[不干预] Level -->|黄| L1[轻微溢价+定向奖励] Level -->|橙| L2[跨区调度+预约限流] Level -->|红| L3[全局调度+时效明示] L1 --> Exec[策略执行] L2 --> Exec L3 --> Exec Exec --> Budget[预算熔断] Exec --> Recall[效果回收
基线vs实际ROI] Recall --> Pred Book[预约单] --> Pool[运力预留池] Pool -->|水位低| Migrate[换时段引导]
调度干预闭环与预约运力池
关键代码
// 供需均衡:热区预测 → 调度干预(奖励/调度单),预约单错峰
public List<Intervention> balance(Region region, Duration horizon) {
    SupplyDemand sd = forecast.predict(region, horizon);      // 需求/运力预测
    List<Intervention> acts = new ArrayList<>();
    if (sd.gap() > GAP_THRESHOLD) {                           // 缺口超阈值才干预
        acts.add(new Incentive(region, sd.gap() * UNIT_SUBSIDY)); // 动态奖励拉运力
        acts.add(new PullOrder(neighborRegions(region), region));  // 邻区调度单引流
    }
    return acts;
}

// 预约单:时间盒分配运力,避免与实时单抢骑手
public boolean reserve(OrderId oid, TimeSlot slot) {
    int n = capacity.tryReserve(slot.regionId(), slot.start(), slot.end(), oid);
    return n > 0;                                    // 超售即拒(进实时池或改约)
}
深度追问
  • 奖励发出去但骑手没去缺口区?
    奖励要有「到达生效」机制(进入网格 + 完单才结算),而非报名即得;奖励梯度与缺口等级匹配;对跨区骑手给「调度单」优先级(到达后优先派单)。纯撒钱是最贵的干预。
  • 预约单大面积违约(用户不留联系方式/商家不出餐)?
    预约单三段风控:接单时校验(商家营业状态)、执行前确认(提前联系用户/商家)、异常升级(改派/取消按规则定责)。预约单的违约率要单列指标,它是体验黑洞。
  • 怎么防止调度策略互相打架?
    策略注册中心 + 冲突检测(同网格同时生效唯一化);策略编排有优先级(全局 > 区域 > 网格);新策略上线先影子运行(只记录不执行)比对效果。
  • 突发事件(演唱会散场)没有历史数据怎么预测?
    事件日历 + 预案模板:场馆类事件提前标注,按容量 × 散场曲线 × 历史类似事件估算;预案提前一小时预置运力(定向奖励预热)。事件型需求的预测靠预案不靠模型。
常见陷阱
  • 纯被动派单:缺口永远追不上;预测驱动的事前干预才是调度。
  • 奖励无熔断:策略bug一晚上撒掉百万;预算日清 + 超支熔断。
  • 预约单无预留:预约单抢不过实时单,承诺全违约;运力池 + 预约优先。
  • 干预无归因:不知道钱花得值不值;基线仿真 + ROI 复盘。
破题点拨
一眼定位:调度 = 预测 → 缺口分级 → 杠杆矩阵 → 预算熔断 → 效果归因。
破题主线:区间预测 → 分级干预(定价/奖励/跨区/限流)→ 预约运力池 → ROI 闭环。
易错点:纯被动;奖励无熔断;预约无预留;干预无归因。
D06.05
骑手风控与安全:轨迹异常、作弊治理与保险理赔高级P2
轨迹风控 反作弊 设备指纹 保险理赔
场景背景

骑手侧黑产与风险:模拟器刷单、虚拟定位(人在家单在跑)、恶意索赔(餐损骗保)、账号买卖、交通安全事故。硬指标:作弊识别准(不误伤正常骑手)、处置可申诉、保险理赔自动化(摔伤/交通事故快速定损)、轨迹隐私合规。上述是观察口径,具体阈值须按本业务标定,禁止把本卡文字直接当成门禁数字。

核心挑战

① 虚拟定位识别:专业改机工具模拟 GPS,检测是猫鼠游戏;② 误伤平衡:误判导致骑手被罚/停单,劳资与舆情风险;③ 实时性:要在一单内识别而非事后;④ 骗保识别:餐损/事故理赔的欺诈模式;⑤ 处置与申诉:模型判罚要有据可查、可人工复核。

分析框架

风控分层:端上检测(改机特征、模拟器特征、传感器一致性——加速计与位移是否匹配)+ 轨迹分析(速度/停顿/跳跃的物理合理性、地址聚集度)+ 关系图谱(设备-账号-支付-提现的关联,团伙识别,复用 D02.02 能力)+ 理赔反欺诈(事故照片元数据、历史索赔频次、时间聚集性)。处置分级:观察 → 限制接单 → 冻结结算 → 封号,全部留痕可申诉。模型只产「风险分」,处置走规则审批,杜绝算法独裁。

领域模型

聚合根:骑手风控档案(RiderRiskProfile:风险分、标记、处置历史)。实体:风险事件(含证据快照)、处置单(可申诉)。值对象:设备指纹、轨迹特征。不变量:任何处置必有证据链(模型分 + 规则 + 快照);处罚可申诉且申诉期冻结执行升级;轨迹数据访问最小授权(隐私)。

解决方案

1. 端上:SDK 采集改机特征(root/模拟器/ hook 框架)、传感器一致性(位移与 GPS 速度匹配度)、上报频率异常;端上模型 + 服务端双判。

2. 轨迹:实时流检测(瞬移、速度超限、长时间静止但单在推进)、地址聚集(多骑手同一取餐点)、完成时效分布异常(总是卡点送达)。

3. 图谱:设备-账号-银行卡关联,团伙刷单识别(同设备多账号、同提现账户多骑手);复用反欺诈平台(D02.02)的图谱能力。

4. 理赔:事故报案自动定损(照片 EXIF、时间地点与轨迹比对、伤情描述结构化);欺诈分高的进人核;正常理赔自动赔付(体验优先)。

5. 处置:风险分 → 自动/人工处置 → 通知骑手(含理由)→ 申诉通道 → 复核纠错;处置记录进档案影响后续权重。

flowchart LR subgraph 信号层 A1[端上改机/传感器] A2[轨迹物理合理性] A3[关系图谱团伙] A4[理赔反欺诈] end A1 --> R[风险评分服务] A2 --> R A3 --> R A4 --> R R --> D{处置分级} D -->|低| Obs[观察] D -->|中| Limit[限制接单] D -->|高| Freeze[冻结结算] D -->|严重| Ban[封号] Limit --> Evidence[证据链+申诉] Freeze --> Evidence Ban --> Evidence Evidence --> Review[人工复核纠错]
骑手风控:信号 → 评分 → 分级处置 → 申诉闭环
关键代码
// 轨迹异常检测:瞬移/悬浮/绕路三类特征打分
public RiskScore detect(RiderTrack t) {
    double score = 0;
    if (t.maxJumpSpeedKph() > 200)   score += 50;        // 瞬移(GPS 作弊)
    if (t.idleSeconds() > 600 && t.inOrder()) score += 20; // 长时间悬浮
    score += detourRatio(t.route(), t.planned()) * 30;      // 绕路度(虚假里程)
    return new RiskScore(Math.min(score, 100), t.riderId());
}

// 处置分级:低分观察、中分限制接单、高分进人工 + 保险理赔标记
public void handle(RiskScore s) {
    if (s.value() >= 80) { riskCtrl.block(s.riderId()); audit.record(s); }  // 全程留痕供申诉
    else if (s.value() >= 50) riskCtrl.limitOrders(s.riderId(), 24);
}
深度追问
  • 误伤正常骑手怎么办?
    三道防线:阈值保守(高分才处置)+ 白名单(老骑手/高信用降权)+ 申诉快速通道(小时内人工复核)。风控指标要同时看「误杀率」——只盯召回的风控是耍流氓。
  • 改机工具升级,模型失效?
    检测特征持续运营(新特征周更)+ 多信号冗余(端上被绕过还有轨迹/图谱)+ 红军演练(定期用最新工具自测)。猫鼠游戏里,冗余比单点精度重要。
  • 轨迹数据给保险公司,合规边界?
    单独授权 + 最小范围(事故时段轨迹段,不出全量)+ 目的限定(仅理赔)+ 脱敏(不出身份信息,出匿名轨迹编号)。没有单独授权的轨迹外供是违法红线。
  • 骑手因封号闹到媒体,怎么自证?
    证据链完备性决定生死:处置单含「模型分 + 触发规则 + 行为快照(时间地点单号)」+ 人工复核记录 + 通知记录。拿不出证据链的处置,内部就要否决——这也是对骑手公平性的保护。
常见陷阱
  • 算法直接处罚:模型误判即停骑手生计,舆情与合规双输;模型产分、规则处置、人工复核。
  • 单信号判罚:只信 GPS 被改机工具绕过;多信号冗余。
  • 无申诉通道:处置不可逆,错误无法纠正;申诉 + 冻结升级。
  • 轨迹滥用:全量轨迹外供保险公司;单独授权 + 最小范围。
破题点拨
一眼定位:骑手风控 = 多信号评分 + 分级处置 + 证据链 + 申诉闭环。
破题主线:端上/轨迹/图谱/理赔四路信号 → 风险分 → 规则处置 → 人工复核 → 误杀率同权监控。
易错点:算法独裁;单信号;无申诉;轨迹滥用。
D06.06
运力预测与热区引导:需求预测、缺口预警与听单引导架构P1
高频 领域模型 架构 时空预测 供需比 听单引导
场景背景

网约车调度平台,早晚高峰供需失衡:核心商圈用车需求暴增而运力不足,用户排队十几分钟。硬指标:预测准确率(网格 × 十五分钟粒度)、供需比(需求/运力)分层准确、引导有效率(被引导司机实际成交占比)、用户排队时长。

核心挑战

① 时空预测:需求有强周期性(早晚高峰)又有突发性(天气/赛事/散会),历史模型对突发事件无力;② 供需分层:城市划网格 × 时间片,实时算供需比,缺口区域要分钟级刷新;③ 引导有效性:引导司机去热区,司机不去(没单白跑)或去了没单(预测错)都会伤害运力信任;④ 激励成本:听单奖励真金白银,要花在缺口处且防套利;⑤ 与派单协同:引导是事前调度、派单是事中匹配(D06.01),两者目标一致但手段不同,不能互相打架。

分析框架

与 D06.04 的边界:本卡是预测与听单引导的执行层;缺口等级上的奖励和预约杠杆见 D06.04,此处不重复。

预测分层:基线预测(历史周期 + 天气特征)+ 实时修正(当前订单流入速率),基线给趋势、实时纠偏差。供需比 = 未来时间片预测需求 / 当前可用运力,按比值分层(紧缺/平衡/过剩)。引导策略按分层触发:紧缺区域发定向听单奖励(向该区域行驶的司机优先派单);引导不承诺单,只提升优先级——避免「引导了必须给单」的信任陷阱。激励预算按缺口大小动态分配,单个司机单位时间奖励上限防套利。

领域模型

聚合根:预测任务(ForecastJob,城市 + 网格集 + 时间片 + 模型版本)。实体:供需快照(SupplyDemand,网格 × 时间片 × 比值 × 分层)、引导策略(GuidancePolicy,范围 + 激励 + 优先级)、引导记录(GuidanceLog,司机 × 区域 × 奖励 × 成交结果)。值对象:网格编号、供需比。不变量:供需比必须基于同一时间片的预测需求与实时运力;引导记录必关联成交结果(效果闭环);单司机奖励有单位时间上限。

解决方案

1. 网格化预测:城市按地理网格(如五百米)× 时间片(十五分钟)产出需求预测;特征含历史同期、天气、节假日、POI 属性;基线模型日更 + 实时流修正(分钟级)。

2. 供需比计算:实时运力(空闲司机位置)× 网格归属,除以预测需求;比值分层输出到调度策略层;缺口排行驱动引导与激励。

3. 听单引导:向「向紧缺区行驶」的司机提升派单优先级 + 定向奖励;司机侧热力可视化 + 预计排队时长;引导不带承诺,防信任透支。

4. 激励与防套利:奖励按缺口梯度发放,单位时间上限 + 成交才计满(避免空跑刷奖);异常模式识别(原地不动反复领奖)自动冻结。

5. 效果闭环:引导成交率、引导后区域供需比变化、用户排队时长三指标日报;预测误差回灌特征迭代模型。

graph TD A[历史同期+天气特征] --> B[基线预测 日更] C[实时订单流] --> D[流式修正 分钟级] B --> E[网格需求预测] D --> E F[空闲司机位置] --> G[供需比 = 需求/运力] E --> G G -->|紧缺| H[引导策略 定向奖励] G -->|平衡| I[常规派单] G -->|过剩| J[抑制流入] H --> K[引导记录 关联成交] K --> L[效果闭环 三指标]
供需分层与引导闭环
关键代码
// 供需比分层:预测需求 / 实时运力,分层决定策略
public Tier tierOf(Cell cell, TimeSlot slot) {
    double demand = forecast.predict(cell, slot);          // 基线+实时修正
    double supply = supplyIndex.idleDrivers(cell, slot);
    double ratio = supply < 1e-6 ? Double.MAX_VALUE : demand / supply;
    if (ratio > 1.5) return Tier.SHORTAGE;              // 紧缺:引导+奖励
    if (ratio < 0.6) return Tier.SURPLUS;               // 过剩:抑制流入
    return Tier.BALANCED;
}

// 引导奖励:单位时间上限防套利,成交才计满
if (tier == Tier.SHORTAGE && rewardCap.allow(driverId, Duration.ofMinutes(30))) {
    guidance.offer(driverId, cell, bonus, priorityBoost);   // 提升优先级,不承诺派单
}

// 效果闭环:引导成交率驱动预算再分配
double conv = guidanceLog.conversionRate(cell, Duration.ofDays(1));
if (conv < 0.2) budget.reallocate(cell);               // 低效区域预算收回
深度追问
  • 预测突然不准(暴雨天需求暴涨)怎么兜底?
    实时修正层吸收突发:当前订单流入速率与预测偏差超过阈值时,以实时数据为准快速上调;同时天气/事件特征进在线模型分钟级刷新。极端天气提前发天气预警(司机侧安全提示)比抢运力更重要——安全永远排在效率前。
  • 引导了司机不来,或者来了没单?
    「引导不承诺、只提优先级」是设计底线,来了没单的体验伤害靠「热区预计排队时长透明」缓解(司机自己决策);引导成交率进效果闭环,连续低效区域收回预算。反过来,若给引导承诺单,预测一错就是大面积投诉。
  • 激励被司机刷怎么防?
    三层:单位时间奖励上限、成交才计满(空跑只给小头)、行为模式识别(原地反复领奖/结队刷区)自动冻结人工复核。激励是补缺口不是发福利——预算与缺口挂钩,不搞普惠。
  • 引导和派单目标冲突怎么办?
    统一到供需比一个北极星:派单就近(用户时效),引导全局均衡(区域缺口),冲突时以「全局供需比改善」为仲裁口径;调度侧给引导来的司机适度优先级,但不过度——过度优先会伤害近处用户体验,两边都要看指标。
常见陷阱
  • 引导承诺单:预测错就翻车,司机信任一次性透支;只提优先级不承诺。
  • 普惠激励:人人有奖等于没奖,还招套利;预算只挂缺口。
  • 只看预测不看实时:突发事件下历史模型是盲的;实时修正是必须层。
  • 引导派单两套逻辑:一个优化用户时效一个优化运力均衡,互相打架;统一到供需比仲裁。
破题点拨
一眼定位:运力调度事前侧 = 网格预测 + 供需比分层 + 定向引导 + 激励挂缺口 + 效果闭环。
破题主线:基线+实时双修正 → 供需比分层 → 引导提优先级不承诺 → 奖励防套利 → 成交率闭环。
易错点:承诺单;普惠激励;无实时层;引导派单打架。

七 物联网与车联网 6 题

本组知识链

IoT 的主线是「海量不可靠客户端 + 命令必达」。覆盖接入、消息、影子、OTA、时序数据与设备全生命周期。

  1. 接入:百万长连接治理、MQTT 协议、一机一密双向鉴权。
  2. 消息:Topic 设计、规则路由、上行下行分流、广播限流。
  3. 影子:设备影子状态、离线缓存、命令 ACK 与重发。
  4. OTA 与时序:差分升级灰度回滚、信号压缩与轨迹回放。
  5. 设备生命周期:注册激活、影子追平、灰度熔断、自愈有界、退役吊销。
工程化要点
  • 长连接治理:单节点承载规划、心跳分级、优雅上下线(会话保持与重连风暴防护)。
  • 命令必达:下发带序列号,ACK 确认 + 超时重发 + 幂等执行。
  • 设备不可信:一切上报数据校验(时间戳、范围、签名),异常数据隔离。
  • 升级安全:差分 + 签名验签 + 断电回滚 + 灰度批次熔断。
本组思维模式
  • 设备是不可靠客户端:会掉线、会重连、时钟会漂、固件会卡;一切按「最终一致 + 可重发」设计。
  • 一切命令可重发:命令必须幂等,设备必须能安全重复执行。
  • 流量是潮汐的:固件集体重连、车队集中上报都是常态,按峰值设计。
  • 安全是出厂设置:一机一密、安全启动、固件签名,事后补安全等于返工。
合规与资损红线
  • 车控指令(远程解锁/启动)必须多重确认(用户二次授权 + 风控校验),误控即安全事故。
  • 车辆轨迹与车主信息属个人敏感信息:采集告知、用途限定、出境合规(地图测绘资质)。
  • OTA 升级失败导致车辆功能异常(刹车/转向相关)属重大安全风险,必须断电保护与回滚。
  • 固件签名私钥分级保管(HSM/KMS),泄露即全车队可控,等于召回级事故。
D07.01
设备接入网关:百万长连接、MQTT 与接入鉴权架构P0
高频 领域模型 架构 长连接 MQTT 一机一密
场景背景

IoT 平台接入五百万设备:智能家居高频小包(秒级上报)、车联网低频大包(秒级信号批量)、工业网关带子设备。硬指标:单集群百万级并发连接、消息端到端秒级、断连快速感知(秒级)、服务端扩容/缩容不抖(重连风暴防护)、鉴权按设备维度(不是用户维度)。

核心挑战

① 连接规模:单机连接数上限、内存与文件描述符、心跳开销;② 协议选择:MQTT(发布订阅、QoS、遗嘱)适配绝大多数场景,但要防 Topic 滥用与 retained 消息堆积;③ 鉴权:设备不能用密码体系(烧录即泄露),要一机一密(X.509 证书或设备密钥 + 平台签名挑战);④ 扩缩容:节点上下线时百万设备重连的惊群;⑤ 可靠性:消息不丢(QoS1/2 的成本)、乱序、重复。

分析框架

接入层用无状态网关 + 有状态会话:网关节点只做协议解析与转发,会话状态(设备-节点映射)集中存 Redis;设备按 ProductKey + DeviceName 路由到网关集群。鉴权走「一机一密」:设备烧录唯一设备密钥(或证书),连接时平台下发挑战,设备签名应答;密钥泄露可单设备吊销(不影响其他)。心跳分级:按设备类型设不同 keepalive(车载长、电池设备长保活用短心跳 + 批量合并);上下线走「遗嘱消息 + 服务端探测」双判定。扩容时网关节点按「随机抖动 + 逐步切流」上下线,避免同时重连。

领域模型

聚合根:设备(Device:ProductKey、DeviceName、状态、影子、固件版本、密钥引用)。实体:连接会话(Session:节点、连接时间、QoS)、子设备(网关拓扑)。值对象:Topic、QoS 级别、设备三元组。不变量:Topic 发布权限按产品模板严格限定(设备只能发自己的 topic);设备状态(在线/离线)以平台探测为准(客户端声称不算);同一 DeviceName 重复连接时旧连接被踢(单会话原则)。

解决方案

1. 协议与网关:MQTT over TLS(证书或密钥);网关集群无状态,会话映射存 Redis(设备 → 网关节点 + 连接 ID);Topic 规范 `/{productKey}/{deviceName}/{up|down}/{property|event|service}`,权限模板在服务端强制(越权 topic 直接拒)。

2. 鉴权:一机一密(设备密钥 HMAC 挑战应答)或 X.509(每设备证书,CA 分级);支持「临时令牌」供受限设备(无安全存储)短期接入。密钥支持单设备吊销与批量轮换。

3. 上下线:MQTT 遗嘱(异常断连即时感知)+ 服务端 keepalive 超时判定(正常关闭不发遗嘱);状态变更事件驱动业务(通知、影子标记)。

4. 消息可靠性:默认 QoS1(至少一次,业务幂等去重);关键控制指令 QoS2;消息总线(Kafka)落盘后消费,消费端按「设备 + 序列号」去重。

5. 扩缩容:新节点注册 → DNS/接入点灰度切流 → 旧节点先停止接受新连接再等存量自然迁移(或主动广播重连,加随机抖动);监控「重连速率」指标。

flowchart LR Dev[设备] -->|MQTT/TLS| GW1[网关节点A] Dev -->|MQTT/TLS| GW2[网关节点B] GW1 --> Auth[接入鉴权
一机一密/证书] GW2 --> Auth Auth --> Redis[(会话映射
设备→节点)] GW1 --> Kafka[消息总线] GW2 --> Kafka Kafka --> Rule[规则引擎路由] Rule --> Biz[业务消费] Biz -->|下发| GW1 Dev --> Will[遗嘱消息
异常断连感知]
设备接入网关:无状态网关 + 集中会话 + 总线解耦
鉴权方案 安全强度 成本 适用
ProductSecret 全局 低(一泄露全军) 零 不推荐
一机一密(HMAC 挑战) 中高(可单设备吊销) 低 通用设备
X.509 每设备证书 高 中(证书管理) 车载/工业
关键代码
// 一机一密:连接时按 deviceSecret 派生 token,禁长期固定口令
public AuthResult authConnect(String deviceId, String ts, String sign) {
    Device d = deviceRepo.find(deviceId).orElseThrow();
    if (!d.online()) return AuthResult.reject("设备未激活");
    if (Math.abs(now() - parse(ts)) > SKEW_MS) return AuthResult.reject("时间戳过期");
    String expect = hmacSha256(d.secretRef(), deviceId + ts);       // 密钥存 KMS,服务不落明文
    return MessageDigest.isEqual(expect, sign)
        ? AuthResult.accept(d.tenantId()) : AuthResult.reject("签名不符");
}

// 长连接配额:单租户/单设备限流,防单设备刷爆总线
public boolean tryAcquire(String deviceId, int qosMsgs) {
    return limiter.tryAcquire("dev:" + deviceId, qosMsgs, Rate.perSecond(50));
}
深度追问
  • 固件 bug 导致百万设备同时重连怎么办?
    防护在网关侧:接入限流(按产品/批次限连接速率)+ 重连抖动(服务端在 CONNACK 返回建议重连间隔,引导散开)+ 降级(拥塞时暂停非关键产品的接入优先级)。同时 OTA 批次本身要限制同时激活数量(见 D07.04)。
  • MQTT 的 retained 消息把网关撑爆?
    retained 只用于「最新状态」类小消息且设 TTL;大消息(如 OTA 通知)走普通发布 + 设备拉取模式(平台只发「有新固件」信号,设备按能力拉)。Topic 规范里明确禁用 retained 传大载荷。
  • 车载网络(4G 弱网)QoS 怎么选?
    信号类数据 QoS0/1 批量合并上报(省流量与电量);控制指令下行 QoS1 + 重发 + ACK;日志/诊断类存本地、连上 Wi-Fi 再批量补传。分级 QoS 是车载场景的基本功。
  • 单设备密钥泄露,如何止损?
    检测(异常连接地点/频率/行为)→ 自动吊销密钥 → 设备下次连接失败进入「安全重烧录」流程 → 运营侧推送固件更新。吊销要秒级生效(网关本地缓存 + 广播失效)。
常见陷阱
  • 全局 ProductSecret:一个设备破解全军沦陷;一机一密是底线。
  • Topic 不鉴权:设备能订阅/发布他人 topic;服务端权限模板强制。
  • 相信客户端在线状态:设备说在线就在线,实际早断;服务端探测 + 遗嘱。
  • 无重连风暴防护:一次固件 bug 引发全平台雪崩;接入限流 + 抖动 + 降级。
破题点拨
一眼定位:接入 = 无状态网关 + 集中会话 + 一机一密 + 分级 QoS。
破题主线:协议选型 → 鉴权(可吊销)→ Topic 权限 → 上下线双判定 → 消息总线解耦 → 扩缩容防风暴。
易错点:全局密钥;Topic 裸奔;客户端自称在线;无风暴防护。
D07.02
消息总线与规则引擎:上行下行、规则路由与广播限流高级P1
消息路由 规则引擎 广播限流 时序分离
场景背景

设备上报的消息要去不同地方:实时控制(空调指令,毫秒级)、业务库(设备属性更新)、时序库(历史曲线)、大数据(训练)、告警(规则触发)。下行也要多形态:单设备指令、群组广播(整车队升级通知)、订阅推送(App 提醒)。硬指标:消息不丢不重、路由规则可配置(运营可改)、广播不炸(百万设备同时推送)、上下行隔离(上行拥塞不影响下行)。

核心挑战

① 路由灵活性:不同产品/消息类型去向不同,规则要可配置且热更新;② 广播风暴:一次百万级广播如果同时下发,网关瞬间打满;③ 上下行隔离:上报洪峰(车队集中上报)不能阻塞指令下行;④ 消息积压:消费端故障时的缓冲与恢复;⑤ 顺序性:同设备消息的顺序保持(属性更新乱序会状态错乱)。

分析框架

消息进总线后按规则路由:规则引擎读「来源 Topic + 产品 + 消息类型」匹配规则(SQL 式或 Topic 通配),转发到对应 sink(Kafka topic / 服务 API / 存储)。实时控制类走独立低延迟通道(不经重路由),批量数据类走总线 + 批量消费。广播走「任务化」:拆分成批次(每批万级)、错峰下发、按在线状态过滤(离线设备转影子/消息中心),全程进度可观测。上行下行物理隔离(独立网关集群或至少独立队列),互不挤占。顺序性按「设备分区键」保证(同设备进同一分区,单分区内有序)。

领域模型

聚合根:路由规则(RouteRule:匹配条件、sink 列表、优先级、状态)。实体:广播任务(BroadcastJob:目标集、批次、进度、熔断)。值对象:Topic 模式、消息类型。不变量:规则按优先级唯一匹配(无歧义);广播任务必带速率限制与熔断(失败率超阈值自动停);同设备消息在同一分区(顺序保持);规则变更不影响在途消息的路由归属(版本绑定)。

解决方案

1. 规则引擎:规则用声明式配置(Topic 通配 + 条件表达式),编译后热加载;规则集版本化,消息路由时绑定规则版本(可追溯)。新增 sink(如新数仓)只加规则,不改代码。

2. 分流:实时指令(下行 QoS1,独立通道);属性/事件(上行,Kafka 按设备 ID 分区);时序数据(旁路进时序库,不阻塞主链);告警(规则引擎内嵌触发器,命中即告警通道)。

3. 广播:任务创建 → 目标解析(按产品/标签/分组)→ 在线过滤(离线转影子 + 消息中心)→ 分批下发(速率可配)→ 进度追踪(成功/失败/未达)→ 失败重试策略。

4. 顺序与积压:分区键 = DeviceName;消费端幂等(消息 ID 去重);积压监控(分区 lag)+ 自动扩容消费组;死信队列隔离毒消息。

flowchart TD Up[设备上报] --> Bus[消息总线 Kafka] Bus --> Rule[规则引擎
版本化+热加载] Rule --> RT[实时控制通道] Rule --> KV[属性存储] Rule --> TS[时序库旁路] Rule --> Alarm[告警触发] RT --> Down[下行网关集群
与上行隔离] BC[广播任务] --> Target[目标集解析] Target --> Online{在线过滤} Online -->|在线| Batch[分批错峰下发] Online -->|离线| Shadow[影子+消息中心] Batch --> Down
规则路由、上下行隔离与广播任务化
规则引擎:声明式路由配置(热加载 + 版本绑定)
// 规则 = Topic 模式 + 条件 → sink 列表;消息路由时记录命中的规则版本
{
  "ruleId": "route-temp-sensor",
  "match": {
    "topic": "/pk/+/up/property",
    "where": "properties.temperature > 80"
  },
  "sinks": ["kafka:alarm.high-temp", "api:notify-service"],
  "priority": 10,
  "enabled": true
}
关键代码
// 规则引擎:SQL 风格规则路由(topic 匹配 → 动作),热更新不重启
public List<Action> route(Message m) {
    List<Action> out = new ArrayList<>();
    for (Rule r : ruleCache.get()) {                        // 规则热更新(版本号比对刷新)
        if (r.match(m.topic(), m.tags())) {
            out.addAll(r.actions());                              // 转发/存储/函数计算/推送
            if (r.terminal()) break;
        }
    }
    return out;
}

// 广播限流:全设备广播按租户分片 + 令牌桶,防一条指令打爆所有连接
public void broadcast(Command c) {
    for (String tenant : sharding.tenants()) {                // 按租户分片串行发
        if (!bucket.tryAcquire(tenant, 1)) { queue.offer(tenant, c); continue; }
        connHub.sendToTenant(tenant, c);                         // 超限的进队列匀速放行
    }
}
深度追问
  • 规则引擎成了单点/瓶颈?
    规则引擎无状态水平扩展;匹配用「Topic 前缀树 + 条件预编译」降单条成本;热点规则(覆盖大量消息的)优先评估;规则数量上限治理(废弃规则定期清理)。压测按「规则数 × 消息速率」双维度。
  • 车队同时上报把 Kafka 打满?
    接入侧限流(按产品配额)+ 消息合并(网关侧对同类型小包按设备批量聚合)+ 分区扩容预案;Kafka 集群按业务隔离(实时/批量分开,互不影响)。容量按「峰值车队规模 × 单车信号频率」预留。
  • 同设备消息乱序导致属性覆盖错?
    消息带设备侧序列号 + 服务端时间戳;属性存储用「版本向量」或「时间戳大的赢」(时钟漂移场景用序列号兜底);消费端按设备串行消费(分区保证)。乱序在 IoT 是常态不是异常,设计时要默认它会来。
  • 广播任务中途要停止(发现内容错误)?
    任务可撤销(未下发批次立即停,已下发的尽力推送「撤回消息」——如下发新内容覆盖);广播内容必须模板化 + 预览 + 审批,禁止临时拼内容直接发。内容管控比技术撤回更根本。
常见陷阱
  • 路由写死在消费端:新产品接入要改代码发版;规则引擎声明式。
  • 广播同时全量下发:百万设备同时推送,网关瞬时过载;分批错峰 + 在线过滤。
  • 上下行共用通道:上报洪峰堵住指令;物理或队列隔离。
  • 假设消息有序:不按设备分区、不幂等;分区键 + 去重 + 版本向量。
破题点拨
一眼定位:消息系统 = 规则路由 + 上下行隔离 + 广播任务化 + 设备分区保序。
破题主线:声明式规则热加载 → 分流(实时/批量/时序/告警)→ 广播分批熔断 → 分区保序 + 幂等。
易错点:路由硬编码;广播全量同发;上下行共通道;假设有序。
D07.03
设备影子与命令管理:影子状态、离线缓存与超时重发高级P1
设备影子 命令 ACK 离线缓存 幂等重发
场景背景

App 要「随时查看/控制设备」,但设备大量时间离线(电池设备休眠、车载熄火)。命令下发时设备可能不在线。硬指标:App 读设备状态毫秒级(不依赖设备在线)、离线指令设备上线后必达、命令执行结果可确认(ACK)、重复下发不重复执行(幂等)、指令状态全程可查(已下发/已送达/已执行/失败)。上述是观察口径,具体阈值须按本业务标定,禁止把本卡文字直接当成门禁数字。

核心挑战

① 状态双源:用户设置的「期望状态」与设备实际上报的「实际状态」会不一致(指令在途/执行失败),展示哪个?② 离线必达:离线设备的指令存储、上线投递、过期清理;③ 命令幂等:重发、重连后重复投递,设备侧要能去重;④ 超时判定:设备收到不回 ACK(固件卡死)与真失败难以区分;⑤ 批量指令:群组控制(全家灯光)的一致性与部分失败处理。

分析框架

设备影子是平台侧的状态缓存:分「期望状态(desired,用户/规则写入)」与「上报状态(reported,设备更新)」两段,差值即待同步指令。App 读影子(合并展示 + 同步中标提示),设备上线后按 desired 驱动执行,执行后更新 reported。命令管理走状态机 + 序列号:命令带全局递增 ID,状态(待下发 → 已送达 → 执行成功/失败),ACK 超时重发(次数上限 + 退避),设备侧按命令 ID 去重(最近 N 条执行记录)。离线指令进「待投递队列」(按设备),设备上线批量投递;队列带 TTL(过期指令作废并通知)。群组指令 = 多个单设备指令的编排(部分失败可重试/可整体回滚声明)。

领域模型

聚合根:设备影子(Shadow:desired / reported / version)、命令(Command:ID、目标、负载、状态、投递记录)。实体:待投递队列项。值对象:命令状态、ACK 结果。不变量:影子 version 单调递增(每次 desired/reported 更新 +1);同一命令 ID 全局唯一且设备侧执行幂等;命令状态单向流转;过期命令作废后 desired 与 reported 的差值由业务规则裁决(重发或通知用户失败)。

解决方案

1. 影子:desired/reported 双段存储 + version;App 读 API 返回合并视图;差值计算服务生成同步指令。

2. 命令下发:写 desired → 若设备在线,立即下发(MQTT down topic);若离线,进待投递队列;设备 ACK 更新命令状态 + 更新 reported。

3. 重发与去重:ACK 超时(按设备类型分级)重发,指数退避,最多 N 次;设备侧维护「已执行命令 ID 集合」(滚动窗口),重复投递直接回 ACK 不执行。

4. 群组:群组指令展开为子命令集,逐设备跟踪;提供「全部成功/部分失败」结果视图,支持失败子集重试。

stateDiagram-v2 [*] --> 待下发: 写入desired 待下发 --> 已送达: 设备在线投递 待下发 --> 排队中: 设备离线 排队中 --> 已送达: 上线投递 排队中 --> 已过期: TTL到期作废 已送达 --> 执行成功: ACK成功+reported更新 已送达 --> 执行失败: ACK失败 已送达 --> 待重发: ACK超时 待重发 --> 已送达: 退避重发 待重发 --> 投递失败: 超过次数上限 执行成功 --> [*] 执行失败 --> [*] 已过期 --> [*] 投递失败 --> [*]
命令状态机(离线排队、超时重发、过期作废)
设备侧命令去重(幂等执行)
// 设备维护最近执行命令 ID 滚动窗口;重复投递直接 ACK 不执行
if (executedWindow.contains(cmd.getId())) {
    mqtt.publish(ackTopic, "{\"id\":\"" + cmd.getId() + "\",\"result\":\"ok\"}");
    return;
}
execute(cmd);                       // 真正执行一次
executedWindow.add(cmd.getId());    // 落盘(防重启丢窗口)
mqtt.publish(ackTopic, "{\"id\":\"" + cmd.getId() + "\",\"result\":\"ok\"}");
关键代码
// 设备影子:上报只改影子,命令读影子(设备离线也能下发)
public Shadow updateShadow(String deviceId, StateDelta delta) {
    Shadow s = shadowRepo.get(deviceId);
    Shadow next = s.apply(delta);                                 // desired vs reported 分离
    if (!next.reported().equals(s.reported())) {
        shadowRepo.save(next);                                // 版本号 +1,供并发合并
        eventBus.publish(new ShadowChanged(deviceId, next.version()));
    }
    return next;
}

// 命令必达:ACK 超时重发 + 次数上限 + 终态通知
public void sendWithRetry(Command cmd) {
    int tries = 0;
    while (tries < MAX_RETRY) {
        connHub.send(cmd.deviceId(), cmd);
        if (ack.await(cmd.cmdId(), TIMEOUT)) return;        // 设备 ACK 即终态
        tries++;
        backoff.sleep(tries);                                  // 指数退避
    }
    notifier.alert("命令未确认: " + cmd.cmdId());          // 转人工/工单,禁无限重发
}
深度追问
  • 用户连点开关(desired 快速变化)怎么处理?
    desired 用「最后写入赢」+ 合并(同一属性的 desired 只保留最新);在途命令若与新 desired 冲突,作废旧命令(通知用户「指令已被新操作取代」)。避免命令排队造成「点了三下灯闪三次」。
  • 设备执行了但 ACK 丢了(重复执行风险)?
    这正是设备侧去重窗口存在的原因:执行与 ACK 之间崩溃,重连后平台重发,设备凭命令 ID 去重直接回 ACK。窗口要落盘,大小按「重连窗口内可能收到的最大重复数」设计。
  • 影子数据量太大(属性上千)?
    影子按「属性分片 + 增量同步」:desired 只存差异(delta),reported 批量更新合并;App 侧按需订阅属性组。别把影子当全量状态库用。
  • 车控指令(远程启动)的安全加固?
    高危指令独立通道:用户二次确认(App 弹窗 + 短信验证码按风险分级)+ 风控校验(账号异常/异地操作拦截)+ 指令短有效期 + 执行结果强通知。与普通属性控制隔离(不同 topic、不同权限、不同审计级别)。
常见陷阱
  • App 直连设备状态:设备不在线就查不到,体验断裂;影子是唯一读源。
  • 离线指令无 TTL:半月前指令上线才执行,用户吓一跳;TTL + 作废通知。
  • 命令不幂等:重发导致灯开关两次;设备侧 ID 去重窗口。
  • 高危指令同通道:远程启动和开灯一个 topic,风险失控;分级通道 + 二次确认。
破题点拨
一眼定位:影子 = desired/reported 双段;命令 = 状态机 + 序列号幂等。
破题主线:影子解耦读写 → 离线队列 + TTL → 超时重发退避 → 设备侧去重 → 高危指令分级。
易错点:直连设备读状态;离线无 TTL;命令不幂等;高危不分级。
D07.04
OTA 升级:差分包、灰度策略与失败回滚架构P1
架构 差分包 灰度发布 断电回滚 签名验签
场景背景

车队十万台车要升级固件:流量贵(4G)、升级窗口短(夜间)、失败代价大(升级变砖 = 拖车召回)。硬指标:差分包体积最小化、分批灰度(先内测车队再公测再全量)、升级成功率可观测(每台的阶段进度)、失败可回滚(A/B 双分区)、固件来源可信(签名验签防刷恶意固件)。

核心挑战

① 包体积:整车固件数 GB,全量包流量与时间不可接受;② 灰度控制:批次比例、批次间观察期、异常熔断(升级后故障率上升自动停);③ 断电安全:升级中断电不能变砖;④ 版本兼容:跨多版本差分包生成与校验(从任意旧版本到新版本);⑤ 回退:升级后功能异常,如何安全退回旧版。

分析框架

与 D07.06 的边界:本卡只讲升级专属的差分、验签、灰度熔断与分区回滚;注册到退役的状态机见 D07.06。

OTA 四件套:差分包(bsdiff 类算法,新旧固件二进制差,通常压到十分之一体积)+ 签名验签(平台私钥签,设备公钥验,验签失败拒绝升级)+ 灰度批次(按设备分组逐批推进,批次间设观察期,核心指标——升级成功率、功能异常率、崩溃率——异常即熔断)+ A/B 双分区(固件写备份分区,启动失败或功能异常回切旧分区,即「失败回滚」)。升级流程状态机:通知 → 下载(差分包 + 校验)→ 验签 → 写入备份分区 → 切换启动 → 健康检查(心跳 + 功能自检)→ 确认升级(超时未确认自动回滚)。

领域模型

聚合根:升级任务(UpgradeJob:固件版本、批次计划、灰度策略、熔断阈值)。实体:设备升级记录(DeviceUpgrade:阶段、结果、回滚标记)。值对象:固件版本、批次策略。不变量:批次推进必须满足前置批次健康阈值;固件包必须验签通过才允许下载执行;每台设备同时至多一个进行中的升级;回滚操作幂等可重试。

解决方案

1. 差分与分发:构建侧为「每个受支持旧版本 → 新版本」生成差分包;CDN 分发 + 断点续传 + 分片校验;下载限速(不影响车机主业)。

2. 升级执行:设备下载差分包 → 校验 → 验签 → 合成新固件写入备份分区 → 标记下次启动分区 → 重启;车机引导层支持双分区切换。

3. 健康确认:新系统启动后 N 分钟内上报「心跳 + 关键功能自检」;超时未确认或自检失败 → 自动回切旧分区并上报;确认成功才清理旧分区(延迟清理,留后悔药窗口)。

4. 灰度与熔断:批次(内部 → 1% → 10% → 50% → 100%),批间观察期;监控升级成功率、变砖率、功能异常率、崩溃率;任一指标越阈自动暂停任务 + 告警,已升级设备评估回滚。

flowchart TD Notify[升级通知] --> Cond[前置条件检查
电量/存储/停车状态] Cond -->|满足| Dl[下载差分包
断点续传] Cond -->|不满足| Retry[延后重试] Dl --> Verify[校验+验签] Verify -->|失败| Abort[拒绝升级] Verify -->|通过| Write[写入备份分区] Write --> Switch[标记切换+重启] Switch --> Health{健康检查} Health -->|超时/失败| Rollback[回切旧分区] Health -->|通过| Confirm[确认升级] Confirm --> Cleanup[延迟清理旧分区] Monitor[灰度监控] -->|指标越阈| Pause[熔断暂停]
OTA 升级流程:差分、验签、双分区、健康确认回滚
机制 防什么 代价
差分包 流量/时间爆炸 构建复杂度、版本矩阵
A/B 双分区 升级变砖 双倍存储占用
灰度熔断 批量故障扩散 发布周期拉长
关键代码
// OTA 灰度:按批次百分比推进,每批失败率超阈值自动熔断
public void grayRelease(Firmware fw) {
    for (int batch : PLAN) {                              // 1% → 5% → 25% → 100%
        List<DeviceId> targets = selector.pick(fw.model(), batch);
        push.sendUpgrade(targets, fw);
        HealthReport r = monitor.await(fw.id(), Duration.ofHours(2));
        if (r.failRate() > FAIL_THRESHOLD) {
            push.rollback(fw);                              // 全量回滚到上一稳定版
            throw new ReleaseAbortedException(r.reason());
        }
    }
}

// 断电回滚:双分区 + 启动计数,连续失败自动切旧分区
public boolean bootGuard(String deviceId) {
    BootRecord b = bootRepo.find(deviceId);
    if (b.failCount() >= 3 && b.partition() == NEW_PART) {
        boot.switchPartition(deviceId, OLD_PART);               // A/B 分区互备
        bootRepo.resetFail(deviceId);
        return false;
    }
    return true;
}
深度追问
  • 差分包对任意旧版本都支持,版本矩阵爆炸?
    只对「在管网状版本集的最近 K 个版本」生成差分包,更老的先推一个「桥接版本」再差分;构建侧自动化为每次发布产出全套矩阵包,不手工维护。支持集声明在升级任务里,设备不在集内走全量包兜底。
  • 升级到一半车辆熄火断电?
    下载和合成阶段可断点续传(电量/停车状态门槛拦截写入阶段);写入分区阶段要求「停车 + 电量 > 阈值」,断电后下次启动继续/重来;分区写入原子性(写完校验才标记切换)保证不会启动半个固件。
  • 灰度到一半发现功能异常,怎么止损?
    熔断停推 + 评估已升级设备:影响严重的(安全相关)全量回滚到旧固件(反向差分包);影响有限的发补丁包修复。回滚本身也是一次 OTA,走同一套安全流程——别临时赶一个「回滚专用通道」,那最容易出事。
  • 固件签名密钥怎么管理?
    签名在构建流水线 HSM/KMS 内完成,私钥不出 HSM;设备侧公钥烧录 + 可吊销列表(CRL);多把密钥分级(量产签/测试签);密钥轮换预案(新旧双签过渡)。这是车队安全的地基,按「泄露 = 召回级」标准防护。
常见陷阱
  • 全量直推:不做灰度,一个固件 bug 全车队瘫痪;批次 + 熔断。
  • 不验签:设备被诱导升级恶意固件,整车可控;签名是底线。
  • 单分区升级:写坏即变砖;A/B 双分区。
  • 升级不确认:新系统起不来没人知道;健康检查 + 超时回滚。
破题点拨
一眼定位:OTA = 差分 + 验签 + 灰度熔断 + 双分区回滚。
破题主线:版本矩阵差分包 → 条件门槛 → 下载验签 → 备份分区 → 健康确认 → 灰度熔断 → 可回滚。
易错点:无灰度;不验签;单分区;无健康确认。
← 技术范式引用 S04.04 灰度发布与流量染色
D07.05
车联网时序数据:信号存储、压缩与轨迹回放高级P2
时序数据库 信号压缩 轨迹回放 冷热分层
场景背景

单车每秒上报数百个信号(车速、电池、电机温度…),十万台车年增 PB 级数据。用途:实时监控告警、用户 App 车况曲线、故障诊断(事故前后回放)、研发数据挖掘。硬指标:写入吞吐(百万点/秒级)、查询延迟(近实时秒级)、存储成本(冷数据压缩 + 分级存储)、回放保真(信号时间戳精度)。

核心挑战

① 写入峰值:车队集中上报的洪峰;② 高基数:设备 × 信号的时间序列数量巨大,索引不能按序列建;③ 压缩:同类信号高度相关(车速与轮速),delta-of-delta + 列式压缩效果显著但要保精度;④ 查询模式:最近数据热查、历史区间稀疏查、跨车聚合统计;⑤ 成本:全量 PB 存三年,对象存储 + 列式格式是必然。

分析框架

分层存储:热层(近数天,时序库如 IoTDB/TDengine/InfluxDB,内存 + SSD,服务实时查询)→ 温层(数月,列式压缩 Parquet 对象存储,服务区间查询)→ 冷层(年以上,深度压缩 + 归档,仅合规与挖掘用)。写入侧「批量 + 乱序容忍」:网关侧按车合并窗口批量落,迟到的数据进乱序缓冲区(时序库原生支持或应用层补洞)。压缩按信号类型分流:连续量(温度/速度)delta-of-delta + 浮点量化;枚举量(挡位/模式)字典编码;告警与故障快照单独存(不全量展开)。回放 = 按车 + 时间窗取全部信号,客户端本地插值对齐(信号上报频率不一)。

领域模型

聚合根:信号流(SignalStream:车 VIN、信号定义、时间序列)。实体:故障快照(FaultSnapshot:触发条件 + 前后窗口数据)。值对象:采样点(时间戳 + 值 + 质量位)。不变量:采样点带质量标识(有效/无效/缺失),缺失不等于零;时间戳以车端采集时刻为准(带时钟偏差校正记录);快照数据不可变(诊断证据)。

解决方案

1. 写入:消息总线(D07.02)按车分区 → 消费端批量组装(一车一批)→ 热层时序库;迟达数据(乱序)按「时间桶」补写,桶未封口可插、已封口进补偿日志。

2. 分层:定时任务把热层封版数据转列式(Parquet + 压缩)进对象存储,建分区索引(车 + 日期);查询引擎按时间路由(近 → 热层,远 → 温层联邦查询)。

3. 压缩:连续信号 delta-of-delta + Gorilla 式压缩;低频信号(分钟级)直接存点;枚举信号字典 + bit-pack。目标:温层压缩到原始的若干分之一。

4. 回放:诊断触发时(故障码/用户报障),自动截取「触发点前后数分钟」全信号快照(结构化存储);App 曲线按需拉取 + 降采样(LTTB 保留形状);研发批量挖掘走数仓 ETL。

flowchart LR Bus[消息总线
按车分区] --> Batch[批量组装
乱序缓冲] Batch --> Hot[(热层时序库
近数天)] Hot --> Job[转列式任务
Parquet+压缩] Job --> Warm[(温层对象存储
数月)] Warm --> Archive[(冷层归档
数年)] Query[查询路由] -->|近| Hot Query -->|远| Warm Fault[故障触发] --> Snap[前后窗口快照] Snap --> Diag[诊断/回放] App[App曲线] --> Hot App -->|降采样| Down[LTTB抽样]
时序数据分层存储与回放链路
关键代码
// 时序写入:按 deviceId + 时间分片,批量落库(禁单条直写)
public void ingest(List<Signal> batch) {
    Map<ShardKey, List<Signal>> byShard = batch.stream()
        .collect(groupingBy(s -> ShardKey.of(s.deviceId(), s.bucket(1, HOURS))));
    for (var e : byShard.entrySet()) {
        tsdb.writeBatch(e.getKey(), compress(e.getValue()));    //// Gorilla 式差分压缩
    }
}

// 轨迹回放:按时间窗抽样 + 关键帧补偿,平衡流畅与成本
public Track replay(String vehicleId, TimeRange range) {
    List<Signal> raw = tsdb.query(vehicleId, range);
    List<Point> pts = raw.stream()
        .filter(s -> s.ts() % SAMPLE_MS == 0 || s.isKeyFrame())  // 关键帧(转向/变速)必留
        .map(Point::fromSignal)
        .toList();
    return new Track(pts, interpolate(pts));              // 抽样点间插值补流畅
}
深度追问
  • 时钟漂移(车机时间不准)怎么办?
    平台记录「车端上报时刻 vs 服务端接收时刻」差值,漂移超阈值的车打标记;回放与对齐时用服务端接收序校正相对时间;关键分析(事故定责)以平台侧时间为准并保留车端原始时间戳备查。
  • 信号定义变更(固件升级改了信号表)?
    信号 schema 版本化(按固件版本绑定),存储带 schema 版本;查询引擎按版本映射(同一物理量不同名字的归一化);历史数据不回改,靠映射层兼容。
  • PB 级成本扛不住?
    三级降本:采样策略(非诊断车队降频/聚合)、保留策略(原始数据短留、聚合数据长留)、压缩升级(冷层换更强编解码)。先算「每车每月存储成本」账,再谈架构——成本模型是时序架构的第一指标。
  • 实时告警与时序库写入耦合导致互相拖?
    告警走独立流(Flink CEP 规则,不查库),时序库只服务查询;告警规则与信号 schema 绑定(版本兼容)。实时链路「流算」与存储链路「批/点查」解耦是时序平台的基本形态。
常见陷阱
  • 信号 Schema 不版本化:固件一改,历史数据全看不懂;按版本绑定 + 映射层。
  • 缺失当零值:断传时段画成零速曲线,误导诊断;质量位区分缺失。
  • 全量数据进热层:成本爆炸、查询变慢;冷热分层 + 保留策略。
  • 告警查库实现:洪峰时告警延迟几十秒;流式 CEP 独立通道。
破题点拨
一眼定位:时序平台 = 冷热分层 + schema 版本化 + 质量位 + 快照回放。
破题主线:分区批量写 → 热层 → 列式温冷层 → 查询路由联邦 → 故障快照 → 成本模型兜底。
易错点:schema 不版本;缺失当零;全量进热层;告警查库。
D07.06
设备生命周期管理:注册激活、批量运维与告警自愈高级P2
高频 领域模型 设备注册 影子设备 批量运维
场景背景

车联网平台管理数百万台车载终端。硬指标:设备激活率(出厂到首次在线)、批量配置下发成功率(灰度升级不能变砖)、告警自愈率(重启类问题不依赖人工)、退役设备数据清理合规。设备从出厂注册到退役回收全生命周期都要平台侧可管可控。

核心挑战

① 身份与激活:一机一密/一机一证书写入产线,激活时设备与车辆绑定,防止克隆设备与凭证泄露;② 批量运维:百万级设备配置变更/固件升级,灰度分批 + 失败止损,一次错误推送就是批量「变砖」事故;③ 状态管理:设备经常离线(车停放),期望配置(desired)与实际上报(reported)要分离,上线后自动追平;④ 告警自愈:终端死机/断连类告警自动执行重启/重置,减少人工上站;⑤ 退役合规:二手车过户/报废,设备解绑、证书吊销、本地数据清除。

分析框架

与 D07.04 的边界:本卡管生命周期与批量运维;差分包、验签和分区回滚见 D07.04,此处不展开。

设备状态机:注册 → 激活 → 在线 ⇄ 离线 → 退役,每次流转留事件。身份分层:产线烧录凭证(不可改)+ 激活时下发业务身份(设备-车辆绑定)。配置管理用影子设备(desired/reported 双状态):云侧改 desired,设备上线后拉取并回 acknowledged,差异即「待追平」。批量任务铁律:灰度(按批次/地域/车型)+ 版本回滚 + 失败率熔断(超阈值自动停推),任何推送可回退。自愈按告警类型路由(重启类自动执行、硬件类转人工工单)。

领域模型

聚合根:设备档案(Device,SN + 凭证 + 状态 + 绑定车辆)。实体:设备影子(Shadow,desired/reported/版本)、运维任务(OpsJob,类型 + 批次 + 灰度策略 + 熔断阈值)、退役单(Retirement,解绑 + 吊销 + 清除)。值对象:固件版本、凭证指纹。不变量:一台设备同一时刻只绑定一辆车;凭证吊销后设备必须无法再连;运维任务失败率超熔断阈值必须停推;退役单完成 = 解绑 + 吊销 + 清除三动作齐备。

解决方案

1. 注册激活:产线烧录唯一凭证(一机一密或 X.509 证书),激活时设备凭证 + 车辆 VIN 双重校验完成绑定;异常激活(异地/重复凭证)风控拦截。

2. 影子配置:云侧维护 desired 配置,设备上报 reported;配置下发 = 改 desired,设备下次连接自动追平并回执;离线设备上线后批量补拉,差异看板实时可见。

3. 批量运维:OTA/配置任务按灰度策略推进(内测 → 小比例 → 全量),每批观察窗口;失败率/变砖率超阈值自动熔断停推;固件签名验签防篡改,回滚包预置。

4. 告警自愈:告警分级(自愈类/人工类),自愈类(死机、断连)自动执行远程重启并验证恢复;恢复失败升级人工工单带设备侧日志。

5. 退役:过户/报废触发退役单:解绑车辆、吊销凭证(CRL/OCSP 实时生效)、远程擦除本地敏感数据、档案转历史态。

graph TD A[产线烧录凭证] --> B[注册] B --> C[激活 车辆VIN绑定] C --> D[在线 影子追平] D -->|休眠| E[离线] E -->|唤醒| D D --> F[退役单] F --> G[解绑+吊销+擦除] H[运维任务 灰度+熔断] --> D I[告警] -->|自愈类| J[远程重启+验证] I -->|硬件类| K[人工工单] J -->|恢复失败| K
设备生命周期与运维安全
关键代码
// 批量任务灰度熔断:每批观察窗口,失败率超阈值自动停推
for (Batch b : job.grayBatches()) {
    BatchResult r = executor.run(b);
    if (r.failureRate() > job.circuitThreshold()) {
        job.abort("CIRCUIT_BREAK");                    // 熔断停推,保留回滚包
        rollback.trigger(job.appliedDevices());
        break;
    }
    if (!r.healthyAfterWindow()) continue;       // 观察期不健康不推进
}

// 影子追平:设备上线后拉 desired,回 acknowledged;差异即待追平
Shadow s = shadowRepo.get(deviceId);
deviceClient.push(deviceId, s.getDesired());
deviceClient.onAck(deviceId, applied -> s.updateReported(applied));

// 退役三动作齐备才闭环
if (ticket.isUnbound() && ticket.isRevoked() && ticket.isWiped()) {
    credential.revoke(deviceId);                       // CRL 实时生效,此后设备无法再连
    archive.close(ticket);
}
深度追问
  • 设备凭证泄露了怎么补救?
    凭证分层是预防:产线烧录凭证只用于激活,激活后业务通信走短期会话 Token(可轮换);发现泄露立即吊销该凭证 + 拉黑设备指纹,风控侧监控异常激活(同凭证多地激活)。固件与通信全程签名验签,防中间人篡改。
  • 百万设备同时升级,怎么不发Together崩?
    推送节奏平台控制:灰度批次 + 拉取速率限制(设备分片错峰)+ 服务端按区域限流;设备侧断点续传(分包下载,失败重试单包);升级窗口避开业务高峰(夜间/车辆闲置)。大版本前全链路压测,固件包 CDN 预热。
  • 离线一个月的设备,配置怎么补?
    影子模型天然支持:desired 已改,设备上线后自动追平;但追平要判版本——配置结构有 breaking change 时按「版本 + 迁移脚本」逐级升,不能跳版本。长期离线设备(超九十天)进异常清单人工核查(是否被盗拆/长期停用)。
  • 自愈重启把设备重启循环了怎么办?
    自愈有次数熔断:单位时间自动重启超 N 次即停止自愈转人工,且上报「自愈无效」标记;重启前后抓取设备侧日志片段(环形缓冲),人工工单直接带现场。自愈是止损不是万能药——设计时就要给自愈设退出条件。
常见陷阱
  • 全量一把推:没有灰度熔断,一个坏固件变砖一片车;灰度 + 回滚包是命根子。
  • 凭证永久有效:泄露即永久失守;短期会话 + 可吊销才是正解。
  • 自愈无退出条件:重启循环把电池耗尽/日志冲掉;自愈要设熔断与现场留存。
  • 退役不吊销:二手设备还能连平台,数据风险 + 归属混乱;吊销是退役核心动作。
破题点拨
一眼定位:设备生命周期 = 状态机(注/激/在/离/退)+ 影子配置 + 灰度熔断 + 自愈有界 + 退役三动作。
破题主线:凭证分层防泄露 → 影子追平配置 → 灰度熔断保升级 → 自愈设退出 → 退役吊销留证。
易错点:全量推送;凭证永久;自愈死循环;退役不吊销。

八 内容社区与 UGC 6 题

本组知识链

内容社区的主线是「生产 → 分发 → 互动 → 生态」的飞轮。覆盖 Feed、审核、互动计数、创作者治理与推荐分发。

  1. Feed:推/拉/推拉结合、时间线、热度衰减与打散。
  2. 审核:机审多模态 + 人审工作台 + 举报闭环。
  3. 互动:计数一致性、弹幕广播、热点打散。
  4. 生态:分成结算、激励与信用分、版权水印。
  5. 推荐分发:多路召回、三级漏斗、重排保生态、冷启动保底。
工程化要点
  • Feed 按关注规模分流:大 V 用拉(读时合并),普通用户用推(写时扩散)。
  • 审核前置:发布链路先审后发(或先发后审 + 限流),违规内容可秒级下架。
  • 计数最终一致:Redis 累加 + 异步落库,对账校准;大 V 计数走分片合并。
  • 弹幕/评论广播分级:小房间全量广播,大房间采样 + 拉取补全。
本组思维模式
  • 内容安全优先于流量:审核能力决定平台生死,不是体验项。
  • 流量可伸缩、关系不可迁移:关注关系是核心资产,迁移即事故。
  • 计数是近似值,资损是真值:点赞可以漂移,分成不能错。
  • 创作者是客户:分成准确及时、规则透明,生态才转得动。
合规与资损红线
  • 内容安全红线:涉政、暴恐、色情、未成年保护内容零容忍,处置时限监管有要求。
  • 个人信息与隐私:用户发布内容含他人信息(晒单露手机号)需有治理通道。
  • 创作者分成涉及资金:结算数据与规则留痕,争议可查(同 D05.02 计费红线)。
  • 版权保护:侵权投诉通道法定时效(通知-删除),重复侵权账号处置。
D08.01
Feed 流架构:推拉结合、时间线与热度分发架构P0
高频 领域模型 架构 推拉结合 时间线 热度衰减
场景背景

社区日活千万,用户关注从几十到上万个(大 V 粉丝千万)。用户打开 App 要求百毫秒出首屏。内容分两类:关注流(我关注的人发了什么,强时序)与推荐流(算法分发,弱时序)。硬指标:首屏延迟、大 V 发帖后粉丝可见延迟、时间线不重不漏(同一帖子不重复出现)、已读状态同步。

核心挑战

① 大 V 写扩散爆炸:一个千万粉大 V 发帖,推模式要写千万个收件箱;② 读扩散合并成本:关注上万人,拉模式每次读要合并上万个时间线;③ 推拉取舍:普通用户推(读快),大 V 拉(写省),混合模式复杂度;④ 时间线一致性:推模式下收件箱分片、乱序到达、失败重试导致乱序或丢失;⑤ 推荐与关注的融合:两套排序口径在一个列表里穿插。

分析框架

经典推拉结合(hybrid):普通作者(粉丝 < 阈值)走推——发帖时写粉丝收件箱(收件箱 = Redis ZSet,按时间戳排序,容量上限淘汰);大 V 走拉——不发收件箱,用户读时实时拉取大 V 最新帖与本地收件箱合并。合并策略「时间线归并排序 + 已读过滤 + 分页游标」。一致性兜底:发帖事件带全局序列号,收件箱写入幂等(作者 ID + 帖子 ID),用户按游标翻页不重不漏。推荐流走独立召回-排序-重排链路(D10.01 检索思路复用),最终与关注流按「穿插规则」融合(如每 N 条插一条推荐,重试降权)。

领域模型

聚合根:内容帖(Post:作者、状态、可见范围、统计)。实体:收件箱项(InboxItem: follower、帖子、序列号)、关注关系。值对象:可见范围(公开/粉丝/私密)、排序分。不变量:收件箱项唯一(follower + post);删帖/违规下架从所有收件箱移除(异步广播删除事件);已读状态单调推进;时间线分页游标内不重不漏。

解决方案

1. 关注流:作者分级(粉丝数阈值)→ 推(MQ 异步写粉丝收件箱,批量合批写,限流保护)或拉(读时合并大 V 时间线);读路径 = 本地收件箱分页 + 大 V 实时拉取 + 归并去重 + 已读过滤。

2. 收件箱:Redis ZSet(score = 全局序列号),容量数千条(淘汰最旧);删帖/置顶/违规通过「 tombstone 列表」过滤;用户清理僵尸粉影响扩散范围。

3. 推荐流:多路召回(关注、兴趣、热门、广告)→ 粗排/精排 → 重排(多样性、疲劳度、已读过滤)→ 与关注流融合。

4. 首屏优化:本地缓存上次浏览位置 + 增量拉取;CDN 缓存静态内容;首屏数据预取(Wi-Fi 下预加载)。

flowchart TD Post[作者发帖] --> Level{粉丝数分级} Level -->|普通| Push[推模式
MQ异步写收件箱] Level -->|大V| PullMark[标记拉模式] Push --> Inbox[(粉丝收件箱
ZSet+容量淘汰)] Read[用户刷新] --> LocalInbox[读本地收件箱] Read --> BigV[拉取大V最新帖] LocalInbox --> Merge[归并排序+去重] BigV --> Merge Merge --> Filter[已读/ tombstone 过滤] Filter --> Mix[与推荐流融合] Mix --> Render[首屏渲染] Del[删帖/下架] --> Tomb[广播删除事件] Tomb --> Inbox
推拉结合 Feed:分级扩散、收件箱、归并融合
模式 写放大 读延迟 适用
推(写扩散) 高(粉丝数线性) 低(读本地) 普通作者
拉(读扩散) 低 高(合并成本) 大 V
推拉结合 中 中 主流方案
关键代码
// Feed 推拉结合:普通作者推(写扩散进收件箱),大 V 拉(读时现拉,不写收件箱)
public List<PostId> timeline(UserId me, Cursor cursor) {
    Set<UserId> bigVs = follows.bigVs(me);                   // 粉丝量超过阈值者走拉
    List<PostId> pushed = inbox.range(me, cursor, LIMIT);      // 普通关注者已写扩散进收件箱
    List<PostId> pulled = bigVs.stream()
        .flatMap(u -> outbox.latest(u, LIMIT).stream())       // 大 V 现拉,不进收件箱
        .sorted(byTimeDesc)
        .limit(LIMIT)
        .toList();
    return merge(pushed, pulled, cursor);                   // 合并去重 + 游标续页
}

// 热度分:时间衰减 + 互动加权,防老内容霸屏
public double hotScore(Post p, double now) {
    return (p.likes() * 1 + p.comments() * 3 + p.shares() * 5)
         / Math.pow(now - p.createdAt() + 2, GRAVITY);         //// 牛顿冷却式衰减
}
深度追问
  • 大 V 发帖后粉丝要等很久才看到?
    拉模式读时合并本身是秒级;感知延迟主要来自「读时机」(用户不刷新就看不到)与推送通道。对时效敏感的场景(直播/突发)用推送服务(APNs/厂商通道)补,不靠 Feed 本身。
  • 收件箱容量淘汰导致漏内容?
    正常——收件箱是「近期摘要」不是全量档案;用户翻到末尾时触发「回源补拉」(按游标从作者时间线补),保证想看历史时不丢。分层:新鲜事收件箱 + 历史归档库。
  • 删帖了但别人时间线还在?
    tombstone(逻辑删除标记)广播:删帖事件进 MQ,各消费端(收件箱、推荐缓存、搜索结果)订阅删除;读路径统一过 tombstone 过滤器。最终一致窗口(秒级)要接受,但合规下架类要求加速(优先通道 + 读时强校验)。
  • Feed 里广告和内容怎么平衡体验与收入?
    广告是内容的一种(带 bid 与 Targeting),走同一召回-排序框架,但在重排层有独立约束(间隔频次、位置权重、负面反馈降权)。把广告特殊化到主链外会处处打桩;纳入统一框架 + 独立约束层是正解。
常见陷阱
  • 大 V 也走推:一条帖子写爆 MQ 与 Redis;粉丝阈值分流。
  • 收件箱当全量库:无限增长内存爆炸;容量淘汰 + 历史回源。
  • 删帖不同步收件箱:违规内容还在别人时间线;tombstone 广播。
  • 已读状态服务端单点:已读同步延迟导致重复内容;游标 + 本地缓存。
破题点拨
一眼定位:Feed = 分级推拉 + 收件箱 ZSet + 归并融合 + tombstone 删除。
破题主线:作者分级 → 推/拉分流 → 读时归并去重 → 已读过滤 → 推荐融合 → 删帖广播。
易错点:大 V 推扩散;收件箱全量化;删帖不同步;已读单点。
D08.02
内容审核:机审人审协同、多模态与举报闭环架构P1
领域模型 架构 分级审核 多模态 人审工作台
场景背景

平台日发布千万级内容(文本/图片/视频/音频/直播)。监管要求违规内容及时处置(涉政暴恐等有时限)。机审覆盖绝大部分,但误杀漏放都有代价;人审是最后防线但成本高。硬指标:违规漏放率(监管指标)、误杀率(创作者体验)、处置时限(先审后发类秒级;直播类实时)、申诉处理时效。

核心挑战

① 多模态理解:视频要帧抽样 + ASR + OCR + 视觉综合,链路长成本高;② 分级策略:哪些必须先审后发(高风险类目/新账号)、哪些先发后审(低风险老账号);③ 机审误杀:误杀创作用户申诉,人审复核压力;④ 变体对抗:黑产用变体字、谐音、图内文字绕过文本机审;⑤ 直播实时性:延时要求秒级,抽帧 + 实时模型 + 断流处置。

分析框架

审核流水线 = 前置过滤(硬规则/黑样本)→ 机审(多模态模型并行)→ 风险分级 → 路由(自动过/机审不确定进人审/直接拦)→ 人审工作台 → 处置执行。分级策略按「内容风险 × 账号信誉」动态调整:高信誉老账号低风险内容走抽查,新账号/高风险类目全审。机审输出「标签 + 置信度」,中置信度进人审(主动学习让人审标注回流模型)。对抗变体用「归一化 + OCR + 多模型集成」。直播走旁路实时流(抽帧 + 音频 ASR + 即时模型),命中高风险即时断流 + 录制复核。所有处置(下架/限流/封号)走统一处置中心(幂等、留痕、可回滚)。

领域模型

聚合根:审核任务(ModerationTask:内容引用、风险分、状态、处置)。实体:机审结果(标签 + 置信度)、人审记录(审核员、结论、时长)。值对象:风险等级、处置类型。不变量:内容状态机(待审 → 通过/拦截/人工中)单向流转;处置与审核结论绑定(留证据链);申诉成立必撤销处置并记录(模型负样本);同一内容重复进入审核幂等(内容指纹去重)。

解决方案

1. 机审:文本(多语言 + 变体归一化)、图片(违规场景 + OCR 图内文字 + 人脸敏感信息)、视频(关键帧 + 场景 + ASR)、音频(ASR + 声纹违规);多模态结果融合加权;模型服务 GPU 集群 + 批量推理降本。

2. 分级与路由:策略引擎(内容类目 × 账号信誉 × 历史违规)→ 先审后发 / 先发后审 / 抽检;中置信度自动进人审队列;人审工作台(优先级排序、相似案例推荐、审核员质量抽检)。

3. 直播:旁路拉流抽帧(秒级)+ 实时模型;命中 → 分级处置(警告/断流/封播);全程录制留证。

4. 申诉与闭环:创作者申诉通道 → 人审复核(优先)→ 成立撤销 + 记录误杀样本回流模型;审核质量指标(漏放率、误杀率、人审一致率)周报驱动模型迭代。

flowchart LR Content[内容发布] --> Pre[前置硬规则过滤] Pre --> Mod[机审并行
文本/图片/视频/音频] Mod --> Score[风险融合评分] Score --> Route{分级路由} Route -->|低风险高信誉| AutoPass[自动通过] Route -->|中置信度| Human[人审工作台] Route -->|高风险| Block[直接拦截] Human --> Action[处置中心
下架/限流/封号] Block --> Action Action --> Appeal[创作者申诉] Appeal --> Review[复核] Review -->|成立| Revoke[撤销+回流样本] Live[直播流] --> RT[实时抽帧+模型] RT --> Cut[分级处置断流]
审核流水线:机审并行、分级路由、人审与申诉闭环
关键代码
// 分级审核:先机审,命中疑点进人审;高危类目强制人审
public AuditDecision review(Content c) {
    AuditResult machine = machineAudit.scan(c);               //// 多模态:文本+图+音视频
    if (machine.blocked()) return AuditDecision.reject(machine.hitRules());
    if (machine.suspicious() || highRiskCategory(c)) {
        return AuditDecision.pending(assignQueue(c));        // 人审工作台(先审后发)
    }
    return AuditDecision.pass();                         //// 低风险老账号先发后审
}

// 申诉复核:用户申诉进优先队列,复审结论覆盖原判并记录理由
public void appeal(ContentId cid, String reason) {
    queue.offerFront(cid, reason);                           //// 插队优先审(误杀成本高)
    audit.record("APPEAL", cid, reason);
}
深度追问
  • 机审模型被对抗样本绕过怎么办?
    纵深防御:前置规则(黑样本库持续更新)+ 多模型异构集成(单模型被绕其他补)+ 人审抽检 + 举报回流(用户举报是最大对抗样本来源)。安全是运营体系不是一次建模。
  • 误杀率高创作者流失怎么办?
    误杀指标与漏放同权监控;对高信誉创作者放宽阈值(信誉模型);申诉绿色通道(小时级);误杀样本自动回流重训。宁可漏放后快速处置,不可系统性误伤——生态伤不起。
  • 视频审核成本扛不住?
    分级抽帧(按时长/动态强度决定帧数)、异步批量(非实时类攒批推理)、模型蒸馏小模型跑高频、CDN 边缘预处理(抽帧在边缘)。审核成本按「每分钟视频审核成本」核算优化。
  • 人审团队绩效怎么设计才不逼审核员乱判?
    指标双轨:效率(单量)+ 质量(与专家基准的一致率、抽检符合率);单纯计件必然导致「一律通过」或「一律拦截」的博弈;抽检校准 + 争议案例集体评议 + 审核员心理健康支持(接触负面内容的劳动保护)。
常见陷阱
  • 机审一条链路走到底:模型不过就没人管;分级路由 + 人审兜底。
  • 先发后审一刀切:新账号高风险内容直接曝光;策略按信誉分级。
  • 处置不留证据链:创作者申诉时无法自证判罚依据;处置绑结论。
  • 误杀不回流:同一误杀月月发生;申诉样本进重训。
破题点拨
一眼定位:审核 = 前置规则 + 多模态机审 + 分级路由 + 人审兜底 + 申诉闭环。
破题主线:策略分级(信誉 × 风险)→ 并行机审 → 中置信进人审 → 处置中心留痕 → 误杀回流。
易错点:机审独走;一刀切先发后审;处置无证据;误杀不回流。
D08.03
互动系统:点赞评论弹幕与计数一致性高级P1
计数一致性 弹幕广播 热点打散 分片合并
场景背景

热点内容点赞百万级/分钟、评论十万条/分钟、直播间弹幕万条/分钟。硬指标:点赞即时反馈(用户点了立刻 +1)、计数不能漂太狠(与真实偏差可接受但要有校准)、弹幕不丢不重(观影体验)、大 V 内容计数不分片瓶颈、互动数据可对账(创作者看到的数=实际数)。

核心挑战

① 计数热点:单 key 千万级 increment,Redis 单分片扛不住;② 一致性:Redis 累加与 DB 落库的最终一致( crash 丢增量);③ 弹幕广播:万人在线房间,广播扇出与消息顺序;④ 取消点赞:点赞/取消的状态机与计数回退幂等;⑤ 创作者视角:后台数据要与前台计数一致(定时校准任务)。

分析框架

计数走分片 + 合并:单内容计数拆 N 个分片 key(如 post_like:{id}:{0..15}),用户按 user_id 哈希路由到固定分片(同一用户加减同分片,保证状态与计数一致);读取 SUM 合并;定期把分片和落库(对账校准)。用户点赞状态单独存(Set/位图),保证「我点过没有」与计数解耦但可对齐。弹幕走长连接广播分级:小房间(千级在线)全量广播;大房间(万级以上)按分片抽样推送 + 客户端拉取补齐 + 去重;消息带序号,客户端按序号重排。热点打散(同话题内容在 Feed 不连续出现)在推荐重排层做(D08.01)。

领域模型

聚合根:互动对象(Post/Comment/Live:计数集合)。实体:互动记录(Like:用户、对象、状态)、弹幕消息。值对象:计数值、分片路由。不变量:同一用户对同一对象点赞状态唯一(Set 判重 + 计数增减成对);计数分片增减必须成对事务(Redis Lua 或事务管道);弹幕序号单调(同房间);创作者数据 = 前台计数(校准任务兜底)。

解决方案

1. 点赞:Lua 原子完成「状态判断 + 分片计数 ±1」(状态 Set 与计数分片同 Lua);下发 MQ 异步落库(用户互动记录表,用于「我赞过的」)。

2. 计数读取:分片 SUM;缓存聚合值(短 TTL)+ 变更失效;创作者后台走校准任务(小时级:分片和 vs DB 记录数,差异修复)。

3. 评论:楼中楼(根评论 + 子评论两级,不分页深树);发布走「先写后审」或「先审后发」(按 D08.02 分级);盖楼计数分页懒加载。

4. 弹幕:网关长连接房间管理(同 D11.01 思路);消息入房间队列,广播按在线人数分级(全量/抽样+拉取);客户端序号缓冲重排 + 本地去重。

flowchart TD Click[用户点赞] --> Lua[Lua原子
状态Set+分片计数±1] Lua --> MQ[异步落库] MQ --> DB[(互动记录表)] Read[计数读取] --> Shards[SUM合并分片] Shards --> Cache[聚合缓存] Calib[校准任务] --> Diff[分片和vs记录数] Diff --> Fix[差异修复] Danmaku[弹幕发送] --> Room[房间队列] Room --> Level{在线人数} Level -->|小房间| Full[全量广播] Level -->|大房间| Sample[抽样+拉取补齐] Full --> Client[客户端序号重排] Sample --> Client
计数分片合并与弹幕分级广播
点赞 Lua:状态判断与分片计数成对原子
-- KEYS[1]=状态Set KEYS[2]=计数分片 ARGV[1]=userId ARGV[2]=对象id
local has = redis.call('SISMEMBER', KEYS[1], ARGV[1])
if has == 1 then
  redis.call('SREM', KEYS[1], ARGV[1])
  return redis.call('DECR', KEYS[2])
else
  redis.call('SADD', KEYS[1], ARGV[1])
  return redis.call('INCR', KEYS[2])
end
关键代码
// 计数一致性:本地累加 + 定时合并落库,禁每赞一次一次写
public void like(PostId pid, UserId uid) {
    if (!likeSet.add(pid, uid)) return;                   //// 本地 set 去重(同人重复赞无效)
    counter.incr(pid, 1);                                  //// 本地计数(Caffeine)
    flushQueue.offer(pid);                                  //// 进合并队列
}

public void scheduledFlush() {                          //// 每 200ms 合并刷库
    Map<PostId, Long> deltas = counter.drainAndReset();
    repo.batchAdd(deltas);                                  //// UPDATE ... SET likes = likes + ?
}

// 弹幕广播:按视频分片房间,本地扇出 + 跨节点走 MQ
public void danmaku(VideoId vid, Danmaku d) {
    for (Channel ch : room(vid).localChannels()) ch.write(d);
    if (room(vid).hasRemoteMembers()) mq.send("danmaku", vid.hash(), d);
}
深度追问
  • Redis 崩了丢了几分钟增量?
    AOF 每秒刷盘(丢秒级)+ 重启后从 DB 记录重建状态 Set 与分片计数(按时间戳增量补偿);更狠的用「计数 binlog 双写」,崩溃后按 binlog 重放。计数允许短暂漂移,校准任务兜底最终一致。
  • 分片数定多少?
    按单内容峰值 QPS / 单分片安全线(万级/秒)估算,取 2 的幂(16/32/64),预留峰值余量;分片扩容要「翻倍 + 合并迁移」(不能中途改数,路由函数一致)。分片数是内容维度的容量规划,不是全局配置。
  • 弹幕顺序乱(先看到后发的)?
    消息带房间内序号(发弹幕服务统一发号);客户端按序号缓冲 + 乱序容忍窗口(几百毫秒)+ 追帧机制(落后太多快进)。实时场景的顺序是「最终顺序 + 容忍窗口」,不是强一致。
  • 刷量(机器刷赞)怎么防?
    接入风控(D02.02 思路):设备指纹 + 行为序列(注册即点赞异常)+ 速率限制;计数侧「影子计数」(原始)与「展示计数」(风控过滤后)分离,创作者看展示值。互动计数永远要假设有假量。
常见陷阱
  • 单 key 计数:热点内容把 Redis 单分片打满;分片 + 合并。
  • 计数与状态分离两次写:点了没加上/加了状态没记;Lua 成对原子。
  • 弹幕全量广播:万人房间广播打满网关;分级广播。
  • 无校准任务:前台数与后台数越来越远;小时级对账修复。
破题点拨
一眼定位:互动 = 分片计数 + Lua 原子 + 分级广播 + 校准兜底。
破题主线:分片(按峰值规划)→ 状态与计数成对 → 弹幕分级 + 序号 → 影子计数防风控 → 校准对账。
易错点:单 key 热点;状态计数两次写;弹幕全量;无校准。
D08.04
创作者生态:分成结算、激励规则与信用体系高级P2
分成结算 激励规则 信用分 MCN
场景背景

创作者收益来源多元:流量分成(广告收益按内容贡献分)、付费分成(付费内容/课程抽成)、打赏、任务激励(平台活动奖金)。还有 MCN 机构代管分账。硬指标:结算准确及时(月结是创作者的生命线)、规则透明可查(每一笔收益可追溯)、激励防刷(羊毛党)、信用体系(违规降权限流)。

核心挑战

① 收益归因:一笔广告收益怎么分到内容(观看/点击/转化归因模型);② 结算一致性:收益数据来自多源(广告平台、打赏、任务),口径与延迟不一;③ MCN 分账:机构与创作者的二级分账、代扣税;④ 激励防刷:任务奖励被羊毛党刷(小号矩阵);⑤ 争议处理:创作者质疑收益缩水,如何举证。

分析框架

收益链路 = 归因 → 计价 → 账单 → 结算(复用 D05.02 计费骨架):广告收益按归因窗口(点击/转化回传,见 D10.03)分到内容;计价按分成规则版本化(内容类型 × 创作者等级 × 结算周期);月账单生成(幂等 + 明细可下载);结算 = 账单 → 扣税 → 分账(创作者/MCN 按合同比例)→ 打款。激励是「虚拟账单」:任务规则 DSL + 完成度校验 + 防刷风控(设备/行为图谱)+ 发奖预算熔断。信用分体系:违规记录、内容质量、履约历史 → 信用等级 → 影响分成系数与流量权重,一切扣减必须通知到人。

领域模型

聚合根:创作者账户(CreatorAccount:等级、信用分、余额、合同)。实体:收益单(Earning:来源、归因、金额、状态)、激励任务、分账记录。值对象:分成规则版本、信用等级。不变量:收益单不可篡改(冲正走新单);结算单 = 收益单合计 − 扣税 − 分账,账账相符;信用扣减必有通知与依据;MCN 分账比例按合同版本生效区间执行。

解决方案

1. 归因与计价:内容带追踪 ID,广告/付费转化回传按归因窗口匹配;收益日报(创作者可见明细)→ 月账单(含规则版本号);

2. 结算:账单确认 → 税务计算(劳务/经营所得代扣)→ MCN 分账(按合同比例拆)→ 打款(企业付款到银行卡/钱包);失败重试 + 人工兜底;

3. 激励:任务规则 DSL(参与条件 + 奖励梯度 + 发放上限);完成度按真实数据(播放/互动增量)校验,风控过滤后发奖;预算日熔断;

4. 信用:信用分模型(违规扣分、优质加分、时效恢复);影响流量与分成系数;扣分必通知(依据 + 申诉通道),恢复有明确路径。

flowchart LR Ads[广告平台] --> Attr[归因引擎
窗口+追踪ID] Pay[付费/打赏] --> Attr Task[激励任务] --> Attr Attr --> Rule[计价规则版本] Rule --> Daily[收益日报] Daily --> Monthly[月账单幂等] Monthly --> Tax[扣税计算] Tax --> Split[MCN分账] Split --> Pay2[打款] Monthly --> Query[明细下载
创作者可查] Violation[违规记录] --> Credit[信用分] Credit --> Coef[流量/分成系数]
创作者收益链路与信用体系
关键代码
// 创作者分成:可结算余额 = 已确认收入 - 已提现 - 冻结(处罚/纠纷)
public Money settleable(CreatorId cid) {
    Money confirmed = revenueRepo.confirmed(cid);           //// 平台已确认口径
    Money withdrawn = payoutRepo.totalWithdrawn(cid);
    Money frozen    = freezeRepo.activeFrozen(cid);           //// 违规处罚/纠纷冻结
    return confirmed.subtract(withdrawn).subtract(frozen).maxZero();
}

// 信用分:多维度衰减,低分降级流量与提现额度
public void applyPenalty(CreatorId cid, Violation v) {
    int delta = -v.severity().weight();
    credit.adjust(cid, delta);                              //// 每次调整落流水(可申诉)
    if (credit.scoreOf(cid) < LOW_THRESHOLD) {
        traffic.throttle(cid);                              //// 降权 + 禁提现 + 强制学习
        notify.send(cid, "信用分扣减说明", v.evidence());
    }
}
深度追问
  • 创作者说「这个月收益少了一半」怎么查?
    明细即证据:创作者可下载「收益单 → 归因 → 计价规则版本」全链路数据;平台侧按月快照(规则版本 + 流量数据),可重放。收益争议的解决靠数据透明,不靠客服解释。
  • 激励活动被羊毛党刷爆?
    防刷在任务设计期:参与门槛(注册时长/实名/历史活跃)+ 设备与行为风控(D02.02)+ 奖励后置(先验证真实增量再发)+ 预算熔断。事后追回是下策,规则设计期就要堵。
  • MCN 与创作者分账纠纷?
    平台按「电子合同」(比例 + 生效区间)执行分账,合同签署流程线上化留痕;纠纷走平台调解 + 合同仲裁,平台分账记录是第三方证据。合同版本与生效区间管理是核心技术点。
  • 信用分误伤优质创作者?
    信用模型可解释(扣分项明示)+ 申诉快速通道 + 恢复机制(连续优质加分恢复);灰度期对头部创作者只观察不处罚。信用是治理工具,不是惩罚工具——用反了生态就死了。
常见陷阱
  • 收益数据黑箱:创作者只看到总数看不到明细,信任崩塌;明细可下载。
  • 规则变更不版本化:历史收益按新规则重算,争议不断;版本 + 快照。
  • 激励无防刷:羊毛党刷走预算,真实创作者没激励;门槛 + 风控 + 后置发奖。
  • 信用只罚不恢复:创作者摆烂或出走;恢复路径 + 加分机制。
破题点拨
一眼定位:生态 = 归因计价账单结算(计费骨架)+ 激励防刷 + 信用可解释。
破题主线:多源归因 → 规则版本化 → 月账单幂等 → 扣税分账 → 激励防刷 → 信用通知与恢复。
易错点:收益黑箱;规则不版本;激励无防刷;信用只罚。
D08.05
版权与内容安全:指纹去重、水印与侵权处置高级P2
内容指纹 水印 侵权处置 通知删除
场景背景

平台上海量搬运(视频切片二剪)、图文洗稿、录播盗播。权利人投诉要依法及时处置(通知-删除规则),重复侵权账号要升级处置。同时平台要用指纹技术主动发现侵权(版权库比对)。硬指标:入库视频比对召回率、误判率(不误伤原创)、投诉处置时效、举证便利(权利人可提交版权证明)。

核心挑战

① 指纹鲁棒性:二剪、缩放、加台标仍能匹配;② 规模:亿级视频库的实时比对(新发布 vs 全库);③ 误判:同源素材(模板、BGM)导致误伤原创;④ 权利管理:谁提交的版权、授权范围(独家/非独家/期限);⑤ 处置合规:通知-删除流程、反通知恢复、重复侵权处置阶梯。

分析框架

指纹用感知哈希 + 关键帧特征:视频抽关键帧提特征(SIFT/深度 embedding),多帧签名组合;入库时建索引(局部敏感哈希),新内容比对召回候选再精排。音频指纹(歌曲)用声纹特征库。侵权发现两条路:主动比对(新发布内容过指纹库)+ 被动投诉(权利人在线提交:版权证明 + 侵权位置)。处置走「通知-删除」:有效通知 → 下架/限流 + 通知被投诉方;被投诉方可反通知(申诉)→ 成立则恢复。重复侵权账号按阶梯处置(警告 → 限流 → 封号),全程留痕。

领域模型

聚合根:版权作品(CopyrightWork:权利人、授权范围、期限、指纹集)。实体:侵权投诉(Complaint:通知内容、状态、处置)、指纹比对记录。值对象:授权范围、处置类型。不变量:投诉必须含法定要素(权利人信息、权属证明、侵权定位)否则不受理;处置与投诉/反通知绑定留痕;重复侵权计数按账号累计;授权过期后指纹库自动退场。

解决方案

1. 指纹入库:权利人提交作品 + 权属证明 → 审核 → 抽帧提特征入库(多帧签名);授权范围结构化(地区/期限/场景)。

2. 主动发现:新发布内容异步过指纹比对(帧特征 LSH 召回 + 精排);命中按授权策略处置(下架/分利/白名单);误判申诉通道。

3. 投诉处置:在线投诉表单(法定要素校验)→ 人工/自动审核 → 处置(下架/断开链接/限流)→ 通知双方;反通知 → 复核 → 恢复/移交司法机关。

4. 水印与溯源:原创内容支持可见水印(作者 ID)+ 隐形数字水印(溯源泄露);下载内容带水印,泄露可追溯。

flowchart TD Owner[权利人] --> Submit[提交作品+权属证明] Submit --> FP[指纹入库
多帧特征+授权策略] New[新发布内容] --> Match[指纹比对
LSH召回+精排] FP --> Match Match -->|命中| Policy{授权策略} Policy -->|独家侵权| Takedown[下架/限流] Policy -->|可分利| Share[分利模式] Policy -->|白名单| Pass[放行] Complaint[侵权投诉] --> Valid{法定要素校验} Valid -->|有效| Deal[处置+通知双方] Valid -->|无效| Reject[不予受理] Deal --> Counter[反通知] Counter --> Restore[复核恢复/移交] Takedown --> Repeat[重复侵权累计] Repeat --> Escalate[阶梯处置]
版权指纹比对与通知-删除处置闭环
关键代码
// 内容指纹:感知哈希 + 分片签名,改尺寸/压缩仍可识别
public Fingerprint extract(Media m) {
    if (m.isVideo()) {                                     //// 抽帧 + 帧间差分
        return Fingerprint.of(frames(m, 10).stream().map(phash).toList());
    }
    return Fingerprint.of(phash(m));
}

public List<Hit> match(Fingerprint fp) {
    return fpIndex.hammingSearch(fp, THRESHOLD);            //// 海明距离阈值(抗压缩)
}

// 侵权处置:通知-删除 + 反通知 + 必要记录(避风港合规)
public void takedown(ContentId cid, Notice n) {
    content.hide(cid, TAKEDOWN_REASON);
    notice.archive(n);                                     //// 权利人通知存档
    notify.send(owner(cid), n, "反通知通道已开启");      //// 给上传者反通知权利
    audit.record("TAKEDOWN", cid, n.claimant());
}
深度追问
  • 二剪作品算侵权吗?
    看授权策略:权利人可设「允许二创分利」(平台代分成,互利)或「全禁」;技术发现后按策略处置。产品上给权利人选项,比一刀切更可持续——很多 IP 方愿意二创带来流量。
  • 误判原创怎么办?
    误杀申诉通道(提交原创证明:工程文件/时间戳/首发记录)+ 模型迭代(误判样本回流);同源素材(模板/BGM)设「合理使用白名单」。版权判罚是法律行为,平台要「审慎 + 可纠错」。
  • 投诉量大人工处理不过来?
    要素齐全 + 指纹命中明确的「自动化处置」(法律上通知-删除本就是准司法流程,平台按程序执行即可);复杂权属(多方授权、期限交叉)进人工。把明确的做自动,模糊的留人工。
  • 隐形水印被破解怎么办?
    水印算法分级(鲁棒 vs 容量)+ 多算法冗余(同时嵌两种)+ 水印泄露事件响应(定位泄露源批次)。水印是威慑与溯源,不是绝对防线——配合账号行为分析定位泄露路径。
常见陷阱
  • 只靠投诉被动处理:侵权内容存活期长,权利人流失;主动指纹比对。
  • 指纹库无授权期限管理:过期授权还在拦内容,违法;授权期限 + 自动退场。
  • 处置不通知被投诉方:程序不合法,平台败诉;通知-删除-反通知全流程。
  • 重复侵权不计较:职业搬运号反复横跳;账号维度累计 + 阶梯处置。
破题点拨
一眼定位:版权 = 指纹比对(主动)+ 通知删除(被动)+ 授权策略 + 阶梯处置。
破题主线:指纹入库(授权期限)→ 新内容比对 → 策略处置(下架/分利/白名单)→ 投诉法定流程 → 误判可纠。
易错点:只被动;授权过期;处置不通知;重复不计较。
D08.06
推荐系统:多路召回、粗排精排与冷启动架构P0
高频 领域模型 架构 多路召回 粗排精排 冷启动
场景背景

内容社区 Feed 流分发,日活千万级,内容池亿级。硬指标:Feed 点击率、人均消费时长、内容多样性(不能全推同质化)、新内容曝光占比(冷启动公平性)、接口耗时(端到端两百毫秒内)。

核心挑战

① 召回覆盖:亿级内容不可能全打分,多路召回(兴趣/相似/热门/关注/新内容)要覆盖广又不丢准;② 排序目标:点击只是起点,时长、互动、负反馈都要进目标,多目标权衡;③ 冷启动:新用户没画像、新内容没交互,两边都要有探索机制;④ 延迟约束:召回粗排精排三级漏斗,每级都有耗时预算,深度模型不能无限大;⑤ 生态健康:马太效应——热内容越推越热,新内容永无出头;推荐系统要对内容生态负责,不只对点击负责。

分析框架

经典三级漏斗:召回(万级候选,多路并行取并集)→ 粗排(轻量模型千级打分)→ 精排(深度模型百级)→ 重排(多样性/疲劳/业务规则)。召回路数按用户状态切换(新用户走热门 + 探索路)。排序目标多目标加权(点击/时长/互动),精排用深度模型(特征交叉),粗排用双塔(用户向量 × 内容向量点积,可预计算)。冷启动两侧:新内容进「探索池」保底曝光(按内容质量分),新用户兴趣快速探测(选择式冷启动 + 热门兜底)。重排解决「精排同质化」:打散(同作者/同话题间隔)、多样性约束(MMR 思路)、负反馈过滤。

领域模型

聚合根:推荐请求(Request,用户态 + 场景 + 上下文)。实体:召回源(RecallSource,路数 + 向量索引 + 更新频率)、排序模型(RankModel,粗/精 + 目标权重)、曝光记录(Impression,内容 × 位置 × 反馈)。值对象:用户画像、内容特征。不变量:曝光必有点击/负反馈回流(训练闭环);同一次请求内同一内容不重复出现;新内容保底曝光配额不被热内容挤占。

解决方案

1. 召回层:多路并行(兴趣向量 ANN、相似用户协同、关注、热门、新内容探索、搜索意图),各路配额 + 去重合并;向量索引(ANN)日更 + 增量更新。

2. 粗排:双塔模型(用户塔实时、内容塔离线预计算),点积打分千级候选;粗排模型小(延迟预算内),筛掉明显不相关的。

3. 精排:深度模型(特征交叉 + 多目标),实时特征(当前会话行为)+ 离线特征;打分后带业务校准(时长权重、时效衰减)。

4. 重排:多样性打散(同作者/同话题最小间隔)、负反馈过滤(点踩类实时降权)、新内容流量扶持(按质量分插位)、疲劳度(已曝光多次的内容降权)。

5. 冷启动与探索:新用户兴趣探测(前几刷混合热门 + 多兴趣试探)、新内容保底曝光(按质量分阶梯给量);探索利用平衡(bandit 思路),探索比例可配。

graph TD A[推荐请求 用户态+上下文] --> B[多路召回 兴趣/协同/关注/热门/新内容] B --> C[合并去重 万级] C --> D[粗排 双塔点积 千级] D --> E[精排 深度多目标 百级] E --> F[重排 打散/负反馈/扶持] F --> G[Feed 输出] H[曝光反馈] --> I[训练闭环] I --> D I --> E
召回-粗排-精排-重排漏斗
关键代码
// 多路召回合并:各路配额 + 去重,新内容探索路保底
Map<String, List<Item>> lanes = new HashMap<>();
lanes.put("interest", ann.search(userVec, quota(400)));
lanes.put("cf",       cf.recall(userId, quota(200)));
lanes.put("fresh",    explore.recall(userId, quota(100)));   // 新内容保底
Set<Item> merged = Lanes.merge(lanes);                      // 配额内并集去重

// 重排:同作者最小间隔 + 负反馈实时降权 + 多样性约束
List<Item> out = new ArrayList<>();
int lastAuthorGap = 0;
for (Item it : ranked) {
    if (feedback.isNegative(userId, it)) continue;          // 负反馈过滤
    if (it.authorId().equals(out.isEmpty() ? null : out.get(out.size() - 1).authorId()) && ++lastAuthorGap < 3) continue; // 同作者间隔
    out.add(it);
    if (out.size() == feedSize) break;
}
// 冷启动:新内容按质量分阶梯给量,探索比例可配
if (user.isNew()) feed = explore.mixWithHot(feed, hotQuota(0.3));
深度追问
  • 多路召回的配额怎么定?
    按用户状态动态调:老用户兴趣路为主(协同 + 兴趣向量),新用户热门 + 探索路为主;各路贡献(曝光占比 × 点击效率)周报复盘,持续挪配额。死规则是「一路打天下」——单路必然有盲区(兴趣路推不出新兴趣)。
  • 精排模型越来越复杂,延迟兜不住怎么办?
    延迟预算是硬约束:粗排挡掉八成(双塔可预计算),精排模型裁剪(特征筛选 + 蒸馏小模型在线服务,大模型离线产出训练数据);复杂逻辑(重排规则)后置到精排后。性能不够先动架构(缓存/预计算/裁剪),不是硬扛超时。
  • 新内容起不来怎么办?
    两层:保底曝光(新内容按质量分进探索池,保证最低曝光量)+ 阶梯给量(早期交互好加速晋级,不好自然沉);创作者侧新作者冷启动扶持(前几篇内容流量加权)。保底配额是硬规则——不给新内容留位置,热内容会把流量全吸走。
  • 点击率高但用户流失,怎么平衡?
    目标函数不能只有点击:多目标(点击 + 时长 + 互动 - 负反馈),负反馈(点踩/快速划走)实时回流降权;长期看留存指标(七日回访)是北极星。标题党点两次就被负反馈压下去——反馈闭环比模型更管用。
常见陷阱
  • 单路召回:兴趣路打天下,用户越推越窄;多路 + 动态配额是底线。
  • 只优化点击:标题党泛滥、用户流失;多目标 + 负反馈闭环。
  • 新内容无保底:马太效应,生态死水;探索池配额写进规则。
  • 反馈不闭环:曝光没回流,模型离线跑天级更新;实时特征 + 准实时回流。
破题点拨
一眼定位:推荐 = 多路召回(配额动态)+ 三级漏斗(粗双塔/精深度)+ 重排(打散/负反馈/扶持)+ 冷启动保底。
破题主线:召回覆盖 → 粗排省算力 → 精排多目标 → 重排保生态 → 冷启动给配额。
易错点:单路召回;只点点击;新内容没保底;反馈不闭环。

九 物流与供应链 6 题

本组知识链

物流的主线是「约束求解 + 异常治理」。覆盖仓配规划、运输调度、面单轨迹、库存计划与供应链协同。

  1. 仓配:分仓策略、前置仓、波次拣货与路径。
  2. 运输:车辆调度、路径优化、装载率与在途跟踪。
  3. 面单轨迹:单号生成、轨迹上报流、时效预测。
  4. 计划与逆向:需求预测、安全库存、退换与理赔。
  5. 供应链协同:分层预测、ATP 三源承诺、S&OP 变更单、偏差周内纠偏。
工程化要点
  • 单号全局唯一且可路由(含承运商编码段),生成器分片发号。
  • 轨迹是高并发追加流:按运单分区写,时序存储,查询按时间窗。
  • 求解类功能(分仓/路径/装载)离线算 + 在线近似:精确解留给夜间,秒级响应用启发式。
  • 异常是常态:超时未揽收、滞留、破损都有监控规则与升级路径。
本组思维模式
  • 履约是约束求解:时效、成本、容量是多目标,不是许愿。
  • 异常是常态不是意外:异常处理路径是主干流程,不是兜底代码。
  • 数据先于优化:没有准确的在途与库存数据,一切调度算法是空中楼阁。
  • 成本模型显式化:每个调度决策要能算出它花了多少钱。
  • 时效承诺有依据:承诺时效 = 预测时效 + 安全余量,拍脑袋承诺就是客诉。
合规与资损红线
  • 物流面单含姓名/电话/地址:隐私面单(虚拟号)与数据留存合规。
  • 理赔定损涉及资金:定损标准、举证、审批链留痕(防骗保)。
  • 危险品/禁寄品查验是法定义务,拦截记录留证。
  • 承运商结算数据防篡改,对账差异处理有据可查。
D09.01
履约网络与仓配:多仓策略、前置仓与波次拣货架构P1
领域模型 架构 分仓策略 波次拣货 履约寻源
场景背景

全国仓配网络:中心仓(大而全)+ 区域仓(覆省级)+ 前置仓/微仓(覆社区级,三十分钟达)。用户下单后要「寻源」:选哪个仓发货、拆不拆单(A 仓发家电 B 仓发耗材)、次日达还是当日达。仓内作业:波次拣货(把一批订单聚成群按路径拣)、打包、交接承运。硬指标:寻源决策秒级、拆单率可控、缺货率低、仓内人效。

核心挑战

① 寻源多目标:时效、运费、仓容、库存健康(临期先出)冲突;② 拆单体验:拆多了物流费贵用户烦,拆少了发不出;③ 库存可见性:可售库存与在途/锁定库存的准确映射;④ 波次与路径:仓内拣货路径优化是组合爆炸;⑤ 大促压测:爆单时仓内产能瓶颈与截单策略。

分析框架

寻源是带约束的多仓分配:候选仓(按库存与距离过滤)→ 成本函数(运费 + 时效惩罚 + 拆单惩罚)→ 求解(小规模精确/大规模启发:先按「时效可达」过滤,再按成本贪心 + 局部交换优化)。拆单决策内嵌寻源(同仓合并优先)。仓内走波次 + 路径:订单聚批(按仓区/承运商/时效)→ 拣货路径(S 型/分区接力,货位热力图优化布局)→ 复核打包。库存视图分「可售/锁定/在途/退货在途」,寻源用可售 + 预留(未付款锁定,超时释放,同 D03.02 三态思路)。大促走「寻源降级 + 截单策略」(偏远线路提前截单,页面明示)。

领域模型

聚合根:履约单(FulfillmentOrder:子单、仓分配、承运、状态)。实体:子单(SubOrder:仓、商品行、状态)、波次(Wave:订单集、拣货路径)、货位。值对象:寻源结果(仓 + 成本 + 时效)、库存视图。不变量:子单库存扣减原子(不超卖);履约状态机(已寻源 → 已拣 → 已打包 → 已交接 → 运输中 → 签收/逆向)单向流转;拆单后父单与子单金额合计一致。

解决方案

1. 寻源服务:输入订单行 + 收货地址;候选仓按「库存覆盖 + 地理距离 + 线路可达」过滤;成本函数综合运费(按重量体积距离报价)、时效等级、拆单数;输出寻源方案(含备选)。高频城市-仓对预计算运费矩阵,冷门实时算。

2. 拆单:按「仓覆盖 + 承运能力」贪心合并(同仓同承运优先);不可合并时拆并通知用户(合并运费规则);拆单结果写履约单,子单独立状态机。

3. 仓内:波次生成器(按截单时间 + 仓区 + 承运商聚合)→ 拣货任务下发(PDA,路径按货位热力排序)→ 复核(称重/扫码)→ 打包 → 交接承运(揽收扫描)。

4. 大促:产能预估(历史峰值 × 增长系数)→ 寻源降级(跨仓调拨预案)→ 截单策略(分线路分时段截单,提前在商品页透出)→ 仓内加班/众包运力预案。

flowchart TD Order[订单] --> Src[寻源服务] Src --> Filter[候选仓过滤
库存+距离+线路] Filter --> Cost[成本函数
运费+时效+拆单] Cost --> Plan[寻源方案] Plan --> Split{需要拆单?} Split -->|否| One[单仓履约单] Split -->|是| Multi[多子单+用户通知] One --> Wave[波次聚合] Multi --> Wave Wave --> Pick[拣货路径下发] Pick --> Pack[复核打包] Pack --> Hand[交接承运] Peak[大促预案] --> Src Peak --> Cut[截单策略]
履约寻源、拆单与仓内波次作业
关键代码
// 寻源成本函数:运费 + 时效惩罚 + 拆单惩罚,取最小
public SourcingPlan source(Order o, List<Warehouse> candidates) {
    return candidates.stream()
        .filter(w -> w.covers(o.lines()) && w.reachable(o.address(), o.promiseLevel()))
        .map(w -> Map.entry(w,
              freight(w, o) * W_FREIGHT
            + latePenalty(w.eta(o)) * W_ETA                        //// 超时效硬惩罚
            + splitPenalty(o, w) * W_SPLIT))                       //// 拆单越多罚越高
        .min(Comparator.comparing(Map.Entry::getValue))
        .map(e -> SourcingPlan.of(e.getKey(), o))
        .orElse(SourcingPlan.crossWarehouse(o));        //// 单仓不覆盖:跨仓拆单方案
}

// 库存校验:可售 = 实物 - 锁定 - 在途未上架,防超卖
public int available(SkuId sku, WarehouseId w) {
    Inv inv = invRepo.find(sku, w);
    return inv.physical() - inv.locked() - inv.inTransitNotShelved();
}
深度追问
  • 寻源结果导致运费亏损(选了贵仓)怎么发现?
    每单记录「寻源成本快照」(仓、运费、时效等级),日级看「单均运费/履约成本」看板;偏离基线的单抽样归因(是库存布局问题还是成本函数权重问题)。成本可视是寻源迭代的前提。
  • 库存数据不准导致超寻源(派单了没货)?
    寻源时锁定库存(预占,同 D03.02 预扣思路);仓内拣货缺件触发「缺货寻源重算」(换仓/换货/取消行);库存准确率是仓的生命线指标(循环盘点 + 差异追责)。
  • 前置仓与中心仓如何分工?
    品类分层:高频快消进前置仓(密度优先,SKU 精选),长尾低频留中心仓;系统上统一寻源(前置仓优先按时效门槛,不可达降级中心仓)。前置仓是密度生意,SKU 过多摊薄坪效。
  • 波次拣货与截单时间的矛盾?
    波次按「承运揽收时刻」倒排:拣货时长 + 打包时长 + 交接时长 → 波次截止下单时刻;晚于截止的进下一波。页面透出「此件预计 Y 时效」即由波次模型支撑,不是随便写的。
常见陷阱
  • 寻源拍脑袋:就近发不看库存与承运能力,到了仓里没货;寻源 = 库存 × 成本 × 线路联合求解。
  • 拆单无预告:用户收到三个包裹三笔运费,客诉;拆单结果 + 运费规则即时告知。
  • 库存不锁定:寻源后拣货前库存被别的单吃掉;预占 + 超时释放。
  • 大促无截单:承诺发不出去,批量违约;分线路截单 + 页面透出。
破题点拨
一眼定位:仓配 = 多目标寻源 + 拆单合并 + 波次路径 + 截单预案。
破题主线:候选仓过滤 → 成本函数 → 拆单决策 → 库存预占 → 波次倒排 → 大促降级。
易错点:寻源不看库存;拆单无预告;库存不锁定;大促不截单。
D09.02
TMS 运输调度:车辆路径、装载优化与在途跟踪架构P1
架构 路径优化 装载率 在途跟踪
场景背景

城配场景:百余辆车、上千订单/天,每单有取货点、送货点、时间窗(客户要求上午到)、车型约束(冷链/厢货)。干线场景:整车与零担,多装卸点。硬指标:车辆装载率(成本核心)、时效达标率、里程/油耗成本、异常可见(车坏在路上、客户不在家)。

核心挑战

① VRP 复杂度:带时间窗的车辆路径问题是 NP-hard,订单规模大不能求精确解;② 动态插单:计划排好后来新单/急单,重排还是追加;③ 装载约束:后进先出(卸货顺序)、重不压轻、冷链混装禁忌;④ 在途可视:司机 App 上报、GPS 轨迹、异常事件(超时停留)判定;⑤ 成本核算:每单分摊到车辆的里程/油耗/司机工时。

分析框架

调度分静态计划 + 动态调整两层:夜间离线跑 VRP 求解(启发式:先聚类分区,再区内路径;求解器用 OR-Tools/自研遗传,分钟级出可行解,不求最优);日间动态事件(新单/取消/车故障)走增量:能插入就插(评估边际成本),不能则进次日计划。装载用「三维装载启发 + 规则约束」(时间窗倒排卸货顺序 → 后进先出;重货贴车头;禁忌品分区)。在途跟踪 = 司机 App 事件上报(到店/离开/签收)+ GPS 后台轨迹 + 规则引擎(偏离路线/超时停留/超速)告警。成本核算:车辆里程按实际 GPS 轨迹,订单按「路径增量」分摊。

领域模型

聚合根:调度计划(DispatchPlan:车辆-路线分配、时间窗、装载方案)。实体:运输任务(Leg:取货/送货点序列)、在途事件。值对象:时间窗、装载单元、成本分摊。不变量:任务时间窗不被违反(硬窗)/ 超时告警(软窗);车辆容量不超限;卸货顺序满足后进先出;在途事件按时间序追加且不可改(改走冲正事件)。

解决方案

1. 离线排线:订单聚类(按地理网格 + 时间窗)→ 车辆匹配(车型/容量/司机班次)→ 路径求解(OR-Tools 启发,超时即用贪心保底)→ 装载方案(三维启发 + 规则校验)→ 司机 App 下发任务清单。

2. 动态调整:新单评估「边际插入成本」(绕路距离 × 成本系数),低于阈值插入并重算该车后续点序;高于阈值进明日计划池。车辆故障:该车任务整体重派(优先同线路空闲车)。

3. 在途:司机 App 关键节点上报(取货成功/送达/拒收/异常)+ GPS 分钟级回传;规则引擎告警(偏离、滞留、超速、疲劳驾驶)推送调度台;客户侧「物流详情」由事件流驱动。

4. 成本:每车实际里程(GPS)+ 油费/过路费 → 车辆日成本;订单成本 = 所在路线增量分摊;月结对账承运商/司机。

flowchart LR Orders[订单池] --> Cluster[聚类分区
网格+时间窗] Cluster --> Match[车辆匹配
车型/容量/班次] Match --> VRP[路径求解
启发式+超时保底] VRP --> Load[装载方案
三维+规则] Load --> App[司机任务下发] New[动态新单] --> Marginal{边际成本评估} Marginal -->|低| Insert[插入重排] Marginal -->|高| Pool[明日计划池] GPS[GPS+事件上报] --> Rule[规则引擎告警] Rule --> Console[调度台干预] Cost[里程/油耗] --> Settle[成本核算分摊]
TMS 静态排线与动态调度闭环
关键代码
// 边际插入评估:新单绕行增量低于阈值才插入,否则进明日池
public boolean tryInsert(Trip t, Order o) {
    double before = routeCost(t.stops());
    List<Stop> next = bestInsertPosition(t.stops(), o);        //// 全位置试插取最小增量
    double marginal = routeCost(next) - before;
    if (marginal > INSERT_THRESHOLD) return false;            //// 不划算
    t.replan(next);                                        //// 重算该车后续点序并下发司机
    return true;
}

// 装载规则:重不压轻、后送先装(LIFO),三维启发 + 规则校验
public LoadPlan planLoad(Trip t) {
    List<Package> sorted = t.packages().stream()
        .sorted(comparing(Package::weight).reversed()
            .thenComparing(Package::deliverSeq).reversed())         //// 重的在下/先送后装
        .toList();
    return pack3d(sorted, t.vehicle().volume());              //// 启发装箱 + 体积/承重校验
}
深度追问
  • 求解器跑得慢/偶发超时,计划出不来?
    求解服务必须带「时间盒」:超时立即输出当前最优可行解(贪心兜底),并标记「待优化」;调度以「有可行计划」为底线,最优是迭代目标。生产系统里,「永远能给出计划」比「偶尔给出最优计划」重要。
  • 司机不按系统路线走怎么办?
    系统路线是建议不是命令:允许司机改序(记录原因),后台分析「司机经验 vs 系统解」的偏差回流优化;同时监控偏离安全(禁行/限行区域告警)。经验数据是排线模型的免费标注。
  • 客户不在家/拒收怎么处理?
    标准异常流:司机上报 → 联系客户(虚拟号,D09.03)→ 二次投递(排入最近班次)/ 拒收入库(逆向,D09.05);异常单全程状态可见,客服可介入。异常流要像正常流一样有状态机和 SLA。
  • 装载率与时效冲突(装满但超窗)?
    成本函数里时效违约是硬惩罚(超窗单不能装),装载率是软目标;系统按「时效优先」求解,装载率靠订单结构优化(同日同时段订单聚类)提升,不靠牺牲承诺。优先级写进成本函数权重,业务共识后执行。
常见陷阱
  • 追求最优解超时:求解器跑不完计划出不来;时间盒 + 可行解底线。
  • 静态计划一刀切:新单急单插不进,全压次日;动态增量评估。
  • 装载无规则:卸货顺序乱、冷链混装;三维启发 + 规则校验。
  • 在途只靠司机自觉:异常发现晚客诉先行;GPS + 事件 + 规则告警。
破题点拨
一眼定位:TMS = 离线 VRP(可行解优先)+ 动态插单 + 装载规则 + 在途事件流。
破题主线:聚类分区 → 车辆匹配 → 时间盒求解 → 装载规则 → 边际插单 → GPS 事件告警。
易错点:求解超时无计划;无动态调整;装载无规则;在途黑盒。
D09.03
电子面单与轨迹:单号生成、轨迹流与时效预测高级P1
单号生成 轨迹流 隐私面单 时效模型
场景背景

电子面单是物流的数字化起点:一个单号串起商家的快递交接、干支线运输、末端派送与签收。单号要全局唯一、含路由信息(承运商/始发地)、可被任意一方扫码识别。轨迹是最高频的物流数据(日增十亿级),用户「查快递」要求秒级返回全量轨迹。硬指标:单号不重复(重复=丢件纠纷)、轨迹写入吞吐、查询延迟、隐私(面单不暴露真实电话)。

核心挑战

① 单号设计:全局唯一 + 可路由 + 防遍历(不能顺序猜单号)+ 各承运商号段协调;② 轨迹高吞吐:十亿级/日追加写,按运单查询;③ 多源合并:直营/加盟/承运商轨迹格式不一,时间对齐;④ 隐私面单:虚拟号、信息脱敏打印与派送端解密;⑤ 时效预测:线路时效模型(分承运 × 分拨 × 线路)。

分析框架

单号生成走分段发号器:号段申请(向承运商/平台申领唯一号段)→ 本地发号(段内递增 + 校验位),全局唯一不依赖中心时序;单号编码含「路由段」(始发分拨编码)+ 序列 + 校验位,扫码即知从哪来。轨迹写入走「按运单分区的追加流」:运单 ID 哈希分区保序,事件带「上报方 + 时间戳 + 类型」,写入时序存储;查询按运单 ID 聚合 + 多源时间对齐(乱序容忍)。隐私面单:打印信息脱敏(姓名部分隐藏、电话转虚拟号),派送员端按权限解密(虚拟号转接,不留真实号)。时效预测:历史线路(分拨间)时效统计(P50/P80)+ 实时因素(天气/大促)修正,用于「预计送达」展示(与 D06.02 ETA 同思路)。

领域模型

聚合根:运单(Waybill:单号、收寄件人(脱敏存储)、路由、状态)。实体:轨迹事件(TraceEvent:类型、地点、时间、操作方)。值对象:单号、隐私联系方式。不变量:单号全局唯一(发号器 + 唯一索引双保险);轨迹事件按运单追加(乱序可容忍,时序展示排序);隐私字段(电话/地址)加密存储,访问需授权审计;状态机与轨迹事件一致(签收事件 → 终态)。

解决方案

1. 单号:平台号段 + 校验位(防遍历);下单即预分配(未支付也可占号,支付后激活);面单打印服务(模板渲染 + 隐私脱敏 + 虚拟号绑定)。

2. 轨迹:各扫描终端(分拨 PDA/司机 App/驿站枪)上报 → 接入网关 → Kafka 按运单分区 → 时序存储;查询服务按运单读 + 多源归并排序;冷数据归档(对象存储)供查询长历史。

3. 隐私:面单打印默认脱敏;真实电话加密存储;派送时虚拟号转接(通话不留真实号,录音合规留存);数据访问分级(客服按工单授权查看)。

4. 时效:线路时效库(分拨对 → 历史 P50/P80,日更);预计送达 = 揽收预测 + 各段时效和 + 安全余量;大促/天气修正系数;承诺时效与实际偏差监控(用于寻源与截单策略,D09.01)。

flowchart LR Order[商家下单] --> Alloc[单号分配
号段+校验位] Alloc --> Print[面单打印
脱敏+虚拟号] Print --> Scan[各端扫描上报] Scan --> GW[接入网关] GW --> Kafka[Kafka按运单分区] Kafka --> TS[轨迹时序存储] TS --> Query[查快递聚合排序] TS --> Cold[冷数据归档] Call[客户来电] --> VNum[虚拟号转接] Line[线路时效库] --> ETA[预计送达计算] ETA --> Show[用户展示]
电子面单与轨迹流:发号、脱敏、分区写入、时效模型
关键代码
// 单号:号段 + 校验位 + 随机步长,防遍历爬取
public String nextWaybillNo(Carrier carrier) {
    long seq = segment.next(carrier);                     //// 号段批量取号(分片发号器)
    long scrambled = seq * STEP + carrier.code() % MOD;          //// 段内随机步长,不连续
    return carrier.prefix() + pad(scrambled) + checksum(scrambled);  //// 末位 Luhn 校验
}

public boolean valid(String waybillNo) {
    if (!waybillNo.matches("\d{12,15}")) return false;
    return luhn(waybillNo.substring(0, waybillNo.length() - 1)) == waybillNo.charAt(waybillNo.length() - 1);
}

// 查询风控:同 IP 高频查不同单号 → 验证码(单号是半公开标识)
public boolean allowQuery(String ip, String waybillNo) {
    return limiter.tryAcquire("wq:" + ip + ":" + waybillNo, 1, Rate.perMinute(10));
}
深度追问
  • 单号被遍历爬取(猜单号查信息)?
    校验位 + 非连续发号(段内随机步长)+ 查询风控(同 IP 高频查不同单号 → 验证码/限流)+ 敏感字段查询需手机号后四位匹配。单号是半公开标识,默认假设会被扫。
  • 加盟网点轨迹格式混乱、时间不准?
    接入规范(事件类型枚举 + 时间以服务端接收为准 + 网点时钟偏差上报校正);格式不符进「清洗层」归一化;质量差的网点考核(轨迹完整率纳入对账)。标准化靠规范 + 激励,不靠事后猜。
  • 用户问「我的件到哪了」但轨迹几小时没更新?
    「静默检测」规则:超过该段正常时长无更新 → 主动核实(联系网点/司机)→ 异常事件推送用户(滞留/错分);好的物流体验是把「查不到」变成「主动告知异常」。
  • 虚拟号成本与接通率?
    虚拟号按需分配(派送环节才绑,签收解绑)控成本;多运营商通道 + 失败重试;接通率监控(低于阈值换通道)。隐私与成本的平衡点是「按需 + 监控」。
常见陷阱
  • 单号顺序发:被遍历爬信息;校验位 + 随机步长 + 查询风控。
  • 面单明文电话:快递袋一撕信息裸奔;脱敏 + 虚拟号。
  • 轨迹单条直写库:高峰写入打爆;分区批量 + 时序库。
  • 预计送达拍脑袋:承诺达不到客诉;线路时效库 + 偏差监控。
破题点拨
一眼定位:面单轨迹 = 分段发号 + 分区轨迹流 + 隐私面单 + 线路时效。
破题主线:号段发号(防遍历)→ 脱敏打印 → 多源上报归一 → 时序存储 → 静默检测 → 时效库承诺。
易错点:单号顺序发;面单明文;轨迹单条写;时效拍脑袋。
D09.04
补货与库存计划:需求预测、安全库存与采购建议高级P2
需求预测 安全库存 补货模型 呆滞治理
场景背景

仓配网络要回答「进多少货、什么时候进、进到哪里」:SKU × 仓的组合十万级。进多了呆滞(资金占用 + 临期报废),进少了缺货(销售损失 + 时效降级)。硬指标:现货率(有货可发)、库存周转天数、呆滞率、预测准确率(WAPE)。

核心挑战

① 预测难点:长尾 SKU 需求稀疏、促销脉冲、季节性、新品无历史;② 分层策略:不同品类(生鲜日配 vs 家电季采)补货逻辑完全不同;③ 多仓协同:总仓-区域仓-前置仓的调拨与补货联动;④ 预测到采购的转化:预测是概率,采购要下确定的单(MOQ/供应商提前期/经济批量);⑤ 呆滞治理:临期预警、清仓决策与降价协同。

分析框架

与 D09.06 的边界:本卡是单仓补货公式(再订货点、安全库存);ATP、牛鞭与产销协同见 D09.06。

补货是预测驱动的库存策略:需求预测(时间序列 + 促销事件特征,输出区间)→ 库存策略(分层:ABC 分类定策略——A 类高频精确预测,C 类长尾用「双箱/定期」简化)→ 补货计算(再订货点 = 提前期需求 + 安全库存;补货量 = 目标库存 − 在途 − 可用)→ 采购/调拨执行。安全库存按「需求波动 × 供应波动 × 服务水平」公式化(Z 值法),不是拍脑袋。多仓:中心仓兜底 + 区域仓按预测分布 + 调拨规则(区域低于下限从中心补)。呆滞:库龄分层 + 临期预警 + 清仓建议(价格协同走 D04.02 引擎)。预测效果按 SKU 分层评估(WAPE/偏差),持续迭代。

领域模型

聚合根:库存计划(InventoryPlan:SKU × 仓的目标/下限/上限、补货策略)。实体:预测记录、补货单(调拨/采购)、呆滞预警。值对象:安全库存参数、服务水平。不变量:补货量 = 目标 − 在途 − 可用(不为负);采购单受供应商提前期与 MOQ 约束;调拨不破坏各仓下限;呆滞处置(降价/清仓)需审批留痕。

解决方案

1. 预测:SKU 分层建模(A 类:时序模型 + 促销特征;B 类:时序 + 季节;C 类:移动平均兜底);促销脉冲单独建模(活动日历进特征);输出需求区间(P50/P90)供策略使用。

2. 策略:ABC 分类(销售额 × 周转)→ 差异化策略(A 高频补,B 定期补,C 双箱/按销补);安全库存参数化(按 SKU 波动算,季度校准);新品期「小批量快反」(试销 → 数据积累 → 转正常)。

3. 补货执行:日批跑补货计算(SKU × 仓)→ 生成调拨单(仓间)/ 采购计划(供应商维度聚合,拼经济批量)→ 审批流 → 执行跟踪;缺货风险单(现货率将破线)实时告警采购。

4. 呆滞:库龄报表(超 N 天未动)→ 呆滞分(金额 × 库龄)→ 处置建议(调拨到需求仓/促销清仓/退货供应商)→ 协同定价(D04.02)→ 复盘。

flowchart TD Sales[销售历史] --> FC[需求预测
分层建模+促销特征] FC --> Band[需求区间P50/P90] Band --> Strat[库存策略
ABC分层+安全库存] Strat --> ROP[再订货点计算] ROP --> Gen[补货单生成
调拨/采购聚合] Gen --> Appr[审批执行] Appr --> Track[在途跟踪] Track --> Sales Age[库龄报表] --> Dead[呆滞评分] Dead --> Deal[处置建议
调拨/清仓/退货] Dead --> Alert[临期预警]
补货闭环:分层预测、安全库存、呆滞治理
关键代码
// 安全库存:需求波动 × 补给周期 × 服务水平因子
public int safetyStock(SkuDemand d, LeadTime lt, double serviceLevel) {
    double z = normalQuantile(serviceLevel);                   //// 95% → 1.65
    double demandSigma = d.stdDev();
    return (int) Math.ceil(z * Math.sqrt(
        lt.days() * demandSigma * demandSigma
      + lt.stdDev() * lt.stdDev() * d.mean() * d.mean()));          //// 需求与提前期双波动
}

// 补货触发:预计库存低于「安全库存 + 在途」即生成采购建议
public Optional<PurchaseAdvice> advise(SkuId sku, WarehouseId w) {
    Inv inv = invRepo.find(sku, w);
    int pipeline = inv.inTransit() + inv.onOrder();
    int ss = safetyStock(demand(sku, w), leadTime(sku), SERVICE_LEVEL);
    if (inv.available() + pipeline < ss) {
        return Optional.of(PurchaseAdvice.of(sku, w, ss - inv.available() - pipeline));
    }
    return Optional.empty();
}
深度追问
  • 促销预测不准导致备货失衡?
    促销单独建模:活动类型 × 折扣深度 × 流量位的历史弹性曲线;大促备货按「基线 + 弹性 × 活动系数」并设「可退换协议」对冲;预测偏差复盘(预测 vs 实际)进活动复盘报告,滚动修正弹性系数。
  • 安全库存公式参数怎么定?
    服务水平按品类定(A 类高现货,C 类低);Z 值查表;需求/供应波动用历史标准差;季度复盘校准(实际缺货率 vs 目标)。参数是「业务共识的量化」,每次调整要记录依据。
  • 多仓补货互相踩(都从中心抢)?
    中心仓库存分配按「各区域需求占比 + 现货率优先级」加权,不是先到先得;调拨优先级(缺货风险高的区域先);总仓兜底规则(不低于全局下限)。规则透明,区域可预期。
  • 新品没历史怎么预测?
    相似品映射(同类目/价位/属性的历史曲线作先验)+ 小批量试销(快速补货机制兜底)+ 爬坡期模型(新品有冷启动曲线)。没有历史不是不预测的理由,是用先验 + 试销的理由。
常见陷阱
  • 一个模型打天下:长尾 SKU 预测稀烂;分层策略。
  • 安全库存拍脑袋:要么压死资金要么天天缺货;公式化 + 校准。
  • 补货不看在途:在途已到又补一批,爆仓;可用 + 在途联合计算。
  • 呆滞没人管:临期报废才被发现;库龄预警 + 处置协同。
破题点拨
一眼定位:补货 = 分层预测 + 安全库存公式 + 在途联合 + 呆滞处置。
破题主线:ABC 分层 → 区间预测 → ROP 计算 → 调拨/采购聚合 → 库龄治理 → 参数季度校准。
易错点:单模型;拍脑袋安全库存;不看在途;呆滞后置。
D09.05
逆向物流与理赔:退换货流、定损与理赔自动化高级P2
逆向物流 定损 理赔自动化 反欺诈
场景背景

逆向是物流的成本黑洞:电商退货率百分之十到三十,退件要从用户手里取回、验损、入库(可二次销售)或报废。货运破损/丢件的理赔涉及资金与保险。硬指标:退货上门时效、验损准确(该谁赔)、理赔时效(客户体验)、骗保识别(黄牛与职业索赔)。

核心挑战

① 逆向寻源:退件回哪个仓(最近仓/发货仓/质检仓),成本与二次销售平衡;② 定损标准:破损责任划分(包装/运输/商家/客户),证据链(开箱视频/照片);③ 理赔时效:小额快赔与大额慎赔的分层;④ 骗保识别:重复索赔、伪造破损、职业索赔团伙;⑤ 逆向库存:良品重新上架、残次品处置渠道。

分析框架

逆向流 = 退货申请 → 上门取件 → 质检定损 → 入库处置 → 退款/理赔,与正向共享运力(逆向寻源用正向网络 + 成本模型)。定损规则化:商品类目 × 损坏类型 → 责任方矩阵 + 赔付标准;证据链(用户上传 + 揽收验视 + 仓内质检照片)结构化归档。理赔分层:小额(阈值以下)凭证据自动赔(秒级),大额人工 + 保险介入;风控前置(设备/行为/历史索赔,D02.02 能力复用)。逆向库存:质检分级(良品/可修/残次)→ 良品重新上架(状态回滚,与 D03.02 库存释放联动)、残次走报废/折价渠道。

领域模型

聚合根:逆向单(ReturnOrder:申请、取件、质检、处置、退款)。实体:质检记录(Grade + 照片)、理赔单(Claim:金额、责任方、状态)。值对象:定损标准、责任方。不变量:退款金额 ≤ 订单实付(按规则算,防超退);理赔单与逆向单/运单绑定(一因一果);质检分级决定库存处置路径;骗保标记账户的理赔必进人工。

解决方案

1. 退货流:用户申请(原因 + 照片)→ 审核(规则自动/人工)→ 上门取件(逆向运单,隐私面单)→ 仓内质检(分级 + 照片归档)→ 处置(良品上架/残次渠道/报废)→ 触发退款(原路,券按 D04.01 规则回退)。

2. 定损与理赔:责任矩阵规则化(谁的责任谁赔);小额自动赔(证据齐全 + 规则明确);大额走保险(报案材料自动组装);赔付进度用户可见。

3. 反欺诈:理赔风控分(索赔频次、金额异常、设备聚集、照片元数据检测)→ 高分人工复核;团伙识别(关系图谱);恶意账户限制并留证。

4. 成本治理:逆向成本看板(单均逆向成本、二次销售率、报废率);高退货率商品/商家治理(退货率异常的商品限流或要求整改)。

flowchart TD Apply[退货申请
原因+照片] --> Audit{审核} Audit -->|自动规则| Pass[通过] Audit -->|可疑| Manual[人工审核] Pass --> Pickup[上门取件] Pickup --> QC[仓内质检
分级+照片] QC --> Grade{分级} Grade -->|良品| Resale[重新上架] Grade -->|可修| Repair[维修渠道] Grade -->|残次| Scrap[报废/折价] QC --> Refund[触发退款] Damage[破损/丢件] --> Liability{责任矩阵} Liability --> Small[小额自动赔] Liability --> Big[大额人工+保险] Small --> Risk[风控评分] Big --> Risk Risk -->|高分| Review[人工复核]
逆向物流与理赔:质检分级、责任定损、风控分层
关键代码
// 逆向理赔状态机:申请 → 定损 → 审核 → 赔付/拒赔,全程留痕
private static final Map<ClaimState, Set<ClaimState>> TRANS = Map.of(
    ClaimState.APPLIED,   Set.of(ClaimState.ASSESSING, ClaimState.CANCELLED),
    ClaimState.ASSESSING, Set.of(ClaimState.REVIEWING, ClaimState.APPLIED),
    ClaimState.REVIEWING, Set.of(ClaimState.PAYING, ClaimState.REJECTED),
    ClaimState.PAYING,    Set.of(ClaimState.PAID, ClaimState.PAY_FAILED),
    ClaimState.PAY_FAILED, Set.of(ClaimState.PAYING));

// 定损:规则自动定损(标准件) + 疑件进人工;举证材料全存档
public AssessResult assess(Claim c) {
    if (c.item().standardPart() && c.photos().complete()) {
        return AssessResult.auto(priceBook.lookup(c.item()), c.evidence());
    }
    return AssessResult.manual(assignAdjuster(c), c.evidence());   //// 疑件人工定损(防骗保)
}

// 赔付幂等:claimId 唯一,重复回调直接返回原结果
public Payout payout(ClaimId cid, Money amount) {
    if (payoutRepo.exists(cid)) return payoutRepo.find(cid);
    Payout p = payService.pay(cid, amount, "claim");              //// 带幂等键
    audit.record("CLAIM_PAYOUT", cid, p.id());
    return p;
}
深度追问
  • 「退货不退款」和「退款不退货」分别什么场景?
    低值商品退货成本 > 货值 → 退款不退货(销毁或捐赠);高信誉用户/已签收很久的售后争议 → 先退后取(体验优先,风控兜底)。规则按「商品价值 × 用户信誉 × 退货成本」决策,不是一刀切。
  • 用户寄回假货/调包怎么防?
    揽收验视(取件时扫码 + 外观拍照)+ 仓内质检(序列号/防伪码比对,高价值品全检)+ 差异留证(照片 + 重量比对);确认调包按规则拒退并留证据链。高价值品的「一物一码」是防调服基础。
  • 理赔时效与准确矛盾(快但错赔)?
    分层解:证据齐全 + 规则明确 + 低金额 → 自动快赔(接受小误差);规则模糊/高金额/风控高分 → 人工慎赔。用「分层」换「时效与准确的帕累托」,不是二选一。
  • 二次销售的商品状态怎么管理?
    良品回归可售库存要「验新」:外观检查 + 功能测试(按类目自动化抽检)+ 效期检查(临期重新标价);状态标记「退货良品」(部分平台公示)。质量口径不降,否则口碑反噬。
常见陷阱
  • 逆向流程靠人工驱动:客服催一遍动一遍;状态机 + 超时升级。
  • 定损无标准:同一破损不同人不同结论;责任矩阵 + 证据链。
  • 全额自动赔:职业骗保批量薅;风控分层 + 高分人工。
  • 良品直接报废:能二次销售的当废品,成本黑洞;质检分级 + 处置路径。
破题点拨
一眼定位:逆向 = 状态机退货流 + 责任定损 + 理赔分层 + 质检分级处置。
破题主线:申请审核 → 取件验视 → 质检分级 → 退款/理赔 → 反欺诈 → 成本看板治理。
易错点:人工驱动;定损无标准;全额自赔;良品报废。
D09.06
供应链计划与协同:需求预测、ATP 与产销协同架构P1
高频 领域模型 架构 需求预测 ATP 承诺 产销协同
场景背景

零售供应链计划层:商品数千 SKU、多仓多供应商。硬指标:预测准确率(MAPE)、现货率(ATP 可承诺比例)、库存周转天数、缺货率与滞销率双低。计划层连接采购、仓储、门店(或电商前台),计划不准就是「缺货与积压并存」。

核心挑战

① 预测难:长尾 SKU 数据稀疏、促销/季节波动大,单一统计模型不够;② ATP 承诺:前台可卖多少要实时可算(在库 + 在途 + 在制,扣掉安全库存与已承诺),承诺超卖就是履约事故;③ 牛鞭效应:需求小波动沿供应链逐级放大,上游库存积压;④ 计划与执行脱节:采购、仓配各干各的,计划变更传导不下去;⑤ 产销协同:销售要货急、生产有周期,S&OP 机制怎么落到系统里。

分析框架

与 D09.04 的边界:本卡是计划协同层;再订货点与安全库存公式见 D09.04,此处不重复推导。

计划滚动闭环:预测(统计基线 + 促销修正)→ 补货计划(安全库存 + 补货点)→ ATP 承诺(在库/在途/在制三源)→ 采购生产执行 → 实际回流纠偏,按周/月滚动刷新,不是一定了之。ATP = 可承诺量 = 可用库存 + 在途(已下单未到)+ 在制(已排产未完工)- 安全库存 - 已承诺未发。牛鞭效应对策:需求信息共享(下游销售数据给上游)、批量透明(促销计划提前同步)、按需补货(拉式而非推式)。协同靠机制 + 系统:S&OP 例会 + 计划变更单(影响范围可视化)落到采购与生产系统。

领域模型

聚合根:计划单(Plan,SKU × 仓 × 周期 × 版本)。实体:预测单(Forecast,基线 + 修正项 + 版本)、ATP 快照(Availability,三源库存 - 安全库存 - 承诺)、补货建议(Replenishment,补货点 + 批量 + 供应商)、计划变更单(PlanChange,影响范围 + 会签)。值对象:安全库存、补货点。不变量:ATP 不得为负(超卖拦截);计划单多版本可追溯(哪版被谁执行);变更单必须评估影响范围后才生效。

解决方案

1. 预测流水线:统计基线(时序模型)+ 促销/季节修正(营销计划联动)+ 人工调整留痕;按 SKU 分层(A 类重点人工参与,C 类全自动);准确率按 SKU 分层复盘。

2. 补货计划:动态安全库存(按需求波动与供应周期算)+ 补货点触发 + EOQ 批量约束;生成采购建议单进供应商协同门户,交期确认回写。

3. ATP 服务:实时计算(在库实物 + 在途 PO + 在制 WIP,扣安全库存与已承诺),前台可卖量 = ATP;大促前锁库与预售单独通道,防超卖。

4. 产销协同:S&OP 周会(销售计划 vs 产能约束),计划变更单驱动采购/生产系统联动;变更影响范围(哪些仓/哪些单会缺)可视化,会签后生效。

5. 闭环纠偏:实际销售回流对比预测,偏差超阈值触发重预测;滞销品(高库存低动销)进清货流程,缺货品(高需求低库存)提优先级。

graph TD A[销售历史+促销计划] --> B[预测流水线 基线+修正] B --> C[补货计划 安全库存+补货点] C --> D[采购建议 供应商协同] D --> E[在途PO] F[生产排产] --> G[在制WIP] H[在库实物] --> I[ATP 计算] E --> I G --> I I -->|可卖量| J[前台销售] J -->|实际回流| K[偏差纠偏 重预测] K --> B L[S&OP 计划变更单] --> C
预测-补货-ATP-协同闭环
关键代码
// ATP = 在库 + 在途 + 在制 - 安全库存 - 已承诺;为负即超卖拦截
public int atp(Sku sku, Warehouse wh) {
    int onHand   = inventory.onHand(sku, wh);
    int inTransit = po.openQty(sku, wh);          // 已下单未到
    int inMake    = wip.openQty(sku, wh);          // 已排产未完工
    int safety    = safetyStock.of(sku, wh);
    int promised  = order.unshippedQty(sku, wh);     // 已承诺未发
    return onHand + inTransit + inMake - safety - promised;
}

// 补货触发:可用(在库+在途)跌破补货点即生成采购建议
if (inventory.onHand(sku, wh) + po.openQty(sku, wh) <= plan.reorderPoint(sku, wh)) {
    procurement.suggest(sku, wh, plan.orderQty(sku, wh));   // EOQ 批量约束
}

// 预测偏差纠偏:实际 vs 预测偏差超阈值触发重预测
double mape = forecast.mape(sku, lastWeek);
if (mape > sku.tier().mapeLimit()) {
    forecast.reRun(sku, "MANUAL_ADJUST");                  // A 类 SKU 转人工复核
}
深度追问
  • 预测一直不准,怎么办?
    接受「预测永远有误差」,把力气花在纠偏速度上:实际回流按天对比、偏差超阈值周内重预测;促销等已知波动提前修正(营销计划联动),比死磕模型收益大。SKU 分层管理——A 类人工参与、C 类自动,准确率指标分层看。
  • ATP 超卖了怎么兜底?
    ATP 扣减必须原子(承诺即占用),大促锁库/预售走独立通道;万一超卖(异步链路延迟),履约侧按优先级分配(先到先得 + 高价单优先)+ 缺货补偿(券/退货运费)。兜底是止损,原子扣减才是根治。
  • 牛鞭效应怎么从系统上缓解?
    信息共享是核心:下游实际销售数据(不是订单)同步给上游,促销计划提前透传;补货拉式(按消耗触发)而非推式(按指标压货);计划变更单可视化影响范围,避免「上游半夜收到翻倍订单」。
  • 产销协同落地靠什么?
    机制 + 系统双落地:S&OP 周会定节奏(销售计划 vs 产能),系统侧变更单驱动(采购/生产系统接收范围与会签),执行结果回流复盘。纯靠例会是人肉,纯靠系统没有全局视角——两者必须闭环。
常见陷阱
  • 预测当承诺:预测有误差,拿预测数直接承诺就是超卖;ATP 按三源实时算。
  • 计划一定不变:市场变了计划不改,缺货积压并存;滚动刷新 + 变更单。
  • 安全库存一刀切:所有 SKU 同一个安全库存,畅销的断货滞销的压资金;按波动分层。
  • 协同靠人肉:微信群催单、Excel 传计划,一变全乱;变更单 + 会签落系统。
破题点拨
一眼定位:供应链计划 = 预测(基线+修正)→ 补货(安全库存+补货点)→ ATP(三源实时)→ 协同(变更单)→ 纠偏(偏差重跑)。
破题主线:分层预测 → 拉式补货 → ATP 原子扣减 → S&OP 变更单 → 偏差周内纠偏。
易错点:预测当承诺;计划一成不变;安全库存一刀切;协同靠人肉。

十 广告与商业化 6 题

本组知识链

广告平台的主线是「三方博弈的收益最大化」:媒体要收入、广告主要效果、用户要体验。覆盖检索、竞价、归因、结算、反作弊与创意优化。

  1. 检索:倒排索引、定向匹配、频次控制。
  2. 竞价:GSP/二价机制、预算平滑、扣费准确。
  3. 归因:点击/转化回传、归因窗口、去重。
  4. 结算与反作弊:实时报表、收入对账、IVT 识别。
  5. 创意优化:bandit 优选、模板化生成、落地页一致性拦截、衰退监测。
工程化要点
  • 召回-粗排-精排-重排四级漏斗,逐层收紧候选,延迟预算逐级分配。
  • 扣费准确性 = 资损红线:竞价记录、扣费事件、对账三方一致。
  • 归因数据量大且乱序:事件流 + 窗口归因 + 幂等去重。
  • 反作弊前置:IVT 过滤在扣费前完成,不作弊流量的收入才是收入。
本组思维模式
  • 机制设计先于算法:竞价机制定格局,模型只是在格局内优化。
  • 预算即约束:没有预算平滑的竞价,客户一早上跑完全天预算。
  • 效果可归因:广告主要的是效果,归因链路是平台的生命线。
  • 体验是长期收益:广告负载与用户体验的平衡是平台级 KPI。
合规与资损红线
  • 广告内容合规:资质审核(医疗/金融等特许行业)、虚假广告处置法定责任。
  • 扣费与结算涉资金:竞价记录留存、扣费可复算、对账差异处理留痕。
  • 用户数据用于定向须符合个保法(告知同意、不搞大数据杀熟)。
  • 未成年人保护:不适配广告定向、广告内容限制。
D10.01
广告检索与投放:索引、召回、定向与频控架构P1
领域模型 架构 倒排索引 定向匹配 频次控制
场景背景

信息流广告:每次请求百毫秒内从百万广告中选出几条展示。定向维度几十个(人群包/地域/时段/设备/兴趣/关键词),频控规则各异(每用户每天每条广告最多 N 次)。硬指标:检索延迟(几十毫秒)、召回率(不漏好广告)、定向准确(不能投错人)、频控严格(用户体验与广告主预算)。

核心挑战

① 多维度检索:定向条件组合爆炸,倒排 + 布隆/位图混合;② 索引实时性:广告上下线、预算变化、人群包更新要秒级生效;③ 频控:跨请求的用户级计数,百万 QPS 下的读写;④ 候选量与延迟平衡:召回太宽排序慢,太窄漏好广告;⑤ 冷启动:新广告无历史数据,探索与利用。

分析框架

漏斗架构:召回(倒排/向量/人群)→ 粗排(轻量模型/规则截断)→ 精排(完整模型预估 CTR/CVR)→ 竞价与重排(机制 + 多样性 + 频控)。定向用「倒排索引(人群/关键词)+ 位图(枚举维度:地域/设备/时段)」,查询时按请求特征取并集 + 位图交。索引更新走「增量 + 版本切换」:广告变更写增量队列,秒级合入,查询带版本号(一致性快照)。频控走 Redis 计数(用户 × 广告 × 时间窗),批量预取 + 本地缓存兜底。冷启动:新广告给探索流量(EE 问题:epsilon-greedy/汤普森),按表现逐步加权。

领域模型

聚合根:广告计划(AdCampaign:预算、定向、出价、创意、排期)。实体:广告创意、频控计数。值对象:定向条件、出价方式(CPC/CPM/oCPC)。不变量:投放中广告必过资质审核;预算消耗 ≤ 预算(扣费原子);频控计数对用户单调累计(按窗淘汰);定向条件集合合法(无矛盾组合)。

解决方案

1. 索引:广告属性拆「可倒排维度(人群/关键词/类目)+ 位图维度(地域/设备/时段/网络)」;查询解析为「倒排查 + 位图交」;人群包超大时用「分层人群」(标签组合动态圈人,查询时计算)。

2. 实时性:广告变更(上线/暂停/调预算)进消息队列 → 索引构建服务增量更新(秒级)→ 版本切换原子生效;查询侧缓存带版本号,旧版本请求自然结束。

3. 频控:Redis Hash 计数(用户 → 各广告当日展示数),批量读 + 本地缓存(短 TTL);频控规则在重排层强制执行(超频候选直接过滤)。

4. 漏斗:召回万级 → 粗排截千级(轻量特征模型)→ 精排百级(深度模型)→ 竞价 + 重排(机制出价 + 多样性打散 + 频控)→ 最终几条。延迟预算逐级分配(召回占大头预算)。

flowchart TD Req[广告请求
用户上下文] --> Parse[查询解析
定向条件] Parse --> Recall[召回
倒排+位图+向量] Recall --> Coarse[粗排
轻量截断] Coarse --> Fine[精排
CTR/CVR预估] Fine --> Bid[竞价机制
GSP出价] Bid --> Rerank[重排
多样性+频控] Rerank --> Show[广告展示] Show --> Log[曝光日志] Update[广告变更] --> Idx[索引增量更新] Idx --> Recall Freq[频控计数] --> Rerank
广告检索漏斗:召回-粗排-精排-竞价-重排
关键代码
// 定向匹配:位图求交(人群标签 × 广告定向),毫秒级过滤
public boolean matchTarget(Ad ad, UserProfile u) {
    RoaringBitmap audience = audienceIndex.get(ad.crowdId());  //// 人群包位图
    RoaringBitmap targeting = RoaringBitmap.or(
        u.interests().stream().map(tagIndex::get).toList());  //// 兴趣标签位图
    return audience.intersects(targeting);                  //// 位图求交,千万级用户毫秒完成
}

// 频次控制:用户 × 广告 × 时间窗计数,超限过滤
public boolean freqOk(AdId ad, UserId uid) {
    String key = "freq:" + uid + ":" + ad;
    long n = counter.incr(key, Duration.ofHours(24));
    return n <= ad.freqCap();                                //// 每人每天曝光上限
}
深度追问
  • 索引更新延迟导致「已暂停广告还展示」?
    两级防护:索引增量(秒级)+ 实时拦截(引擎层查「暂停/超预算黑名单」,内存缓存秒级同步)。资损类状态(暂停、预算耗尽)走强实时通道,不等索引。
  • 精排模型延迟超预算?
    模型分层:核心模型保延迟(特征裁剪、量化、GPU 批推理),高级特征走粗排截断后的少量候选;超时降级链(完整模型 → 轻量模型 → 热度兜底),保证永远出结果。
  • 新广告冷启动效果差,广告主流失?
    探索机制(保证新广告基础曝光量)+ 自动出价辅助(平台代出价,按转化目标优化)+ 诊断工具(告诉广告主「创意/定向/出价」哪里有问题)。冷启动体验是平台留住广告主的关键。
  • 人群包规模百万级,索引怎么存?
    大人群包不展开成人 ID 索引:用「人群标签 + 请求时匹配」或「用户画像位图」(用户属性位图 ∩ 人群规则位图);人群更新走「差量同步」。位图化 + 规则化是大人群的标准解法。
常见陷阱
  • 索引全量重建:广告变更等几十分钟;增量 + 版本切换。
  • 频控只在前端:接口直调绕过,广告主投诉超投;引擎层强制。
  • 漏斗一级到底:百万候选直接精排,延迟爆炸;逐级截断。
  • 无探索机制:新广告永远没曝光;EE 平衡。
破题点拨
一眼定位:广告检索 = 倒排 + 位图混合索引 + 漏斗截断 + 实时黑名单。
破题主线:定向解析 → 召回 → 粗精排 → 竞价重排(频控/多样性)→ 变更增量 + 资损状态强实时。
易错点:全量重建;频控旁路;无截断;无探索。
D10.02
竞价与计费:GSP、二价与预算平滑专家P1
领域模型 架构 GSP 竞价 二价计费 预算平滑
场景背景

广告位拍卖:多个广告主竞争同一展示机会。机制决定平台收入与广告主 ROI 的长期均衡。硬指标:扣费准确(这是资损红线:少扣平台亏、多扣客诉)、预算可控( advertiser 设日预算,不能一早上跑完)、机制激励相容(广告主没有操纵动机)。

核心挑战

① 机制选择:GSP(广义二价,按下一名出价 + 最小增量计费)与 VCG(机制最优但复杂难解释)的权衡;② 质量分:出价 × 质量分的排序,质量分怎么算(CTR/体验/广告质量);③ 预算平滑:日预算约束下的 pacing(均匀消耗 vs 抢高价值流量);④ 扣费一致性:竞价记录、扣费日志、结算账单三方一致;⑤ oCPC 自动出价:平台按转化目标代出价,机制与计费的重设计。

分析框架

主流是 GSP + 质量分:排序分 = 出价 × 质量分(质量分含预估 CTR、广告体验、相关性);赢家扣费 = 下一名排序分 / 自己质量分 + 最小增量(保证激励相容:按真实价值扣费,不按出价)。预算约束走 pacing:把「日预算」转化为「每小时预算上限」,按流量价值动态调节(价值高的时段放宽,低的收紧);oCPC 下平台按预估转化价值自动出价(广告主设目标成本),竞价时用「预估 CVR × 目标 CPA」折算等效出价。扣费链路:竞价服务写「竞价记录」(含机制参数)→ 曝光触发扣费事件(带扣费金额与依据)→ 实时计数(Redis 预算扣减)→ 日终结算对账(竞价记录 vs 扣费日志 vs 账单)。

领域模型

聚合根:广告账户(Account:预算、余额、出价策略)。实体:竞价记录(Auction:候选排序、赢家、扣费)、扣费事件。值对象:出价方式、质量分。不变量:扣费金额 = 机制计算结果(可复算);账户消耗 ≤ 预算(超预算停止投放);竞价记录与扣费事件一一对应(曝光即扣);oCPC 折算透明(预估 CVR 与目标成本可查)。

解决方案

1. 竞价服务:候选精排分 → GSP 计算(排序 + 下一名折价)→ 写竞价记录(含所有入参快照,供复算)→ 返回赢家与扣费价。

2. 预算 pacing:预算控制器按「剩余预算 / 剩余时间 × 流量节奏」算当前 pacing 系数,乘进出价(或直接截断低价值竞价);高价值流量(转化率高的人群)优先分配余额;支持「加速模式」(尽快花完,用于冲量)。

3. 扣费:曝光事件 → 扣费服务(按竞价记录算,幂等键 = 请求 ID + 广告)→ Redis 预算实时扣减 + MQ 异步落扣费流水;余额不足自动停投并通知。

4. 对账:日终三方比对(竞价记录、扣费流水、账户消耗),差异进差异表人工/自动修;月账单可按竞价记录重放复算(广告主质疑时可自证)。

sequenceDiagram participant E as 广告引擎 participant A as 竞价服务 participant B as 预算控制器 participant C as 扣费服务 participant S as 结算对账 E->>A: 候选+精排分 A->>B: 查询pacing系数 B-->>A: 当前系数 A->>A: GSP计算
出价×质量分排序 A->>A: 竞价记录(可复算) A-->>E: 赢家+扣费价 E->>C: 曝光事件 C->>C: 扣费(幂等)+预算扣减 C->>S: 扣费流水 S->>S: 日终三方对账
GSP 竞价、预算 pacing 与扣费对账链路
机制 计费方式 优点 缺点
GSP(广义二价) 下一名排序分/自己质量分 激励相容、易解释 非机制最优
VCG 外部性之和 社会最优 复杂、广告主难理解
一价(first price) 按自己出价 简单 易操纵、体验差
关键代码
// GSP 二价计费:扣下一名竞价 + 最小增量,禁自我竞价抬价
public Money charge(Bid winner, List<Bid> others) {
    if (others.isEmpty()) return winner.bid().min(reservePrice());  //// 无竞品按起拍价
    Bid second = others.stream().max(comparing(Bid::bid)).get();
    return second.bid().add(MIN_INCREMENT);               //// 二价:第二 + 最小增量
}

// 预算平滑: pacing = 剩余预算/剩余时长占比,动态调参与出价
public double pacingFactor(Budget b, double now) {
    double expectedSpendRatio = (now - b.startAt()) / (b.endAt() - b.startAt());
    double actualRatio = b.spent() / b.total();
    return expectedRatio / Math.max(actualRatio, EPS);         //// 花太快 → 压低出价;花太慢 → 提价
}
深度追问
  • pacing 系数导致该拿的流量没拿到?
    pacing 按「流量价值」调节而非平均:高价值流量(预估转化高)少打折,低价值多打折;广告主可设「优先拿量模式」。pacing 的目标是「花完预算且花在价值上」,不是「匀速花完」。
  • oCPC 的预估 CVR 不准,广告主成本失控?
    预估模型持续校准(转化回传闭环,D10.03)+ 成本护栏(实际成本超目标 N 成自动降出价/暂停)+ 透明报表(展示预估 vs 实际)。平台代出价的前提是预估可信 + 护栏兜底。
  • 竞价记录对不上扣费日志?
    设计上一一对应(曝光事件带竞价记录 ID);差异只会来自「曝光了没扣」(丢消息)或「扣了没曝光」(重复)。兜底:曝光日志与扣费日志双写 + 日终比对 + 差异按竞价记录重放修复。资损链路宁可有冗余不可有洞。
  • 广告主刷自己的广告(骗赔付/骗量)?
    IVT 过滤(D10.05)在计费前完成,作弊流量不计费不计数;异常点击/曝光的竞价结果作废(不扣费、不进结算)。反作弊是计费链的前置过滤器,不是事后追缴。
常见陷阱
  • 一价直扣:广告主频繁试探性出价,机制抖动;GSP 按下一名折算。
  • 预算无 pacing:客户早上十点跑完全天预算,下午流量空置;pacing 控制器。
  • 竞价记录不落:广告主质疑扣费无法自证;记录可复算。
  • 超预算继续投:免费送量给广告主,资损;预算原子扣减 + 超限强停。
破题点拨
一眼定位:竞价计费 = GSP + 质量分排序 + pacing 控节奏 + 记录可复算。
破题主线:排序分计算 → 下一名折价扣费 → 预算 pacing → 曝光幂等扣费 → 三方对账 → IVT 前置过滤。
易错点:一价直扣;无 pacing;记录不落;超预算不停。
← 技术范式引用 S01.05 热点账户高并发扣款
D10.03
归因与转化:点击归因、转化回传与去重高级P1
归因窗口 转化回传 事件去重 oCPC 闭环
场景背景

广告主要在自家 App/落地页埋点上报转化(下载、注册、付费)。平台要把转化归到广告上:用户先看了 A 广告、后点了 B 广告、再转化,功劳算谁的?归因结果驱动 oCPC 出价(D10.02)与广告主报表。硬指标:归因规则明确(广告主可理解)、回传链路可靠(丢了转化模型就瞎)、延迟可控(实时回传秒级、批量分钟级)、反作弊(假转化骗模型)。上述是观察口径,具体阈值须按本业务标定,禁止把本卡文字直接当成门禁数字。

核心挑战

① 多触点归因:末次点击/末次曝光/线性/时间衰减多种模型,广告主口径不一;② 归因窗口:点击后多久内的转化算(行业常见数天,跨窗口归因);③ 身份识别:设备 ID/账号 ID/手机号的匹配(隐私政策下 idfa 获取难);④ 回传可靠性:广告主 SDK 上报丢/重/迟;⑤ 假转化:刷量转化骗平台的量(oCPC 下平台按转化出价,假转化=平台白送量)。

分析框架

归因链路 = 点击/曝光日志(带设备 ID + 点击 ID)→ 转化事件上报(带关联参数)→ 归因匹配(设备/账号 + 时间窗)→ 归因结果(模型选择)→ 回流(模型特征 + oCPC 信号 + 报表)。归因模型默认「末次点击」(广告主最易理解),同时提供多模型报表(同一次转化在不同模型下的归属);匹配靠「点击时下发的 click_id/回调参数」+ 设备 ID 双通道(SDK 回传 click_id 最可靠,服务端回传靠 ID 匹配)。回传走 SDK(实时)+ 服务端 API(批量)双通道,事件带「事件 ID + 发生时间」,平台侧按 ID 去重。假转化防控:转化事件过风控(设备/行为/IP 异常)+ oCPC 模型对异常转化降权 + 广告主侧转化验证工具。

领域模型

聚合根:转化事件(Conversion:事件 ID、类型、值、发生时间、归因结果)。实体:触点记录(Touch:曝光/点击 + 上下文)。值对象:归因模型、归因窗口。不变量:一次转化至多归属一个触点(单模型口径);转化事件按事件 ID 幂等;归因结果随触点数据到达可修正(窗口内补归因);模型使用的转化必过风控过滤。

解决方案

1. 点击链:曝光/点击生成全局 click_id,跳转落地页时带参(utm + click_id);广告主落地页 SDK 捕获参数存本地。

2. 回传:SDK 实时回传(带 click_id + 设备 ID + 事件 ID)+ 服务端批量回传(带业务单号,平台按单号幂等);回传失败重试 + 补传窗口。

3. 归因:窗口内匹配(click_id 优先,设备 ID 兜底)→ 多触点去重(按窗口 + ID 匹配排他)→ 归因模型计算(默认末次点击)→ 写归因结果 + 回流特征。

4. 反作弊:转化风控(设备聚集、时间聚集、IP 异常、转化率异常)→ 假转化不进模型(标记过滤)→ oCPC 模型用「验证转化」训练;广告主可查转化明细(时间/设备/触点)自证。

flowchart LR Ad[广告点击] --> Cid[生成click_id带参] Cid --> LP[落地页SDK捕获] LP --> Conv[用户转化] Conv --> SDK[SDK实时回传] Conv --> API[服务端批量回传] SDK --> Dedup[事件ID去重] API --> Dedup Dedup --> Match{归因匹配} Match -->|click_id| Direct[直接归因] Match -->|设备ID| Fuzzy[模糊归因] Match -->|无触点| Organic[自然流量] Direct --> Model[归因模型
默认末次点击] Fuzzy --> Model Model --> Ocpc[oCPC模型回流] Model --> Report[广告主报表] Risk[转化风控] --> Filter[假转化过滤]
转化归因链路:click_id 双通道、去重、模型回流
关键代码
// 归因窗口:点击后 N 天内转化归因到该点击(末次触点)
public Optional<ClickId> attribute(Conversion c, Duration window) {
    List<Click> clicks = clickStore.recentClicks(c.userId(), c.convertTs().minus(window), c.convertTs());
    return clicks.stream()
        .filter(cl -> !dedup.alreadyAttributed(cl.id(), c.convertId()))
        .max(comparing(Click::ts));                            //// 末次点击归因
}

// 回传去重:广告主回调幂等(广告主侧按 convertId 去重)
public void postback(Conversion c, ClickId clickId) {
    String key = "pb:" + c.advertiserId() + ":" + c.convertId();
    if (!dedup.tryAdd(key)) return;                        //// 同转化只回传一次
    callbackClient.send(c.advertiserId(), Postback.of(c, clickId));
}
深度追问
  • iOS 隐私政策后归因不准怎么办?
    多通道降级:click_id + SKAN(苹果归因框架,转化值回传)+ 设备指纹(概率匹配)+ 广告主侧第一方数据(登录态匹配)。没有单一银弹,工程上做「通道冗余 + 模型对不确定归因降权」。
  • 转化延迟(用户三天后才付费)怎么归因?
    归因窗口覆盖(点击后 N 天),窗口内迟到的转化「补归因」(更新归因结果 + 回流补训 oCPC 模型);窗口外归为自然。窗口长度是产品决策(与类目决策周期匹配,电商短、金融长)。
  • 广告主说「我的转化被漏报了」?
    平台侧对账工具:广告主上报量 vs 平台归因量差异分析(未匹配的原因分布:窗口外/无触点/去重/风控);SDK 集成诊断(参数缺失/版本问题)。归因透明度是信任基础。
  • 同一个转化既有点击又有曝光怎么算?
    按模型口径:末次点击优先;无点击看末次曝光(曝光归因单独权重);多模型报表并列展示(点击归因/曝光归因/线性)。广告主选口径,平台保证各口径可复算。
常见陷阱
  • 单通道回传:SDK 挂了转化全丢;实时 + 批量双通道。
  • 事件不去重:重复回传导致转化虚高、oCPC 模型被骗;事件 ID 幂等。
  • 假转化进模型:模型越学越歪;风控前置过滤。
  • 归因黑箱:广告主看不懂为什么转化算/不算;明细可查 + 口径可选。
破题点拨
一眼定位:归因 = click_id 双通道 + 窗口匹配 + 幂等去重 + 风控过滤。
破题主线:点击带参 → 双通道回传 → 去重 → 归因匹配(click_id/设备/自然)→ 模型口径 → oCPC 回流 → 对账透明。
易错点:单通道;不去重;假转化进模型;归因黑箱。
D10.04
结算与报表:实时报表、收入对账与充值消耗高级P2
实时报表 收入对账 充值消耗 发票
场景背景

广告主三问:钱花哪了(消耗明细)、效果怎样(转化报表)、账对不对(结算单)。同时平台内部:媒体分成结算(给内容创作者/站长的分成)、渠道返点结算。硬指标:报表实时性(分钟级消耗可见)、数据准确(消耗 = 扣费流水合计)、结算周期(月结准时)、发票与充值闭环。

核心挑战

① 实时聚合:亿级扣费流水的多维实时聚合(账户 × 计划 × 创意 × 时间);② 口径一致:实时报表与日终结算的差异解释(数据延迟、IVT 过滤时点);③ 充值消耗模型:预付 + 消耗 + 退款 + 返点的账户体系;④ 分成结算:媒体方分成规则复杂(品类 × 形式 × 结算周期);⑤ 发票与合规:充值开票 vs 消耗开票的税务处理。

分析框架

数据流:扣费流水(D10.02)→ 实时管道(Flink 多维聚合,分钟级精度,标注「实时口径」)→ 离线数仓(T+1 全量,含 IVT 过滤后的「结算口径」)→ 两层口径并行展示(实时看趋势、结算看准确)。账户体系:充值(进)、消耗(出,与扣费流水一一对应)、返点(周期结算)、退款(冲正);所有资金动作走「凭证 + 幂等」。分成:按「有效收入(扣费 − IVT − 退款)× 分成规则版本」计算媒体方分成,月结对账(平台收入 vs 媒体分成 vs 渠道返点闭环)。发票:充值开票(预收)或消耗开票(按结算单),二选一配置,红冲规则同 D05.02。

领域模型

聚合根:广告账户(余额、充值记录、消耗流水)、结算单(Settlement:口径、金额、状态)。实体:充值单、分成单、发票。值对象:分成规则版本、口径标签(实时/结算)。不变量:账户余额 = 充值 − 消耗 − 退款(恒等,可对账);结算单 = 扣费流水结算口径合计(可重放);分成单按规则版本生效区间计算;发票金额锚定结算单。

解决方案

1. 实时报表:Flink 消费扣费流水 → 多维聚合(账户/计划/创意/小时)→ 写 OLAP(ClickHouse/Doris)→ 报表服务;实时数据标注「含未过滤 IVT」。

2. 结算:日终跑结算口径(IVT 过滤 + 退款冲正)→ 日消耗账单;月终出结算单(月消耗合计)→ 广告主确认 → 开票;差异(实时 vs 结算)有解释看板。

3. 账户:充值单(支付渠道对接,幂等)→ 余额;消耗实时扣(与 D10.02 同链);余额不足预警 + 自动停投;退款走冲正凭证。

4. 分成:媒体分成规则配置化(品类 × 形式 × 阶梯);月终按有效收入算分成 → 分成单 → 媒体确认 → 付款;渠道返点同理。

flowchart TD Charge[扣费流水] --> RT[Flink实时聚合] RT --> OLAP[(OLAP)] OLAP --> Dash[实时报表
趋势口径] Charge --> IVT[IVT过滤+退款冲正] IVT --> Daily[日账单] Daily --> Monthly[月结算单] Monthly --> Invoice[开票] Recharge[充值] --> Balance[账户余额] Charge --> Balance Monthly --> Share[媒体分成计算] Share --> MediaBill[媒体结算单] Rebate[渠道返点] --> ChannelBill[渠道结算]
结算报表体系:实时与结算双口径、账户恒等
关键代码
// 收入对账:平台口径 vs 客户口径逐项 diff,差异进差异表挂账
public ReconReport reconcile(LocalDate day) {
    List<BillItem> ours  = billingRepo.byDay(day);            //// 平台计费明细
    List<BillItem> theirs = customerReport.fetch(day);       //// 客户/媒体回传
    Map<Key, BillItem> merged = new HashMap<>();
    ours.forEach(i -> merged.merge(i.key(), i, BillItem::add));
    theirs.forEach(i -> merged.merge(i.key(), i.negate(), BillItem::add));  //// 异号相抵
    List<Diff> diffs = merged.values().stream()
        .filter(i -> i.amount().abs().compareTo(TOLERANCE) > 0)     //// 容差内抹平
        .map(Diff::of)
        .toList();
    return ReconReport.of(day, diffs);                    //// 差异挂账:待查/待退/待补
}

// 实时报表:Kafka 流式聚合 + 分钟级落宽表,禁直接查明细库
public void buildRealtime(Stream<BillEvent> events) {
    events.groupBy(e -> e.minuteKey())
        .window(Tumbling.of(Duration.ofMinutes(1)))
        .aggregate(BillAgg::new, BillAgg::add)
        .toStream()
        .foreach(agg -> wideTable.upsert(agg));               //// 分钟级宽表供 dashboard
}
深度追问
  • 实时报表和月结账对不上,广告主吵架?
    口径透明是解药:报表页明示「实时值含未过滤 IVT,日终修正」;提供「实时 → 结算」的差异桥(IVT 多少、退款多少);月结单可下钻到日账单到扣费流水。吵不清的账大多是看不清的账。
  • 余额恒等被破坏(对不上账)?
    所有资金动作凭证化(充值单/扣费单/退款单三张表),余额是派生值(SUM 凭证)而非独立可变值;日终跑恒等校验(凭证合计 vs 余额),差异进工单。资金系统里「余额是投影不是状态」。
  • 大广告主要 API 拉数据做 BI?
    开放报表 API(OAuth 授权 + 维度指标自选 + 分页限流)+ 离线大文件(对象存储,日切);口径文档公开(指标定义 SSOT)。开放能力是 B 端平台标配。
  • 充值开票还是消耗开票?
    两种税务处理都合法但逻辑不同:充值开票(预收,简单,但退款要红冲);消耗开票(按实际,准确,但跨月复杂)。平台按客户类型配置,红冲链路与退款联动。这是财务域决策,技术保证两种模式都支持。
常见陷阱
  • 余额当可变状态:并发扣减丢失更新;凭证派生 + 幂等扣减。
  • 实时结算一个口径:要么实时不准要么结算太慢;双口径并行。
  • IVT 过滤时点不清:月结突然少一笔,广告主炸;口径明示 + 差异桥。
  • 发票与结算单脱钩:票账不符,税务风险;发票锚定结算单。
破题点拨
一眼定位:结算报表 = 双口径(实时/结算)+ 凭证化账户 + 分成规则版本化。
破题主线:扣费流水 → 实时聚合 + 结算口径 → 账单结算单 → 发票锚定 → 分成返点 → 恒等校验。
易错点:余额可变状态;单口径;IVT 时点不清;发票脱钩。
D10.05
广告反作弊:无效流量识别与刷量治理高级P2
IVT 识别 设备指纹 刷量模型 GIVT
场景背景

无效流量(IVT)类型:机器人点击、点击农场(人机混合)、恶意重复点击(竞争对手耗预算)、曝光作伪(隐藏/堆叠展示)、归因作弊(假转化)。硬指标:IVT 识别率(过滤)、误判率(不误伤真实流量,那是资损 + 客诉)、处置时效(实时拦截)、对抗能力(黑产升级快)。上述是观察口径,具体阈值须按本业务标定,禁止把本卡文字直接当成门禁数字。

核心挑战

① 人机混合:众包点击农场用真人,行为像人;② 设备伪造:改机工具伪造设备 ID,新设备涌现异常;③ 实时性:点击流百万 QPS,判定要毫秒级(在扣费前);④ 误伤平衡:误判真实用户 = 广告主损失 + 用户投诉;⑤ 灰产对抗:模式一更新黑产就绕,猫鼠游戏。

分析框架

IVT 过滤前置在点击/曝光入口(竞价后、计费前):多层防线 —— ① 规则层(已知恶意 IP/设备黑名单、异常 UA、数据中心 IP、点击频率超限);② 设备指纹层(改机特征 + 设备信誉分,新设备/低分设备严格);③ 行为模型层(点击间隔分布、页面轨迹、转化序列的异常检测,无监督 + 有监督混合);④ 图挖掘层(设备-IP-账户关联,点击农场团伙识别,D02.02 复用)。GIVT(一般无效流量:机器人抓取等)规则层基本覆盖;SIVT(复杂无效:农场/恶意软件)靠模型 + 图谱。误判控制:白名单(高信誉用户/广告主自有流量)+ 抽样人工评估 + 申诉通道。

领域模型

聚合根:流量风控判定(TrafficVerdict:流量 ID、判定、依据)。实体:设备信誉档案、团伙簇。值对象:设备指纹、风险分。不变量:计费的流量必过风控判定(无旁路);判定留证据链(命中的规则/模型版本);申诉成立必退款/恢复;白名单流量单独审计(防白名单滥用)。

解决方案

1. 实时拦截:点击流 → 风控服务(规则 + 设备信誉,毫秒级)→ 通过/拦截/观察;拦截流量不计费不进竞价(或竞价不计费)。

2. 设备指纹:端上 SDK + 服务端采集多特征(硬件/网络/行为)生成稳定指纹;改机检测(模拟器/root/hook 特征);设备信誉分历史累积。

3. 团伙挖掘:离线图计算(设备-IP-账户-支付)聚类,识别点击农场(行为同步性、收益归集);线上实时团伙特征(同 IP 簇高频点击)。

4. 误判治理:广告主自助诊断(为什么流量被过滤)+ 申诉通道;模型灰度(新模型先影子再生效);白名单审计(季度复核白名单合理性)。

flowchart LR Click[点击/曝光流] --> RT[实时风控
规则+设备信誉] RT -->|通过| Bid[竞价计费] RT -->|拦截| Block[不计费+证据链] RT -->|观察| Queue[离线分析队列] Click --> SDK[设备指纹采集] SDK --> Rep[设备信誉档案] Queue --> Model[行为异常模型] Model --> Cluster[图挖掘团伙] Cluster --> Rules[规则库更新] Complaint[广告主申诉] --> Audit[抽样人工评估] Audit --> Fix[误判恢复/退款] White[白名单] --> Audit
IVT 多层过滤:实时拦截、设备信誉、团伙挖掘、误判治理
关键代码
// IVT 识别:GIVT(已知爬虫/IP 库)+ SIVT(行为模型)双层过滤
public IvVerdict screen(Impression imp) {
    if (givtDb.isBot(imp.ip(), imp.ua()))                  //// 已知机器人库直拒
        return IvVerdict.givt(imp);
    double score = sivtModel.score(features(imp));          //// 行为特征:停留/点击模式/频率
    return score > THRESHOLD ? IvVerdict.sivt(score) : IvVerdict.valid();
}

// 刷量团伙:设备/IP/账号聚类,同簇集中异常即整簇降权
public Set<ClusterId> detectFraudCluster(Window w) {
    Graph g = graphBuilder.build(w.impressions());              //// 边 = 共享设备/IP/账号/支付
    return community.detect(g).stream()
        .filter(c -> c.anomalyScore() > CLUSTER_THRESHOLD)
        .map(Cluster::id)
        .collect(toSet());                                   //// 整簇进观察名单
}
深度追问
  • 点击农场用真人,行为模型怎么分?
    真人农场的行为有「组织痕迹」:任务时间同步(同一秒批量点击)、设备聚集(同 Wifi/同型号)、收益路径归集(同提现账户)、无自然转化(点了不买)。个体行为像人,群体特征不像——图挖掘是核心,单点模型是辅助。
  • 竞争对手恶意点击耗预算怎么防?
    异常点击模式(同设备/IP 短时间内重复点击不同广告、无转化意图)实时拦截 + 竞价侧对可疑点击不计费;严重恶意账号封禁 + 广告主侧报告(告知「您的预算被异常消耗,已过滤 N 次」)。
  • 误判真实流量导致广告主效果下降?
    误判率与拦截率同权监控(红线:误判导致的 GMV/转化损失);灰度 + 影子模式(新策略先比对再生效);白名单机制(广告主自有 App 内流量、再营销流量)。反作弊的 KPI 是「净收益 = 拦截作弊 − 误判损失」。
  • 黑产绕过检测的响应流程?
    监控告警(拦截率突降/新型模式涌现)→ 样本分析(人工 + 半监督)→ 规则/模型迭代(小时级发版)→ 效果回收(对抗样本回流)。安全运营的节奏是「发现-响应-迭代」循环,不是一次建模。
常见陷阱
  • 事后过滤:扣费了再追,广告主不认;计费前实时拦截。
  • 只堆规则:黑产一绕全失效;规则 + 模型 + 图三层。
  • 只看拦截率:误判真实流量,广告主流失;误判率同权。
  • 白名单无人审:内部/关系流量白名单滥用;季度审计。
破题点拨
一眼定位:IVT = 计费前实时过滤 + 设备信誉 + 团伙图挖掘 + 误判同权。
破题主线:规则层 → 设备指纹 → 行为模型 → 图谱团伙 → 白名单审计 → 运营循环迭代。
易错点:事后过滤;只堆规则;只看拦截率;白名单滥权。
D10.06
广告创意优化:多创意优选、素材生成与落地页一致性高级P1
高频 领域模型 创意优选 素材生成 创意衰退
场景背景

广告平台创意侧:一个广告主一个计划下挂多条创意(文案 × 图片 × 视频组合),日增素材百万级。硬指标:创意 CTR/CVR、素材审核通过率与时效、创意衰退发现速度、落地页与素材一致性(货不对板投诉率)。

核心挑战

① 多创意竞争:同一计划下多创意按 eCPM 竞价争流量,探索(新创意没数据)与利用(老创意有数据)怎么平衡;② 创意衰退:同一素材看久了用户无感,CTR 逐日衰减,衰退了怎么发现与替换;③ 素材生成:模板化拼装(标题/图片/视频)批量生成,质量参差,要过审核;④ 落地页一致性:素材说「九块九」落地页卖九十九,货不对板伤害用户体验与平台口碑;⑤ 合规审核:极限词、虚假宣传、低质素材,机审 + 人审两级。

分析框架

创意优选是 bandit 问题:每条创意有先验(相似素材历史表现),探索期给基础流量,数据起来后利用(赢家通吃),衰退期(CTR 低于自身基线)降权替换。素材生成走「模板 + 元素」拼装:商品信息(标题/图/价)自动填充多模板生成批量候选,机审过滤(敏感词/清晰度/重复度)后进探索池。落地页一致性:素材卖点词与落地页标题/价格比对,差异大的进人工审核;白名单商品(价格稳定)走快速通道。审核机审先行(OCR + NLP 模型),不确定的进人审队列,审核时效 SLA(分钟级)。

领域模型

聚合根:创意(Creative,素材组合 + 落地页 + 审核态 + 状态)。实体:创意实验记录(Experiment,曝光/点击/转化序列)、衰退监测(DecayWatch,CTR 基线 + 当前值)、审核工单(ReviewTicket,机审结果 + 人审结论)。值对象:素材元素(标题/图/视频 URL)、eCPM。不变量:创意必须审核通过才能进竞价;衰退创意降权但不直接下线(保留学习信号);素材与落地页价格/卖点差异超阈值必须人审。

解决方案

1. 创意竞价:创意级 eCPM 预估(pCTR × Bid × 质量分),与计划级预算协同(D10 预算约束);新创意探索流量(基础曝光包),胜出后自动加权。

2. 优选策略:bandit 思路(探索/利用动态配比),同人群素材去重(同一用户单位时间同一计划最多 N 条);衰退监测(CTR 相对自身历史基线的衰减),衰退自动降权 + 通知补新。

3. 素材生成:模板库 × 商品信息自动拼装(标题多版本 + 图多裁切 + 视频多模板),批量生成后机审过滤;生成素材带「模板 ID」方便效果归因(哪个模板出好料)。

4. 一致性校验:素材卖点/价格 vs 落地页标题/价格比对(价格超差、卖点夸大即拦截),白名单商品快速通道;落地页可用性(打开速度/死链)纳入质量分。

5. 审核流水线:机审(敏感词 + 图像识别 + 落地页抓取)先过,不确定项人审;审核状态机(待审→机审→人审→通过/驳回),驳回理由结构化反馈给广告主。

graph TD A[模板库 x 商品信息] --> B[素材批量生成] B --> C[机审 敏感词/图像] C -->|不确定| D[人审队列] C -->|通过| E[探索池 基础曝光] D -->|通过| E E --> F[创意竞价 eCPM] F --> G[胜出加权] H[CTR 监测] -->|衰退| I[降权+通知补新] J[素材卖点/价格] --> K[落地页一致性校验] K -->|超差| D K -->|通过| F
创意生成-审核-优选闭环
关键代码
// 创意 eCPM 竞价:pCTR x Bid x 质量分,与预算约束协同
double ecpm = ctrModel.predict(user, creative) * creative.bid() * quality.score(creative);
if (budget.remaining(plan) <= 0) return;                       // 计划预算兜底

// bandit 探索利用:新创意探索流量,胜出后加权;衰退降权
double ctrNow = stats.ctr(creative, Window.DAY_1);
double ctrBase = stats.ctrBaseline(creative, Window.DAY_7);
if (creative.isFresh() && creative.impressions() < EXPLORE_FLOOR) weight = 1.5;   // 探索加权
else if (ctrNow < ctrBase * 0.7) { weight = 0.3; notify.decay(creative); }  // 衰退降权+通知

// 落地页一致性:价格超差 + 卖点夸大即拦截进人审
if (lp.priceDiff(creative) > 0.05 || lp.claimsMismatch(creative)) {
    review.submit(creative, Reason.LP_INCONSISTENT);      // 不进竞价,先审
}
深度追问
  • 新创意没数据,eCPM 预估不准怎么办?
    冷启动用先验:相似素材(同模板/同商品类目/同人群)的历史 pCTR 做先验,快速收敛;同时给探索流量保底(基础曝光包),有真实数据后切模型预估。两边都不能少——没先验收敛慢,没探索永远起不来。
  • 创意衰退怎么定义?
    相对自身基线:近一天 CTR 低于近七天基线的七成且统计显著(曝光量足够),判定衰退;衰退创意降权不直接下线(保留学习信号),同时通知广告主补新素材。绝对阈值会误杀低频计划,相对基线更稳。
  • 素材批量生成质量差怎么办?
    生成侧做「模板分级」:模板按历史效果分优质/普通,新素材优先用优质模板;机审过滤低质(模糊/重复/文字过多);生成素材按「模板 ID」归因,淘汰效果差的模板。生成是放大模板质量,不是创造质量——模板库运营比生成引擎更重要。
  • 落地页一致性怎么防「素材九块九、落地九十九」?
    价格字段结构化(素材挂商品 ID 而非图片写死),系统直接比对落地页实时价格,超差即拦截;白名单商品(价格稳定)走快速通道,非白名单全量校验。图片里写价格的(非结构化)用 OCR 抽取比对,兜底。一致性是平台口碑的生命线,宁可误拦不可漏放。
常见陷阱
  • 老创意一统天下:没探索机制,新素材永远没量,计划整体衰退;bandit 探索流量是必须。
  • 衰退不处理:用户看腻了 CTR 衰减,成本越跑越高;相对基线监测 + 自动降权。
  • 货不对板:素材价格/卖点与落地页不符,投诉与监管双风险;结构化价格比对。
  • 机审当唯一:极限词变形、图像违规漏检;机审 + 人审两级,人审有时效 SLA。
破题点拨
一眼定位:创意侧 = bandit 优选(探索+衰退)+ 模板化生成 + 一致性拦截 + 机审人审两级。
破题主线:先验收敛 → 探索保底 → 胜出加权 → 相对基线衰退监测 → 价格结构化比对。
易错点:无探索;衰退不处理;货不对板;机审唯一。

十一 游戏与实时互动 6 题

本组知识链

游戏后端的主线是「高并发连接 + 实时同步 + 服务端权威」。覆盖网关接入、房间匹配、帧/状态同步、战斗结算、排行榜经济与游戏内社交。

  1. 网关:长连接管理、协议设计、广播扇出与掉线恢复。
  2. 房间匹配:匹配评分、分服扩容与房间生命周期。
  3. 同步:帧同步确定性、状态同步 AOI 与反外挂。
  4. 结算与经济:BUFF 结算一致性、货币账务、榜单与防刷。
  5. 游戏内社交:seq 消息流、世界频道读扩散、私聊信箱兜底、分频道审核。
工程化要点
  • 网关与逻辑分离:网关只管连接、协议、转发,无业务状态,故障域最小。
  • 广播按房间分片:同房同网关本地扇出,跨网关走消息队列按 roomId 分区,禁全站广播。
  • 帧同步必须确定性:定点数运算、固定步长、输入全集有序、逐帧哈希校验。
  • 一切数值服务端权威:客户端只上报意图,伤害/货币/排名以服务端计算结果为准。
本组思维模式
  • 一致性让位实时性:实时体验优先,用校验与回补追一致性,而非锁住一切。
  • 服务端权威:客户端是「不可信输入源」,任何信任客户端的数值都等于送外挂。
  • 房间是调度单元:扩容、广播、容错都以房间为边界设计。
  • 经济系统先算账:货币与道具是账务问题,产出消耗模型要先于数值配置。
  • 异常流是主干:掉线、秒退、卡顿、结算失败每天都在发生,不是边缘 case。
合规与资损红线
  • 未成年人保护是硬约束:实名认证、时段限制、消费限额与退费流程法定化。
  • 虚拟货币不得反向兑换法币或提现,交易行只允许道具流转且全程留痕。
  • 房间名、聊天、自定义内容过内容安全审核,涉政涉暴实时处置。
  • 概率掉落必须公示概率,合成/开箱类玩法有保底与记录可查。
D11.01
游戏网关:长连接管理、协议设计与广播扇出架构P1
领域模型 架构 长连接 协议设计 广播扇出
场景背景

实时对战游戏后端:百万级注册、峰值数十万同时在线,每个客户端维持一条长连接(TCP/KCP/WebSocket)。客户端登录、选模式、进房、战斗中实时收发指令,同时接收房间内其他玩家位置/技能事件。单局同屏数十人,一个服务节点承载上万房间。硬指标:单网关几十万连接、端到端延迟百毫秒内、广播不能写爆网关、掉线能恢复继续战斗。

核心挑战

① 连接规模:单机百万级连接的文件描述符、内存、心跳成本;② 广播扇出:一个事件要复制给房间内所有玩家,万房并发时写放大;③ 协议语义:哪些消息必须可靠有序(技能释放),哪些可丢(位置同步);④ 有状态 vs 无状态:网关掉线不能丢战斗状态;⑤ 重连恢复:客户端闪退重连后要补齐断线期间的世界快照。

分析框架

核心决策是网关只做连接与转发,业务状态全部下沉逻辑服。协议分两个通道:可靠通道(序号 + ACK + 重传,用于登录/战斗指令/结算)与不可靠通道(直接写,用于位置/状态快照,丢了下次快照自然覆盖)。广播路径分级:房内同网关(本地写,最快)→ 跨网关(消息队列按 roomId 分区,同房必落同分区保证序)→ 全服广播(罕见,走独立推送服务,禁用在战斗路径)。连接层维护 userId → connId → gateway 映射(Redis),逻辑服通过网关集群 API 定向推送。重连走「战斗服拉快照 + 网关重绑定」,不在网关注销战斗。

领域模型

聚合根:连接会话(Session:connId、userId、gateway、roomId、lastSeq、state)。实体:房间(Room:roomId、mode、memberSet、broadcastCtx)。值对象:消息信封(Envelope:msgId、seq、channel、payload)、路由键(RouteKey:roomId 分区)。不变量:同一 userId 同时只有一条有效会话(踢旧连);seq 在会话内单调递增;房间成员上限不超模式配置;广播只送达当前房间成员集合。

解决方案

1. 网关进程:Netty 实现,单机五十万连接;IO 线程只做编解码与写,业务线程池做登录握手;心跳三十秒 + 客户端随机抖动,读超时三倍心跳断开;连接元信息进 Redis(带 TTL),供逻辑服反查推送目标。

2. 协议:定长头(消息号两字节 + 长度四字节 + 通道一字节 + 序号四字节)+ protobuf body;可靠通道 ACK 累确认,超时重传上限三次;不可靠通道直接写 SocketChannel;协议带 version 字段,支持灰度期新旧并存。

3. 广播:房内事件先进房间广播队列,按「本地成员」直接写、按「远端成员」发 MQ(topic = battle-broadcast,partition key = roomId);消费端在目标网关本地再写连接。同房同分区保证序,天然避免跨网关乱序。

4. 重连与容灾:网关无状态,挂掉后客户端重连到新网关,登录态从 Redis 恢复;战斗状态在战斗服,不受网关故障影响;闪断重连由战斗服下发增量快照补齐。

flowchart LR C1[客户端A] -->|可靠通道| GW1[网关1] C2[客户端B] -->|不可靠通道| GW1 C3[客户端C] --> GW2[网关2] GW1 --> Auth[登录态服务] GW2 --> Auth GW1 --> MQ[广播MQ
partition=roomId] MQ --> GW2 Room[战斗服房间] --> Push[推送API] Push --> GW1 Push --> GW2
网关集群与广播分级路径:本地扇出优先,跨网关走分区队列
关键代码
// 房间广播:本地连接直接写,跨网关发分区队列(同房同分区保序)
public void broadcast(RoomId roomId, Envelope env) {
    Set<ConnId> local = roomLocalMembers.get(roomId);
    for (ConnId c : local) {
        Channel ch = channels.get(c);
        if (ch != null && ch.isActive()) ch.writeAndFlush(env); // 本地零拷贝路径
    }
    if (roomRemoteMembers.containsKey(roomId)) {           // 有远端成员才发 MQ
        mq.send(TOPIC_BROADCAST, roomId.hash(), env);      // partition key = roomId
    }
}

// 可靠通道:序号 + 累确认 + 有限重传,不可靠通道直接丢弃 ACK 逻辑
public void onReliable(ConnId c, Envelope env) {
    session.checkSeq(c, env.seq());          // 乱序/重放入队等待
    if (!ackTracker.confirm(c, env.seq())) {
        retryQueue.offer(c, env);            // 超时未 ACK 进重试,上限三次
    }
}
深度追问
  • 战斗中网关整体宕机,玩家体验如何?
    网关无状态所以秒级切换:客户端重连新网关(TCP 断开即触发),登录态从 Redis 恢复,战斗服感知「连接重绑定」后下发断线期间的快照 + 增量。战斗本身不中断,玩家只感知一次卡顿。这正是把状态从网关剥离的回报。
  • 广播延迟在某几毫秒内突然抬高,怎么定位?
    分层打点:网关本地写耗时、MQ 生产耗时、MQ 消费到写耗时分别上报。常见元凶是 GC 停顿与跨可用区消费;对策是消费端本地化部署(同 AZ 消费)+ Netty 堆外内存 + 大对象池化。定位靠分位延迟曲线而非平均值。
  • 协议要加字段/改消息,老客户端怎么办?
    protobuf 向后兼容(只加字段不改号)+ 协议 version 协商:登录时客户端上报版本,服务端按版本选择编解码器。灰度期新旧编解码并存,全量后再下线旧版本。禁直接改消息号语义。
  • 一台网关能带多少房间,怎么评估?
    看两个约束:连接数(文件描述符 × 每连接内存,约几十万)与广播写吞吐(房间数 × 每房每秒事件 × 房间人数)。压测按「最热房型」建模,留三成余量;接近上限时按房间维度迁到新网关,客户端透明重连。
常见陷阱
  • 全服广播走同一通道:一条公告写爆所有网关;全服推送独立服务 + 限速。
  • 心跳太频:三十万连接每秒心跳就是几十万包;三十秒级心跳 + 超时三倍。
  • 网关注销战斗状态:网关一挂战斗全丢;状态下沉战斗服。
  • MQ 按 userId 分区:同房玩家落不同分区导致房间事件乱序;必须按 roomId 分区。
破题点拨
一眼定位:游戏网关 = 无状态连接层 + 双通道协议 + 房间分区广播 + 状态下沉。
破题主线:网关只管转发 → 可靠/不可靠双通道 → 本地扇出优先、MQ 按 roomId 分区 → 重连快照补齐。
易错点:全服广播同通道;心跳过频;网关注销状态;分区键选错。
D11.02
帧同步与状态同步:确定性、回放与反外挂专家P1
领域模型 架构 帧同步 定点数 反外挂
场景背景

RTS、格斗、音游、MOBA 类游戏对「所有客户端看到的世界必须一致」有强要求:同一局对战中,伤害判定、技能命中、位移结果在所有端上必须逐帧一致,否则出现「我明明躲开了却被打死」。帧同步方案是客户端各跑一份相同逻辑,服务端只转发输入;同时要支持观战回放、断线重连、战斗复盘,以及对抗加速挂、透视挂、修改器。

核心挑战

① 浮点不确定性:不同 CPU/编译器/优化级别下浮点结果可能不同,逐帧累积即分叉;② 随机数同步:暴击、掉落若各端随机种子不同直接分叉;③ 输入收集:每帧必须等到最慢玩家的输入才能推进(帧等待),网络抖动即卡顿;④ 回放:要能完整重放一局并验证一致性;⑤ 外挂:改内存、加速、全图透视,帧同步架构下客户端算逻辑等于把规则交给玩家。

分析框架

确定性四要素:定点数运算(整数模拟小数,禁 float/double 参与逻辑)、固定逻辑帧长(逻辑三十毫秒一步,与渲染帧率解耦)、输入全集有序(每帧广播收集到的操作集合,各端按同一顺序执行)、种子统一(开局服务端发种子,随机数只用种子派生)。帧驱动器:收集本帧输入 → 广播 → 等最慢输入(或超时用上帧重复)→ 各端同序执行 → 算 stateHash → 每若干帧服务端比对,不一致即判定分叉(踢出并标记)。状态同步则相反:服务端算权威状态,按 AOI 增量下发,客户端做插值与预测(命中即时反馈,服务端矫正)。反外挂靠「服务端权威 + 关键信息最小下发 + 行为模型」三层,不靠客户端本地判断。

领域模型

聚合根:战斗房间(BattleRoom:battleId、seed、frameNo、inputFrames、stateHashChain)。实体:输入帧(InputFrame:frameNo、playerId、ops、checksum)、战斗实体(BattleEntity:hp、pos、buffs)。值对象:定点坐标(FixedVec2)、随机流(SeedStream:确定性派生序列)。不变量:同帧所有客户端持有的输入集合完全相同;stateHash 逐帧一致;battleId + frameNo 唯一;回放数据(种子 + 输入流)与战斗过程一一对应。

解决方案

1. 定点数库:FixedPoint32(三十二位整数,低十六位小数)实现加减乘除与开方,三角函数用查表;所有战斗数值(坐标、速度、血量、伤害)走该类型,代码评审禁浮点进战斗模块。

2. 帧驱动器:固定三十毫秒 tick;输入窗口收集(本 tick 内到达的操作)→ 满员或超时(两倍 tick)推进 → 输入集合广播给所有客户端 → 各端按 playerId 排序后同序执行 → 计算 stateHash 上报。

3. 一致性校验:服务端维护每房间的「输入集合指纹 + stateHash 链」;客户端每若干帧上报 hash,不一致立即标记该客户端分叉(踢出对局并进观察名单)。同时全量落输入流用于回放与仲裁。

4. 回放:只存「种子 + 逐帧输入集合 + 版本号」,存储量极小;观战端用同一引擎重放;纠纷申诉时重放比对即可判定谁在说谎。

5. 反外挂:伤害/命中由各端计算但服务端用轻量校验(输入合法性 + 频率 + 统计异常)发现即处理;透视挂靠「视野外实体不下发」根治(帧同步下用服务端裁剪视野集合);加速挂靠服务端时间戳校验。

flowchart LR A[客户端输入] --> B[网关] B --> S[战斗服收集
固定tick] S -->|输入集合广播| C1[客户端1执行] S -->|输入集合广播| C2[客户端2执行] C1 --> H1[stateHash上报] C2 --> H2[stateHash上报] H1 --> CK[一致性校验] H2 --> CK CK -->|不一致| Kick[分叉处置] S --> Replay[输入流存储
回放重放]
帧同步闭环:输入全集广播、同序执行、哈希链校验与回放
关键代码
// 定点数:整数模拟小数,跨平台结果完全一致
public final class Fixed {
    public static final int SHIFT = 16;
    private final long raw;                        // 低16位为小数部分

    public static Fixed mul(Fixed a, Fixed b) {
        return new Fixed((a.raw * b.raw) >> SHIFT);   // 先乘后移位,禁浮点
    }
    public static Fixed sqrt(Fixed a) {            // 牛顿迭代,整数运算
        long x = a.raw, g = x;
        while (true) { long n = (g + x / g) >> 1; if (n == g) break; g = n; }
        return new Fixed(g << (SHIFT / 2));
    }
}

// 帧推进:输入集合按 playerId 排序后同序执行,保证各端分叉可检测
public void tick(long frameNo) {
    List<Op> ops = pending.get(frameNo);              // 本帧输入全集
    ops.sort(Comparator.comparing(Op::playerId)); // 固定顺序
    for (Op op : ops) world.apply(op);              // 各端同一份逻辑
    long h = world.stateHash();                  // 逐帧哈希
    hashChain.append(frameNo, h);                 // 链式:h_n = H(h_{n-1}, state)
    replayLog.write(frameNo, ops);                // 只存输入,回放可完整重放
}
深度追问
  • 移动端发热降频后逻辑帧变慢,会分叉吗?
    不会——逻辑帧是固定步长的「逻辑时间」,渲染帧慢只影响表现层(插值补帧),不影响逻辑推进。要保证的是逻辑单帧耗时预算(低端机也要在一帧内算完),超出就要砍单帧实体数或简化公式。分叉只源于运算不一致,不源于算得慢。
  • 某个玩家网络差导致帧等待,全员卡顿怎么办?
    超时策略:超过两倍帧长未到输入,用该玩家上帧输入(或空输入)推进,并在客户端做平滑;连续超时的玩家标记弱网,匹配阶段就规避(同段位只匹配同网络质量)。极致做法是「乐观执行 + 服务端矫正」,但复杂度高,先做超时兜底。
  • 外挂作者直接伪造输入包打服务端,如何发现?
    输入合法性校验(坐标必须可达、频率必须人类可能、资源消耗必须有来源)+ 统计异常检测(命中率/反应时间分布离群)+ 对战结果异常(长期胜率偏离段位)。处置分档:观察 → 限制匹配 → 封禁,全部有证据链(输入流可回放举证)。
  • 为什么不全服用状态同步,还要维护帧同步引擎?
    状态同步流量大(全量状态增量)、服务端算力贵(每房间一份世界模拟),且表现层受网络延迟直接影响;帧同步流量小(只传输入)、客户端算力免费、延迟由操作预表现抵消。选型看对局规模与一致性要求:百人吃鸡用状态同步,五人 MOBA 用帧同步。
常见陷阱
  • 战斗模块用浮点:跨端累积误差分叉;定点数库 + 评审禁入。
  • 随机数不播种子:各端暴击不同直接分叉;种子服务端下发。
  • 输入无序执行:按到达顺序执行;固定排序后同序执行。
  • 信任客户端上报结果:伤害/命中本地算完上报即作弊;服务端校验 + 视野裁剪。
破题点拨
一眼定位:帧同步 = 定点数 + 固定步长 + 输入全集同序 + 哈希链校验 + 输入流回放。
破题主线:定点化 → 种子统一下发 → 输入集合广播排序执行 → 逐帧哈希比对 → 超时兜底 → 回放存证。
易错点:浮点进战斗;随机数不同步;输入无序;信客户端。
D11.03
房间与匹配:匹配算法、分服扩容与房间状态机高级P1
领域模型 匹配算法 分服扩容 房间状态机
场景背景

对局开始前,玩家点击「开始匹配」,系统要在几十秒内凑齐一局(五人 MOBA、百人吃鸡、三人麻将),并保证双方实力接近、延迟可接受。同时运营要控制开服节奏:新区滚动开放、老区合服、大版本新区爆满与长草期闲置。房间本身有生命周期:等待 → 满员 → 加载 → 战斗中 → 结算 → 销毁,任何阶段都有玩家退出。

核心挑战

① 匹配质量与时长矛盾: strict 段位匹配等不到人,放宽又出现炸鱼;② 匹配池分区:按段位/模式/地域/网络质量分区,池子越小越难凑;③ 分服与合服:滚动开服扩容量,合服又涉及数据合并与社交关系;④ 房间状态机:秒退、掉线、超时未准备、结算中退出都要有定义;⑤ 机器人填充:低峰期匹配慢,机器人填充影响体验与公平。

分析框架

匹配建模为带约束的评分问题:候选组合(双方实力差、平均等待时长、延迟差、组队情况)→ 评分函数(实力差权重最高,等待时长作为放宽依据)→ 超时策略(等待越久越放宽实力差上限,直到「能开局」优先于「开好局」)。分服用容量驱动滚动开服:按在线人数与排队时长决定开新区,按水位决定合服;匹配路由到「同模式 + 低负载 + 延迟达标」的服务器组。房间用显式状态机驱动生命周期,每个状态定义进入条件、退出条件、超时行为;异常退出统一走「AI 托管 + 信誉扣减 + 补偿」而不是特殊逻辑散落。

领域模型

聚合根:房间(Room:roomId、mode、serverGroup、state、slots)。实体:房位(Slot:userId、team、ready、loadProgress)、匹配票据(MatchTicket:userId、criteria、state、expireAt)。值对象:匹配评分(MatchScore:skillGap + waitPenalty + latencyPenalty)。不变量:开局时各队人数符合模式要求;userId 全局最多持有一张有效票据;房间状态只能按状态机迁移;战斗中掉线有托管标记且不可重复占据新房间。

解决方案

1. 匹配服务:票据进匹配池(Redis 结构 + 分区);匹配器定时扫描(百毫秒级),按条件过滤候选 → 评分排序 → 组队成功即锁定票据并创建房间;超时逐级放宽(先放宽实力差,再放宽延迟,最后允许跨服)。

2. 房间管理:房间管理器持有状态机(Waiting → ReadyCheck → Loading → Battling → Settling → Disposed),每个状态配超时定时器;未准备/未加载的按超时踢出并记信誉;结算服务 Saga 化(算奖 → 发奖 → 记账,失败补偿邮件补发)。

3. 分服扩容:服务器组注册到调度中心(上报负载);新区按「老区排队时长 > 阈值」触发;合服窗口按活跃度合并,社交关系(好友/公会)迁移走离线任务 + 冲突编号重映射。

4. 异常与填充:开局前秒退无惩罚返还;战斗中掉线 AI 托管(战斗结算按托管标记打折);低峰期机器人填充只进「休闲模式」且标识明确,排位赛禁填充。

flowchart LR P[玩家票据] --> Pool[匹配池
按模式分区] Pool --> M[匹配器
评分+超时放宽] M -->|成功| R[创建房间] R --> W[Waiting等待] W --> RC[ReadyCheck准备] RC --> L[Loading加载] L --> B[Battling战斗] B --> S[Settling结算] S --> D[Disposed销毁] W -->|秒退| Refund[无惩罚返还] RC -->|超时| Kick[踢出+信誉] B -->|掉线| AI[AI托管标记]
匹配到房间的完整生命周期:票据、状态机与异常分支
关键代码
// 匹配评分:实力差主导,等待时长与延迟作为放宽依据
public double score(Ticket a, Ticket b) {
    double skillGap = Math.abs(a.skill() - b.skill());
    double wait     = Math.min(a.waitMs(), b.waitMs());
    double latency  = Math.abs(a.ping() - b.ping());
    return skillGap * 1.0 + wait / 1000.0 * 0.05 + latency * 0.01; // 等待越久越容忍差距
}

// 房间状态机:迁移白名单 + 每状态超时行为,禁散落 if-else
private static final Map<RoomState, Set<RoomState>> TRANSITIONS = Map.of(
    RoomState.WAITING,   Set.of(RoomState.READY_CHECK, RoomState.DISPOSED),
    RoomState.READY_CHECK, Set.of(RoomState.LOADING, RoomState.WAITING),
    RoomState.LOADING,   Set.of(RoomState.BATTLING, RoomState.DISPOSED),
    RoomState.BATTLING,  Set.of(RoomState.SETTLING),
    RoomState.SETTLING,  Set.of(RoomState.DISPOSED));

public void transit(RoomId id, RoomState target) {
    if (!TRANSITIONS.get(room.state()).contains(target))
        throw new IllegalStateException("非法房间迁移: " + target);
    room.enter(target);   // 进入即注册超时定时器(未准备踢出/未加载踢出)
}
深度追问
  • 高段位半夜匹配要几分钟,产品要求缩短,怎么办?
    三档手段:放宽(同段位扩到相邻段位,页面提示「实力相近」)→ 扩池(跨地域,延迟阈值从五十毫秒放到一百五十)→ 兜底(引导玩家换模式/组队,或进「训练场」)。不能既保质量又保时长,要把权衡结果显式告诉玩家,而不是偷偷放宽。
  • 匹配成功了,创建房间的服务挂了?
    创建是幂等事务:票据锁定与房间创建在同一本地事务(票据状态 → MATCHED,带 roomId),房间注册失败则票据回退到池子(带重试退避)。客户端收到「进入房间」指令后按 roomId 重试查询,不缓存中间态。核心是票据为唯一事实源,房间可重建。
  • 结算中有人退出,奖励怎么发?
    结算服务先落「结算单」(含所有参与者与奖惩明细,含退出者),再异步发奖;退出者按参与度(存活时长/输出占比)打折发放,邮件兜底补发。结算单幂等(battleId 唯一),发奖失败可重放。
  • 合服时好友、公会、昵称冲突怎么处理?
    昵称冲突按注册时间优先(后到者改 ID 后缀并送改名卡);好友/公会关系按 userId 合并(两边都有的去重);角色数据只合并账号层,各角色保留。合服是离线任务:停服窗口 + 数据迁移脚本 + 双写校验 + 回滚预案,全程可观测。
常见陷阱
  • 匹配条件写死:低峰永远凑不齐;超时逐级放宽是必需逻辑。
  • 房间不回收:内存泄漏到爆炸;DISPOSED 是终态且必须可达。
  • 结算非幂等:重试导致重复发奖;结算单按 battleId 唯一。
  • 排位塞机器人:公平性崩塌;机器人只进休闲且有标识。
破题点拨
一眼定位:匹配 = 评分 + 超时放宽;房间 = 状态机 + 超时定时;扩容 = 容量驱动滚动开合服。
破题主线:票据分区入池 → 评分组队 → 房间状态机 → 异常统一托管/信誉 → 结算 Saga 幂等。
易错点:条件写死;房间不回收;结算不幂等;排位塞机器人。
D11.04
排行榜与经济系统:榜单架构、虚拟货币与防刷高级P2
领域模型 ZSet 榜单 虚拟货币 经济平衡
场景背景

游戏经济是长线运营的核心:全服战力榜/段位榜/好友榜实时变动;金币、钻石、绑钻多种货币各有用途与获取渠道;商城直购、活动兑换、交易行玩家间道具流转。运营要监控通胀(货币贬值、道具泛滥),打击工作室打金与洗钱式交易。硬指标:榜单单元素百万级、更新频繁;货币收支零差错(玩家资产是口碑生命线);交易异常可追溯。

核心挑战

① 榜单规模与实时性:百万元素排序 + 秒级更新 + 附近排名查询;② 货币一致性:并发扣减(购买/交易/过期)不能超发,也不能丢流水;③ 经济平衡:产出与消耗失衡即通胀,道具价格体系崩溃;④ 交易行风险:洗钱(低价挂牌转移资产)、哄抬价格、外挂脚本扫货;⑤ 赛季结算:榜单结算奖励要准时要幂等。

分析框架

榜单走Redis ZSet 分赛季分区:key = rank:season:mode,member = playerId,score = 战力(注意并列时用 userId 做 tiebreaker 保证稳定序);分页用 ZREVRANGE,附近排名用 ZREVRANK + ZREVRANGE 窗口;冷数据按赛季归档,历史榜单可查不可改。虚拟货币按账务系统设计(与 D01.02 同源思想):余额表 + 流水表,一切变动必须落流水(bizNo 幂等),禁止无流水 UPDATE 余额;扣减用「条件更新 + 乐观重试」。经济平衡用水龙头/下水道模型:监控全服货币总量 M0/M1、产出消耗比、道具流通速度,作为数值调优输入。交易行做「挂牌-成交-税-过期」四态,加价格区间限制(防异常低价转移资产)+ 异常流水监控(同一 IP/设备段集中买卖即预警)。

领域模型

聚合根:玩家资产(PlayerAsset:playerId、wallets、inventories、tradeQuota)。实体:资产流水(AssetFlow:flowId、bizNo、currency、amount、direction、reason)、交易挂单(TradeOrder:orderId、item、price、state)。值对象:货币金额(Money:currency + amount,非负)、价格区间(PriceBand)。不变量:任一钱包余额 = Σ流水(按方向);同一 bizNo 只能产生一次流水;挂单价格必须在运营区间内;成交时买方扣款与卖方到账在同一事务。

解决方案

1. 榜单服务:ZSet 分赛季 + 分片(按战力段位分桶防大 key);写走「批量合并」(同玩家高频更新聚合后写);结算时刻生成快照表(奖励发放的依据),发奖按快照幂等执行。

2. 货币账务:wallet 表(player_id + currency + balance + version)+ flow 表(bizNo 唯一索引);发放/扣减统一走账务服务:条件 UPDATE(balance >= amount)+ 插流水,唯一键冲突即重复请求返回原结果;过期货币走「失效流水」而非直接扣。

3. 经济监控:每日全服产出/消耗按渠道汇总,产出消耗比偏离阈值告警;道具价格指数(成交价中位数)异常波动触发运营审查;工作室识别(同设备/IP/支付账号聚类 + 行为序列)进风控。

4. 交易行:挂单四态(在售/成交/过期/下架);成交走事务(扣款 + 过户 + 手续费流水);价格区间按道具类型配置(低于区间下限的单子进人工审核);大额交易二次验证。

flowchart LR Biz[业务方
带bizNo] --> Acct[账务服务] Acct --> W[钱包表
条件更新] Acct --> F[流水表
bizNo唯一] Acct -->|重复bizNo| Reject[幂等返回] Fight[战力变更] --> Rank[ZSet榜单] Rank --> Snap[赛季快照] Snap --> Award[结算发奖
幂等] Sell[卖家挂单] --> Mkt[交易行] Buy[买家下单] --> Mkt Mkt -->|事务| Settle[扣款+过户+税] Mkt --> Risk[异常流水监控]
经济系统闭环:账务三件套(条件更新+流水+幂等)与榜单结算
关键代码
// 货币扣减:条件更新 + 流水唯一键,重复 bizNo 天然幂等
public Receipt debit(String playerId, Currency cur, long amount, String bizNo, String reason) {
    int n = walletRepo.debitIfEnough(playerId, cur, amount);   // UPDATE ... SET balance=balance-? WHERE balance>=?
    if (n == 0) throw new InsufficientException(cur);
    try {
        flowRepo.insert(new AssetFlow(bizNo, playerId, cur, -amount, reason)); // bizNo 唯一索引
    } catch (DuplicateKeyException e) {                      // 已处理过:补偿余额并返回原结果
        walletRepo.credit(playerId, cur, amount);
        return flowRepo.findByBizNo(bizNo).toReceipt();
    }
    return Receipt.ok();
}

// 榜单:ZSet 分赛季,score 并列用 userId 作 tiebreaker 保证稳定序
public long rankOf(String playerId) {
    double score = zset.score(key, playerId);
    long ahead = zset.count(key, score + 1, Double.MAX_VALUE);  // 严格大于者计数
    long tie = zset.rank(key, playerId) - zset.rank(key, minIdAbove(score)); // 同分内按 id 序
    return ahead + tie + 1;
}
深度追问
  • 玩家投诉「余额莫名变少」,怎么查证?
    流水表按 bizNo + 时间窗全量可查:每笔扣减有原因、关联订单、操作终端。只要没有无流水扣减的路径(事务保证),投诉即可用流水自证。关键在建设期就禁掉任何绕过账务服务的余额写路径,代码评审 + 定时对账(余额与流水合计比对)双保险。
  • 大 key 榜单拖慢 Redis,怎么拆?
    三层:分桶(按战力段位/公会拆多个 ZSet,全局排名由桶汇总近似)→ 分赛季(旧赛季归档到冷存储,ZSet 只存当前)→ 合并写(同玩家高频更新先在本地缓冲,批量 ZADD)。查询侧:附近排名只查所在桶 + 相邻桶,不扫全量。
  • 交易行出现明显洗钱(低价转移资产),系统层能做什么?
    价格下限(低于区间进审核队列而非直接成交)+ 关联识别(买卖双方同设备/同支付账号/历史频繁交易即拦截)+ 限额(单日交易次数与金额上限)+ 全程留痕(供运营封禁举证)。系统做「能发现、能拦截、能举证」,判定与处置归运营风控。
  • 赛季切换那一刻榜单怎么写?
    双缓冲:新赛季 key 预创建,切换时刻「快照 + 清空」原子化(Lua 脚本);结算奖励以快照为准(与切换解耦,允许延迟发放);切换瞬间的写入进「旧赛季尾 + 新赛季头」都不丢(写双 key 过渡窗口)。核心是快照为准、发放幂等、过渡期双写。
常见陷阱
  • 无流水改余额:投诉无法自证;一切变动必须落流水。
  • 扣减无条件更新:并发下超发;balance >= amount 条件更新。
  • 榜单全量排序:大 key 拖垮;分桶 + 分赛季 + 合并写。
  • 交易行无价格约束:洗钱通道;价格区间 + 关联识别。
破题点拨
一眼定位:经济系统 = 账务三件套(条件更新 + 流水 + 幂等)+ 分桶榜单 + 水龙头监控。
破题主线:货币账务化 → 榜单分赛季分桶 → 产出消耗监控 → 交易行四态 + 风控。
易错点:无流水改余额;并发超发;榜单大 key;交易无价格约束。
D11.05
战斗与结算一致性:BUFF 结算、伤害公式与补偿高级P2
领域模型 BUFF 结算 伤害公式 补偿机制
场景背景

战斗结算是游戏数值的核心:一个技能命中要经过「基础伤害 → 攻防修正 → 暴击 → 穿透 → 阵营克制 → 护盾/免伤 → DOT 跳伤」多层计算;同时目标身上挂着若干 BUFF(中毒、加速、眩晕、护盾),各有叠加层数、持续帧、优先级与互斥关系。赛季/活动结束要结算排名与奖励,任何一次算错都是玩家投诉与资争。另外版本迭代会改公式,老战斗记录要能解释。

核心挑战

① BUFF 组合爆炸:覆盖/叠加/驱散的优先级规则极易出错,且策划会频繁调整;② 公式版本兼容:改公式后历史战绩、回放、申诉要用旧版解释;③ 并发修改:同帧多个事件同时改同一实体状态(A 的盾挡 B 的伤),顺序即结果;④ 结算可靠性:算奖/发奖/记账任何一步失败都要可补偿;⑤ 纠纷可仲裁:玩家说「我盾明明挡了」,要能重放举证。

分析框架

伤害计算走版本化公式引擎:公式参数化配置(基础值 × 系数 + 加成项),每次变更生成新 formulaVersion,战斗记录带版本号,回放/申诉按版本重算,杜绝「现在算得对但当时不对」。BUFF 走帧事件驱动模型:每个 BUFF 实例有生效帧、过期帧、堆叠与互斥组;每帧统一处理 onTick(DOT/回血)/ onExpire / onDispel,优先级决定覆盖关系,同帧内事件按固定序执行(先结算控制再伤害再死亡)。状态修改全部经由战斗实体的「帧内单线程」执行,天然无并发问题。结算走 Saga:算奖(纯函数,输入战斗结果 + 版本)→ 发奖(道具/货币,走账务服务 D11.04)→ 记账(结算单落库),每步幂等,失败按补偿动作(补发邮件)而不是回滚已发放奖励。

领域模型

聚合根:战斗实体(BattleEntity:entityId、hp、shield、attrs、buffs)。实体:BUFF 实例(BuffInstance:buffId、stacks、expireFrame、casterId、priority、mutexGroup)、结算单(SettleOrder:battleId、rewards、state)。值对象:伤害结果(DamageResult:raw、mitigated、crit、final)、公式版本(FormulaVersion)。不变量:hp ∈ [0, maxHp];同互斥组同时只保留最高优先级实例;结算单按 battleId 唯一且状态单向;发奖必带 bizNo。

解决方案

1. 公式引擎:公式存配置中心(如 damage = base × (1 + atkCoeff) × defenseMitigate × critMul × (1 - resist)),带 formulaVersion;计算是纯函数(输入属性快照 + 公式版本 → 结果),无副作用、可单测、可回放。

2. BUFF 管理器:帧事件队列(该帧到期/触发的事件按类型排序);施加时查互斥组(低优先级被顶掉,发 onDispel 事件);堆叠到上限不再增层只刷新时间;护盾作为独立减伤层先于 hp 结算。

3. 伤害结算管线:原始值 → 防御减免 → 暴击 → 穿透 → 阵营克制 → 护盾吸收 → hp 扣减,每步记录中间值(DamageResult 全字段),既是日志也是举证材料。

4. 结算 Saga:算奖(版本化公式,结果落结算单)→ 调账务服务发奖(bizNo = battleId + rewardId)→ 标记完成;任一步失败进重试队列,超限转人工 + 邮件补发;重复触发按 battleId 幂等跳过。

flowchart LR Hit[技能命中] --> Pipe[伤害管线
原始→防御→暴击→穿透→克制→护盾] Buff[BUFF帧事件] --> Pipe Pipe --> HP[hp扣减
记录中间值] HP --> Death{死亡判定} Death -->|是| Drop[掉落/算奖] Drop --> Saga[结算Saga] Saga --> Calc[算奖
版本化公式] Calc --> Grant[发奖
账务bizNo] Grant --> Done[结算单完成] Grant -->|失败| Retry[重试队列] Retry --> Mail[邮件补发]
伤害结算管线与结算 Saga:每步留痕、可回放、可补偿
关键代码
// 伤害管线:纯函数 + 每步留痕,回放/申诉按同一版本重算
public DamageResult computeDamage(HitCtx ctx, FormulaVersion ver) {
    long raw = formula.base(ctx, ver);                 // 配置化公式,按版本取参
    long afterDef = raw * (DEFENSE_CAP - ctx.defense()) / DEFENSE_CAP; // 防御减免
    long afterCrit = ctx.rng().nextFixed() < ctx.critRate() ? afterDef * ctx.critMul() / 100 : afterDef;
    long afterResist = afterCrit * (100 - ctx.resist()) / 100;
    long byShield = Math.min(ctx.shield(), afterResist);     // 护盾先于 hp
    long toHp = afterResist - byShield;
    return new DamageResult(raw, afterDef, afterCrit, afterResist, byShield, toHp, ver);
}

// BUFF 帧事件:互斥组只留最高优先级,堆叠到顶只刷新时间
public void applyBuff(BattleEntity e, BuffInstance b) {
    for (BuffInstance old : e.buffsOfGroup(b.mutexGroup())) {
        if (old.priority() < b.priority()) { e.removeBuff(old); emit(new BuffDispelled(old, b)); }
        else return;                          // 已有更强同组 BUFF,忽略施加
    }
    BuffInstance cur = e.buffOfId(b.buffId());
    if (cur != null && cur.stacks() >= b.maxStacks()) { e.refreshBuff(cur, b.expireFrame()); return; }
    e.addBuff(b);                                // 进帧事件队列驱动 tick/expire
}
深度追问
  • 策划每周改公式,怎么保证不发版就能生效又不炸历史数据?
    公式全部参数化进配置中心,改公式 = 发布新 formulaVersion(灰度到新对局);历史战绩/回放/申诉带版本号,用旧版本重算解释。禁在代码里写死数值公式——那既是发版瓶颈,也是历史不可解释的根源。
  • 两个事件同帧命中同一目标(破盾同时中毒),顺序影响结果吗?
    影响,所以顺序必须确定:帧内事件按固定类型序执行(控制 → 护盾变化 → 伤害 → 死亡判定 → DOT 跳伤),同类型按事件 id 序。帧同步下所有客户端同一套序函数,结果一致;状态同步下服务端单线程执行,本身就是唯一顺序。关键不是「哪个顺序对」,而是「顺序确定且可复现」。
  • 结算服务发奖发到一半宕机,玩家奖励丢了吗?
    不会。结算单是唯一事实源(battleId 唯一 + 状态机),发奖是幂等的(bizNo 幂等);宕机恢复后按结算单状态重放:未发则继续发,已发则跳过。极端情况(发了但状态没推进)由对账任务补偿(账务流水 vs 结算单核对)。宁可重发不可丢,重发靠幂等兜住。
  • 外挂改了本地伤害显示,服务器怎么发现数值异常?
    帧同步下服务端用轻量规则校验(输入合法性、单次伤害上限、DPS 分布离群);状态同步下服务端本来就是权威值,客户端显示只是镜像。另外统计层做「装备水平 vs 实际输出」回归,长期偏离即进观察名单。本地显示随便改,服务端数值不动。
常见陷阱
  • 公式硬编码在代码:改数值要发版,历史不可解释;参数化 + 版本化。
  • BUFF 无互斥组概念:效果叠成怪物;互斥组 + 优先级。
  • 结算不幂等:重试重复发奖;结算单唯一 + bizNo 幂等。
  • 伤害不留中间值:纠纷无法举证;DamageResult 全字段落库。
破题点拨
一眼定位:战斗结算 = 版本化公式 + 帧事件 BUFF + 留痕伤害管线 + Saga 幂等结算。
破题主线:公式参数化版本化 → BUFF 互斥/堆叠/帧事件 → 伤害管线每步留痕 → 结算单唯一 + 发奖幂等 + 对账补偿。
易错点:公式硬编码;BUFF 无优先级;结算不幂等;伤害不留痕。
D11.06
游戏内社交与聊天:频道消息、必达性与离线补偿高级P1
高频 领域模型 seq 消息 必达性 离线信箱
场景背景

手游内置 IM:世界频道、公会频道、队伍频道、私聊四类会话。硬指标:世界频道单服十万级并发、消息必达(私聊/公会通知不能丢)、离线消息补偿(上线补齐)、内容审核(敏感词 + 图片,合规要求)、防骚扰(刷屏/辱骂处理)。

核心挑战

① 世界频道爆炸:单服十万人,一条消息扇出十万接收,集中活动时消息洪峰,推送侧直接被打死;② 必达性:私聊与公会通知必须到(游戏内社交信任),推送失败要重传,用户离线要进信箱;③ 消息顺序:同一会话内消息不乱序(seq 单调),跨设备同步进度一致;④ 审核实时性:先审后发影响体验、先发后审有合规风险,频道的差异化策略;⑤ 防骚扰:刷屏(高频)、辱骂(内容)、广告(导流),实时识别 + 处置(禁言/封号)。

分析框架

消息模型:会话(频道)维度单调递增 seq,客户端按 seq 对齐与去重;必达 = 发送端 ack + 服务端重传 + 离线信箱。扇出分型:世界频道读扩散(拉模式:客户端拉增量 seq,服务端只存频道消息流)+ 私聊写扩散(推模式:直接投递 + 信箱兜底)。世界频道抗洪峰:消息分片(按 seq 段聚合推送)+ 限速(用户发送频率限制)+ 分级(世界频道可容忍秒级延迟,私聊必须实时)。审核策略分型:世界频道先审后发(合规要求)+ 私聊异步审(先发后审,命中再处置);离线补偿:信箱按 seq 拉取,上限(如最近两百条)+ 关键消息(公会通知/私聊)永久保留。

领域模型

聚合根:会话(Channel,类型 + 成员 + 最新 seq)。实体:消息(Message,会话 + seq + 内容 + 审核态 + 发送者)、信箱(Mailbox,用户 × 会话 × 未读 seq 区间)、处置单(Punishment,类型 + 期限 + 证据)。值对象:消息内容(文本/图片/语音)、审核结果。不变量:同一会话 seq 严格递增不重复;未审消息世界频道不可见;信箱未读 seq 连续(缺段即补拉);处置单必须引用消息证据。

解决方案

1. 消息流:频道消息按 seq 追加写(消息表 + 缓存最近窗口),客户端连接后拉「本地 seq → 频道最新 seq」的增量;长连接推送只做「有新消息」通知,内容客户端自拉(读扩散,服务端零扇出压力)。

2. 必达:私聊/关键通知写扩散直投在线用户;失败进重试(指数退避,有限次);用户离线进信箱(关键消息永久、普通消息最近窗口);上线时按各会话 seq 对齐补拉。

3. 世界频道抗峰:发送侧限速(用户 N 秒一条 + 等级要求);推送侧聚合(同一秒消息合并通知);极端洪峰降级为纯拉模式(推送只发心跳,客户端高频拉取)。

4. 审核:世界频道先审后发(机审敏感词 + 图片机审,分钟级 SLA);私聊/队伍异步审(先发后审 + 抽检);命中处置(禁言/封号)实时生效,处置记录留证可申诉。

5. 防骚扰:发送频率限制 + 内容风控(导流广告模型)+ 举报通道(一键举报 + 证据快照);累犯升级处置。

graph TD A[发送消息] --> B[频率/等级校验] B --> C[频道类型分流] C -->|世界频道| D[先审后发 机审] C -->|私聊/队伍| E[先发 异步审] D -->|通过| F[消息流 seq追加] E --> F F -->|读扩散| G[长连接通知+客户端拉取] F -->|写扩散| H[在线直投] H -->|失败| I[重试+信箱] G --> J[seq对齐补拉] I --> J K[命中违规] --> L[禁言/封号 留证]
频道消息必达与审核分流
关键代码
// seq 对齐补拉:客户端带本地 seq,服务端返回增量;缺段即补
public List<Message> sync(String channelId, long localSeq) {
    long latest = channel.latestSeq(channelId);
    List<Message> inc = msgStore.range(channelId, localSeq + 1, latest);
    if (inc.isEmpty() && latest > localSeq) throw new GapException(localSeq);  // 缺段:触发补拉
    return inc;
}

// 私聊必达:在线直投,失败重试有限次,仍失败进信箱
for (int i = 0; i < 3; i++) {
    if (conn.deliver(toUserId, msg).success()) return;
    Thread.sleep(backoff(i));
}
mailbox.save(toUserId, msg, Retention.FOREVER);   // 关键消息永久保留

// 世界频道抗峰:发送限速 + 推送聚合,洪峰降级纯拉
if (!rateLimit.tryAcquire(senderId, Duration.ofSeconds(3))) return;
if (worldFlood.overload()) push.silent();            // 洪峰:只发心跳,客户端高频拉
深度追问
  • 世界频道十万人,怎么不把推送打爆?
    核心是读扩散:服务端只维护频道消息流,推送只发「有新消息」轻通知,内容客户端自己拉(增量按 seq);再叠加发送限速与洪峰降级(纯拉模式)。写扩散在世界频道是自杀——十万扇出任何推送通道都扛不住。
  • 私聊怎么保证必达?
    三层:在线长连接直投(ack 确认)→ 失败有限次重试(指数退避)→ 仍失败进信箱(关键消息永久保留);客户端上线按 seq 对齐补拉。必达不是单次投递成功,是「最终可拉取」——信箱是最后兜底。
  • 先审后发还是先发后审?
    分频道:世界频道公开可见、合规要求高,先审后发(机审分钟级,接受小延迟);私聊/队伍私密性强,先发后审 + 抽检 + 命中处置。两类都不能裸奔——公开频道裸奔是监管风险,私聊全审是体验灾难。
  • 消息乱序怎么处理?
    会话维度 seq 单调递增(生成消息时原子分配),客户端按 seq 排序 + 去重;补拉按 seq 区间,缺段再拉。绝对时间戳不可靠(多机时钟偏差),seq 是唯一排序依据;跨设备同步也按 seq 对齐,进度一致。
常见陷阱
  • 世界频道写扩散:一条消息扇出十万,洪峰直接打爆推送;读扩散 + 聚合通知。
  • 私聊无信箱:离线消息只推一次,丢了就丢了;信箱是必达底线。
  • 用时间戳排序:多机时钟偏差消息乱序;seq 单调递增才是正解。
  • 世界频道裸奔:不审核直接广播,违规内容即平台事故;先审后发分频道策略。
破题点拨
一眼定位:游戏 IM = seq 消息流 + 世界频道读扩散/私聊写扩散 + 信箱兜底必达 + 分频道审核。
破题主线:seq 排序 → 世界拉模式抗峰 → 私聊直投+重试+信箱 → 审核分频道 → 防骚扰处置留证。
易错点:世界写扩散;无信箱;时间戳排序;世界频道不审。

十二 强合规行业 6 题

本组知识链

强合规领域的主线是「合规是设计输入,不是上线补丁」。覆盖医疗、教育、政务场景的集成范式、国产化适配、审计追溯与一网通办。

  1. 行业集成:医疗 HL7/FHIR 对接、教育音视频与互动。
  2. 数据合规:分类分级、跨境传输、删除权实现。
  3. 信创适配:国产化栈、国密算法、等保密评。
  4. 审计追溯:不可篡改审计、数据血缘、监管报送。
  5. 一网通办:共享目录分级、授权链网关、流程编排、隐私计算不出域。
工程化要点
  • 合规需求前置到架构评审:数据分级、保留期、法源在表设计阶段就要有字段。
  • 集成靠标准不靠私有对接:HL7/FHIR/国标报文进集成引擎,点对点私有协议是技术债。
  • 国密改造分层做:传输层国密协商、存储层字段级加密、签名验签全链路。
  • 审计数据只写不改:WORM 存储 + 哈希链,删除只能到期归档。
本组思维模式
  • 合规是设计输入:上线前补合规 = 重构;需求阶段没有分级与法源字段,后面补不回来。
  • 患者/学生/公民数据默认敏感:最小必要、调阅留痕、脱敏展示是默认路径。
  • 审计不可篡改是底线:能改的日志不是审计,是记事本。
  • 兼容性靠适配层:国产化栈差异用抽象层吸收,业务代码不感知芯片与数据库方言。
  • 文档即交付物:等保/密评材料与系统同周期建设,不能事后编。
合规与资损红线
  • 患者病历属敏感个人信息:调阅授权、留痕、脱敏展示,泄露即重大事故。
  • 互联网处方必须医师实名签名 + 审方,禁 AI 自动开方,处方流转全程可追溯。
  • 未成年人信息与学习数据有专门保护要求,收集需监护人同意。
  • 密钥管理合规:国密算法 + 密钥分级存储,禁硬编码密钥与私自外传。
D12.01
医疗健康:HIS 集成、电子病历与隐私合规高级P1
领域模型 HL7/FHIR 电子病历 EMPI 隐私合规
场景背景

医院核心系统群:HIS(挂号/收费/药品)、EMR(电子病历)、LIS(检验)、PACS(影像)、手麻/重症,各自厂商不同、年代不同、接口标准不同。患者就医动线:挂号 → 就诊 → 开医嘱 → 检验检查 → 报告回传 → 处方 → 取药/住院。互联网医院还要支持在线问诊、电子处方流转到院外药房。影像数据动辄 PB 级,病历修改必须留痕,患者隐私调阅要授权。系统可用性要求极高——诊疗活动不能停。上述是观察口径,具体阈值须按本业务标定,禁止把本卡文字直接当成门禁数字。

核心挑战

① 异构集成:HL7 v2 管道消息、CDA 文档、私有 Socket 协议并存,点对点对接变成蜘蛛网;② 术语标准化:诊断要 ICD-10、检验项要 LOINC,各家字典不一致;③ 患者主索引:同一人多渠道建档重复,合并要保历史;④ 病历隐私:谁能看什么、调阅留痕、修改不可覆盖;⑤ 互联网处方合规:医师实名签名、审方规则、处方不得重复使用。

分析框架

集成走集成引擎(ESB/医疗集成平台,如 Mirth):各厂商系统只与引擎对接,引擎负责协议适配(HL7 v2 ↔ FHIR ↔ 私有报文)、格式转换、路由与重试,把点对点蜘蛛网收敛为星型。患者身份用 EMPI(患者主索引):多源建档记录按身份证/手机号/姓名生日模糊匹配合并成全局 PID,历史数据通过别名映射保留。病历用结构化 + 文档双模:结构化字段(诊断/医嘱/结果)支撑统计与质控,文档(CDA/PDF)保原文;病历修改走「留痕模式」(新版本 + 变更记录,禁物理覆盖)。隐私靠授权 + 留痕 + 脱敏:调阅需角色授权 + 患者授权(科研/外院),每次访问写审计;展示层按角色脱敏(手机号/身份证掩码)。互联网医院处方走「医师 CA 实名签名 + 药师审方 + 处方单次有效 + 流转签收」,全程可追溯。

领域模型

聚合根:就诊(Encounter:encounterId、patientId、dept、state、orders)。实体:医嘱(Order:orderId、type、state、execRecords)、报告(Report:reportId、type、content、version)、患者档案(Patient:pid、identities、consents)。值对象:诊断编码(ICD-10 组合)、检验编码(LOINC)、授权凭据(Consent:scope、grantor、expireAt)。不变量:病历任何修改产生新版本且保留变更链;调阅操作必留审计事件;处方未经审方不可发药;就诊状态按门诊/住院动线合法推进。

解决方案

1. 集成引擎:接入适配器按协议分(HL7 v2 MLLP、FHIR REST、私有 Socket、数据库直采——最后一种尽量禁);消息标准化为内部 FHIR 模型再路由;失败重试 + 死信队列 + 消息轨迹查询(哪个报文在哪个环节丢了,运维第一问)。

2. EMPI:建档统一入口;匹配合并(确定匹配 + 概率匹配,冲突进人工队列);合并保留全部历史标识(alias),对外只用全局 PID;跨院调阅按 PID + 授权。

3. 病历服务:结构化模板(专科病历 schema)+ 文档存储;时间轴视图(就诊历次、检验趋势图);修改 = 新版本 + 变更原因 + 操作人;质控规则(必填项、合理用药提醒)在保存时校验。

4. 互联网医院:实名(医师/患者人脸 + 身份证);问诊会话(图文/视频,录像存档);处方开具(电子病历 + CA 签名)→ 审方(合理用药规则引擎)→ 流转药房(签收核验)→ 配送;禁重复开方与修改已审处方。

flowchart LR HIS[HIS厂商A] --> IE[集成引擎
协议适配+转换] LIS[LIS厂商B] --> IE PACS[PACS厂商C] --> IE IE --> FHIR[内部FHIR模型] FHIR --> EMR[电子病历服务] FHIR --> EMPI[患者主索引] FHIR --> Order[医嘱中心] Order --> EXEC[执行系统] EXEC --> Report[报告回传] EMR --> Audit[调阅审计] Net[互联网医院] --> CA[CA签名处方] CA --> Review[药师审方] Review --> Pharma[院外药房]
医疗集成星型架构:厂商只对接引擎,身份/病历/处方各自成服务
关键代码
// 病历修改留痕:新版本 + 变更链,禁物理覆盖
public ReportVersion revise(ReportId id, String content, String reason, Operator op) {
    Report r = reportRepo.find(id);
    int next = r.currentVersion() + 1;
    reportRepo.insertVersion(new ReportVersion(id, next, content, reason, op.id(), now()));
    audit.record(new AuditEvent(op, "REPORT_REVISE", id, "v" + next));
    return reportRepo.findVersion(id, next);     // 历史版本只读,任何角色不可删改
}

// 调阅授权:先查授权再查数据,授权过期即拒
public boolean canView(Operator viewer, PatientId pid, Scope scope) {
    Consent c = consentRepo.findActive(pid, viewer.id(), scope);
    if (c == null || c.expireAt().isBefore(now())) return false;
    audit.record(new AuditEvent(viewer, "RECORD_VIEW", pid, scope));
    return true;                                 // 每次调阅(含拒绝)都留痕
}
深度追问
  • 影像数据 PB 级,存储与调阅怎么设计?
    分层存储:近线(三个月内,SSD,秒级调阅)+ 离线(对象存储/磁带库,分钟级取回);影像按 study 分片 + 预取(关联检查一起拉);调阅走 PACS 专用网关(DICOMweb),不经过业务数据库。成本与体验的平衡点是「分层 + 按检查预取」。
  • 医院要求停机窗口极短,系统怎么发版?
    核心诊疗链路(挂号/收费/医嘱)双活 + 本地缓存兜底(数据库短暂不可用时窗口业务照常);非核心(报表/统计)随时发。发版走「蓝绿 + 数据库前向兼容(先加字段后改代码)」,回滚脚本预演。原则是「诊疗不能停,报表可以等」。
  • 跨院调阅患者历史病历,合规路径是什么?
    区域平台级授权(患者签约「同意跨院调阅」→ 平台签发短期令牌 → 调阅记录双方留痕);未授权时只能看到「摘要卡片」(过敏史/近期诊断,急救场景法定豁免并事后补录)。跨院靠区域平台,不靠两院私接——私接既不合规也难审计。
  • 老系统只有私有 Socket 接口,怎么收敛?
    在集成引擎侧写协议适配器(老系统零改造),把私有报文翻译成内部 FHIR;同时推动厂商提供标准接口(写进续约合同)。短期适配器是务实,长期标准化是目标——但不要在业务系统里直接写私有协议解析。
常见陷阱
  • 点对点私有对接:系统变蜘蛛网;全部收敛到集成引擎星型。
  • 病历可物理覆盖:违规且丢证据;版本链 + 留痕。
  • 调阅无审计:泄露无法追溯;每次访问(含失败)落审计。
  • 处方无签名流转:合规事故;CA 签名 + 审方 + 签收。
破题点拨
一眼定位:医疗架构 = 集成引擎星型 + EMPI 主索引 + 病历版本链 + 授权调阅 + 签名处方。
破题主线:异构协议收敛引擎 → FHIR 内部模型 → EMPI 合并身份 → 病历留痕 → 调阅授权审计 → 处方 CA 全链。
易错点:点对点对接;病历覆盖;调阅无审计;处方无签名。
D12.02
在线教育:课堂互动、作业系统与音视频架构高级P1
领域模型 RTC 连麦 互动白板 大班直播 作业批改
场景背景

在线教育三种课型并存:大班直播课(一名讲师对上万学生,单向为主 + 弹幕答题)、小班互动课(一到六人,全程连麦 + 白板互动 + 上台)、录播课(随时看 + 作业)。附加系统:互动答题器(全班答题秒级出统计)、红包/激励、作业提交与批改(主观题老师批、客观题自动判)、课后回放与学习报告。硬指标:大班成本要低(不能人人 RTC)、互动延迟要低(连麦两百毫秒内)、进场高峰(整点开课)要扛住。

核心挑战

① 成本与体验分层:上万人全走 RTC 带宽成本爆炸;② 白板信令同步:笔画/教具操作要全员一致且低延迟;③ 答题瞬时峰值:一道题下发瞬间万人上报;④ 录制与回放:音视频 + 白板操作 + 答题要按时间轴对齐;⑤ 弱网体验:学生端网络差,卡顿即投诉。

分析框架

核心决策是大小流分离:大班用 CDN 直播(讲师推流 → CDN 分发,学生拉流,成本与规模兼得),互动环节只把「上台的少数人」切 RTC(连麦上行 + 混流后进直播流),万人同时看的是混流后的直播。信令与媒体分离:房间信令服务管白板操作、上下台、答题指令、聊天,走 WebSocket 房间广播;音视频走 RTC 媒体通道;答题/统计走状态服务(允许秒级延迟)。白板操作同步走「操作流(op log)+ 版本向量」:每条笔画是带序号的操作,后到者按序补放,冲突以教师端为准(教师权威)。录制走旁路:直播流推录制服务 + 信令操作流按时间戳落库,回放时音视频按进度条驱动信令重演,天然对齐。

领域模型

聚合根:课堂(Classroom:classId、courseId、mode、state、members)。实体:白板文档(BoardDoc:pages、ops、version)、课节(Lesson:startAt、state、recordingRef)、作业(Assignment:items、submissions)。值对象:白板操作(BoardOp:seq、type、payload、ts)、课堂时刻表(Schedule)。不变量:课堂状态按(预告 → 直播中 → 已结束)推进;白板 op seq 在文档内单调递增;上台人数不超连麦位上限;回放必须有完整录制引用。

解决方案

1. 大班直播:讲师端推流(RTMP)→ CDN;学生拉流按网络自适应(多码率 + 秒切);弹幕/答题走独立信令通道,不混媒体流。

2. 小班互动:全员 RTC(SFU 架构,每人发一路流);上台/下台是信令指令 + 媒体订阅关系变更;弱网学生降级为「只看不听 + 文字互动」。

3. 白板:教师端产生 op(笔画/翻页/教具)→ 信令服带 seq 广播 → 学生端按序重演;seq 缺口缓存等待(两百毫秒内必达),仍缺则触发全量快照同步;教师端拥有绝对权威(冲突时学生本地回滚到教师状态)。

4. 答题与作业:答题下发(信令)→ 学生上报(状态服聚合,按班级分区)→ 统计结果广播(延迟可接受秒级);作业客观题自动判(答案 + 步骤匹配),主观题老师端批改(截图/音频批注),成绩进学习报告。

5. 录制回放:直播流旁路推录制服务(mp4 分片);信令操作流按 ts 落库;回放 = 播放器进度驱动信令重演 + 视频 seek 对齐;分片上传容错(断点续传 + 合并校验)。

flowchart LR T[教师端] -->|RTMP推流| CDN[CDN分发] T -->|op| Sig[房间信令服] Sig --> S1[学生端信令] CDN --> S1 T -->|上台| RTC[SFU连麦] S1 -->|连麦上行| RTC RTC --> Mix[混流] Mix --> CDN Quiz[答题下发] --> Sig S1 -->|上报| Stat[状态服务] Stat -->|统计广播| S1 CDN --> Rec[录制服务] Sig --> Ops[信令操作流] Rec --> VOD[回放: 视频+信谓重演] Ops --> VOD
大小流分离:CDN 扛规模、RTC 保互动、信令与媒体解耦
关键代码
// 白板操作同步:seq 保序 + 缺口等待 + 超时快照对齐
public void onBoardOp(BoardOp op) {
    if (op.seq() <= lastAppliedSeq) return;            // 乱序/重放丢弃
    pending.put(op.seq(), op);
    while (pending.containsKey(lastAppliedSeq + 1)) {
        apply(pending.remove(++lastAppliedSeq));              // 连续才推进
    }
    if (pending.size() > 0 && !gapTimer.running()) {
        gapTimer.start(300, () -> {                      // 三百毫秒仍缺 → 拉教师端快照对齐
            boardClient.requestSnapshot(docId, lastAppliedSeq);
            pending.clear();
        });
    }
}

// 答题聚合:按班级分区缓冲,窗口结算(秒级延迟可接受)
public void onAnswer(AnswerMsg m) {
    if (!dedup.tryAdd(m.quizId() + ":" + m.studentId())) return; // 一人一答
    agg.merge(m.quizId(), m.choice());                        // 本地聚合
    if (agg.shouldFlush(m.quizId())) {                       // 每五百毫秒或满阈值
        statService.publish(m.quizId(), agg.snapshot(m.quizId()));
    }
}
深度追问
  • 整点开课瞬间百万人进场,系统怎么扛?
    进场削峰:客户端按 hash 错峰拉流(避免同时打 CDN 节点);信令连接按课堂分桶(单房间信令服有上限,爆满走多副本 + 负载均衡);答题下发预创建聚合分片。压测按「最大课型 × 整点曲线」建模,提前扩容到预测水位。
  • 学生网络差连麦卡成电音,怎么处理?
    分层降级:RTC 探测到弱网 → 先降码率/分辨率 → 仍差则关闭摄像头保音频 → 极端情况退回 CDN 观看(只听不说);教师端看到每个学生的网络质量徽标,可引导其关闭视频。体验管理的核心是「有明确降级路径」,不是硬扛。
  • 白板操作冲突(师生同时画)以谁为准?
    教师权威 + 操作日志:所有 op 带 seq,教师端是 seq 的最终裁决者;学生本地冲突时回滚到教师快照再重放。教学场景不需要协同编辑的复杂合并,需要的是「永不分裂的确定性」。
  • 回放里视频与白板不同步,怎么排查?
    时间轴对齐靠「录制时共同时间源」:录制启动时由信令服下发 recordSessionId + 起始时间戳,视频分片与 op 流都以该时钟打标;回放以视频当前时间为驱动查 op。出现漂移先查打标逻辑,再做播放器侧微调(上限两秒的偏移校正)。
常见陷阱
  • 大班全员 RTC:带宽成本爆炸;CDN 直播 + 少数人连麦。
  • 白板全量状态同步:笔画一多流量炸;op 流 + 序号重演。
  • 信令媒体混一通道:卡顿互相拖累;信令 WebSocket 与媒体 RTC 分离。
  • 录制只录视频:回放丢互动;旁路录制 + 信令流对齐。
破题点拨
一眼定位:在线教育 = 大小流分离(CDN + RTC)+ 信令媒体解耦 + 白板 op 同步 + 旁路录制对齐。
破题主线:大班 CDN → 上台切 RTC → 信令管白板/答题 → op 保序重演 → 录制双流打标 → 回放驱动重演。
易错点:全员 RTC;白板全量同步;信令媒体混通道;录制丢信令。
D12.03
信创与国产化:全栈适配、等保与密评高级P1
领域模型 国产化栈 国密算法 等保密评
场景背景

央国企/政务/金融项目普遍要求信创替代:芯片(鲲鹏/飞腾 ARM 架构)、操作系统(麒麟/统信)、数据库(达梦/人大金仓/ OceanBase 国产版)、中间件(东方通/金蝶天燕)、JDK(毕昇/OpenHarmony 方向)、办公与浏览器。同时要过等保(二/三/四级)与密评(商用密码应用安全性评估):数据传输、存储、签名验签要用国密算法(SM2/SM3/SM4)。现实是既有系统多年迭代,深度依赖 x86 特性、Oracle/MySQL 方言、开源组件生态。

核心挑战

① 兼容性黑洞:JDBC 方言、native 库(JNI/压缩/PDF/OCR)、系统调用、路径与编码差异;② 性能差异:ARM 与 x86 指令集差异导致加密/压缩密集型模块降频明显;③ 国密改造范围:哪些通道必须国密、密钥怎么管、与现有 TLS/HTTPS 关系;④ 合规材料:等保测评、密评、测试报告要成体系且与系统一致;⑤ 双轨并行:替换不能停机,新旧栈要能共存灰度。

分析框架

适配的核心是分层抽象 + 适配层:数据库访问走「方言适配层」(屏蔽达梦/MySQL 语法差异,禁裸写方言 SQL)、文件与编码走平台适配(路径分隔、默认编码、行尾符)、native 依赖做能力矩阵盘点(有国产版的替换,无的封装隔离 + 降级)。国密改造分三层:传输层(网关支持国密 TLS 双证书,兼容国际 TLS 过渡)、存储层(敏感字段 SM4 加密 + SM3 完整性校验)、签名层(关键操作 SM2 签名 + 验签,配合 CA)。密钥管理走专用 KMS/密码机,分级(根密钥/工作密钥/会话密钥),禁硬编码。合规策略是「设计输入」:需求阶段出《数据安全/等级保护/密评方案》,开发同步做「合规检查单」自动化(密钥扫描、明文传输检测、弱算法检测),验收材料随版本产出,不事后编。

领域模型

聚合根:部署环境配置(EnvProfile:arch、os、db、middleware、jdk、cipherSuite)。实体:适配器(Adapter:capability、targetEnv、status)、密钥(CryptoKey:level、algorithm、rotateAt、kmsRef)。值对象:密码套件(CipherSuite:SM2/SM3/SM4 组合)、合规等级(ComplianceLevel)。不变量:环境与适配器必须匹配(不支持的能力禁启用);密钥不出 KMS(业务只持引用);合规等级与数据分级联动(高敏数据必须高套件)。

解决方案

1. 适配层:数据访问用 MyBatis 方言插件(同 SQL 多 Dialect 渲染);native 依赖盘点成「能力矩阵」(组件 × 国产栈支持度),分「直接可用/需适配/需替换/阻塞」四类,阻塞项早暴露;CI 增加国产化栈 nightly 构建(真实环境跑测试套件)。

2. 国密改造:接入层 Nginx 国密网关(SM2 双证书 + 协商);业务层字段级加密(SM4-GCM,密钥走 KMS 按数据分级派生);签名(SM2 + SM3 摘要)覆盖「处方/审批/支付类」关键操作;国密与国际算法并存期由 cipherSuite 配置驱动,按客户端能力协商。

3. 性能调优:ARM 上 SM4/SM3 用国密指令加速库;压缩/序列化换国产栈友好实现;JVM 参数按 ARM 重新调校(对齐毕昇 JDK 调优指南);压测对比基线(x86 原系统 vs 国产化新系统),差异超阈值触发优化。

4. 合规自动化:CI 门禁扫密钥硬编码、明文传输、弱算法(MD5/DES/1024 位 RSA);依赖 SBOM 自动生成(许可证 + 漏洞);等保/密评证据(架构图、数据流图、测试报告)随发版归档,版本可追溯。

flowchart LR C[客户端] -->|国密TLS| GW[国密网关
SM2双证书] GW --> App[业务应用
毕昇JDK] App --> DA[方言适配层] DA --> DM[达梦/金仓] App --> KMS[密码机/KMS] KMS -->|派生密钥| Field[字段级SM4] Field --> DM App --> Sign[SM2签名
关键操作] App --> MW[东方通中间件] OS[麒麟OS] --> App CI[CI门禁
密钥/弱算法扫描] --> Repo[合规证据库
SBOM+测试报告]
信创全栈与国密改造:适配层吸收差异,密码分层管理
关键代码
// 字段级国密加密:密钥引用不出 KMS,按数据分级选套件
public final class SmFieldCrypto {
    private final KmsClient kms;

    public String encrypt(String plain, DataLevel level) {
        SecretKeyRef ref = kms.deriveKey("field-crypto:" + level, Cipher.SM4_GCM);
        byte[] iv = SecureRandom.nextBytes(12);        // GCM 随机 nonce
        byte[] ct = sm4Gcm.encrypt(kms.use(ref), iv, plain.getBytes(UTF_8));
        return "v1:" + ref.id() + ":" + Base64.encode(iv) + ":" + Base64.encode(ct);
    }                                                // 密文带密钥引用,支持轮换后旧数据可解

    public String sign(String payload) {                // SM2 签名 + SM3 摘要
        byte[] digest = sm3.digest(payload.getBytes(UTF_8));
        return Base64.encode(sm2.sign(kms.signKey("biz-sign"), digest));
    }
}

// 方言适配:同 SQL 多 Dialect 渲染,禁裸写方言
public String pageSql(String base, Dialect d) {
    return switch (d) {
        case DAMENG      -> base + " LIMIT :offset, :size";      // 达梦兼容 MySQL 分页语法
        case KINGBASE    -> base + " LIMIT :size OFFSET :offset";
        case MYSQL       -> base + " LIMIT :offset, :size";
    };
}
深度追问
  • 某个依赖(如 PDF 引擎)没有国产栈版本,怎么办?
    四条路按序尝试:找国产替代(福昕/数科)→ 依赖纯 Java 实现替掉 native → 封装成独立服务(x86 小节点 + 国产主系统调服务,过渡方案)→ 阻塞项升级推动开源社区适配。切忌把 native 调用散落在业务代码里——封装隔离是底线,阻塞早暴露是责任。
  • ARM 上加密吞吐下降明显,如何优化?
    用带国密指令加速的库(鲲鹏有 SM 系列加速指令,吞吐可提升数倍);批量合并加密(一次处理多字段省调用开销);非敏感字段不加密(按分级,避免全表加密);对称加密放独立线程池避免阻塞 IO。优化前先压测定位(算法库/调用方式/线程模型哪个是瓶颈)。
  • 国密改造后老客户端(只支持国际 TLS)连不上?
    双证书协商:网关同时挂国际 RSA 证书与国密 SM2 证书,按客户端 ClientHello 能力协商选套件;过渡期两者并存,明确下线时间表后强制国密。密评要求的是「商密合规可用」,不是「一刀切禁用国际算法导致业务中断」。
  • 等保测评前三个月才准备材料,来得及吗?
    来不及是常态——材料要与系统同周期:需求阶段出定级备案与方案,开发同步记录(架构图、数据流、接口清单),测试阶段出漏洞扫描与渗透报告。CI 把「密钥硬编码、明文传输、弱算法」扫成门禁,等保的技术测查项就自动满足大半。合规是流水线产物,不是考前突击。
常见陷阱
  • 裸写数据库方言:换库即重写;方言适配层统一渲染。
  • native 调用散落业务:无法替换;封装隔离 + 能力矩阵。
  • 密钥硬编码/配置明文:合规一票否决;KMS 引用 + CI 扫描。
  • 合规事后补材料:系统与材料两张皮;随版本产出 + 自动归档。
破题点拨
一眼定位:信创 = 适配层吸收栈差异 + 国密三层改造 + 密钥 KMS 化 + 合规随版本产出。
破题主线:能力矩阵盘点 → 方言/native 适配层 → 传输/存储/签名三层国密 → 性能压测调优 → CI 合规门禁 + 证据库。
易错点:裸写方言;native 散落;密钥硬编码;合规事后补。
D12.04
数据合规通用范式:分类分级、跨境传输与删除权架构P1
领域模型 架构 分类分级 跨境传输 删除权
场景背景

个保法/GDPR 类合规要求落到工程:个人数据分类分级(公开/内部/敏感/高危)、最小必要采集、处理活动有法源(同意/合同/法定义务)、跨境传输合规(安全评估/标准合同/认证)、用户权利(查阅/复制/更正/删除/可携带)、PIA 评估、保留期到期清除。现实是数据散落在数十个库表、数仓、日志、缓存、报表、备份与第三方,一条「删除某用户」的指令要触达全链路,而备份里的数据不能物理删。

核心挑战

① 删除权的技术不可能:备份不可改、日志要留、衍生数据(画像/统计)已聚合;② 跨境合规路径:多地部署 + 云服务,数据出境要评估与合同,工程上要能路由与隔离;③ 同意的采集与撤回:多端入口、多渠道授权,撤回要能生效到处理链路;④ 数据地图缺失:不知道数据在哪,一切合规是空话;⑤ 匿名化边界:假名化仍属个人数据,真匿名化标准苛刻。

分析框架

合规落地的地基是数据地图:自动扫描(库表/字段打标 + ETL 血缘解析)+ 人工标注,形成「数据资产 → 分级 → 处理活动 → 保留期 → 法源」台账,后续所有合规能力(删除、跨境、审计)都建在它上面。删除权的工程解是「分层删除」:在线库物理/逻辑删除 → 数仓与衍生数据走「去标识化重算或隔离」→ 备份标记到期(保留期一到随备份轮换自然消失)→ 日志按保留期策略脱敏;对外承诺「合理期限内完成全链路不可达」而非「物理擦除每一份副本」。跨境走「合规网关」:数据分级 + 目的地策略决定路由(境内数据不出境、出境走评估通过的通道),PIA 与标准合同作为上线门禁。同意由同意管理平台(CMP)统一:授权/撤回全链路留痕,处理系统消费「同意状态」决定能否处理。匿名化只对「真无法还原」的数据宣称,其余按假名化继续保护。

领域模型

聚合根:数据主体(DataSubject:subjectId、identities、consents、requests)。实体:处理活动记录(ProcessingActivity:purpose、legalBasis、retention、systems)、数据资产(DataAsset:location、level、owners)、权利请求(RightsRequest:type、state、deadline)。值对象:数据分级标签(DataLevel)、法源(LegalBasis:consent/contract/legalDuty)、保留策略(RetentionPolicy)。不变量:每个处理活动必须有法源与保留期;权利请求在法定期限内完成并留痕;高敏数据禁止路由到未评估目的地;撤回同意后相关处理必须停止(已发生的按保留期处理)。

解决方案

1. 数据地图:扫描器按 schema + 样本识别敏感字段(身份证/手机号模式 + 分类模型),产出资产清单;ETL 血缘解析(SQL 解析 + 任务 DAG)形成字段级流向;运营后台维护分级与保留期。地图是所有合规能力的索引。

2. 删除编排:删除引擎按 subjectId 查地图 → 生成任务链(在线库删除 → 数仓隔离 → 缓存清除 → 文件删除 → 备份标记)→ 各系统执行回报 → 完成证明存档;备份采用「标记 + 到期轮换」,不物理改备份。重复请求按 requestId 幂等。

3. 跨境网关:数据处理路由带「目的地标签」;跨境调用先过合规网关(查资产分级 + 目的地评估状态 + 合同有效性),不合规直接拒绝并告警;多 region 部署按数据驻留要求隔离。

4. 同意中心:同意采集(多端 SDK 上报,带版本与文案哈希)→ 存储(含撤回记录)→ 分发(业务系统订阅同意状态,营销类处理必须先查同意);撤回即时生效到推送/营销链路,数据类处理按保留期收尾。

flowchart LR Scan[扫描器
字段识别] --> Map[数据地图
分级+血缘] Map --> Del[删除编排引擎] Del --> Online[在线库删除] Del --> DW[数仓隔离] Del --> Cache[缓存清除] Del --> Backup[备份标记到期] Map --> XBorder[跨境合规网关] XBorder -->|不合规| Reject[拒绝+告警] XBorder -->|合规| Route[目的地路由] User[用户请求] --> CMP[同意中心] CMP --> Mkt[营销链路
先查同意] Scan --> Audit[合规证据] Del --> Audit
数据合规闭环:地图为索引,删除/跨境/同意三大能力
关键代码
// 删除编排:地图驱动任务链,每步回报 + 幂等
public DeletionProof executeErase(String subjectId, String requestId) {
    if (proofRepo.exists(requestId)) return proofRepo.find(requestId); // 幂等
    List<DataAsset> assets = dataMap.assetsOf(subjectId);      // 全链路触达点
    List<TaskResult> results = new ArrayList<>();
    for (DataAsset a : assets) {
        results.add(switch (a.kind()) {
            case ONLINE_TABLE -> onlineStore.erase(a, subjectId);   // 物理/逻辑删
            case WAREHOUSE    -> warehouse.isolate(a, subjectId); // 去标识化/隔离
            case OBJECT_FILE  -> objectStore.delete(a, subjectId);
            case BACKUP       -> backup.markExpire(a, subjectId); // 到期随轮换消失
        });
    }
    DeletionProof proof = new DeletionProof(requestId, subjectId, results, now());
    proofRepo.save(proof);                                // 完成证明存档(举证材料)
    return proof;
}

// 营销处理前置同意校验:撤回即时生效
public boolean mayProcess(String subjectId, Purpose p) {
    Consent c = cmp.latestConsent(subjectId, p);
    return c != null && c.granted() && c.version() == p.requiredConsentVersion();
}
深度追问
  • 备份里的个人数据怎么满足删除权?
    两条务实路径:备份「标记 + 缩短保留期」(删除请求进备份元数据,备份到期即物理销毁,恢复演练时也跳过被标记主体);或「加密隔离」(备份加密,删除 = 销毁该主体的数据加密密钥,密文不可读即等效删除)。对监管与用户的承诺写「合理期限内不可达」,技术上用标记 + 轮换兜底,别承诺物理擦除每一份副本。
  • 跨境场景多云部署,怎么保证数据不出境?
    三层:网络层(region 隔离 + 跨境链路审批)、应用层(数据路由带目的地标签,服务调用过合规网关校验)、数据层(资产分级与目的地绑定,出境资产走专用加密通道 + 合同备案)。再配「出境检测」巡检(扫描跨 region 数据流),发现即阻断告警。合规是工程约束,不是文档承诺。
  • 统计报表里的聚合数据还受删除权约束吗?
    看可识别性:真聚合(无法定位到个人,如分布统计)不受约束;带用户 ID 的明细报表、画像标签属衍生个人数据,删除请求要覆盖(隔离或重算)。工程上给数仓加「主体维度删除」能力(按 subjectId 分区清除),报表重建走批处理。关键在数据地图阶段就把衍生数据登记在册。
  • 撤回同意后,历史日志里的同意记录还能用吗?
    同意记录本身是合规证据(证明何时授权/撤回),按审计类数据保留,不属营销可用数据;撤回只影响「后续处理」,历史已合法发生的处理(如已送达的邮件)不追溯删除。日志里其他个人数据按保留期与脱敏策略走。区分「证据数据」与「处理数据」是关键。
常见陷阱
  • 删除只删主库:数仓/日志/第三方仍在;地图驱动的全链路编排。
  • 无数据地图:合规靠人肉盘点;扫描器 + 血缘自动生成。
  • 同意无版本管理:文案变了无法举证;同意记录带版本与文案哈希。
  • 把假名化当匿名化:仍属个人数据;宣称匿名须证明不可还原。
破题点拨
一眼定位:数据合规 = 数据地图为索引 + 删除编排 + 跨境网关 + 同意中心 + 保留期引擎。
破题主线:扫描建地图 → 分级/法源/保留期台账 → 删除全链编排(备份标记到期)→ 跨境网关路由 → 同意版本化分发。
易错点:只删主库;无地图;同意无版本;假名化当匿名化。
D12.05
审计与追溯:操作审计、数据血缘与监管报送高级P2
领域模型 操作审计 数据血缘 合规报送 哈希链
场景背景

强合规行业的运转依赖「可证明」:安全事件要能回答「谁、在何时、通过哪台机器、改了什么、结果如何」;数据问题要能回答「这张报表的每个字段从哪来、经过哪些加工」;监管要按期报送(交易报告、安全事件、经营数据),且报送数据要与源系统一致可验。审计数据本身要防篡改——运维 DBA 能改的日志没有证据价值。同时审计量巨大(每次操作一条),存储与查询都是挑战。

核心挑战

① 审计不可篡改:高权限账号(DBA/运维)理论上能改任何库;② 采集覆盖面:应用层、网关层、数据库层、运维通道(堡垒机)都要有,漏一层即盲区;③ 字段级血缘:数仓 ETL 链路长,口径变化要能追到源字段;④ 报送一致性:报送口径与源系统口径不一致就是监管风险;⑤ 成本与时效:审计数据只增不减,留存期长达数年,查询要秒级。

分析框架

审计防篡改靠只写存储 + 哈希链:审计事件写入 WORM(对象存储合规保留)/ 追加式存储,每条事件带 prevHash + 自身内容哈希形成链条,任何历史修改都会断链;校验服务定期重算验证,异常即告警。采集分四层互补:应用层(AOP 记录业务操作,字段级前后值)、网关层(访问日志、鉴权结果)、数据库层(审计插件/日志,防绕过应用直接改库)、运维层(堡垒机录屏 + 命令审计)。血缘靠自动解析 + DAG:SQL 解析(表级/字段级依赖)+ 调度任务 DAG + 人工标注补充,形成「源表 → ETL → 报表字段」的可查图谱;口径变更走血缘影响分析(改一个字段会影响哪些报表)。报送靠口径 SSOT:指标定义(口径、公式、来源表)集中管理,报送报表按模板自动生成,数字与源系统对账一致;报送记录留档(含版本与校验值),支持重放举证。

领域模型

聚合根:审计事件(AuditEvent:eventId、actor、action、target、result、prevHash、hash、ts)。实体:血缘边(LineageEdge:upstream、downstream、transform)、报送任务(ReportTask:template、period、state、checksum)。值对象:操作者(Actor:userId、role、sourceIp、channel)、留存策略(RetentionPolicy:years、worm)。不变量:hash = H(prevHash + 事件规范化内容),链连续不可断裂;留存期内事件不可删除(只能到期归档);报送任务数据必须与源系统对账一致;血缘图谱中下游必须可达上游。

解决方案

1. 审计采集:应用层 AOP 注解(@Audited 声明敏感操作,自动记录前后值);网关层统一记录(who/when/what/result);数据库层审计插件(DDL/DML 全量 + 直连变更);堡垒机录屏。四层数据进同一 Kafka topic,按规范 schema 归一。

2. 防篡改存储:Kafka → 实时写对象存储(按天分区)+ 哈希链计算(链尾状态独立存储,防尾篡改);定期全链重算校验;访问审计数据需双人授权 + 自身也被审计。

3. 血缘:SQL 解析器提取表/字段依赖(INSERT/SELECT/JOIN 关系);调度 DAG 补任务级边;前端可视化(点击报表字段 → 反查全链路 + 每跳转换逻辑);口径变更发起影响分析(受影响报表清单自动生成)。

4. 报送:指标口径库(定义 + 公式 + 源表 + 负责人);报送模板渲染(按监管模板取数,保留计算过程);报送前自动对账(报送数 vs 源系统聚合数,差异阈值告警);报送产物归档(版本 + 校验和 + 审批记录),支持事后重放。

flowchart LR App[应用层AOP] --> K[审计Kafka] GW[网关层] --> K DB[数据库审计] --> K BAST[堡垒机] --> K K --> Chain[哈希链计算] Chain --> WORM[对象存储WORM] Chain --> Verify[定期全链校验] Verify -->|断链| Alert[篡改告警] SQL[SQL解析] --> Lin[血缘图谱] DAG[调度DAG] --> Lin Lin --> Impact[影响分析] Metric[指标口径库] --> Rpt[报送渲染] Rpt --> Recon[源系统对账] Recon -->|差异| Alarm2[口径告警] Rpt --> Arch[报送归档
版本+校验]
审计四层采集 + 哈希链防篡改,血缘与报送同源
关键代码
// 哈希链:任何历史修改都会断链;链尾状态独立存储防尾篡改
public AuditEvent append(Actor actor, String action, String target, String result) {
    String prev = chainHead.get();                       // 独立存储的链尾
    String canonical = canonicalForm(actor, action, target, result); // 规范化序列化(字段定序)
    String hash = sm3(prev + "|" + canonical);           // 国密摘要
    AuditEvent ev = new AuditEvent(nextId(), actor, action, target, result, prev, hash, now());
    wormStore.append(ev);                               // 只写存储,禁更新接口
    chainHead.set(hash);                               // 链尾推进(写入也走 WORM 日志)
    return ev;
}

// 血缘影响分析:改一个上游字段,自动列出受影响下游
public Set<String> impactOf(FieldRef field) {
    Set<String> hit = new HashSet<>();
    Deque<FieldRef> q = new ArrayDeque<>(List.of(field));
    while (!q.isEmpty()) {
        FieldRef f = q.poll();
        for (LineageEdge e : lineage.downstreamOf(f)) { // 下游边
            if (hit.add(e.downstream().key())) q.add(e.downstream());
        }
    }
    return hit;                                      // 报表/接口/口径清单,送变更评审
}
深度追问
  • DBA 自己就是高权限账号,怎么防「管理员删审计」?
    审计数据与应用数据物理隔离(独立存储 + 独立账号体系);写入口只有审计服务(应用无删改接口);链尾状态与事件流分开存(改事件会断链,改链尾会被独立存储的追加日志发现);校验服务独立部署定期重算。再加治理层:审计数据的访问本身要双人授权且留痕。技术 + 治理双保险,单靠数据库权限是不够的。
  • 审计量太大(每天数十亿条),查询慢怎么办?
    分层:热数据(近七天)进检索引擎(按 actor/target/action/时间倒排,秒级);温数据(三个月)列存按天分区;冷数据对象存储 + 预聚合索引(按人/按资源的操作摘要)。查询默认带时间范围 + 至少一个过滤条件(禁全表扫)。日常告警与调查走热/温层,历史举证走冷层检索(分钟级可接受)。
  • 报送数字与源系统对不上,常见原因?
    三类:口径不一致(报送模板口径 vs 源表聚合口径,靠口径库统一 + 变更评审)、时点差异(报送截止时点 vs 源系统统计时点,明确快照时点规则)、数据修复(问题数据订正后未重算报送)。对策是报送也走「血缘可查 + 快照固化」:每次报送保存取数时点与中间结果,差异可逐层下钻定位。
  • 多系统 ETL 链路复杂,血缘自动解析覆盖率低怎么办?
    混合策略:调度平台任务级依赖先全覆盖(必有 DAG);SQL 解析覆盖标准 ETL(覆盖率通常八成以上);剩余「存储过程/脚本/人工导入」用人工登记补 + 采集代理(存储过程执行日志反推);关键报表(监管报送类)强制全链路登记,覆盖率纳入数据团队考核。血缘的价值在关键链路完整,不是全量完美。
常见陷阱
  • 审计可改可删:无证据价值;WORM + 哈希链 + 接口层禁改。
  • 只采应用层:直连改库成盲区;四层采集互补。
  • 血缘靠口口相传:人员流动即失传;解析自动生成 + 人工补录。
  • 报送手工凑数:口径漂移风险;口径库 + 模板渲染 + 自动对账。
破题点拨
一眼定位:审计追溯 = 四层采集 + WORM 哈希链 + 血缘图谱 + 口径 SSOT 报送。
破题主线:应用/网关/数据库/堡垒机四层归一 → 哈希链只写存储 → 定期重算校验 → SQL 解析建血缘 → 口径库渲染报送 + 对账归档。
易错点:审计可改;只采一层;血缘靠人肉;报送手工。
D12.06
政务与公共服务:一网通办、数据共享与隐私计算架构P1
高频 领域模型 架构 一网通办 数据共享 隐私计算
场景背景

省级政务服务平台:一网通办(群众只跑一次)、跨部门数据共享(户籍/社保/医保/不动产互相调)、敏感场景隐私计算(联合核验不出域)。硬指标:事项网办率、数据共享接口可用性、授权合规(每次调用有授权链)、等保密评(三级等保是底线)。

核心挑战

① 部门壁垒:数据在各委办局自己的库里,共享靠协调不靠系统,接口各搞各的(无目录、无标准);② 授权与审计:谁调了什么数据、为什么调,必须全程授权链 + 审计留痕,监管可回溯;③ 隐私计算场景:联合核验(如「是否低保户」)不能把明细给出域,要用隐私计算(安全多方/TEE/联邦);④ 一网通办流程:一个事项跨多部门审批,流程编排 + 材料复用(电子证照),时限管控;⑤ 信创适配:国产芯片/操作系统/数据库/中间件全栈适配(D12.03 的信创要求)。

分析框架

数据共享分级治理:目录化管理(共享目录 = 数据源 + 分级 + 授权规则),公开数据直接开放、授权数据接口调用(带授权链)、敏感数据隐私计算(可用不可见)。一网通办 = 事项标准化(材料/流程/时限)+ 统一受理 + 流程编排(跨部门串联/并联)+ 电子证照复用(免提交)。隐私计算三路线:TEE(硬件可信执行环境,性能好)、安全多方计算(密码学,无信任假设)、联邦学习(模型不出数据)。架构上「数据不动模型动」:核验类场景出结果不出明细。所有共享调用过统一网关(认证 + 授权 + 审计 + 限流),审计日志哈希链防篡改(D12.05 同源)。

领域模型

聚合根:政务事项(Item,材料 + 流程 + 时限 + 部门)。实体:共享目录(Catalog,数据源 + 分级 + 授权规则)、共享调用记录(ShareLog,调用方 + 授权链 + 用途 + 结果)、隐私计算任务(PCTask,场景 + 参与方 + 结果)。值对象:授权凭据、数据分级。不变量:无授权链不允许调用;目录外数据不允许共享;隐私计算结果不含原始明细;审计日志不可篡改(哈希链)。

解决方案

1. 共享交换平台:统一目录(部门数据资源注册 + 分级 + 授权规则),统一网关(认证 + 授权 + 限流 + 审计);接口标准化(REST + 国密算法),部门侧适配器接入。

2. 授权链:调用方资质审核 → 授权单(范围 + 用途 + 期限)→ 调用时带授权凭据 → 网关校验 + 全程审计;授权过期自动失效,超范围调用拦截告警。

3. 一网通办:事项标准化录入(一表通办),流程编排引擎(跨部门串联审批 + 并联会签),电子证照库复用(户口簿/营业执照免提交),时限看板(临期预警 + 超时通报)。

4. 隐私计算:联合核验场景(低保资格/人口比对)走 TEE 或安全多方计算:数据不出域,只出核验结果;任务全程审计(参与方 + 用途 + 结果哈希)。

5. 安全合规:等保三级建设(分区 + 国密 + 审计),信创全栈适配;运维审计(堡垒机 + 操作留痕),数据安全事件应急预案与演练。

graph TD A[部门数据资源] --> B[共享目录 分级+授权规则] B -->|公开| C[开放数据] B -->|授权| D[共享网关 认证/授权/审计] B -->|敏感| E[隐私计算 TEE/MPC] F[办事申请] --> G[一网通办受理] G --> H[流程编排 跨部门] H --> I[电子证照复用] H --> J[数据共享调用] J --> D J --> E D --> K[审计日志 哈希链] E --> K
共享分级与一网通办架构
关键代码
// 共享网关:无授权链一律拒绝,授权链校验 + 全程审计
public ShareResult invoke(ShareRequest req) {
    Grant grant = grantRepo.findValid(req.callerId(), req.catalogId(), now);
    if (grant == null || !grant.covers(req.purpose())) {
        audit.deny(req, "NO_VALID_GRANT");                    // 拒绝也留痕
        throw new AuthDeniedException();
    }
    ShareResult r = adapter.call(req.catalogId(), req.params());
    audit.allow(req, grant, r.digest());              // 哈希链上链
    return r;
}

// 隐私计算:数据不出域,只出核验结果(TEE 任务)
PCTask task = pc.submit("lowIncomeCheck", parties, params);
Boolean result = task.awaitResult();                 // 只回布尔核验结论
audit.pcTask(task.id(), parties, result);          // 任务级审计,无原始明细

// 一网通办流程编排:跨部门串联,材料复用电子证照
Item item = itemRepo.find(itemId);
List<Cert> certs = certRepo.reusable(applicant, item.materials());  // 免提交
engine.start(item.flow(), context.withCerts(certs));            // 时限看板自动挂接
深度追问
  • 部门不愿意共享数据,怎么推?
    技术上降低接入成本(适配器 + 网关托管,部门只维护自己适配器),机制上目录化管理(哪些能共享是目录定的,不是每次协调);授权链明确责任边界(调用方担用途责任,数据方只对质量负责)。系统把「共享一次的成本」降到接近零,机制上才有得谈。
  • 隐私计算和直接调接口怎么选?
    看数据分级:授权数据走网关接口(授权链 + 审计),敏感数据(不出域红线)走隐私计算。隐私计算有性能成本(TEE 快、MPC 慢),只用在必须「结果不出明细」的场景(联合核验/征信);能授权的别上隐私计算,杀鸡用牛刀还慢。
  • 一网通办跨部门流程怎么保时限?
    流程编排引擎管状态(每个节点的受理/办理/时限),时限看板自动挂接:临期预警(办结前 N 小时)、超时通报(超期自动上报督办)。节点超时不自动放行——超时的正确处置是通报,不是默认通过。
  • 审计日志怎么防篡改?
    哈希链(每条日志含前条哈希,改一条断链,与 D12.05 同源)+ 独立存储(审计库只追加,管理员也改不了)+ 定期校验(链完整性扫描)。监管抽查时哈希链就是证据链,断链即安全事件。
常见陷阱
  • 共享无目录:部门私下对接、接口无标准,一换人就断;目录 + 网关是治理底座。
  • 授权链缺失:谁都能调、调了没人知,监管一问三不知;无授权链不调用,全程审计。
  • 敏感数据直接出域:联合核验把明细给出域方,合规事故;隐私计算保不出域。
  • 超时默认通过:流程节点超时自动放行,群众材料没审就办结;超时是通报不是放行。
破题点拨
一眼定位:政务平台 = 共享目录分级治理 + 授权链网关 + 一网通办流程编排 + 隐私计算不出域 + 等保密评。
破题主线:目录降成本 → 授权链定责任 → 网关统一审计 → 敏感场景隐私计算 → 流程时限看板。
易错点:无目录共享;授权链缺失;明细出域;超时默认通过。