Appearance
保险分销平台(亮点与难点)
[附件: Java工程师 .pdf](./attachments/YqQlLeBGib0Px00Z/Java工程师 .pdf)
上面这位学员简历的项目没什么亮点和难点,投出去面试机会可能不多,经过和老师沟通后,着重优化了第一个项目《分销领客牛平台》,这个项目本质上就是一个保险分销平台,优化如下:
项目名称:分销领客牛平台(保险产品分销系统)
项目描述:
领客牛是一个大型保险产品分销平台,日均服务10万+代理人,支持数百款保险产品的在线销售。系统实现了完整的多级分销体系,包括渠道管理、团队管理、产品配置、佣金计算、投保流程等核心功能。
技术架构:
- 前端:Vue.js + Element UI
- 后端:SpringBoot + SpringCloud Alibaba + MySQL + Redis + kafka + Apollo
- 监控:Prometheus + Grafana + ELK
核心亮点:
- 高并发订单处理 (技术亮点 0-3年)
- 采用多级缓存(本地缓存+Redis集群)架构,将热点产品信息查询延迟控制在100ms以内
- 设计了分布式锁+消息队列的架构,实现订单峰值1000+TPS的稳定处理
- 使用分库分表技术,解决了亿级订单数据存储问题(视情况写)
- 复杂佣金计算系统 (业务亮点 3-5年)
- 设计了灵活的多级分销佣金计算规则引擎,支持20+种佣金计算规则
- 采用责任链+策略模式处理不同产品类型的佣金计算逻辑
- 实现佣金计算的实时预览功能,提升用户体验
- 投保计划书智能生成 (业务点 3-5年)
- 设计了基于模板引擎的动态计划书生成系统,支持100+种产品组合方案
- 实现了计划书异步生成和缓存机制,将生成时间从20秒优化到1秒内
- 采用多级缓存+延迟队列,解决了高峰期计划书并发生成问题
技术难点及解决方案:
- 订单数据一致性问题 (实际问题)
- 实现了基于TCC模式的分布式事务处理机制,确保订单与支付状态的最终一致性
- 设计了订单状态机,通过状态流转实现订单全生命周期的有效管理
- 采用补偿机制处理分布式事务异常,确保数据最终一致性
- 复杂业务规则处理(业务)
- 引入规则引擎,将90+种业务规则配置化,提升系统可维护性
- 设计了产品规则动态装载机制,支持业务规则热更新
- 实现了规则验证的并行处理,提升规则校验效率
- 系统性能优化 (性能优化)
- 结合业务场景优化复杂SQL,将30秒级查询优化到1秒内
- 实现读写分离,利用多级缓存减少数据库访问压力
- 通过异步处理+任务队列,提升系统整体吞吐量
- 安全性设计
- 实现了基于RBAC的细粒度权限控制
- 设计防刷限流机制,有效防止恶意攻击
- 实现敏感数据加密存储和传输机制
这样的包装既突出了技术深度,又结合了具体的业务场景,面试时可以重点展开这些亮点,并准备具体的技术方案和数据支撑。每个点都可以深入展开讨论,展现你的技术实力和解决问题的能力。
面试如何回答( 怎么hold住 ?)
一、采用多级缓存(本地缓存+Redis集群)架构,将热点产品信息查询延迟控制在50ms以内
我们的保险产品信息查询是个高频操作,日均查询量在千万级别。产品信息包含基础信息、费率表、保障责任等数据,所以我们设计了多级缓存架构来提升查询性能。具体实现如下:
- 缓存架构设计:
- 一级缓存:使用Caffeine实现本地缓存,缓存热点产品的完整信息
- 二级缓存:使用Redis集群存储所有产品信息
- 最终数据源:MySQL数据库
- 查询流程:
java
用户请求 -> 查本地缓存(Caffeine) -> 查Redis集群 -> 查数据库- 本地缓存:设置容量为200个产品,采用LRU淘汰策略
- Redis集群:采用Redis Cluster模式保证高可用,按产品ID分片存储
- 通过本地缓存命中率达到90%以上,Redis集群命中率达到99%以上
- 缓存一致性保证:
采用Cache Aside Pattern(旁路缓存模式)+ 最终一致性方案:
- 更新操作:先更新数据库,再删除缓存
- 引入消息队列实现异步更新:
java
更新数据库 -> 发送更新消息 -> 消费消息更新Redis -> 通知各节点清除本地缓存- 设置合理的缓存过期时间作为兜底方案(本地缓存5分钟,Redis 30分钟)
java
这个可以提升缓存与数据库的一致性,因为要保证缓存与数据库的绝对一致(要用读写互斥的分布式锁)代价是很高的如果有对多级缓存架构代码实现不清楚的可以参考直播课《京东生产环境Redis高并发缓存架构实战》
- 使用带版本号的乐观锁保证并发更新的正确性(视情况选择)
java
写写并发导致的数据覆盖:
时间1: 节点A读取产品价格100元 version=1
时间2: 节点B读取产品价格100元 version=1
时间3: 节点A将价格更新为120元 version=2
时间4: 节点B将价格更新为110元 version=1
最终结果: 价格变成110元,节点A的更新被覆盖- 性能优化措施:
- 产品信息分级缓存:
- 热点数据:完整产品信息放入本地缓存
- 常规数据:基础信息放入本地缓存,详细信息放入Redis
- 实现缓存预热机制:
- 系统启动时预加载热门产品
- 定时任务更新热门产品列表
- 使用布隆过滤器防止缓存穿透
- 通过互斥锁防止缓存击穿
- 监控和异常处理:
- 实现缓存监控:缓存命中率、延迟、内存使用率等
- 配置熔断降级机制:
- Redis集群异常时降级使用本地缓存
- 本地缓存异常时直接查询Redis
- 设置告警阈值,及时发现并处理异常情况
- 监控数据采集:
- 使用Prometheus + Grafana架构
- Redis指标通过Redis Exporter采集
- 本地缓存指标通过JMX采集
通过这套方案,我们将产品信息查询延迟控制在10ms以内,其中:
- 本地缓存访问延迟:0.1ms以内
- Redis集群访问延迟:5ms以内
- 数据库访问延迟:50ms左右
这个方案在我们的业务场景中运行良好,既保证了查询性能,又确保了数据一致性。
二、设计了分布式锁+消息队列的架构,实现订单峰值1000+TPS的稳定处理
详细说明参考直播课《双十一高并发抢购(秒杀)系统三高架构实战》
三、使用分库分表技术,解决了亿级订单数据存储问题(视情况写)
详细说明参考直播课《面试必问海量数据分库分表架构实战》
四、设计了灵活的多级分销佣金计算规则引擎,支持20+种佣金计算规则(业务架构设计亮点)
在保险分销场景中,佣金计算是个复杂的业务,主要涉及多级渠道分佣、团队分佣、个人分佣等场景。我们设计了一个灵活的佣金计算引擎来处理这些复杂的规则。
- 传统方式实现(不使用规则引擎):
java
public class TraditionalCommissionService {
public BigDecimal calculateCommission(Order order) {
// 基础佣金
BigDecimal commission = BigDecimal.ZERO;
// 计算基础佣金
if (order.getProductType().equals("寿险")) {
commission = order.getAmount().multiply(new BigDecimal("0.1"));
} else if (order.getProductType().equals("重疾险")) {
commission = order.getAmount().multiply(new BigDecimal("0.15"));
}
// 计算团队奖励
if (order.getTeamPerformance().compareTo(new BigDecimal("1000000")) > 0) {
commission = commission.add(order.getAmount().multiply(new BigDecimal("0.01")));
}
// 计算渠道奖励
if (order.getChannelLevel().equals("A")) {
commission = commission.add(order.getAmount().multiply(new BigDecimal("0.02")));
} else if (order.getChannelLevel().equals("B")) {
commission = commission.add(order.getAmount().multiply(new BigDecimal("0.015")));
}
// 更多的if-else...
return commission;
}
}这种传统方式的问题:
- 代码臃肿:随着规则增加,if-else嵌套越来越多
- 难维护:修改一个规则可能需要改动多处代码
- 不灵活:规则修改需要改代码重新发布
- 难扩展:新增规则需要修改原有代码
- 业务耦合:业务规则和代码逻辑混在一起
- 使用规则引擎的方式:
java
// 1. 领域模型定义
// 1.1 代理人模型
public class Agent {
private String agentId;
private String name;
private String level; // 代理人等级:普通、高级、专家
private String channelLevel; // 所属渠道等级:A、B、C
private String teamId; // 所属团队ID
private BigDecimal monthlyPerformance; // 月度业绩
private LocalDateTime joinTime; // 入职时间
private boolean isNewAgent; // 是否新人
}
// 1.2 订单模型
public class Order {
private String orderId;
private String productType; // 产品类型
private BigDecimal amount; // 订单金额
private LocalDateTime createTime; // 订单创建时间
private Agent agent; // 代理人信息
}
// 2. 佣金规则接口
public interface CommissionRule {
BigDecimal calculate(Order order);
String getRuleName();
}
// 3. 具体规则实现
// 3.1 基础佣金规则
public class BaseCommissionRule implements CommissionRule {
private final Map<String, BigDecimal> productRates = new HashMap<>();
public BaseCommissionRule() {
productRates.put("寿险", new BigDecimal("0.1"));
productRates.put("重疾险", new BigDecimal("0.15"));
productRates.put("年金险", new BigDecimal("0.08"));
}
@Override
public BigDecimal calculate(Order order) {
BigDecimal rate = productRates.getOrDefault(order.getProductType(), BigDecimal.ZERO);
return order.getAmount().multiply(rate);
}
@Override
public String getRuleName() {
return "基础佣金规则";
}
}
// 3.2 代理人等级奖励规则
public class AgentLevelBonusRule implements CommissionRule {
private final Map<String, BigDecimal> levelRates = new HashMap<>();
public AgentLevelBonusRule() {
levelRates.put("专家", new BigDecimal("0.05"));
levelRates.put("高级", new BigDecimal("0.03"));
levelRates.put("普通", new BigDecimal("0.01"));
}
@Override
public BigDecimal calculate(Order order) {
Agent agent = order.getAgent();
BigDecimal rate = levelRates.getOrDefault(agent.getLevel(), BigDecimal.ZERO);
return order.getAmount().multiply(rate);
}
@Override
public String getRuleName() {
return "代理人等级奖励规则";
}
}
// 3.3 团队业绩奖励规则
public class TeamPerformanceBonusRule implements CommissionRule {
private static final BigDecimal PERFORMANCE_THRESHOLD = new BigDecimal("1000000");
private static final BigDecimal BONUS_RATE = new BigDecimal("0.01");
@Override
public BigDecimal calculate(Order order) {
Agent agent = order.getAgent();
if (agent.getMonthlyPerformance().compareTo(PERFORMANCE_THRESHOLD) > 0) {
return order.getAmount().multiply(BONUS_RATE);
}
return BigDecimal.ZERO;
}
@Override
public String getRuleName() {
return "团队业绩奖励规则";
}
}
// 3.4 渠道奖励规则
public class ChannelBonusRule implements CommissionRule {
private final Map<String, BigDecimal> channelRates = new HashMap<>();
public ChannelBonusRule() {
channelRates.put("A", new BigDecimal("0.02"));
channelRates.put("B", new BigDecimal("0.015"));
channelRates.put("C", new BigDecimal("0.01"));
}
@Override
public BigDecimal calculate(Order order) {
Agent agent = order.getAgent();
BigDecimal rate = channelRates.getOrDefault(agent.getChannelLevel(), BigDecimal.ZERO);
return order.getAmount().multiply(rate);
}
@Override
public String getRuleName() {
return "渠道奖励规则";
}
}
// 3.5 新人奖励规则
public class NewAgentBonusRule implements CommissionRule {
private static final BigDecimal BONUS_RATE = new BigDecimal("0.02");
private static final long NEW_AGENT_PERIOD_DAYS = 90; // 入职90天内视为新人
@Override
public BigDecimal calculate(Order order) {
Agent agent = order.getAgent();
if (agent.isNewAgent() &&
ChronoUnit.DAYS.between(agent.getJoinTime(), LocalDateTime.now()) <= NEW_AGENT_PERIOD_DAYS) {
return order.getAmount().multiply(BONUS_RATE);
}
return BigDecimal.ZERO;
}
@Override
public String getRuleName() {
return "新人奖励规则";
}
}
// 4. 佣金计算结果
public class CommissionResult {
private String orderId;
private String agentId;
private String agentName;
private BigDecimal orderAmount;
private BigDecimal totalCommission = BigDecimal.ZERO;
private Map<String, BigDecimal> ruleResults = new LinkedHashMap<>();
private LocalDateTime calculateTime;
public void addRuleResult(String ruleName, BigDecimal commission) {
ruleResults.put(ruleName, commission);
totalCommission = totalCommission.add(commission);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("\n佣金计算结果明细:\n");
sb.append(String.format("订单号:%s\n", orderId));
sb.append(String.format("代理人:%s(%s)\n", agentName, agentId));
sb.append(String.format("订单金额:¥%s\n", orderAmount));
sb.append("规则计算明细:\n");
ruleResults.forEach((rule, amount) ->
sb.append(String.format("- %s: ¥%s\n", rule, amount)));
sb.append(String.format("总佣金:¥%s\n", totalCommission));
return sb.toString();
}
}
// 5. 规则链处理器
@Slf4j
public class CommissionRuleChain {
private final List<CommissionRule> rules = new ArrayList<>();
public CommissionRuleChain() {
// 按照处理顺序添加规则
rules.add(new BaseCommissionRule());
rules.add(new AgentLevelBonusRule());
rules.add(new TeamPerformanceBonusRule());
rules.add(new ChannelBonusRule());
rules.add(new NewAgentBonusRule());
}
public CommissionResult calculateCommission(Order order) {
CommissionResult result = new CommissionResult();
result.setOrderId(order.getOrderId());
result.setAgentId(order.getAgent().getAgentId());
result.setAgentName(order.getAgent().getName());
result.setOrderAmount(order.getAmount());
result.setCalculateTime(LocalDateTime.now());
for (CommissionRule rule : rules) {
try {
BigDecimal ruleCommission = rule.calculate(order);
result.addRuleResult(rule.getRuleName(), ruleCommission);
log.debug("规则[{}]计算结果: {}", rule.getRuleName(), ruleCommission);
} catch (Exception e) {
log.error("规则[{}]计算异常: {}", rule.getRuleName(), e.getMessage());
}
}
return result;
}
}
// 6. 佣金规则引擎服务
public class CommissionRuleEngineService {
private final CommissionRuleChain ruleChain = new CommissionRuleChain();
public CommissionResult calculateCommission(Order order) {
log.info("开始计算订单[{}]的佣金,代理人:{}", order.getOrderId(), order.getAgent().getName());
CommissionResult result = ruleChain.calculateCommission(order);
log.info("订单[{}]佣金计算完成,总佣金:{}", order.getOrderId(), result.getTotalCommission());
return result;
}
}使用示例:
java
public class CommissionCalculationExample {
public static void main(String[] args) {
// 创建代理人
Agent agent = new Agent();
agent.setAgentId("AG001");
agent.setName("张三");
agent.setLevel("专家");
agent.setChannelLevel("A");
agent.setTeamId("TEAM001");
agent.setMonthlyPerformance(new BigDecimal("2000000"));
agent.setJoinTime(LocalDateTime.now().minusDays(30));
agent.setNewAgent(true);
// 创建订单
Order order = new Order();
order.setOrderId("ORD" + LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "001");
order.setProductType("寿险");
order.setAmount(new BigDecimal("100000"));
order.setCreateTime(LocalDateTime.now());
order.setAgent(agent);
// 用规则引擎计算佣金
CommissionRuleEngineService service = new CommissionRuleEngineService();
CommissionResult result = service.calculateCommission(order);
// 输出结果
System.out.println(result.toString());
}
}
运行结果:
开始计算订单ORD20241220001的佣金,代理人:张三
规则基础佣金规则计算结果: 10000.0
规则代理人等级奖励规则计算结果: 5000.00
规则团队业绩奖励规则计算结果: 1000.00
规则渠道奖励规则计算结果: 2000.00
规则新人奖励规则计算结果: 2000.00
订单ORD20241220001佣金计算完成,总佣金:20000.00
佣金计算结果明细:
订单号:ORD20241220001
代理人:张三(AG001)
订单金额:¥100000
规则计算明细:
- 基础佣金规则: ¥10000.0
- 代理人等级奖励规则: ¥5000.00
- 团队业绩奖励规则: ¥1000.00
- 渠道奖励规则: ¥2000.00
- 新人奖励规则: ¥2000.00
总佣金:¥20000.00这个实现的优点:
- 代码结构清晰,每个规则独立封装
- 易于添加新规则,不需要修改现有代码
- 规则的执行顺序可控
- 便于单元测试
- 规则配置可以轻松扩展为从配置文件或数据库加载
总的来说,规则引擎特别适合保险分销这种业务规则复杂、经常变动的场景。它通过将业务规则从代码中抽离出来,实现了规则的灵活配置和管理,大大提高了系统的可维护性和扩展性。
PS:上面规则引擎详细代码讲解参考直播课《传统CRUD保险系统亮点与难点优化实战》
五、采用责任链+策略模式处理不同产品类型的佣金计算逻辑
详细代码讲解参考直播课《传统CRUD保险系统亮点与难点优化实战》
六、结合业务场景优化复杂SQL,将30秒级查询优化到1秒内
这是一个代理人业绩计算场景的性能优化案例。原来计算sql需要30秒,我们通过以下步骤将其优化到1秒内。
1. 业务背景
需求是统计每个代理人近12个月的:
- 订单总金额
- 服务客户数
- 按业绩降序排名
原始SQL是这样的:
sql
SELECT a.agent_id, a.level,
SUM(o.amount) as total_amount,
COUNT(DISTINCT o.customer_id) as customer_count
FROM agent a
LEFT JOIN orders o ON a.agent_id = o.agent_id
LEFT JOIN performance p ON a.agent_id = p.agent_id
WHERE o.create_time >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
GROUP BY a.agent_id, a.level
ORDER BY total_amount DESC;2. 性能问题分析
我用EXPLAIN分析后发现几个主要问题:
1. 数据量大
- orders表每天新增约几十万订单
- 12个月数据量有数千万条
2. 查询效率低
- 大范围时间查询
- 全表扫描没用上索引
- 大量临时表操作
3. 统计计算慢
- COUNT DISTINCT很耗资源
- 大表JOIN性能差
- GROUP BY临时表过大
3**. 优化方案**
考虑到业务特点:
- 报表查询频繁
- 可以接受T+1数据,就是可以等到第二天再查询截止到前一天的数据
- 实时性要求不高
采用了两个主要优化策略:
1. **预计算机制:**
sql
-- 创建每日统计表
CREATE TABLE agent_daily_stats (
stat_date DATE,
agent_id VARCHAR(50),
daily_amount DECIMAL(18,2),
daily_customer_count INT,
PRIMARY KEY (stat_date, agent_id)
);
-- 创建月度统计表
CREATE TABLE agent_monthly_stats (
stat_month DATE,
agent_id VARCHAR(50),
monthly_amount DECIMAL(18,2),
monthly_customer_count INT,
PRIMARY KEY (stat_month, agent_id)
);2. **查询优化:**
sql
-- 优化后的查询
-- with as就是创建一张临时表yearly_stats,能够降低sql复杂度,性能上也有一定提升
WITH yearly_stats AS (
SELECT agent_id,
SUM(monthly_amount) as total_amount,
SUM(monthly_customer_count) as customer_count
FROM agent_monthly_stats
WHERE stat_month >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
GROUP BY agent_id
)
SELECT a.agent_id, a.level,
COALESCE(ys.total_amount, 0) as total_amount,
COALESCE(ys.customer_count, 0) as customer_count
FROM agent a
LEFT JOIN yearly_stats ys ON a.agent_id = ys.agent_id
ORDER BY total_amount DESC;4.** 具体实施步骤**
1. 数据预计算:
sql
-- 每日凌晨2点执行
INSERT INTO agent_daily_stats
SELECT
DATE(create_time),
agent_id,
SUM(amount),
COUNT(DISTINCT customer_id)
FROM orders
WHERE DATE(create_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)
GROUP BY DATE(create_time), agent_id;
-- 每月1号凌晨执行
INSERT INTO agent_monthly_stats
SELECT
DATE_FORMAT(stat_date, '%Y-%m-01'),
agent_id,
SUM(daily_amount),
SUM(daily_customer_count)
FROM agent_daily_stats
WHERE stat_date >= DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')
GROUP BY DATE_FORMAT(stat_date, '%Y-%m-01'), agent_id;2. 索引优化:
sql
-- 订单表添加复合索引
ALTER TABLE orders ADD INDEX idx_agent_time(agent_id, create_time);
-- 统计表添加查询索引
ALTER TABLE agent_monthly_stats ADD INDEX idx_month(stat_month);5**. 效果验证**
1. 性能提升:
- 查询时间:从30秒降到800ms
- CPU使用率:降低90%以上
- IO消耗:降低95%
2. 数据量对比:
- 原查询:扫描3000万条订单记录
- 优化后:只读取12条月度汇总数据
6. 额外收益
1. 系统稳定性提升:
- 降低数据库负载
- 减少慢查询影响
- 提高并发能力
2. 功能扩展:
- 支持多维度统计
- 便于添加新指标
- 历史数据快速查询
7. 经验总结
1. 优化原则:
- **充分理解业务需求**
- **从根本解决问题**
- **在合适场景使用预计算**
- **持续监控和优化**
2. 注意事项:
- 数据一致性保证
- 异常情况处理
- 存储成本控制
- 维护方案完善
通过这次优化,不仅解决了性能问题,还提升了系统整体质量。最重要的是,这个方案很好地平衡了开发成本和业务收益,得到了业务方的认可。
PS:关于数据库优化的更多细节可以参考直播课《阿里巴巴内部Mysql性能优化最佳实践》
七、服务器部署情况(需根据实际情况考虑冗余)
从各个维度详细分析服务器配置方案:
- 用户请求分析:
java
峰值TPS: 1000+
日活用户: 10万+
估算日订单量: 约50万单 (考虑到保险业务的特点,单个代理人日均成单5单)
数据存储增量: 每单约2KB,日增50万单约1GB- 服务器配置建议:
A. 应用服务器集群:
java
规格:
- CPU: 16核
- 内存: 32GB
- 磁盘: 200GB SSD
- 操作系统: CentOS 7.9
数量:12台
- 8台用于订单服务集群
- 4台用于其他微服务(用户、产品、佣金等)
原因分析:
1. 单台服务器优化后可支持200+ TPS
2. 考虑峰值1000+ TPS,需要至少6台服务器
3. 考虑冗余和故障转移,总共配置8台
4. 其他微服务配置4台保证基础服务可用性B. 数据库服务器:
java
主库规格(订单库):
- CPU: 32核
- 内存: 256GB
- 磁盘: 2TB NVMe SSD
- 数据库:MySQL 8.0企业版
数量:2组(每组1主2从)
- 2个主库做分库分表
- 每个主库配2个从库
- 总共6台数据库服务器
分库分表方案:
- 按代理人ID范围分库(2个库)
- 每个库按订单ID范围分表(每库16张表)
- 支持未来2年数据增长C. 缓存服务器集群:
java
规格:
- CPU: 16核
- 内存: 64GB
- 磁盘: 200GB SSD
- Redis 7.0企业版
数量:6台(3主3从)
- 2组用于订单缓存
- 1组用于其他业务缓存
缓存策略:
- 热点订单数据
- 代理人会话信息
- 产品和佣金规则D. 消息队列服务器:
java
规格:
- CPU: 16核
- 内存: 32GB
- 磁盘: 500GB SSD
- RocketMQ 5.0
数量:4台(2主2从)
用途:
- 订单异步处理
- 佣金计算任务
- 消息通知E. 文件存储服务器:
java
规格:
- CPU: 8核
- 内存: 16GB
- 磁盘: 5TB(可扩展)
数量:2台(主备)
用途:
- 保单文件存储
- 系统日志归档
- 数据备份- 总体架构设计:
java
流量入口层:
- 2台负载均衡器(F5/Nginx)
- 4台接入层服务器(Nginx反向代理)
应用服务层:
- 8台订单服务器
- 4台其他微服务器
数据存储层:
- 6台MySQL服务器
- 6台Redis服务器
- 4台消息队列服务器
- 2台文件存储服务器
总计:36台服务器更新: 2025-03-26 20:21:44
原文: https://www.yuque.com/tulingzhouyu/db22bv/nn6meztozbalk68e