╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ CoDealBgProReCodeTimeScheduler.executeOnTime() 完整执行流程图 ║ ╠════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣ ║ 输入参数: Map<String, String> map ║ ║ 依赖服务: ICoExerciseDomainService, IMessageNoticeService ║ ║ 复用组件: BgProReCodeAlarmTimeScheduler (getEtrConfig, changeLine方法) ║ ║ 常量定义: SUBJECT_LABEL = "tmc.CoDealBgProReCodeTimeScheduler.label" ║ ║ LABEL_1 = "/deal/currency-option-detail?dealNumber=交易号" ║ ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ │ ▼ ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ 【步骤1】获取ET级联配置(复用FX Scheduler) ████████████████████████████████████████████████████ ║ ║ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ║ ║ ║ ║ ┌─────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────────────┐ ║ ║ │ 操作层 (怎么做) │ │ 设计依据层 (为什么) │ ║ ║ ├─────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────┤ ║ ║ │ │ │ │ ║ ║ │ 1. 调用FX Scheduler的getEtrConfig()方法 │ │ 🟡 设计目的: 配置复用 │ ║ ║ │ List<HedgeCurrencyEntity> coDealModelList = │ │ · FX和CO交易共享相同的E-Treasury配置 │ ║ ║ │ bgProReCodeAlarmTimeScheduler.getEtrConfig(); │ │ · 避免重复调用同一个REST API │ ║ ║ │ │ │ · 降低网络开销,提升性能 │ ║ ║ │ │ │ │ ║ ║ │ │ │ 🔵 技术实现: │ ║ ║ │ │ │ · 注入BgProReCodeAlarmTimeScheduler实例 │ ║ ║ │ │ │ · 直接调用其getEtrConfig()方法 │ ║ ║ │ │ │ │ ║ ║ │ │ │ 🟣 依赖关系: │ ║ ║ │ │ │ · FX Scheduler必须先成功获取配置 │ ║ ║ │ │ │ · 如果FX配置获取失败,CO Scheduler也无法执行 │ ║ ║ │ │ │ │ ║ ║ └─────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────────────┘ ║ ║ 输出: List<HedgeCurrencyEntity> coDealModelList ║ ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ │ ▼ ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ 【决策点A】配置数据为空检查 ║ ║ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ║ ║ ║ ║ ┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐ ║ ║ │ │ │ ║ ║ ▼ ▼ │ ║ ║ ┌───────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────────────────────┐ ║ ║ │ 条件: coDealModelList == null │ │ 条件: coDealModelList != null && size() > 0 │ ║ ║ │ || size() == 0 │ │ │ ║ ║ ├───────────────────────────────┤ ├───────────────────────────────────────────────────────────────────────────────────┤ ║ ║ │ │ │ │ ║ ║ │ 🟢 日志: │ │ ▼ │ ║ ║ │ LOGGER.info("There is no │ │ │ ║ ║ │ data from source config") │ │ ┌────────────────────────────────────────────────────┐ │ ║ ║ │ │ │ │ 设计依据层 │ │ ║ ║ │ 🟡 设计目的: │ │ ├────────────────────────────────────────────────────┤ │ ║ ║ │ · 无配置时任务无意义 │ │ │ │ │ ║ ║ │ · 直接返回,避免NPE │ │ │ 🟡 防御性编程: │ │ ║ ║ │ · 日志便于排查问题 │ │ │ · 配置不存在时不执行后续逻辑 │ │ ║ ║ │ │ │ │ · 日志便于排查问题 │ │ ║ ║ │ │ │ │ │ │ ║ ║ │ │ │ │ 🔵 证据: │ │ ║ ║ │ │ │ │ 代码位置: CoDealBgProReCodeTimeScheduler.java:58-61 │ │ ║ ║ │ │ │ └────────────────────────────────────────────────────┘ │ ║ ║ └───────────────────────────────┘ └───────────────────────────────────────────────────────────────────────────────────┘ ║ ║ │ ║ ║ ▼ ║ ║ ╔═══════════════════════╗ ║ ║ ║ 【方法返回】 ║ ║ ║ ╚═══════════════════════╝ ║ ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ │ ▼ ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ 【步骤2】查询最近N天的CO交易列表 ████████████████████████████████████████████████████ ║ ║ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ║ ║ ║ ║ ┌─────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────────────┐ ║ ║ │ 操作层 (怎么做) │ │ 设计依据层 (为什么) │ ║ ║ ├─────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────┤ ║ ║ │ │ │ │ ║ ║ │ 1. 获取监控时间段配置 │ │ 🟣 配置参数: │ ║ ║ │ String alarmPeriodStr = │ │ · Key: Tmc.Deal.BgProReCodeAlarmPeriod │ ║ ║ │ ApplicationConfigProperties │ │ · 默认值: "7" (天) │ ║ ║ │ .getContextProperty( │ │ · 含义: 监控最近7天内创建的CO交易 │ ║ ║ │ "Tmc.Deal.BgProReCodeAlarmPeriod", │ │ · 与FX Scheduler共用相同配置 │ ║ ║ │ "7"); │ │ │ ║ ║ │ int alarmPeriod = Integer.parseInt(alarmPeriodStr); │ │ │ ║ ║ │ │ │ 🟡 业务规则: │ ║ ║ │ 2. 调用领域服务查询CO交易 │ │ · 只需检测最近N天内的新交易 │ ║ ║ │ List<CoDealEntity> coDealEntities = │ │ · 历史交易已确认无误,减少扫描数据量提升性能 │ ║ ║ │ coExerciseDomainService │ │ │ ║ ║ │ .findCoDealForAlarm(alarmPeriod); │ │ 🔵 技术实现: │ ║ ║ │ │ │ · 调用ICoExerciseDomainService查询CO交易 │ ║ ║ │ │ │ · alarmPeriod作为SQL查询参数 │ ║ ║ └─────────────────────────────────────────────────────────┘ │ │ ║ ║ │ 🔵 证据: │ ║ ║ │ 代码位置: CoDealBgProReCodeTimeScheduler.java:64-67 │ ║ ║ └──────────────────────────────────────────────────────────────────────┘ ║ ║ 输出: List<CoDealEntity> coDealEntities ║ ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ │ ▼ ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ 【决策点B】交易列表为空检查 ║ ║ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ║ ║ ║ ║ ┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐ ║ ║ │ │ │ ║ ║ ▼ ▼ │ ║ ║ ┌───────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────────────────────┐ ║ ║ │ 条件: coDealEntities == null │ │ 条件: coDealEntities != null && size() > 0 │ ║ ║ │ || size() == 0 │ │ │ ║ ║ ├───────────────────────────────┤ ├───────────────────────────────────────────────────────────────────────────────────┤ ║ ║ │ │ │ │ ║ ║ │ 🟢 日志: │ │ ▼ │ ║ ║ │ LOGGER.info("There is no │ │ │ ║ ║ │ conforming co deal") │ │ ┌────────────────────────────────────────────────────┐ │ ║ ║ │ │ │ │ 设计依据层 │ │ ║ ║ │ 🟡 设计目的: │ │ ├────────────────────────────────────────────────────┤ │ ║ ║ │ · 无交易数据时跳过筛选 │ │ │ │ │ ║ ║ │ · 减少不必要的计算开销 │ │ │ 🟡 业务规则: │ │ ║ ║ │ │ │ │ · 无待检测交易时跳过二次筛选逻辑 │ │ ║ ║ │ │ │ │ │ │ ║ ║ │ │ │ │ 🔵 证据: │ │ ║ ║ │ │ │ │ 代码位置: CoDealBgProReCodeTimeScheduler.java:68-71 │ │ ║ ║ │ │ │ └────────────────────────────────────────────────────┘ │ ║ ║ └───────────────────────────────┘ └───────────────────────────────────────────────────────────────────────────────────┘ ║ ║ │ ║ ║ ▼ ║ ║ ╔═══════════════════════╗ ║ ║ ║ 【方法返回】 ║ ║ ║ ╚═══════════════════════╝ ║ ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ │ ▼ ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ 【步骤3】二次筛选 - 核心业务逻辑 ████████████████████████████████████████████████████ ║ ║ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ║ ║ ║ ║ ┌─────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────────────┐ ║ ║ │ 操作层 (怎么做) │ │ 设计依据层 (为什么) │ ║ ║ ├─────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────┤ ║ ║ │ │ │ │ ║ ║ │ FOR循环遍历每个CO交易: │ │ 🟡 业务规则(三项核心检测): │ ║ ║ │ for (CoDealEntity entity : coDealEntities) { │ │ │ ║ ║ │ │ │ 规则R1: 币种+属性一致性检测 │ ║ ║ │ 【3.1】实体转换 │ │ · buyCcy和sellCcy都不在ET配置中 │ ║ ║ │ CoDealDTO coDealDTO = │ │ · 且BG/Product/Region至少有一项不为空 │ ║ ║ │ CoDealAssembler.convertToDto(entity); │ │ → 判定为数据不一致,发送预警 │ ║ ║ │ │ │ │ ║ ║ │ 【3.2】币种过滤 - 买侧 │ │ 规则R2: 级联属性匹配检测 │ ║ ║ │ List<HedgeCurrencyEntity> buyCcyList = │ │ · 币种匹配时,BG/Product/Region三者必须同时匹配 │ ║ ║ │ coDealModelList.stream() │ │ → 任一不匹配触发预警 │ ║ ║ │ .filter(coDealModel -> │ │ │ ║ ║ │ coDealModel.getEtCurrencyCode() │ │ 🟠 风控目的: │ ║ ║ │ .equals(coDealDTO.getCurrencyCode())) │ │ · 确保CO交易属性与E-Treasury主数据一致 │ ║ ║ │ .collect(Collectors.toList()); │ │ · 防止数据不一致导致的合规风险 │ ║ ║ │ │ │ │ ║ ║ │ 【3.3】币种过滤 - 卖侧 │ │ 🔵 技术实现: │ ║ ║ │ List<HedgeCurrencyEntity> sellCcyList = │ │ · Stream API进行集合过滤 │ ║ ║ │ coDealModelList.stream() │ │ · HashSet去重存储待预警交易号 │ ║ ║ │ .filter(coDealModel -> │ │ · 使用CollectionUtils.isEmpty()检查集合为空 │ ║ ║ │ coDealModel.getEtCurrencyCode() │ │ │ ║ ║ │ .equals(coDealDTO.getCurrencyCode2())) │ │ │ ║ ║ │ .collect(Collectors.toList()); │ │ │ ║ ║ │ } │ │ │ ║ ║ │ │ │ │ ║ ║ └─────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────────────┘ ║ ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ │ ▼ ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ 【步骤3详情】二次筛选 - 决策分支逻辑 ║ ║ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ║ ║ ║ ║ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ║ ║ │ │ ║ ║ │ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ ║ ║ │ │ 【决策点C】币种是否在ET配置中? │ │ ║ ║ │ │ 条件: CollectionUtils.isEmpty(buyCcyList) && CollectionUtils.isEmpty(sellCcyList) │ │ ║ ║ │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ ║ ║ │ │ │ ║ ║ │ ┌─────────────────────┴─────────────────────┐ │ ║ ║ │ ▼ ▼ │ ║ ║ │ ┌────────────────────┐ ┌─────────────────────────┐ │ ║ ║ │ │ 是 │ │ 否 │ │ ║ ║ │ ├────────────────────┤ ├─────────────────────────┤ │ ║ ║ │ │ │ │ │ │ ║ ║ │ │ ▼进入子分支A │ │ ▼合并配置列表 │ │ ║ ║ │ │ │ └────────────┬────────────┘ │ ║ ║ │ └────────────────────┘ │ │ ║ ║ │ │ │ │ ║ ║ │ ▼ ▼ │ ║ ║ │ ┌────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐ │ ║ ║ │ │ 【决策点C.1】 │ │ 【决策点D】BG/Product/Region组合匹配? │ │ ║ ║ │ │ BG/Product/Reg │ │ 条件: 币种在配置中,校验三者是否完全匹配 │ │ ║ ║ │ │ 任一不为空? │ └────────────────────────────────┬────────────────────────────────┘ │ ║ ║ │ └─────────┬──────────┘ │ │ ║ ║ │ │ │ │ ║ ║ │ ┌───────────┴───────────┐ ┌─────────────┴─────────────┐ │ ║ ║ │ ▼ ▼ ▼ ▼ │ ║ ║ │ ┌──────────┐ ┌──────────────┐ ┌─────────────┐ ┌───────────────┐ │ ║ ║ │ │ 是 │ │ 否 │ │ 是 │ │ 否 │ │ ║ ║ │ ├──────────┤ ├──────────────┤ ├─────────────┤ ├───────────────┤ │ ║ ║ │ │ │ │ │ │ │ │ │ │ ║ ║ │ │【预警】 │ │ 不预警 │ │ 不预警 │ │ 【预警】 │ │ ║ ║ │ │加入 │ │ (跳过) │ │ (通过检测)│ │ 加入 │ │ ║ ║ │ │alarmSet │ │ │ │ │ │ alarmSet │ │ ║ ║ │ │ │ │ │ │ │ │ │ │ ║ ║ │ └──────────┘ └──────────────┘ └─────────────┘ └───────────────┘ │ ║ ║ │ │ ║ ║ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ ║ ║ ║ ║ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ ║ ║ │ 决策结果汇总: │ ║ ║ │ ┌─────────────────┬──────────────────┬────────────┬──────────────────────────────────────────────────────────────────────────┐ │ ║ ║ │ │ 场景 │ 币种在配置中 │ BG/Prod/Reg│ 结果/操作 │ │ ║ ║ │ ├─────────────────┼──────────────────┼────────────┼──────────────────────────────────────────────────────────────────────────┤ │ ║ ║ │ │ 场景A │ 都不在 │ 任一不为空 │ 【预警】币种缺失但填写了业务属性,数据可能不一致 │ │ ║ ║ │ │ 场景B │ 都不在 │ 全部为空 │ 【不预警】有意留空,不算数据问题 │ │ ║ ║ │ │ 场景C │ 至少一个在 │ 组合不匹配 │ 【预警】三者必须同时匹配才认为一致 │ │ ║ ║ │ │ 场景D │ 至少一个在 │ 组合匹配 │ 【不预警】完全匹配,数据一致性通过 │ │ ║ ║ │ └─────────────────┴──────────────────┴────────────┴──────────────────────────────────────────────────────────────────────────┘ │ ║ ║ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ ║ ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ │ ▼ ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ 【步骤4】构建预警消息内容 ████████████████████████████████████████████████████ ║ ║ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ║ ║ ║ ║ ┌─────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────────────┐ ║ ║ │ 操作层 (怎么做) │ │ 设计依据层 (为什么) │ ║ ║ ├─────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────┤ ║ ║ │ │ │ │ ║ ║ │ 1. 初始化消息构建器 │ │ 🟡 消息格式设计: │ ║ ║ │ StringJoiner stringJoiner = │ │ · 与FX Scheduler相同的消息格式 │ ║ ║ │ new StringJoiner(","); │ │ · eSpaceDigestsBuilder: 摘要信息 │ ║ ║ │ StringBuilder eSpaceDigestsBuilder = │ │ · stringJoiner: 详细内容(CO交易详情链接) │ ║ ║ │ new StringBuilder( │ │ │ ║ ║ │ "There is inconsistency of BG, region code, │ │ 🔵 技术实现: 复用FX Scheduler的方法 │ ║ ║ │ product code for below TMC deals, pls check" + │ │ · 调用bgProReCodeAlarmTimeScheduler.changeLine() │ ║ ║ │ System.lineSeparator() + "TMC deal no: "); │ │ · 传入不同的LABEL_1参数,构建CO交易详情链接 │ ║ ║ │ │ │ │ ║ ║ │ 2. 复用FX Scheduler的changeLine()方法 │ │ 🟣 链接参数: │ ║ ║ │ String digests = │ │ · LABEL_1 = "/deal/currency-option-detail?dealNumber=交易号" │ ║ ║ │ bgProReCodeAlarmTimeScheduler.changeLine( │ │ · 区别于FX的"/deal/fx-detail?dealNumber=交易号" │ ║ ║ │ alarmDealSet, │ │ │ ║ ║ │ eSpaceDigestsBuilder, │ │ │ ║ ║ │ count, │ │ │ ║ ║ │ stringJoiner, │ │ │ ║ ║ │ LABEL_1); │ │ │ ║ ║ │ │ │ │ ║ ║ └─────────────────────────────────────────────────────────┘ │ 🔵 证据: │ ║ ║ │ 代码位置: CoDealBgProReCodeTimeScheduler.java:77-88 │ ║ ║ └──────────────────────────────────────────────────────────────────────┘ ║ ║ 输出: StringJoiner (CO交易超链接列表), String digests (摘要) ║ ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ │ ▼ ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ 【决策点E】待预警交易号集合为空检查 ║ ║ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ║ ║ ║ ║ ┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐ ║ ║ │ │ │ ║ ║ ▼ ▼ │ ║ ║ ┌───────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────────────────────┐ ║ ║ │ 条件: alarmDealSet.size() == 0│ │ 条件: alarmDealSet.size() > 0 │ ║ ║ ├───────────────────────────────┤ ├───────────────────────────────────────────────────────────────────────────────────┤ ║ ║ │ │ │ │ ║ ║ │ 🟢 日志: │ │ ▼ │ ║ ║ │ LOGGER.info("There is no │ │ │ ║ ║ │ satisfied") │ │ ┌────────────────────────────────────────────────────┐ │ ║ ║ │ │ │ │ 设计依据层 │ │ ║ ║ │ 🟡 设计目的: │ │ ├────────────────────────────────────────────────────┤ │ ║ ║ │ · 无需发送空消息 │ │ │ │ │ ║ ║ │ · 减少无效消息推送 │ │ │ 🟡 业务规则: │ │ ║ ║ │ │ │ │ · 有不一致数据时才发送预警消息 │ │ ║ ║ │ │ │ │ │ │ ║ ║ │ │ │ │ 🔵 证据: │ │ ║ ║ │ │ │ │ 代码位置: CoDealBgProReCodeTimeScheduler.java:81-84 │ │ ║ ║ │ │ │ └────────────────────────────────────────────────────┘ │ ║ ║ └───────────────────────────────┘ └───────────────────────────────────────────────────────────────────────────────────┘ ║ ║ │ ║ ║ ▼ ║ ║ ╔═══════════════════════╗ ║ ║ ║ 【方法返回】 ║ ║ ║ ╚═══════════════════════╝ ║ ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ │ ▼ ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ 【步骤5】发送预警消息到ES ████████████████████████████████████████████████████ ║ ║ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ║ ║ ║ ║ ┌─────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────────────┐ ║ ║ │ 操作层 (怎么做) │ │ 设计依据层 (为什么) │ ║ ║ ├─────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────┤ ║ ║ │ │ │ │ ║ ║ │ executeOnTimeSplit(stringJoiner, digests); │ │ 🟠 消息通知目的: │ ║ ║ │ │ │ · 及时通知相关人员关注CO交易数据不一致问题 │ ║ ║ │ 内部调用: │ │ · 消息包含可点击链接,便于快速定位问题交易 │ ║ ║ │ messageNoticeService.sendAlarmEsMsg( │ │ │ ║ ║ │ SUBJECT_LABEL, │ │ 🔵 技术实现: │ ║ ║ │ // "tmc.CoDealBgProReCodeTimeScheduler.label" │ │ · 与FX Scheduler相同的消息发送机制 │ ║ ║ │ stringJoiner.toString(), // 超链接内容 │ │ · 使用不同的SUBJECT_LABEL区分消息模板 │ ║ ║ │ digests // 摘要 │ │ │ ║ ║ │ ); │ │ 🟣 消息模板: │ ║ ║ │ │ │ · "tmc.CoDealBgProReCodeTimeScheduler.label" │ ║ ║ │ 🟢 日志记录: │ │ · 区别于FX的"tmc.bgProReCodeAlarmTimeScheduler.label" │ ║ ║ │ LOGGER.info("send esMessage completed"); │ │ │ ║ ║ │ │ │ │ ║ ║ └─────────────────────────────────────────────────────────┘ │ 🔵 证据: │ ║ ║ │ 代码位置: CoDealBgProReCodeTimeScheduler.java:138-142 │ ║ ║ │ MessageNoticeService.java:54-76 │ ║ ║ └──────────────────────────────────────────────────────────────────────┘ ║ ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ │ ▼ ╔════════════╗ ║ 【结束】 ║ ╚════════════╝