Commit 70a63b781a80532fbe994946d6dc7b870d7e6144

Authored by 王彪总
1 parent 116f6bb0

feat(garden): 添加团队考核查询功能

- 新增 TeamAssessmentReqVO 和 TeamAssessmentRespVO 数据传输对象
- 在 OpenApiService 中添加 queryTeamAssessment 接口定义
- 实现团队考核查询逻辑,支持按班组和人员两种模式查询
- 添加考勤统计、工单统计、耗材统计等考核指标计算
- 创建 TeamAssessmentOpenController 提供开放 API 接口
- 实现个人考核指标和明细数据查询功能
- 添加扣分规则和考核得分计算机制
docs/all_test.html 0 → 100644
  1 +<!DOCTYPE html>
  2 +<html lang="zh-CN">
  3 +<head>
  4 +<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
  5 +<title>UrbanOps 全域平台 · 技术架构全景分享</title>
  6 +<link rel="stylesheet" href="ppt-assets/fonts.css">
  7 +<link rel="stylesheet" href="ppt-assets/base.css">
  8 +<link rel="stylesheet" href="ppt-assets/animations.css">
  9 +<link rel="stylesheet" href="ppt-assets/style.css">
  10 +<style>
  11 +.tpl-tech-sharing .h1{font-size:68px;line-height:1.05}
  12 +.tpl-tech-sharing .h2{font-size:44px}
  13 +.tpl-tech-sharing .slide{padding:56px 72px}
  14 +
  15 +/* SVG diagram area */
  16 +.svg-area{width:100%;max-width:1050px;flex:1;display:flex;align-items:center;justify-content:center;margin-top:8px}
  17 +.svg-area svg{width:100%;height:100%;max-height:480px}
  18 +
  19 +/* Architecture layers */
  20 +.arch-layers{margin-top:14px;display:grid;grid-template-rows:auto auto auto auto;gap:12px}
  21 +.arch-layers .al{display:grid;grid-template-columns:110px 1fr;align-items:stretch;gap:12px}
  22 +.arch-layers .al .al-name{display:flex;align-items:center;justify-content:center;background:var(--surface-2);border-radius:var(--radius);padding:12px 10px;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:600;color:var(--accent);text-align:center;border:1px solid var(--border)}
  23 +.arch-layers .al .al-cells{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px}
  24 +.arch-layers .al .acell{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 10px;text-align:center}
  25 +.arch-layers .al .acell .aci{font-size:20px;margin-bottom:2px}
  26 +.arch-layers .al .acell h4{font-size:13px;color:#fff;margin:3px 0 1px}
  27 +.arch-layers .al .acell p{font-size:10px;color:var(--text-3);margin:0}
  28 +.arch-layers .al.hl .acell{border-top:3px solid var(--accent);border-color:rgba(126,231,135,.35)}
  29 +.arch-layers .al.fw .acell{border-top:3px solid var(--accent-2);border-color:rgba(121,192,255,.35)}
  30 +
  31 +.biz-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:14px}
  32 +.biz-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:22px 18px}
  33 +.biz-card .biz-icon{font-size:32px;margin-bottom:8px}
  34 +.biz-card h4{font-size:17px;color:#fff;margin-bottom:6px}
  35 +.biz-card p{font-size:12px;color:var(--text-2);line-height:1.6;margin:0}
  36 +.biz-card .biz-tags{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}
  37 +.biz-card .biz-tags span{font-family:'JetBrains Mono',monospace;font-size:10px;padding:2px 8px;border-radius:4px;background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
  38 +.biz-card.ac{border-top:3px solid var(--accent)}
  39 +.biz-card.blue{border-top:3px solid var(--accent-2)}
  40 +.biz-card.purple{border-top:3px solid #d2a8ff}
  41 +
  42 +.col2{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:14px}
  43 +.col2 .box{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:22px}
  44 +
  45 +.flow-steps{display:flex;align-items:center;gap:8px;margin-top:14px}
  46 +.flow-steps .fs{flex:1;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 12px;text-align:center}
  47 +.flow-steps .fs .fs-icon{font-size:26px;margin-bottom:4px}
  48 +.flow-steps .fs h4{font-size:13px;color:#fff;margin:3px 0}
  49 +.flow-steps .fs p{font-size:10px;color:var(--text-3);margin:0}
  50 +.flow-steps .fs-arr{color:var(--accent);font-size:20px;font-family:'JetBrains Mono',monospace;flex-shrink:0}
  51 +.flow-steps .fs.hl{border-top:3px solid var(--accent)}
  52 +
  53 +.tg{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:12px}
  54 +.tg .tgc{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 12px;text-align:center}
  55 +.tg .tgc .tgci{font-size:28px;margin-bottom:4px}
  56 +.tg .tgc h4{font-size:14px;color:#fff;margin:4px 0}
  57 +.tg .tgc .tgt{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;margin-top:8px}
  58 +.tg .tgc .tgt span{font-family:'JetBrains Mono',monospace;font-size:10px;padding:2px 7px;border-radius:4px;background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
  59 +
  60 +.timeline{margin-top:14px}
  61 +.timeline .tl-item{display:grid;grid-template-columns:100px 1fr;gap:16px;padding:14px 0;border-bottom:1px dashed var(--border)}
  62 +.timeline .tl-item .tl-phase{font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent);font-weight:600}
  63 +.timeline .tl-item .tl-body h4{font-size:16px;color:#fff;margin:0 0 4px}
  64 +.timeline .tl-item .tl-body p{font-size:12px;color:var(--text-2);margin:0}
  65 +
  66 +.fw-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:12px}
  67 +.fw-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 14px}
  68 +.fw-card .fwi{font-size:22px;margin-bottom:6px}
  69 +.fw-card h4{font-size:14px;color:#fff;margin:4px 0}
  70 +.fw-card p{font-size:11px;color:var(--text-3);margin:0}
  71 +.fw-card.hl{border-top:2px solid var(--accent-2)}
  72 +
  73 +.roadmap-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:14px}
  74 +.roadmap-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:22px 18px}
  75 +.roadmap-card .r-period{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--accent);font-weight:600;margin-bottom:8px}
  76 +.roadmap-card h4{font-size:18px;color:#fff;margin-bottom:8px}
  77 +.roadmap-card ul{margin:0;padding-left:16px;font-size:12px;color:var(--text-2);line-height:1.8}
  78 +.roadmap-card.now{border-top:3px solid var(--accent)}
  79 +.roadmap-card.next{border-top:3px solid var(--accent-2)}
  80 +.roadmap-card.future{border-top:3px solid #d2a8ff}
  81 +
  82 +.stats-row{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-top:14px}
  83 +.stat-item{text-align:center;padding:18px 10px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}
  84 +.stat-item .stat-num{font-size:38px;font-weight:800;color:var(--accent);font-family:'JetBrains Mono',monospace}
  85 +.stat-item .stat-label{font-size:12px;color:var(--text-2);margin-top:4px}
  86 +
  87 +.invoc-flow{display:flex;flex-direction:column;gap:10px;margin-top:12px}
  88 +.invoc-flow .invoc-row{display:flex;align-items:center;gap:8px}
  89 +.invoc-flow .invoc-row .ir-label{width:100px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--accent);font-weight:600;flex-shrink:0}
  90 +.invoc-flow .invoc-row .ir-items{display:flex;gap:6px;flex:1;flex-wrap:wrap}
  91 +.invoc-flow .invoc-row .ir-items span{font-family:'JetBrains Mono',monospace;font-size:10px;padding:4px 10px;border-radius:5px;background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
  92 +.invoc-flow .ir-arrow{color:var(--text-3);font-size:16px;font-family:'JetBrains Mono',monospace;text-align:center;padding:2px 0}
  93 +
  94 +.deploy-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:14px}
  95 +.deploy-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px 16px;text-align:center}
  96 +.deploy-card .di{font-size:34px;margin-bottom:8px}
  97 +.deploy-card h4{font-size:16px;color:#fff;margin-bottom:6px}
  98 +.deploy-card ul{margin:0;padding:0;list-style:none;font-size:11px;color:var(--text-2);line-height:1.8}
  99 +.deploy-card.ci{border-top:3px solid var(--accent)}
  100 +.deploy-card.cn{border-top:3px solid var(--accent-2)}
  101 +.deploy-card.mon{border-top:3px solid #d2a8ff}
  102 +
  103 +.code-text{font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--text-2);background:var(--surface-2);padding:8px 14px;border-radius:8px;border:1px solid var(--border);display:inline-block}
  104 +
  105 +/* Layer stack (from file1) */
  106 +.layer-stack{display:flex;flex-direction:column;gap:10px;width:100%;max-width:1000px;margin-top:14px}
  107 +.layer-stack .layer{display:flex;align-items:center;gap:20px;padding:14px 22px;border-radius:12px;background:var(--surface);border:1px solid var(--border)}
  108 +.layer-stack .layer-label{font-weight:700;font-size:12px;min-width:110px;text-align:right;color:var(--accent);font-family:'JetBrains Mono',monospace}
  109 +.layer-stack .layer-content{display:flex;gap:6px;flex-wrap:wrap}
  110 +
  111 +/* Evolution flow */
  112 +.evo-flow{display:flex;align-items:center;gap:12px;flex-wrap:wrap;justify-content:center;margin-top:12px}
  113 +.evo-flow .evo-box{background:var(--surface);border:2px solid var(--border);border-radius:14px;padding:16px 24px;text-align:center;font-weight:600;min-width:120px;font-size:13px}
  114 +.evo-flow .evo-box.accent{border-color:var(--accent);background:rgba(126,231,135,.08)}
  115 +.evo-flow .evo-box.purple{border-color:#d2a8ff;background:rgba(210,168,255,.08)}
  116 +.evo-flow .evo-box.green{border-color:var(--accent-2);background:rgba(121,192,255,.08)}
  117 +.evo-flow .evo-arrow{font-size:1.6em;color:var(--text-3)}
  118 +</style>
  119 +</head>
  120 +<body class="tpl-tech-sharing">
  121 +<div class="deck">
  122 +
  123 + <!-- ====== 1. Cover ====== -->
  124 + <section class="slide" data-title="Cover">
  125 + <p class="kicker">urbanops · tech-sharing</p>
  126 + <h1 class="h1 anim-fade-up" data-anim="fade-up">城市运营全域平台<br><span style="background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent">技术架构全景解析</span></h1>
  127 + <p class="lede mt-m">业务架构 × 技术架构 × 系统架构 × 部署架构 — 一次讲透全域平台。</p>
  128 + <div class="speaker"><div class="av"></div><div><b>架构组</b><span>platform infra · 60 min + Q&amp;A</span></div></div>
  129 + <div class="deck-footer"><span class="mono">#spring-boot #smart-garden #urbanops #architecture #microservices</span><span class="slide-number" data-current="1" data-total="26"></span></div>
  130 + </section>
  131 +
  132 + <!-- ====== 2. Agenda ====== -->
  133 + <section class="slide" data-title="Agenda">
  134 + <p class="kicker">agenda.yaml</p>
  135 + <h2 class="h2">今日路线图</h2>
  136 + <div class="stack mt-l">
  137 + <div class="agenda-row"><span class="num">01</span><span class="t">项目概述与业务架构全景</span><span class="d">~6min</span></div>
  138 + <div class="agenda-row"><span class="num">02</span><span class="t">业务布局:园林 · 工单 · BPM · 全局</span><span class="d">~10min</span></div>
  139 + <div class="agenda-row"><span class="num">03</span><span class="t">系统架构总览 & 模块依赖关系</span><span class="d">~6min</span></div>
  140 + <div class="agenda-row"><span class="num">04</span><span class="t">技术架构分层 & 基础设施层</span><span class="d">~6min</span></div>
  141 + <div class="agenda-row"><span class="num">05</span><span class="t">核心技术栈 & 数据存储策略</span><span class="d">~6min</span></div>
  142 + <div class="agenda-row"><span class="num">06</span><span class="t">数据流 & 模块调用逻辑</span><span class="d">~6min</span></div>
  143 + <div class="agenda-row"><span class="num">07</span><span class="t">安全架构 & 可观测性</span><span class="d">~6min</span></div>
  144 + <div class="agenda-row"><span class="num">08</span><span class="t">部署架构 & 后续演进规划</span><span class="d">~6min</span></div>
  145 + <div class="agenda-row"><span class="num">09</span><span class="t">总结 &amp; Q&amp;A</span><span class="d">~5min</span></div>
  146 + </div>
  147 + </section>
  148 +
  149 + <!-- ====== 3. Project Overview ====== -->
  150 + <section class="slide" data-title="Project Overview">
  151 + <p class="kicker">// overview</p>
  152 + <h2 class="h2">UrbanOps 全域平台:<br>智慧城市运营管理底座</h2>
  153 + <p class="lede">基于 RuoYi-Vue-Pro 演进而来,面向城市园林绿化、工单调度、流程协同的综合运营管理平台。模块化单体架构向微服务演进中。</p>
  154 + <div class="stats-row mt-l">
  155 + <div class="stat-item anim-fade-up" data-anim="fade-up"><div class="stat-num">17</div><div class="stat-label">Maven 模块</div></div>
  156 + <div class="stat-item anim-fade-up" data-anim="fade-up" style="animation-delay:.05s"><div class="stat-num">7</div><div class="stat-label">激活业务模块</div></div>
  157 + <div class="stat-item anim-fade-up" data-anim="fade-up" style="animation-delay:.1s"><div class="stat-num">15</div><div class="stat-label">框架 Starter</div></div>
  158 + <div class="stat-item anim-fade-up" data-anim="fade-up" style="animation-delay:.15s"><div class="stat-num">10+</div><div class="stat-label">数据库支持</div></div>
  159 + <div class="stat-item anim-fade-up" data-anim="fade-up" style="animation-delay:.2s"><div class="stat-num">3.5.5</div><div class="stat-label">Spring Boot</div></div>
  160 + </div>
  161 + <div class="col2 mt-l">
  162 + <div class="box" style="border-top:3px solid var(--accent)">
  163 + <h4 style="color:#fff;margin:0 0 8px">后端技术基石</h4>
  164 + <p class="dim" style="font-size:13px;line-height:1.7">Java 17 · Spring Boot 3.5.5 · MyBatis Plus 3.5.14 · Spring Security 6.x · Flowable 7.0 · Redisson 3.51 · SkyWalking 9.5 · RocketMQ · Vert.x + MQTT · PF4J 插件框架</p>
  165 + </div>
  166 + <div class="box" style="border-top:3px solid var(--accent-2)">
  167 + <h4 style="color:#fff;margin:0 0 8px">前端生态 & 核心能力</h4>
  168 + <p class="dim" style="font-size:13px;line-height:1.7">Vue3 + Element Plus / Vben 双管理后台 · uni-app 移动端 · 大屏报表设计器 · 多租户 SaaS · IoT 设备接入 · 代码生成器 · Spring AI 集成</p>
  169 + </div>
  170 + </div>
  171 + </section>
  172 +
  173 + <!-- ====== 4. Business Architecture Overview ====== -->
  174 + <section class="slide" data-title="Business Architecture">
  175 + <p class="kicker">business · architecture</p>
  176 + <h2 class="h2">业务架构全景:<br>四大核心域 + 两大支撑域</h2>
  177 + <div class="biz-grid">
  178 + <div class="biz-card ac anim-fade-up" data-anim="fade-up">
  179 + <div class="biz-icon">🌳</div><h4>智慧园林管理</h4>
  180 + <p>树木资产管理、巡检计划、养护计划、绩效评估、物料出入库、机械成本核算</p>
  181 + <div class="biz-tags"><span>Garden</span><span>巡检</span><span>养护</span><span>绩效</span></div>
  182 + </div>
  183 + <div class="biz-card blue anim-fade-up" data-anim="fade-up" style="animation-delay:.05s">
  184 + <div class="biz-icon">📋</div><h4>工单调度系统</h4>
  185 + <p>事件上报、工单流转、物料明细、附件管理、地理位置编码、园林事件映射</p>
  186 + <div class="biz-tags"><span>Workorder</span><span>事件</span><span>物料</span></div>
  187 + </div>
  188 + <div class="biz-card purple anim-fade-up" data-anim="fade-up" style="animation-delay:.1s">
  189 + <div class="biz-icon">🔄</div><h4>工作流引擎</h4>
  190 + <p>Flowable 驱动,仿钉钉/飞书 + BPMN 双设计器,会签、或签、转办、委派、加签全覆盖</p>
  191 + <div class="biz-tags"><span>BPM</span><span>Flowable</span><span>审批</span></div>
  192 + </div>
  193 + <div class="biz-card ac anim-fade-up" data-anim="fade-up" style="animation-delay:.15s">
  194 + <div class="biz-icon">🌐</div><h4>全局业务管理</h4>
  195 + <p>问题上报、道路信息、工单提交详情、流程路径想法、跨模块数据聚合</p>
  196 + <div class="biz-tags"><span>Global</span><span>道路</span><span>问题</span></div>
  197 + </div>
  198 + <div class="biz-card blue anim-fade-up" data-anim="fade-up" style="animation-delay:.2s">
  199 + <div class="biz-icon">⚙️</div><h4>系统管理</h4>
  200 + <p>用户/角色/菜单/部门/岗位管理、租户与套餐、字典、通知公告、应用管理</p>
  201 + <div class="biz-tags"><span>System</span><span>RBAC</span><span>多租户</span></div>
  202 + </div>
  203 + <div class="biz-card purple anim-fade-up" data-anim="fade-up" style="animation-delay:.25s">
  204 + <div class="biz-icon">🏗️</div><h4>基础设施</h4>
  205 + <p>代码生成、文件存储、定时任务、配置管理、消息队列、API 日志、监控追踪</p>
  206 + <div class="biz-tags"><span>Infra</span><span>代码生成</span><span>监控</span></div>
  207 + </div>
  208 + </div>
  209 + </section>
  210 +
  211 + <!-- ====== 5. Garden Business ====== -->
  212 + <section class="slide" data-title="Business · Garden">
  213 + <p class="kicker">garden · smart-greening</p>
  214 + <h2 class="h2">核心业务:智慧园林管理</h2>
  215 + <p class="lede">覆盖园林绿化全生命周期——从资产登记到巡检养护,从绩效评估到成本核算。</p>
  216 + <div class="arch-layers">
  217 + <div class="al hl"><div class="al-name">管理端</div>
  218 + <div class="al-cells">
  219 + <div class="acell"><div class="aci">🌲</div><h4>树木资产管理</h4><p>古树名木 / 变更日志</p></div>
  220 + <div class="acell"><div class="aci">📅</div><h4>巡检计划管理</h4><p>计划制定 / 明细配置</p></div>
  221 + <div class="acell"><div class="aci">🔧</div><h4>养护计划管理</h4><p>养护任务 / 执行跟踪</p></div>
  222 + <div class="acell"><div class="aci">📊</div><h4>绩效管理</h4><p>指标定义 / 专家评定</p></div>
  223 + <div class="acell"><div class="aci">📦</div><h4>物料管理</h4><p>物料类型 / 出入库</p></div>
  224 + <div class="acell"><div class="aci">💰</div><h4>机械成本</h4><p>折旧 / 租赁核算</p></div>
  225 + </div>
  226 + </div>
  227 + <div class="al fw"><div class="al-name">移动端</div>
  228 + <div class="al-cells" style="grid-template-columns:repeat(5,1fr)">
  229 + <div class="acell"><div class="aci">📱</div><h4>首页概览</h4><p>数据汇总</p></div>
  230 + <div class="acell"><div class="aci">📍</div><h4>GPS 定位</h4><p>位置上报</p></div>
  231 + <div class="acell"><div class="aci">✅</div><h4>巡检提交</h4><p>现场打卡</p></div>
  232 + <div class="acell"><div class="aci">🛠️</div><h4>养护提交</h4><p>任务执行</p></div>
  233 + <div class="acell"><div class="aci">📝</div><h4>变更记录</h4><p>树木变更</p></div>
  234 + </div>
  235 + </div>
  236 + </div>
  237 + <div class="grid g4 mt-m">
  238 + <div class="card card-accent"><h4>定时任务</h4><p class="dim">自动生成巡检/养护计划</p></div>
  239 + <div class="card card-accent"><h4>数据权限</h4><p class="dim">按区域隔离园林数据</p></div>
  240 + <div class="card card-accent"><h4>时间区间</h4><p class="dim">灵活配置任务执行周期</p></div>
  241 + <div class="card card-accent"><h4>防重复提交</h4><p class="dim">计划明细幂等性保证</p></div>
  242 + </div>
  243 + </section>
  244 +
  245 + <!-- ====== 6. Workorder Business ====== -->
  246 + <section class="slide" data-title="Business · Workorder">
  247 + <p class="kicker">workorder · dispatch</p>
  248 + <h2 class="h2">工单调度:事件驱动 + 流程闭环</h2>
  249 + <div class="col2">
  250 + <div>
  251 + <div class="card card-accent" style="padding:22px">
  252 + <h4 style="color:#fff;margin:0 0 12px">工单核心实体模型</h4>
  253 + <div class="stack">
  254 + <div class="tag">📋 MainInfo — 工单主信息</div>
  255 + <div class="tag">📎 Attachment — 附件管理</div>
  256 + <div class="tag">📦 MaterialDetail — 物料明细</div>
  257 + <div class="tag">🎯 EventInfo — 事件信息</div>
  258 + <div class="tag">🔗 EventMappingInfo — 事件映射</div>
  259 + <div class="tag">🌳 GardenRef — 园林业务关联</div>
  260 + </div>
  261 + </div>
  262 + </div>
  263 + <div>
  264 + <div class="card" style="padding:22px;border-top:3px solid var(--accent-2)">
  265 + <h4 style="color:#fff;margin:0 0 12px">工单处理流程</h4>
  266 + <div style="display:flex;flex-direction:column;gap:8px">
  267 + <div style="display:flex;align-items:center;gap:8px">
  268 + <div class="fs" style="flex:1"><div class="fs-icon">📝</div><h4>事件上报</h4></div>
  269 + <span class="fs-arr">→</span>
  270 + <div class="fs" style="flex:1"><div class="fs-icon">🔍</div><h4>工单生成</h4></div>
  271 + <span class="fs-arr">→</span>
  272 + <div class="fs hl" style="flex:1"><div class="fs-icon">👤</div><h4>人员指派</h4></div>
  273 + </div>
  274 + <div style="display:flex;align-items:center;gap:8px">
  275 + <div class="fs" style="flex:1"><div class="fs-icon">🔧</div><h4>现场处理</h4></div>
  276 + <span class="fs-arr">→</span>
  277 + <div class="fs" style="flex:1"><div class="fs-icon">📋</div><h4>结果反馈</h4></div>
  278 + <span class="fs-arr">→</span>
  279 + <div class="fs" style="flex:1"><div class="fs-icon">✅</div><h4>审核结单</h4></div>
  280 + </div>
  281 + </div>
  282 + </div>
  283 + </div>
  284 + </div>
  285 + <div class="code-text mt-m">WorkorderModule → GardenModule (Feign API) · EventMapping 策略 · GeoCode 位置服务 · 数据权限隔离</div>
  286 + </section>
  287 +
  288 + <!-- ====== 7. BPM Business ====== -->
  289 + <section class="slide" data-title="Business · BPM">
  290 + <p class="kicker">bpm · flowable-engine</p>
  291 + <h2 class="h2">工作流引擎:同时支持<br>「轻量配置」与「深度编排」</h2>
  292 + <div class="grid g2 mt-l" style="align-items:start">
  293 + <div>
  294 + <div class="card card-accent" style="padding:22px">
  295 + <h4 style="color:var(--accent);font-family:'JetBrains Mono',monospace;font-size:13px">SIMPLE 设计器</h4>
  296 + <p class="dim" style="font-size:13px">仿钉钉/飞书体验,拖拽搭建表单流程,10 分钟完成配置</p>
  297 + <div class="stack mt-s">
  298 + <div class="tag">✦ 拖拽式表单设计</div>
  299 + <div class="tag">✦ 审批节点可视化配置</div>
  300 + <div class="tag">✦ 抄送人 / 条件分支</div>
  301 + <div class="tag">✦ 超时审批 / 自动提醒</div>
  302 + </div>
  303 + </div>
  304 + </div>
  305 + <div>
  306 + <div class="card" style="padding:22px;border-top:3px solid #d2a8ff">
  307 + <h4 style="color:#d2a8ff;font-family:'JetBrains Mono',monospace;font-size:13px">BPMN 设计器</h4>
  308 + <p class="dim" style="font-size:13px">基于 BPMN 2.0 标准,适配复杂多层级审批及自动化场景</p>
  309 + <div class="stack mt-s">
  310 + <div class="tag">✦ 会签 / 或签 / 依次审批</div>
  311 + <div class="tag">✦ 转办 / 委派 / 加签 / 减签</div>
  312 + <div class="tag">✦ 并行 / 包容 / 路由分支</div>
  313 + <div class="tag">✦ 父子流程 / 触发节点</div>
  314 + </div>
  315 + </div>
  316 + </div>
  317 + </div>
  318 + <div class="grid g3 mt-m">
  319 + <div class="card"><h4 style="color:#fff;font-size:14px">Flowable 7.0</h4><p class="dim">核心流程引擎</p></div>
  320 + <div class="card"><h4 style="color:#fff;font-size:14px">信创数据库兼容</h4><p class="dim">DM8 / KingBase / OpenGauss</p></div>
  321 + <div class="card"><h4 style="color:#fff;font-size:14px">动态表单</h4><p class="dim">节点级只读/编辑/隐藏权限</p></div>
  322 + </div>
  323 + </section>
  324 +
  325 + <!-- ====== 8. Global & System Business ====== -->
  326 + <section class="slide" data-title="Business · Global & System">
  327 + <p class="kicker">global · system · infra</p>
  328 + <h2 class="h2">业务支撑域:<br>公共服务与系统管理</h2>
  329 + <div class="grid g2 mt-l" style="align-items:start;gap:20px">
  330 + <div>
  331 + <h4 style="color:var(--accent-2);font-family:'JetBrains Mono',monospace;font-size:13px;margin-bottom:12px">🌐 Global 全局模块</h4>
  332 + <div class="stack">
  333 + <div class="card card-accent" style="padding:14px 16px"><h4 style="font-size:14px">问题上报管理</h4><p class="dim" style="font-size:11px">公共问题发现 → 流转到工单系统</p></div>
  334 + <div class="card card-accent" style="padding:14px 16px"><h4 style="font-size:14px">道路信息管理</h4><p class="dim" style="font-size:11px">道路基础数据维护</p></div>
  335 + <div class="card card-accent" style="padding:14px 16px"><h4 style="font-size:14px">工单提交详情</h4><p class="dim" style="font-size:11px">跨模块工单数据聚合视图</p></div>
  336 + <div class="card card-accent" style="padding:14px 16px"><h4 style="font-size:14px">流程路径想法</h4><p class="dim" style="font-size:11px">流程优化建议收集</p></div>
  337 + </div>
  338 + </div>
  339 + <div>
  340 + <h4 style="color:#d2a8ff;font-family:'JetBrains Mono',monospace;font-size:13px;margin-bottom:12px">⚙️ System + Infra 模块</h4>
  341 + <div class="stack">
  342 + <div class="card" style="padding:14px 16px;border-top:2px solid var(--accent-2)"><h4 style="font-size:14px">RBAC 权限体系</h4><p class="dim" style="font-size:11px">用户 → 角色 → 菜单 → 按钮权限 · 数据权限</p></div>
  343 + <div class="card" style="padding:14px 16px;border-top:2px solid var(--accent-2)"><h4 style="font-size:14px">SaaS 多租户</h4><p class="dim" style="font-size:11px">租户管理 · 套餐配置 · 数据隔离 · 透明化封装</p></div>
  344 + <div class="card" style="padding:14px 16px;border-top:2px solid var(--accent-2)"><h4 style="font-size:14px">代码生成器</h4><p class="dim" style="font-size:11px">一键生成 Java · Vue · SQL · 接口文档</p></div>
  345 + <div class="card" style="padding:14px 16px;border-top:2px solid var(--accent-2)"><h4 style="font-size:14px">消息通知中心</h4><p class="dim" style="font-size:11px">短信 · 邮件 · 站内信 · 多通道</p></div>
  346 + </div>
  347 + </div>
  348 + </div>
  349 + </section>
  350 +
  351 + <!-- ====== 9. Tech Stack Layers (from file1) ====== -->
  352 + <section class="slide" data-title="Tech Stack Layers">
  353 + <p class="kicker">tech-stack · layers</p>
  354 + <h2 class="h2">技术栈七层全景</h2>
  355 + <div class="layer-stack">
  356 + <div class="layer"><div class="layer-label">前端</div><div class="layer-content"><span class="tag">Vue 3</span><span class="tag">Element Plus</span><span class="tag">Vben Admin</span><span class="tag">uni-app</span><span class="tag">TypeScript</span></div></div>
  357 + <div class="layer"><div class="layer-label">网关 & 接入</div><div class="layer-content"><span class="tag">Vert.x</span><span class="tag">MQTT</span><span class="tag">HTTP/2</span><span class="tag">TCP</span><span class="tag">WebSocket</span></div></div>
  358 + <div class="layer"><div class="layer-label">业务框架</div><div class="layer-content"><span class="tag">Spring Boot 3.5</span><span class="tag">Spring Security</span><span class="tag">Flowable 7</span><span class="tag">Quartz</span><span class="tag">Spring AI</span><span class="tag">PF4J</span></div></div>
  359 + <div class="layer"><div class="layer-label">数据层</div><div class="layer-content"><span class="tag">MyBatis Plus</span><span class="tag">Druid</span><span class="tag">MySQL 8</span><span class="tag">Redis Sentinel</span><span class="tag">Redisson</span><span class="tag">Dynamic DS</span></div></div>
  360 + <div class="layer"><div class="layer-label">消息 & 集成</div><div class="layer-content"><span class="tag">RocketMQ</span><span class="tag">Kafka</span><span class="tag">RabbitMQ</span><span class="tag">JustAuth</span><span class="tag">UAA SSO</span><span class="tag">S3</span></div></div>
  361 + <div class="layer"><div class="layer-label">可观测性</div><div class="layer-content"><span class="tag">SkyWalking</span><span class="tag">SBA Admin</span><span class="tag">OpenTracing</span><span class="tag">Actuator</span><span class="tag">Knife4j</span></div></div>
  362 + <div class="layer"><div class="layer-label">部署 & DevOps</div><div class="layer-content"><span class="tag">Docker</span><span class="tag">docker-compose</span><span class="tag">Eclipse Temurin 21</span><span class="tag">Maven</span><span class="tag">Git</span></div></div>
  363 + </div>
  364 + </section>
  365 +
  366 + <!-- ====== 10. System Architecture Overview SVG (from file1 slide 4) ====== -->
  367 + <section class="slide" data-title="System Architecture Overview">
  368 + <p class="kicker">architecture · system-overview</p>
  369 + <h2 class="h2">系统架构总览 — 五层全景</h2>
  370 + <div class="svg-area">
  371 + <svg viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
  372 + <defs>
  373 + <linearGradient id="gAccent" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#38bdf8"/><stop offset="100%" stop-color="#818cf8"/></linearGradient>
  374 + <linearGradient id="gGreen" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#34d399"/><stop offset="100%" stop-color="#22c55e"/></linearGradient>
  375 + <linearGradient id="gPink" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f472b6"/><stop offset="100%" stop-color="#e879f9"/></linearGradient>
  376 + <filter id="shadow"><feDropShadow dx="0" dy="2" stdDeviation="4" flood-opacity="0.3"/></filter>
  377 + </defs>
  378 + <rect x="30" y="10" width="940" height="60" rx="10" fill="#161b22" stroke="#1e2d45" stroke-width="1.5"/>
  379 + <text x="500" y="35" text-anchor="middle" fill="#8b949e" font-size="12" font-weight="600">接入层 CLIENT LAYER</text>
  380 + <rect x="60" y="42" width="100" height="22" rx="6" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="1"/><text x="110" y="57" text-anchor="middle" fill="#38bdf8" font-size="10">Vue 3 Web</text>
  381 + <rect x="180" y="42" width="100" height="22" rx="6" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="1"/><text x="230" y="57" text-anchor="middle" fill="#38bdf8" font-size="10">uni-app 小程序</text>
  382 + <rect x="300" y="42" width="100" height="22" rx="6" fill="rgba(129,140,248,.12)" stroke="#818cf8" stroke-width="1"/><text x="350" y="57" text-anchor="middle" fill="#818cf8" font-size="10">IoT 设备</text>
  383 + <rect x="420" y="42" width="100" height="22" rx="6" fill="rgba(129,140,248,.12)" stroke="#818cf8" stroke-width="1"/><text x="470" y="57" text-anchor="middle" fill="#818cf8" font-size="10">第三方系统</text>
  384 + <line x1="500" y1="70" x2="500" y2="90" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="4,3"/>
  385 +
  386 + <rect x="30" y="90" width="940" height="80" rx="10" fill="#161b22" stroke="#1e2d45" stroke-width="1.5"/>
  387 + <text x="500" y="115" text-anchor="middle" fill="#8b949e" font-size="12" font-weight="600">网关与接入 GATEWAY LAYER</text>
  388 + <rect x="60" y="127" width="200" height="32" rx="8" fill="rgba(56,189,248,.1)" stroke="#38bdf8" stroke-width="1.5" filter="url(#shadow)"/><text x="160" y="147" text-anchor="middle" fill="#38bdf8" font-size="11" font-weight="600">UrbanOps Server :48081</text>
  389 + <rect x="300" y="127" width="200" height="32" rx="8" fill="rgba(52,211,153,.1)" stroke="#34d399" stroke-width="1.5" filter="url(#shadow)"/><text x="400" y="147" text-anchor="middle" fill="#34d399" font-size="11" font-weight="600">IoT 网关 :8092 (独立部署)</text>
  390 + <rect x="540" y="127" width="180" height="32" rx="8" fill="rgba(129,140,248,.1)" stroke="#818cf8" stroke-width="1.5" filter="url(#shadow)"/><text x="630" y="147" text-anchor="middle" fill="#818cf8" font-size="11" font-weight="600">UAA SSO :28201</text>
  391 + <line x1="500" y1="170" x2="500" y2="190" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="4,3"/>
  392 +
  393 + <rect x="30" y="190" width="940" height="130" rx="10" fill="#161b22" stroke="#1e2d45" stroke-width="1.5"/>
  394 + <text x="500" y="215" text-anchor="middle" fill="#8b949e" font-size="12" font-weight="600">业务模块层 BUSINESS MODULES</text>
  395 + <rect x="50" y="228" width="145" height="42" rx="8" fill="rgba(56,189,248,.1)" stroke="#38bdf8" stroke-width="1.5"/><text x="122" y="246" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">system</text><text x="122" y="260" text-anchor="middle" fill="#8b949e" font-size="9">用户·权限·租户</text>
  396 + <rect x="210" y="228" width="145" height="42" rx="8" fill="rgba(129,140,248,.1)" stroke="#818cf8" stroke-width="1.5"/><text x="282" y="246" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">garden</text><text x="282" y="260" text-anchor="middle" fill="#8b949e" font-size="9">园林养护·巡检</text>
  397 + <rect x="370" y="228" width="145" height="42" rx="8" fill="rgba(52,211,153,.1)" stroke="#34d399" stroke-width="1.5"/><text x="442" y="246" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">workorder</text><text x="442" y="260" text-anchor="middle" fill="#8b949e" font-size="9">工单·调度</text>
  398 + <rect x="530" y="228" width="145" height="42" rx="8" fill="rgba(244,114,182,.1)" stroke="#f472b6" stroke-width="1.5"/><text x="602" y="246" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">bpm</text><text x="602" y="260" text-anchor="middle" fill="#8b949e" font-size="9">Flowable 流程</text>
  399 + <rect x="690" y="228" width="145" height="42" rx="8" fill="rgba(251,191,36,.1)" stroke="#fbbf24" stroke-width="1.5"/><text x="762" y="246" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">gloabl</text><text x="762" y="260" text-anchor="middle" fill="#8b949e" font-size="9">全域运营</text>
  400 + <rect x="250" y="280" width="500" height="30" rx="8" fill="rgba(255,255,255,.03)" stroke="#334155" stroke-width="1" stroke-dasharray="4,3"/><text x="500" y="299" text-anchor="middle" fill="#8b949e" font-size="10">API 契约层: garden-api · workorder-api (接口定义 + Feign 风格调用)</text>
  401 + <line x1="500" y1="320" x2="500" y2="340" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="4,3"/>
  402 +
  403 + <rect x="30" y="340" width="940" height="50" rx="10" fill="#161b22" stroke="#1e2d45" stroke-width="1.5"/>
  404 + <text x="500" y="370" text-anchor="middle" fill="#8b949e" font-size="11" font-weight="600">基础设施层 INFRASTRUCTURE: 15个Spring Boot Starter (Web · Security · MyBatis · Redis · MQ · Job · Excel · WebSocket · Monitor · Protection · Tenant · DataPermission · IP · Test)</text>
  405 + <line x1="500" y1="390" x2="500" y2="408" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="4,3"/>
  406 +
  407 + <rect x="30" y="408" width="940" height="45" rx="10" fill="#0d1117" stroke="#1e2d45" stroke-width="1.5"/>
  408 + <text x="500" y="435" text-anchor="middle" fill="#8b949e" font-size="11" font-weight="600">数据层: MySQL 8 · Redis Sentinel · RocketMQ · Kafka · RabbitMQ · S3 · Druid · Redisson</text>
  409 + </svg>
  410 + </div>
  411 + </section>
  412 +
  413 + <!-- ====== 11. Module Dependencies SVG (from file1 slide 5) ====== -->
  414 + <section class="slide" data-title="Module Dependencies">
  415 + <p class="kicker">modules · dependency-graph</p>
  416 + <h2 class="h2">模块依赖关系图</h2>
  417 + <div class="svg-area">
  418 + <svg viewBox="0 0 1000 440" xmlns="http://www.w3.org/2000/svg">
  419 + <defs>
  420 + <marker id="arrowBlue" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#38bdf8"/></marker>
  421 + <marker id="arrowGray" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#64748b"/></marker>
  422 + </defs>
  423 + <rect x="350" y="8" width="300" height="38" rx="10" fill="rgba(251,191,36,.1)" stroke="#fbbf24" stroke-width="2"/><text x="500" y="32" text-anchor="middle" fill="#fbbf24" font-size="13" font-weight="700">urbanops-dependencies (BOM)</text>
  424 + <line x1="500" y1="46" x2="500" y2="70" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#arrowBlue)"/>
  425 +
  426 + <rect x="80" y="72" width="840" height="58" rx="10" fill="#161b22" stroke="#1e2d45" stroke-width="1.5"/>
  427 + <text x="500" y="94" text-anchor="middle" fill="#8b949e" font-size="11" font-weight="600">urbanops-framework</text>
  428 + <text x="500" y="113" text-anchor="middle" fill="#64748b" font-size="9">common · web · security · mybatis · redis · mq · job · excel · websocket · monitor · protection · tenant · data-permission · ip · test</text>
  429 +
  430 + <line x1="300" y1="130" x2="300" y2="155" stroke="#64748b" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  431 + <line x1="500" y1="130" x2="500" y2="155" stroke="#64748b" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  432 + <line x1="700" y1="130" x2="700" y2="155" stroke="#64748b" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  433 +
  434 + <rect x="210" y="158" width="180" height="40" rx="8" fill="rgba(99,102,241,.12)" stroke="#6366f1" stroke-width="1.5"/><text x="300" y="183" text-anchor="middle" fill="#a5b4fc" font-size="12" font-weight="600">module-infra</text>
  435 + <rect x="410" y="158" width="180" height="40" rx="8" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="1.5"/><text x="500" y="183" text-anchor="middle" fill="#38bdf8" font-size="12" font-weight="600">module-system</text>
  436 + <rect x="610" y="158" width="180" height="40" rx="8" fill="rgba(251,191,36,.12)" stroke="#fbbf24" stroke-width="1.5"/><text x="700" y="183" text-anchor="middle" fill="#fbbf24" font-size="12" font-weight="600">module-gloabl</text>
  437 +
  438 + <line x1="300" y1="198" x2="300" y2="230" stroke="#64748b" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  439 + <line x1="500" y1="198" x2="360" y2="230" stroke="#38bdf8" stroke-width="1.2"/>
  440 + <line x1="500" y1="198" x2="640" y2="230" stroke="#38bdf8" stroke-width="1.2"/>
  441 +
  442 + <rect x="210" y="233" width="180" height="40" rx="8" fill="rgba(244,114,182,.12)" stroke="#f472b6" stroke-width="1.5"/><text x="300" y="258" text-anchor="middle" fill="#f472b6" font-size="12" font-weight="600">module-bpm</text>
  443 + <rect x="85" y="308" width="200" height="40" rx="8" fill="rgba(52,211,153,.12)" stroke="#34d399" stroke-width="2"/><text x="185" y="333" text-anchor="middle" fill="#34d399" font-size="12" font-weight="600">module-garden</text>
  444 + <rect x="420" y="308" width="200" height="40" rx="8" fill="rgba(52,211,153,.12)" stroke="#34d399" stroke-width="2"/><text x="520" y="333" text-anchor="middle" fill="#34d399" font-size="12" font-weight="600">module-workorder</text>
  445 + <rect x="680" y="308" width="220" height="40" rx="8" fill="rgba(129,140,248,.08)" stroke="#818cf8" stroke-width="1" stroke-dasharray="5,3"/><text x="790" y="333" text-anchor="middle" fill="#818cf8" font-size="11" font-weight="600">module-api (契约层)</text>
  446 +
  447 + <line x1="240" y1="273" x2="185" y2="307" stroke="#f472b6" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  448 + <line x1="360" y1="273" x2="520" y2="307" stroke="#f472b6" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  449 + <line x1="285" y1="330" x2="420" y2="330" stroke="#818cf8" stroke-width="1" stroke-dasharray="5,3" marker-end="url(#arrowGray)"/>
  450 + <line x1="620" y1="330" x2="680" y2="330" stroke="#818cf8" stroke-width="1" stroke-dasharray="5,3" marker-end="url(#arrowGray)"/>
  451 +
  452 + <rect x="420" y="380" width="220" height="40" rx="8" fill="rgba(239,68,68,.12)" stroke="#ef4444" stroke-width="1.5" stroke-dasharray="6,3"/><text x="530" y="405" text-anchor="middle" fill="#ef4444" font-size="12" font-weight="600">IoT 网关 (独立进程 :8092)</text>
  453 + <line x1="620" y1="348" x2="560" y2="379" stroke="#ef4444" stroke-width="1" stroke-dasharray="5,3"/>
  454 +
  455 + <rect x="30" y="415" width="12" height="12" rx="2" fill="none" stroke="#34d399" stroke-width="1.5"/><text x="48" y="425" fill="#64748b" font-size="9">核心业务域</text>
  456 + <rect x="150" y="415" width="12" height="12" rx="2" fill="none" stroke="#38bdf8" stroke-width="1.5"/><text x="168" y="425" fill="#64748b" font-size="9">基础服务</text>
  457 + <rect x="260" y="415" width="12" height="12" rx="2" fill="none" stroke="#ef4444" stroke-width="1.5" stroke-dasharray="3,2"/><text x="278" y="425" fill="#64748b" font-size="9">独立部署</text>
  458 + <line x1="370" y1="421" x2="400" y2="421" stroke="#818cf8" stroke-width="1" stroke-dasharray="5,3"/><text x="408" y="425" fill="#64748b" font-size="9">API 契约调用</text>
  459 + </svg>
  460 + </div>
  461 + </section>
  462 +
  463 + <!-- ====== 12. Tech Architecture 5-Layer Model ====== -->
  464 + <section class="slide" data-title="Tech Architecture">
  465 + <p class="kicker">architecture · layers</p>
  466 + <h2 class="h2">技术架构五层模型</h2>
  467 + <p class="lede">从用户端到底层基础设施,清晰分层、逐级解耦。</p>
  468 + <div class="arch-layers anim-stagger-list" data-anim-target>
  469 + <div class="al fw"><div class="al-name" style="color:var(--accent-2)">接入层</div>
  470 + <div class="al-cells" style="grid-template-columns:repeat(4,1fr)">
  471 + <div class="acell"><div class="aci">🖥️</div><h4>Web Admin</h4><p>Vue3 + Element Plus</p></div>
  472 + <div class="acell"><div class="aci">📱</div><h4>Mobile App</h4><p>uni-app 跨端</p></div>
  473 + <div class="acell"><div class="aci">📊</div><h4>大屏展示</h4><p>GoView 可视化</p></div>
  474 + <div class="acell"><div class="aci">🔗</div><h4>开放 API</h4><p>第三方对接</p></div>
  475 + </div>
  476 + </div>
  477 + <div class="al fw"><div class="al-name" style="color:var(--accent-2)">网关层</div>
  478 + <div class="al-cells" style="grid-template-columns:repeat(4,1fr)">
  479 + <div class="acell"><div class="aci">🔐</div><h4>认证鉴权</h4><p>Spring Security + Token</p></div>
  480 + <div class="acell"><div class="aci">🚪</div><h4>路由转发</h4><p>Nginx / Gateway</p></div>
  481 + <div class="acell"><div class="aci">⚖️</div><h4>负载均衡</h4><p>多实例分发</p></div>
  482 + <div class="acell"><div class="aci">🛡️</div><h4>安全防护</h4><p>XSS / CSRF / SQL注入</p></div>
  483 + </div>
  484 + </div>
  485 + <div class="al hl"><div class="al-name">服务层</div>
  486 + <div class="al-cells" style="grid-template-columns:repeat(6,1fr)">
  487 + <div class="acell"><div class="aci">🌳</div><h4>Garden</h4><p>园林管理</p></div>
  488 + <div class="acell"><div class="aci">📋</div><h4>Workorder</h4><p>工单调度</p></div>
  489 + <div class="acell"><div class="aci">🔄</div><h4>BPM</h4><p>工作流</p></div>
  490 + <div class="acell"><div class="aci">🌐</div><h4>Global</h4><p>全局服务</p></div>
  491 + <div class="acell"><div class="aci">⚙️</div><h4>System</h4><p>系统管理</p></div>
  492 + <div class="acell"><div class="aci">🏗️</div><h4>Infra</h4><p>基础设施</p></div>
  493 + </div>
  494 + </div>
  495 + <div class="al"><div class="al-name">中间件</div>
  496 + <div class="al-cells" style="grid-template-columns:repeat(5,1fr)">
  497 + <div class="acell"><div class="aci">🗄️</div><h4>MySQL</h4><p>OLTP</p></div>
  498 + <div class="acell"><div class="aci">⚡</div><h4>Redis</h4><p>缓存/锁</p></div>
  499 + <div class="acell"><div class="aci">📨</div><h4>RocketMQ</h4><p>消息队列</p></div>
  500 + <div class="acell"><div class="aci">🔍</div><h4>Elasticsearch</h4><p>全文检索</p></div>
  501 + <div class="acell"><div class="aci">📡</div><h4>TDengine</h4><p>时序数据</p></div>
  502 + </div>
  503 + </div>
  504 + </div>
  505 + </section>
  506 +
  507 + <!-- ====== 13. Backend Layered Architecture ====== -->
  508 + <section class="slide" data-title="Backend Architecture">
  509 + <p class="kicker">backend · layered</p>
  510 + <h2 class="h2">后端分层架构:<br>Controller → Service → DAL 三层解耦</h2>
  511 + <div class="grid g2 mt-l" style="align-items:start;gap:20px">
  512 + <div class="stack">
  513 + <div class="card card-accent" style="padding:16px"><h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent)">controller/</h4><p class="dim" style="font-size:12px;margin-bottom:6px">接收请求、参数校验、VO 转换</p><div class="biz-tags"><span>admin/</span><span>app/</span><span>VO 对象</span><span>@Valid</span></div></div>
  514 + <div class="card card-accent" style="padding:16px"><h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent)">service/</h4><p class="dim" style="font-size:12px;margin-bottom:6px">业务逻辑编排、事务管理、策略模式</p><div class="biz-tags"><span>接口+实现</span><span>@Transactional</span><span>策略</span><span>Listener</span></div></div>
  515 + <div class="card card-accent" style="padding:16px"><h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent)">dal/</h4><p class="dim" style="font-size:12px;margin-bottom:6px">数据访问层:MySQL Mapper + Redis Cache</p><div class="biz-tags"><span>mysql/</span><span>redis/</span><span>dataobject/</span></div></div>
  516 + </div>
  517 + <div class="stack">
  518 + <div class="card" style="padding:16px;border-top:2px solid var(--accent-2)"><h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent-2)">convert/</h4><p class="dim" style="font-size:12px">MapStruct 对象转换层,编译期生成高性能映射代码</p></div>
  519 + <div class="card" style="padding:16px;border-top:2px solid var(--accent-2)"><h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent-2)">api/</h4><p class="dim" style="font-size:12px">模块间 Feign 接口定义 + DTO,实现跨模块调用</p></div>
  520 + <div class="card" style="padding:16px;border-top:2px solid var(--accent-2)"><h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent-2)">mq/ + job/</h4><p class="dim" style="font-size:12px">异步消息(producer/consumer)+ 定时任务调度</p></div>
  521 + <div class="card" style="padding:16px;border-top:2px solid var(--accent-2)"><h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent-2)">framework/</h4><p class="dim" style="font-size:12px">模块内框架代码:数据权限配置、Web 配置、操作日志等</p></div>
  522 + </div>
  523 + </div>
  524 + </section>
  525 +
  526 + <!-- ====== 14. Framework Infrastructure ====== -->
  527 + <section class="slide" data-title="Framework Infrastructure">
  528 + <p class="kicker">framework · starters</p>
  529 + <h2 class="h2">基础设施层:<br>15 个 Spring Boot Starter</h2>
  530 + <p class="lede">将通用能力抽象为可复用的 Starter 组件,所有业务模块按需引入。</p>
  531 + <div class="fw-grid">
  532 + <div class="fw-card hl anim-fade-up" data-anim="fade-up"><div class="fwi">🌐</div><h4>web</h4><p>全局异常处理 · 统一返回 · CORS · Swagger</p></div>
  533 + <div class="fw-card hl anim-fade-up" data-anim="fade-up" style="animation-delay:.05s"><div class="fwi">🔐</div><h4>security</h4><p>Spring Security · Token 鉴权 · SSO</p></div>
  534 + <div class="fw-card hl anim-fade-up" data-anim="fade-up" style="animation-delay:.1s"><div class="fwi">🗄️</div><h4>mybatis</h4><p>MyBatis Plus · 多数据源 · 联表查询</p></div>
  535 + <div class="fw-card hl anim-fade-up" data-anim="fade-up" style="animation-delay:.15s"><div class="fwi">⚡</div><h4>redis</h4><p>Redisson · 分布式锁 · 缓存策略</p></div>
  536 + <div class="fw-card hl anim-fade-up" data-anim="fade-up" style="animation-delay:.2s"><div class="fwi">📨</div><h4>mq</h4><p>RocketMQ 集成 · 消息幂等 · 重试</p></div>
  537 + <div class="fw-card hl anim-fade-up" data-anim="fade-up" style="animation-delay:.25s"><div class="fwi">🛡️</div><h4>protection</h4><p>分布式锁 · 幂等 · 限流 · 熔断</p></div>
  538 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.3s"><div class="fwi">📊</div><h4>monitor</h4><p>SkyWalking · Admin · 日志中心</p></div>
  539 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.35s"><div class="fwi">⏰</div><h4>job</h4><p>Quartz 定时任务 · 执行日志</p></div>
  540 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.4s"><div class="fwi">📝</div><h4>excel</h4><p>FastExcel 导入导出</p></div>
  541 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.45s"><div class="fwi">🔌</div><h4>websocket</h4><p>实时通信 · Token 校验 · 集群</p></div>
  542 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.5s"><div class="fwi">🏢</div><h4>biz-tenant</h4><p>多租户数据隔离 · 透明封装</p></div>
  543 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.55s"><div class="fwi">🔒</div><h4>biz-data-permission</h4><p>数据权限 · 行级过滤</p></div>
  544 + </div>
  545 + </section>
  546 +
  547 + <!-- ====== 15. Core Tech Stack Grid ====== -->
  548 + <section class="slide" data-title="Tech Stack Grid">
  549 + <p class="kicker">tech-stack · ecosystem</p>
  550 + <h2 class="h2">核心技术栈全景</h2>
  551 + <div class="tg">
  552 + <div class="tgc anim-fade-up" data-anim="fade-up"><div class="tgci">☕</div><h4>核心框架</h4><div class="tgt"><span>Spring Boot 3.5.5</span><span>Spring MVC 6.x</span><span>Spring Security 6.x</span></div></div>
  553 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.05s"><div class="tgci">🗄️</div><h4>持久层</h4><div class="tgt"><span>MyBatis Plus 3.5.14</span><span>Druid 1.2.27</span><span>Dynamic-DS 4.3.1</span></div></div>
  554 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.1s"><div class="tgci">⚡</div><h4>缓存 & 锁</h4><div class="tgt"><span>Redis 7.x</span><span>Redisson 3.51</span><span>Lock4j 2.2.7</span></div></div>
  555 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.15s"><div class="tgci">📨</div><h4>消息队列</h4><div class="tgt"><span>RocketMQ</span><span>Spring-RocketMQ</span><span>Redis Stream</span></div></div>
  556 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.2s"><div class="tgci">🔄</div><h4>工作流</h4><div class="tgt"><span>Flowable 7.0.1</span><span>BPMN 2.0</span><span>SIMPLE 设计器</span></div></div>
  557 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.25s"><div class="tgci">📡</div><h4>IoT & 通信</h4><div class="tgt"><span>Vert.x 4.5.13</span><span>Netty 4.2.4</span><span>MQTT 1.2.5</span></div></div>
  558 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.3s"><div class="tgci">🔍</div><h4>可观测性</h4><div class="tgt"><span>SkyWalking 9.5</span><span>Admin 3.5.2</span><span>OpenTracing</span></div></div>
  559 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.35s"><div class="tgci">📋</div><h4>API 文档</h4><div class="tgt"><span>Springdoc 2.8.11</span><span>Knife4j 4.5.0</span><span>Swagger 3</span></div></div>
  560 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.4s"><div class="tgci">🔌</div><h4>插件 & 工具</h4><div class="tgt"><span>PF4J 0.9.0</span><span>MapStruct 1.6.3</span><span>Hutool 5.8+6.0</span></div></div>
  561 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.45s"><div class="tgci">🧪</div><h4>测试</h4><div class="tgt"><span>JUnit 5</span><span>Mockito 5.x</span><span>Jedis-Mock</span></div></div>
  562 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.5s"><div class="tgci">🗄️</div><h4>多数据库</h4><div class="tgt"><span>MySQL 8.x</span><span>DM8</span><span>KingBase</span><span>OpenGauss</span></div></div>
  563 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.55s"><div class="tgci">🐳</div><h4>部署运维</h4><div class="tgt"><span>Docker</span><span>K8s</span><span>Jenkins</span><span>Harbor</span></div></div>
  564 + </div>
  565 + </section>
  566 +
  567 + <!-- ====== 16. Data Flow & Communication SVG (from file1 slide 7) ====== -->
  568 + <section class="slide" data-title="Data Flow & Communication">
  569 + <p class="kicker">data-flow · communication</p>
  570 + <h2 class="h2">数据流与通信架构</h2>
  571 + <div class="svg-area">
  572 + <svg viewBox="0 0 1000 420" xmlns="http://www.w3.org/2000/svg">
  573 + <defs>
  574 + <marker id="arrGreen" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#34d399"/></marker>
  575 + <marker id="arrBlue2" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#38bdf8"/></marker>
  576 + <marker id="arrPink" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#f472b6"/></marker>
  577 + <marker id="arrYellow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#fbbf24"/></marker>
  578 + </defs>
  579 + <rect x="350" y="160" width="300" height="60" rx="14" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="2.5" filter="url(#shadow)"/><text x="500" y="187" text-anchor="middle" fill="#e2e8f0" font-size="15" font-weight="700">UrbanOps Server</text><text x="500" y="205" text-anchor="middle" fill="#8b949e" font-size="10">核心业务处理引擎</text>
  580 +
  581 + <rect x="60" y="60" width="160" height="50" rx="10" fill="rgba(59,130,246,.12)" stroke="#3b82f6" stroke-width="1.5"/><text x="140" y="82" text-anchor="middle" fill="#60a5fa" font-size="12" font-weight="600">MySQL 8</text><text x="140" y="98" text-anchor="middle" fill="#8b949e" font-size="9">主从 + Druid 连接池</text>
  582 + <line x1="220" y1="85" x2="348" y2="175" stroke="#3b82f6" stroke-width="1.2" stroke-dasharray="6,3" marker-end="url(#arrBlue2)"/>
  583 +
  584 + <rect x="60" y="150" width="160" height="50" rx="10" fill="rgba(239,68,68,.12)" stroke="#ef4444" stroke-width="1.5"/><text x="140" y="172" text-anchor="middle" fill="#f87171" font-size="12" font-weight="600">Redis Sentinel</text><text x="140" y="188" text-anchor="middle" fill="#8b949e" font-size="9">缓存 · 分布式锁 · Token</text>
  585 + <line x1="220" y1="175" x2="348" y2="180" stroke="#ef4444" stroke-width="1.2" stroke-dasharray="6,3" marker-end="url(#arrBlue2)"/>
  586 +
  587 + <rect x="60" y="240" width="160" height="50" rx="10" fill="rgba(52,211,153,.12)" stroke="#34d399" stroke-width="1.5"/><text x="140" y="262" text-anchor="middle" fill="#4ade80" font-size="12" font-weight="600">MQ 消息队列</text><text x="140" y="278" text-anchor="middle" fill="#8b949e" font-size="9">RocketMQ · Kafka · RabbitMQ</text>
  588 + <line x1="220" y1="265" x2="348" y2="195" stroke="#34d399" stroke-width="1.2" stroke-dasharray="6,3" marker-end="url(#arrGreen)"/>
  589 +
  590 + <rect x="740" y="60" width="200" height="50" rx="10" fill="rgba(244,114,182,.12)" stroke="#f472b6" stroke-width="1.5"/><text x="840" y="82" text-anchor="middle" fill="#f472b6" font-size="12" font-weight="600">IoT 网关 :8092</text><text x="840" y="98" text-anchor="middle" fill="#8b949e" font-size="9">Vert.x · MQTT · TCP</text>
  591 + <line x1="740" y1="85" x2="652" y2="175" stroke="#f472b6" stroke-width="1.2" stroke-dasharray="6,3" marker-end="url(#arrPink)"/>
  592 +
  593 + <rect x="740" y="160" width="200" height="50" rx="10" fill="rgba(251,191,36,.12)" stroke="#fbbf24" stroke-width="1.5"/><text x="840" y="182" text-anchor="middle" fill="#fbbf24" font-size="12" font-weight="600">IoT 设备层</text><text x="840" y="198" text-anchor="middle" fill="#8b949e" font-size="9">传感器 · GPS · 控制器</text>
  594 + <line x1="740" y1="185" x2="940" y2="185" stroke="#fbbf24" stroke-width="1.2" stroke-dasharray="4,3"/>
  595 + <line x1="840" y1="210" x2="840" y2="350" stroke="#fbbf24" stroke-width="1" stroke-dasharray="4,3"/>
  596 +
  597 + <rect x="740" y="240" width="200" height="50" rx="10" fill="rgba(129,140,248,.12)" stroke="#818cf8" stroke-width="1.5"/><text x="840" y="262" text-anchor="middle" fill="#a5b4fc" font-size="12" font-weight="600">对象存储 S3</text><text x="840" y="278" text-anchor="middle" fill="#8b949e" font-size="9">文件 · 图片 · 附件</text>
  598 + <line x1="740" y1="265" x2="652" y2="205" stroke="#818cf8" stroke-width="1.2" stroke-dasharray="6,3" marker-end="url(#arrBlue2)"/>
  599 +
  600 + <rect x="60" y="330" width="160" height="50" rx="10" fill="rgba(244,114,182,.12)" stroke="#f472b6" stroke-width="1.5"/><text x="140" y="352" text-anchor="middle" fill="#f472b6" font-size="12" font-weight="600">Flowable 7</text><text x="140" y="368" text-anchor="middle" fill="#8b949e" font-size="9">工作流引擎</text>
  601 + <line x1="220" y1="355" x2="348" y2="220" stroke="#f472b6" stroke-width="1.2" stroke-dasharray="6,3"/>
  602 +
  603 + <rect x="320" y="340" width="160" height="50" rx="10" fill="rgba(251,191,36,.12)" stroke="#fbbf24" stroke-width="1.5"/><text x="400" y="362" text-anchor="middle" fill="#fbbf24" font-size="12" font-weight="600">Quartz 调度</text><text x="400" y="378" text-anchor="middle" fill="#8b949e" font-size="9">定时任务集群</text>
  604 + <line x1="480" y1="340" x2="500" y2="222" stroke="#fbbf24" stroke-width="1.2" stroke-dasharray="6,3"/>
  605 +
  606 + <rect x="560" y="340" width="190" height="50" rx="10" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="1.5"/><text x="655" y="362" text-anchor="middle" fill="#38bdf8" font-size="12" font-weight="600">WebSocket</text><text x="655" y="378" text-anchor="middle" fill="#8b949e" font-size="9">实时消息推送</text>
  607 + <line x1="560" y1="365" x2="502" y2="222" stroke="#38bdf8" stroke-width="1.2" stroke-dasharray="6,3"/>
  608 + </svg>
  609 + </div>
  610 + </section>
  611 +
  612 + <!-- ====== 17. Data Storage Strategy ====== -->
  613 + <section class="slide" data-title="Data Storage">
  614 + <p class="kicker">database · strategy</p>
  615 + <h2 class="h2">数据存储策略:<br>多数据源 + 多数据库 + 读写分离</h2>
  616 + <div class="grid g2 mt-l" style="align-items:start;gap:20px">
  617 + <div>
  618 + <div class="card card-accent" style="padding:22px"><h4 style="font-family:'JetBrains Mono',monospace;font-size:14px;color:var(--accent)">Dynamic-Datasource 多数据源</h4>
  619 + <div class="stack mt-m"><div class="tag"><span style="color:var(--accent)">●</span> 一主多从读写分离</div><div class="tag"><span style="color:var(--accent)">●</span> @DS 注解切换数据源</div><div class="tag"><span style="color:var(--accent)">●</span> Druid 连接池监控</div></div></div>
  620 + <div class="card" style="padding:22px;border-top:3px solid var(--accent-2);margin-top:12px"><h4 style="font-family:'JetBrains Mono',monospace;font-size:14px;color:var(--accent-2)">Redis 缓存体系</h4>
  621 + <div class="stack mt-m"><div class="tag">○ Redisson 分布式锁 · 幂等键</div><div class="tag">○ Redis Stream 轻量 MQ</div><div class="tag">○ Token 存储 & 在线用户管理</div></div></div>
  622 + </div>
  623 + <div>
  624 + <div class="card" style="padding:22px;border-top:3px solid #d2a8ff"><h4 style="font-family:'JetBrains Mono',monospace;font-size:14px;color:#d2a8ff">多数据库兼容矩阵</h4>
  625 + <div class="stack mt-m"><div class="tag"><span style="color:#d2a8ff">■</span> MySQL 5.7/8.0+ — 主力库</div><div class="tag"><span style="color:#d2a8ff">■</span> 达梦 DM8 — 国产信创</div><div class="tag"><span style="color:#d2a8ff">■</span> 人大金仓 KingBase 8.6 — 信创</div><div class="tag"><span style="color:#d2a8ff">■</span> OpenGauss 5.1 — 华为开源</div><div class="tag"><span style="color:#d2a8ff">■</span> Oracle / PostgreSQL / SQL Server</div><div class="tag"><span style="color:#d2a8ff">■</span> TDengine 3.7 — IoT 时序数据</div></div></div>
  626 + <div class="card card-accent" style="padding:22px;margin-top:12px"><h4 style="font-size:14px;color:#fff">数据隔离策略</h4><p class="dim" style="font-size:12px">SaaS 多租户:共享数据表 + tenant_id 字段隔离,biz-tenant Starter 透明拦截 SQL</p></div>
  627 + </div>
  628 + </div>
  629 + </section>
  630 +
  631 + <!-- ====== 18. Module Invocation Logic ====== -->
  632 + <section class="slide" data-title="Invocation Logic">
  633 + <p class="kicker">invocation · communication</p>
  634 + <h2 class="h2">模块调用逻辑:三种通信通道</h2>
  635 + <div class="invoc-flow mt-l">
  636 + <div class="invoc-row"><div class="ir-label" style="color:var(--accent)">Feign API</div><div class="ir-items"><span>System → Dept API</span><span>System → User API</span><span>Workorder → Garden API</span><span>Garden → System API</span></div></div>
  637 + <div class="ir-arrow">▼ 同步调用 · 模块间 RPC · 请求/响应模式 ▼</div>
  638 + <div class="invoc-row"><div class="ir-label" style="color:var(--accent-2)">MQ 消息</div><div class="ir-items"><span>SMS Producer → Consumer</span><span>Mail Producer → Consumer</span><span>Event → 多消费者</span><span>RocketMQ Topic</span><span>Redis Stream</span></div></div>
  639 + <div class="ir-arrow">▼ 异步解耦 · 事件驱动 · 最终一致性 ▼</div>
  640 + <div class="invoc-row"><div class="ir-label" style="color:#d2a8ff">直接依赖</div><div class="ir-items"><span>Server → All Modules</span><span>Workorder → Garden Service</span><span>BPM → System User</span><span>Garden → Infra File</span></div></div>
  641 + </div>
  642 + <div class="grid g3 mt-l">
  643 + <div class="card card-accent"><h4 style="font-size:14px">api/ 包设计</h4><p class="dim" style="font-size:12px">每个模块暴露 Feign 接口 + DTO,其他模块引入依赖即可调用,编译期类型安全。</p></div>
  644 + <div class="card card-accent"><h4 style="font-size:14px">mq/ 包设计</h4><p class="dim" style="font-size:12px">Producer 发送消息,Consumer 消费处理。SMS / Mail 异步发送,不解耦主流程。</p></div>
  645 + <div class="card card-accent"><h4 style="font-size:14px">service/ 直接注入</h4><p class="dim" style="font-size:12px">同模块内 Service 互相注入。跨模块通过 api 包或直接依赖。</p></div>
  646 + </div>
  647 + </section>
  648 +
  649 + <!-- ====== 19. Message & Async ====== -->
  650 + <section class="slide" data-title="Message & Async">
  651 + <p class="kicker">async · event-driven</p>
  652 + <h2 class="h2">异步消息与事件驱动</h2>
  653 + <div class="col2">
  654 + <div><div class="card card-accent" style="padding:20px"><h4 style="color:#fff;margin:0 0 10px">RocketMQ 消息架构</h4>
  655 + <div class="stack"><div class="tag">📤 Producer → Topic</div><div class="tag">📥 Consumer 拉取并处理</div><div class="tag">🔄 消费失败自动重试</div><div class="tag">⚡ 顺序消息: 全局/分区有序</div><div class="tag">🔒 事务消息: 分布式事务最终一致性</div></div></div></div>
  656 + <div><div class="card" style="padding:20px;border-top:3px solid var(--accent-2)"><h4 style="color:#fff;margin:0 0 10px">典型异步场景</h4>
  657 + <div class="stack"><div class="tag">📧 SMS → MQ → 消费者推送</div><div class="tag">📨 邮件 → MQ → 消费者投递</div><div class="tag">🔔 站内信 → Redis Stream 广播</div><div class="tag">📋 操作日志 → 异步记录</div><div class="tag">🔄 工单事件 → 通知园林模块</div></div></div></div>
  658 + </div>
  659 + <div class="grid g2 mt-m">
  660 + <div class="card card-accent"><h4 style="font-size:14px">定时任务 (Quartz)</h4><p class="dim" style="font-size:12px">巡检计划自动生成 · 养护计划定时创建 · 数据统计报表 · 超时任务自动处理</p></div>
  661 + <div class="card card-accent"><h4 style="font-size:14px">服务保障 (Protection)</h4><p class="dim" style="font-size:12px">@Lock4j 分布式锁防并发 · @Idempotent 幂等性 · @RateLimiter 限流保护 · 熔断降级</p></div>
  662 + </div>
  663 + </section>
  664 +
  665 + <!-- ====== 20. Security Architecture SVG (from file1 slide 8) ====== -->
  666 + <section class="slide" data-title="Security Architecture">
  667 + <p class="kicker">security · auth-flow</p>
  668 + <h2 class="h2">安全与认证架构</h2>
  669 + <div class="svg-area">
  670 + <svg viewBox="0 0 1000 400" xmlns="http://www.w3.org/2000/svg">
  671 + <rect x="30" y="30" width="120" height="60" rx="10" fill="rgba(56,189,248,.1)" stroke="#38bdf8" stroke-width="1.5"/><text x="90" y="55" text-anchor="middle" fill="#38bdf8" font-size="11" font-weight="600">客户端</text><text x="90" y="73" text-anchor="middle" fill="#8b949e" font-size="9">Web / 小程序 / App</text>
  672 +
  673 + <rect x="200" y="30" width="760" height="60" rx="10" fill="#161b22" stroke="#1e2d45" stroke-width="1.5"/><text x="580" y="52" text-anchor="middle" fill="#8b949e" font-size="11" font-weight="600">Security Filter Chain</text>
  674 + <rect x="215" y="58" width="130" height="24" rx="6" fill="rgba(239,68,68,.12)" stroke="#ef4444" stroke-width="1"/><text x="280" y="74" text-anchor="middle" fill="#f87171" font-size="9">API 签名校验</text>
  675 + <rect x="355" y="58" width="120" height="24" rx="6" fill="rgba(251,191,36,.12)" stroke="#fbbf24" stroke-width="1"/><text x="415" y="74" text-anchor="middle" fill="#fbbf24" font-size="9">限流 & 幂等</text>
  676 + <rect x="485" y="58" width="120" height="24" rx="6" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="1"/><text x="545" y="74" text-anchor="middle" fill="#38bdf8" font-size="9">Token 认证</text>
  677 + <rect x="615" y="58" width="100" height="24" rx="6" fill="rgba(129,140,248,.12)" stroke="#818cf8" stroke-width="1"/><text x="665" y="74" text-anchor="middle" fill="#a5b4fc" font-size="9">权限校验</text>
  678 + <rect x="725" y="58" width="100" height="24" rx="6" fill="rgba(52,211,153,.12)" stroke="#34d399" stroke-width="1"/><text x="775" y="74" text-anchor="middle" fill="#4ade80" font-size="9">数据脱敏</text>
  679 + <rect x="835" y="58" width="110" height="24" rx="6" fill="rgba(244,114,182,.12)" stroke="#f472b6" stroke-width="1"/><text x="890" y="74" text-anchor="middle" fill="#f472b6" font-size="9">操作日志</text>
  680 + <line x1="150" y1="60" x2="198" y2="60" stroke="#38bdf8" stroke-width="2" marker-end="url(#arrBlue2)"/>
  681 +
  682 + <rect x="200" y="120" width="760" height="110" rx="10" fill="#161b22" stroke="#1e2d45" stroke-width="1.5"/><text x="580" y="145" text-anchor="middle" fill="#8b949e" font-size="11" font-weight="600">认证与授权中心</text>
  683 + <rect x="220" y="155" width="200" height="42" rx="8" fill="rgba(56,189,248,.1)" stroke="#38bdf8" stroke-width="1.5"/><text x="320" y="174" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">JWT Token</text><text x="320" y="189" text-anchor="middle" fill="#8b949e" font-size="9">Redis 存储 · 自动续期</text>
  684 + <rect x="450" y="155" width="200" height="42" rx="8" fill="rgba(129,140,248,.1)" stroke="#818cf8" stroke-width="1.5"/><text x="550" y="174" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">UAA SSO</text><text x="550" y="189" text-anchor="middle" fill="#8b949e" font-size="9">OAuth2 · 单点登录</text>
  685 + <rect x="680" y="155" width="250" height="42" rx="8" fill="rgba(52,211,153,.1)" stroke="#34d399" stroke-width="1.5"/><text x="805" y="174" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">JustAuth 社交登录</text><text x="805" y="189" text-anchor="middle" fill="#8b949e" font-size="9">钉钉 · 企业微信 · 小程序 · 公众号</text>
  686 + <rect x="220" y="205" width="340" height="20" rx="5" fill="rgba(251,191,36,.08)" stroke="#fbbf24" stroke-width="0.8"/><text x="390" y="219" text-anchor="middle" fill="#fbbf24" font-size="9">多租户数据隔离 (SaaS)</text>
  687 + <rect x="580" y="205" width="350" height="20" rx="5" fill="rgba(244,114,182,.08)" stroke="#f472b6" stroke-width="0.8"/><text x="755" y="219" text-anchor="middle" fill="#f472b6" font-size="9">部门/用户级数据权限 (RBAC + Data Scope)</text>
  688 + <line x1="580" y1="230" x2="580" y2="260" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="5,3"/><text x="600" y="250" fill="#64748b" font-size="9">授权通过</text>
  689 +
  690 + <rect x="200" y="260" width="760" height="50" rx="10" fill="#0d1117" stroke="#1e2d45" stroke-width="1.5"/><text x="580" y="290" text-anchor="middle" fill="#8b949e" font-size="11" font-weight="600">业务模块: garden · workorder · bpm · gloabl · infra · system</text>
  691 + <rect x="30" y="320" width="120" height="50" rx="10" fill="rgba(251,191,36,.08)" stroke="#fbbf24" stroke-width="1"/><text x="90" y="342" text-anchor="middle" fill="#fbbf24" font-size="11" font-weight="600">AJ Captcha</text><text x="90" y="358" text-anchor="middle" fill="#8b949e" font-size="9">滑块验证</text>
  692 + <line x1="150" y1="345" x2="198" y2="285" stroke="#fbbf24" stroke-width="0.8" stroke-dasharray="4,3"/>
  693 + <rect x="30" y="200" width="120" height="50" rx="10" fill="rgba(239,68,68,.08)" stroke="#ef4444" stroke-width="1"/><text x="90" y="222" text-anchor="middle" fill="#f87171" font-size="11" font-weight="600">API 加密</text><text x="90" y="238" text-anchor="middle" fill="#8b949e" font-size="9">AES 算法</text>
  694 + </svg>
  695 + </div>
  696 + </section>
  697 +
  698 + <!-- ====== 21. Security & Auth Details ====== -->
  699 + <section class="slide" data-title="Security & Auth">
  700 + <p class="kicker">security · auth · tenant</p>
  701 + <h2 class="h2">安全与权限体系详解</h2>
  702 + <div class="grid g3 mt-l">
  703 + <div class="card card-accent anim-fade-up" data-anim="fade-up" style="padding:22px"><h4 style="color:#fff;margin-bottom:8px">🔐 认证体系</h4><ul style="font-size:12px;color:var(--text-2);padding-left:16px;line-height:1.8;margin:0"><li>Spring Security 6.x 核心</li><li>Token + Redis 无状态会话</li><li>OAuth2 授权码模式</li><li>多终端(Web / App / 小程序)</li><li>SSO 单点登录</li><li>社交登录(JustAuth)</li></ul></div>
  704 + <div class="card card-accent anim-fade-up" data-anim="fade-up" style="padding:22px;animation-delay:.1s"><h4 style="color:#fff;margin-bottom:8px">🛡️ 授权体系</h4><ul style="font-size:12px;color:var(--text-2);padding-left:16px;line-height:1.8;margin:0"><li>RBAC:用户 → 角色 → 菜单</li><li>按钮级别权限控制</li><li>动态菜单加载</li><li>Redis 缓存权限信息</li><li>数据权限:行级过滤</li><li>部门数据范围隔离</li></ul></div>
  705 + <div class="card card-accent anim-fade-up" data-anim="fade-up" style="padding:22px;animation-delay:.2s"><h4 style="color:#fff;margin-bottom:8px">🏢 多租户</h4><ul style="font-size:12px;color:var(--text-2);padding-left:16px;line-height:1.8;margin:0"><li>tenant_id 字段隔离</li><li>MyBatis 拦截器自动注入</li><li>租户套餐权限定制</li><li>每个租户独立菜单/角色</li><li>透明化封装无侵入</li></ul></div>
  706 + </div>
  707 + <div class="code-text mt-l">SystemModule → Security Starter → Token + Redis → @PreAuthorize → DataPermission AOP → SQL Interceptor</div>
  708 + </section>
  709 +
  710 + <!-- ====== 22. Observability ====== -->
  711 + <section class="slide" data-title="Observability">
  712 + <p class="kicker">observability · monitoring</p>
  713 + <h2 class="h2">可观测性与监控体系</h2>
  714 + <div class="grid g3 mt-l">
  715 + <div class="card" style="padding:22px;border-top:3px solid var(--accent)"><h4 style="color:#fff;font-size:16px;margin-bottom:8px">📝 日志体系</h4><p class="dim" style="font-size:12px;line-height:1.7">Logback + SkyWalking Logback Appender,TraceId 自动注入</p><div class="biz-tags mt-s"><span>操作日志</span><span>登录日志</span><span>API 访问日志</span><span>错误日志</span></div></div>
  716 + <div class="card" style="padding:22px;border-top:3px solid var(--accent-2)"><h4 style="color:#fff;font-size:16px;margin-bottom:8px">📊 指标监控</h4><p class="dim" style="font-size:12px;line-height:1.7">Spring Boot Admin + Druid SQL 监控 + Redis 监控面板</p><div class="biz-tags mt-s"><span>JVM 指标</span><span>连接池</span><span>QPS/RT</span><span>慢 SQL</span></div></div>
  717 + <div class="card" style="padding:22px;border-top:3px solid #d2a8ff"><h4 style="color:#fff;font-size:16px;margin-bottom:8px">🔍 链路追踪</h4><p class="dim" style="font-size:12px;line-height:1.7">Apache SkyWalking 9.5 分布式追踪,服务拓扑自动发现</p><div class="biz-tags mt-s"><span>调用链</span><span>拓扑图</span><span>慢端点</span><span>告警</span></div></div>
  718 + </div>
  719 + <div class="grid g2 mt-m">
  720 + <div class="card card-accent"><h4 style="font-size:14px">Monitor Starter 集成</h4><p class="dim" style="font-size:12px">SkyWalking · Admin Client · OpenTracing · 日志中心 · 一键接入</p></div>
  721 + <div class="card card-accent"><h4 style="font-size:14px">保护机制</h4><p class="dim" style="font-size:12px">分布式锁 (Lock4j) · 幂等性保证 · 限流降级 · 异常全局处理 · 错误码统一管理</p></div>
  722 + </div>
  723 + </section>
  724 +
  725 + <!-- ====== 23. Deployment Architecture SVG (from file1 slide 9) ====== -->
  726 + <section class="slide" data-title="Deployment Architecture SVG">
  727 + <p class="kicker">deployment · docker</p>
  728 + <h2 class="h2">部署架构 — Docker 容器化</h2>
  729 + <div class="svg-area">
  730 + <svg viewBox="0 0 1000 400" xmlns="http://www.w3.org/2000/svg">
  731 + <rect x="20" y="20" width="960" height="360" rx="16" fill="#161b22" stroke="#1e2d45" stroke-width="2"/><text x="500" y="48" text-anchor="middle" fill="#64748b" font-size="13" font-weight="600">Docker Host</text>
  732 +
  733 + <rect x="50" y="65" width="290" height="160" rx="12" fill="rgba(56,189,248,.08)" stroke="#38bdf8" stroke-width="2"/><text x="195" y="92" text-anchor="middle" fill="#38bdf8" font-size="14" font-weight="700">urbanops-server</text><text x="195" y="110" text-anchor="middle" fill="#8b949e" font-size="10">Eclipse Temurin 21 JRE</text><text x="195" y="126" text-anchor="middle" fill="#64748b" font-size="9">-Xms512m -Xmx512m</text>
  734 + <rect x="70" y="137" width="120" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/><text x="130" y="152" text-anchor="middle" fill="#8b949e" font-size="9">Port 48081</text>
  735 + <rect x="200" y="137" width="120" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/><text x="260" y="152" text-anchor="middle" fill="#8b949e" font-size="9">Actuator /actuator</text>
  736 + <rect x="70" y="165" width="250" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/><text x="195" y="180" text-anchor="middle" fill="#8b949e" font-size="9">包含所有业务模块 (fat JAR)</text>
  737 + <rect x="70" y="193" width="250" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/><text x="195" y="208" text-anchor="middle" fill="#8b949e" font-size="9">SpringDoc /v3/api-docs · Knife4j</text>
  738 +
  739 + <rect x="370" y="65" width="260" height="160" rx="12" fill="rgba(239,68,68,.08)" stroke="#ef4444" stroke-width="2"/><text x="500" y="92" text-anchor="middle" fill="#f87171" font-size="14" font-weight="700">iot-gateway-server</text><text x="500" y="110" text-anchor="middle" fill="#8b949e" font-size="10">独立部署 · Vert.x 引擎</text>
  740 + <rect x="390" y="122" width="220" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/><text x="500" y="137" text-anchor="middle" fill="#8b949e" font-size="9">Port 8092</text>
  741 + <rect x="390" y="150" width="220" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/><text x="500" y="165" text-anchor="middle" fill="#8b949e" font-size="9">协议: MQTT · HTTP · TCP</text>
  742 + <rect x="390" y="178" width="220" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/><text x="500" y="193" text-anchor="middle" fill="#8b949e" font-size="9">设备接入 · 数据采集</text>
  743 + <rect x="390" y="206" width="220" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/><text x="500" y="221" text-anchor="middle" fill="#8b949e" font-size="9">HTTP RPC → Server :48080</text>
  744 +
  745 + <rect x="660" y="65" width="180" height="72" rx="10" fill="rgba(59,130,246,.08)" stroke="#3b82f6" stroke-width="1.5"/><text x="750" y="92" text-anchor="middle" fill="#60a5fa" font-size="12" font-weight="600">MySQL 8</text><text x="750" y="110" text-anchor="middle" fill="#8b949e" font-size="9">:3306</text><text x="750" y="125" text-anchor="middle" fill="#64748b" font-size="8">主从读写分离</text>
  746 + <rect x="660" y="150" width="180" height="72" rx="10" fill="rgba(239,68,68,.08)" stroke="#ef4444" stroke-width="1.5"/><text x="750" y="177" text-anchor="middle" fill="#f87171" font-size="12" font-weight="600">Redis Sentinel</text><text x="750" y="195" text-anchor="middle" fill="#8b949e" font-size="9">:26379</text><text x="750" y="210" text-anchor="middle" fill="#64748b" font-size="8">高可用集群</text>
  747 +
  748 + <rect x="860" y="65" width="110" height="72" rx="10" fill="rgba(52,211,153,.08)" stroke="#34d399" stroke-width="1.5"/><text x="915" y="92" text-anchor="middle" fill="#4ade80" font-size="11" font-weight="600">RocketMQ</text><text x="915" y="110" text-anchor="middle" fill="#8b949e" font-size="9">:9876</text>
  749 + <rect x="860" y="150" width="110" height="72" rx="10" fill="rgba(129,140,248,.08)" stroke="#818cf8" stroke-width="1.5"/><text x="915" y="177" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="600">Kafka</text><text x="915" y="195" text-anchor="middle" fill="#8b949e" font-size="9">:9092</text>
  750 +
  751 + <rect x="660" y="240" width="310" height="40" rx="10" fill="rgba(251,191,36,.08)" stroke="#fbbf24" stroke-width="1.5"/><text x="815" y="265" text-anchor="middle" fill="#fbbf24" font-size="11" font-weight="600">urbanops-admin (前端 Nginx) :8080</text>
  752 + <rect x="50" y="245" width="580" height="40" rx="10" fill="rgba(129,140,248,.06)" stroke="#6366f1" stroke-width="1.5"/><text x="340" y="270" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="600">可观测性: SkyWalking Agent · Spring Boot Admin · OpenTracing · Actuator · 日志采集</text>
  753 + <rect x="50" y="298" width="580" height="32" rx="8" fill="rgba(244,114,182,.06)" stroke="#f472b6" stroke-width="1" stroke-dasharray="5,3"/><text x="340" y="319" text-anchor="middle" fill="#f472b6" font-size="10">PF4J 插件目录: ../plugins (动态扩展)</text>
  754 +
  755 + <rect x="50" y="350" width="12" height="12" rx="2" fill="rgba(56,189,248,.3)" stroke="#38bdf8" stroke-width="1"/><text x="68" y="360" fill="#64748b" font-size="9">fat JAR</text>
  756 + <rect x="140" y="350" width="12" height="12" rx="2" fill="rgba(239,68,68,.3)" stroke="#ef4444" stroke-width="1"/><text x="158" y="360" fill="#64748b" font-size="9">独立进程</text>
  757 + <rect x="240" y="350" width="12" height="12" rx="2" fill="rgba(59,130,246,.3)" stroke="#3b82f6" stroke-width="1"/><text x="258" y="360" fill="#64748b" font-size="9">数据服务</text>
  758 + <rect x="340" y="350" width="12" height="12" rx="2" fill="rgba(52,211,153,.3)" stroke="#34d399" stroke-width="1"/><text x="358" y="360" fill="#64748b" font-size="9">消息中间件</text>
  759 + </svg>
  760 + </div>
  761 + </section>
  762 +
  763 + <!-- ====== 24. CI/CD & Deployment Details ====== -->
  764 + <section class="slide" data-title="CI/CD & Deployment">
  765 + <p class="kicker">deployment · ci-cd</p>
  766 + <h2 class="h2">CI/CD 流水线与运维监控</h2>
  767 + <div class="deploy-grid mt-l">
  768 + <div class="deploy-card ci anim-fade-up" data-anim="fade-up"><div class="di">🚀</div><h4>CI 持续集成</h4><ul><li>Jenkins Pipeline</li><li>Maven 多模块构建</li><li>单元测试 + 代码扫描</li><li>Docker 镜像构建</li><li>推送 Harbor 私有仓库</li></ul></div>
  769 + <div class="deploy-card cn anim-fade-up" data-anim="fade-up" style="animation-delay:.1s"><div class="di">☸️</div><h4>容器编排</h4><ul><li>Kubernetes 集群</li><li>Deployment 滚动更新</li><li>Service 服务发现</li><li>ConfigMap 配置管理</li><li>Ingress 流量入口</li></ul></div>
  770 + <div class="deploy-card mon anim-fade-up" data-anim="fade-up" style="animation-delay:.2s"><div class="di">📊</div><h4>运维监控</h4><ul><li>Prometheus + Grafana</li><li>SkyWalking 链路追踪</li><li>ELK 日志收集</li><li>AlertManager 告警</li><li>Admin 应用监控</li></ul></div>
  771 + </div>
  772 + <div class="card card-accent mt-l" style="padding:22px">
  773 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent);margin-bottom:12px">Dockerfile — 多阶段构建</h4>
  774 + <div class="terminal"><div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span>urbanops-server/Dockerfile</span></div>
  775 + <pre><span class="cmt"># Stage 1: 构建</span>
  776 +<span class="kw">FROM</span> maven:3.9-eclipse-temurin-17 <span class="kw">AS</span> builder
  777 +<span class="kw">COPY</span> . /app && <span class="kw">WORKDIR</span> /app
  778 +<span class="kw">RUN</span> mvn clean package -DskipTests
  779 +
  780 +<span class="cmt"># Stage 2: 运行</span>
  781 +<span class="kw">FROM</span> eclipse-temurin:17-jre
  782 +<span class="kw">COPY</span> --from=builder /app/urbanops-server/target/*.jar app.jar
  783 +<span class="kw">ENTRYPOINT</span> ["java","-jar","/app.jar"]</pre>
  784 + </div>
  785 + </div>
  786 + </section>
  787 +
  788 + <!-- ====== 25. Future Planning & Roadmap ====== -->
  789 + <section class="slide" data-title="Future Planning">
  790 + <p class="kicker">roadmap · planning</p>
  791 + <h2 class="h2">后续扩展规划</h2>
  792 + <div class="roadmap-grid">
  793 + <div class="roadmap-card now anim-fade-up" data-anim="fade-up"><div class="r-period">Phase 1 · 近期</div><h4>🏗️ 架构夯实</h4><ul><li>完善 IoT 设备接入能力</li><li>会员中心 (Member) 激活</li><li>支付系统 (Pay) 集成</li><li>微信公众号 (MP) 接入</li><li>数据报表与大屏 (Report)</li><li>提升单元测试覆盖率</li></ul></div>
  794 + <div class="roadmap-card next anim-fade-up" data-anim="fade-up" style="animation-delay:.1s"><div class="r-period">Phase 2 · 中期</div><h4>🚀 业务扩展 & 微服务化</h4><ul><li>ERP / CRM 系统搭建</li><li>商城系统 (Mall)</li><li>AI 大模型集成 (AI)</li><li>Spring Cloud 微服务拆分</li><li>Nacos 注册 & 配置中心</li><li>Gateway + Sentinel 流量治理</li></ul></div>
  795 + <div class="roadmap-card future anim-fade-up" data-anim="fade-up" style="animation-delay:.2s"><div class="r-period">Phase 3 · 远期</div><h4>🌐 平台化 & 智能化</h4><ul><li>多租户 SaaS 化运营</li><li>开放平台 & API 市场</li><li>低代码/无代码搭建</li><li>PF4J 插件生态</li><li>边缘计算 + IoT 规模化</li><li>数据中台 & 智能决策</li></ul></div>
  796 + </div>
  797 + <div class="timeline mt-l">
  798 + <div class="tl-item"><div class="tl-phase">Q2 2025</div><div class="tl-body"><h4>物联网接入打通</h4><p>MQTT 协议适配 · 设备影子 · TDengine 时序存储</p></div></div>
  799 + <div class="tl-item"><div class="tl-phase">Q3 2025</div><div class="tl-body"><h4>业务模块激活</h4><p>Member + Pay + MP + Report 模块上线,补齐业务闭环</p></div></div>
  800 + <div class="tl-item"><div class="tl-phase">Q4 2025</div><div class="tl-body"><h4>微服务演进</h4><p>核心域拆分 · Spring Cloud 迁移 · 服务独立部署</p></div></div>
  801 + </div>
  802 + </section>
  803 +
  804 + <!-- ====== 26. Architecture Evolution & Summary ====== -->
  805 + <section class="slide" data-title="Architecture Evolution">
  806 + <p class="kicker">evolution · target-architecture</p>
  807 + <h2 class="h2">架构演进目标:微服务全景</h2>
  808 + <div class="card card-accent" style="padding:24px;margin-top:14px">
  809 + <h4 style="color:var(--accent);font-family:'JetBrains Mono',monospace;font-size:13px;margin-bottom:12px">目标微服务架构</h4>
  810 + <div class="evo-flow">
  811 + <span class="evo-box accent">Nginx</span><span class="evo-arrow">→</span>
  812 + <span class="evo-box purple">Gateway</span><span class="evo-arrow">→</span>
  813 + <span class="evo-box green">Nacos</span><span class="evo-arrow">⇢</span>
  814 + <div style="display:flex;flex-direction:column;gap:6px">
  815 + <span class="tag">garden-service</span><span class="tag">workorder-service</span><span class="tag">bpm-service</span><span class="tag">system-service</span><span class="tag">iot-gateway (已有)</span>
  816 + </div>
  817 + <span class="evo-arrow">⇢</span>
  818 + <div style="display:flex;flex-direction:column;gap:6px">
  819 + <span class="tag">Sentinel</span><span class="tag">Seata</span><span class="tag">SkyWalking</span>
  820 + </div>
  821 + </div>
  822 + </div>
  823 + <div class="grid g3 mt-l">
  824 + <div class="card card-accent"><h4>1 · 业务驱动架构</h4><p class="dim">智慧园林 + 工单调度 + 流程协同,三大核心域支撑城市运营。</p></div>
  825 + <div class="card card-accent"><h4>2 · 分层清晰可扩展</h4><p class="dim">15 个 Starter 组件化,模块间 Feign API + MQ 双通道解耦。</p></div>
  826 + <div class="card card-accent"><h4>3 · 信创全栈兼容</h4><p class="dim">MySQL / DM8 / KingBase / OpenGauss 多数据库无缝切换。</p></div>
  827 + <div class="card card-accent"><h4>4 · 可观测性内置</h4><p class="dim">SkyWalking + Admin + 日志中心,三大支柱保障系统可靠。</p></div>
  828 + <div class="card card-accent"><h4>5 · 渐进式演进</h4><p class="dim">模块化先行 → API 契约驱动 → 渐进式拆分 → 独立部署验证。</p></div>
  829 + <div class="card card-accent"><h4>6 · IoT 独立先行</h4><p class="dim">IoT 网关已实现独立部署,为后续微服务化积累了宝贵经验。</p></div>
  830 + </div>
  831 + <div style="margin-top:24px;text-align:center">
  832 + <div class="mono" style="font-size:56px;color:var(--accent);font-weight:800;letter-spacing:-.04em">?</div>
  833 + <h2 class="h2">Questions &amp; Discussion</h2>
  834 + <p class="lede">感谢聆听 · 欢迎交流</p>
  835 + <div class="row mt-s" style="justify-content:center"><span class="tag">#urbanops</span><span class="tag">#spring-boot</span><span class="tag">#microservices</span><span class="tag">#smart-garden</span><span class="tag">#cloud-native</span></div>
  836 + </div>
  837 + </section>
  838 +
  839 +</div>
  840 +<script src="ppt-assets/runtime.js"></script>
  841 +</body></html>
docs/ppt-assets/animations.css 0 → 100644
  1 +/* html-ppt :: animations.css
  2 + * Apply by adding class="anim-<name>" or data-anim="<name>".
  3 + * Durations are deliberately snappy; tweak --anim-dur per element.
  4 + */
  5 +:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
  6 +
  7 +/* ---------- FADE DIRECTIONALS ---------- */
  8 +@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
  9 +@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
  10 +@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
  11 +@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
  12 +.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
  13 +.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
  14 +.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
  15 +.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
  16 +
  17 +/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
  18 +@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
  19 +@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
  20 +@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
  21 +@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
  22 +@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
  23 + 20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
  24 + 40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
  25 + 60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
  26 + 80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
  27 + 100%{opacity:1;transform:none}}
  28 +.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
  29 +.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
  30 +.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
  31 +.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
  32 +.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
  33 +
  34 +/* ---------- TYPEWRITER ---------- */
  35 +.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
  36 + width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
  37 +@keyframes kf-type{to{width:100%}}
  38 +@keyframes kf-caret{50%{border-color:transparent}}
  39 +
  40 +/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
  41 +@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
  42 + 50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
  43 +.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
  44 +
  45 +.anim-shimmer-sweep{position:relative;overflow:hidden}
  46 +.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
  47 + background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
  48 + transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
  49 +@keyframes kf-shimmer{to{transform:translateX(100%)}}
  50 +
  51 +.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
  52 + background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
  53 + animation:kf-gradflow 4s linear infinite}
  54 +@keyframes kf-gradflow{to{background-position:300% 0}}
  55 +
  56 +/* ---------- STAGGER LIST ---------- */
  57 +.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
  58 +.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
  59 +.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
  60 +.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
  61 +.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
  62 +.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
  63 +.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
  64 +.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
  65 +.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
  66 +.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
  67 +
  68 +/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
  69 +.counter{font-variant-numeric:tabular-nums}
  70 +
  71 +/* ---------- SVG PATH DRAW ---------- */
  72 +.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
  73 + stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
  74 +@keyframes kf-draw{to{stroke-dashoffset:0}}
  75 +
  76 +/* ---------- PARALLAX TILT (hover) ---------- */
  77 +.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
  78 +.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
  79 +
  80 +/* ---------- CARD FLIP 3D ---------- */
  81 +@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
  82 + to{transform:perspective(1200px) rotateY(0);opacity:1}}
  83 +.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
  84 +
  85 +/* ---------- CUBE ROTATE 3D ---------- */
  86 +@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
  87 + to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
  88 +.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
  89 +
  90 +/* ---------- PAGE TURN 3D ---------- */
  91 +@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
  92 + to{transform:perspective(1600px) rotateY(0);opacity:1}}
  93 +.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
  94 +
  95 +/* ---------- PERSPECTIVE ZOOM ---------- */
  96 +@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
  97 + to{opacity:1;transform:none}}
  98 +.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
  99 +
  100 +/* ---------- MARQUEE SCROLL ---------- */
  101 +.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
  102 +@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
  103 +
  104 +/* ---------- KEN BURNS ---------- */
  105 +@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
  106 +.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
  107 +
  108 +/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
  109 +.anim-confetti-burst{position:relative}
  110 +.anim-confetti-burst::before,.anim-confetti-burst::after{
  111 + content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
  112 + background:var(--accent);box-shadow:
  113 + 20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
  114 + 30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
  115 + 40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
  116 + 10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
  117 + opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
  118 +.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
  119 +@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
  120 +
  121 +/* ---------- SPOTLIGHT ---------- */
  122 +@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
  123 +.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
  124 +
  125 +/* ---------- MORPH SHAPE (SVG) ---------- */
  126 +.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
  127 +@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
  128 + 100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
  129 +
  130 +/* ---------- RIPPLE REVEAL ---------- */
  131 +@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
  132 + 100%{clip-path:circle(160% at 20% 80%);opacity:1}}
  133 +.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
  134 +
  135 +/* reduced motion */
  136 +@media (prefers-reduced-motion: reduce){
  137 + [class*="anim-"]{animation:none!important;transition:none!important}
  138 +}
docs/ppt-assets/base.css 0 → 100644
  1 +/* html-ppt :: base.css — reset + shared tokens + layout primitives */
  2 +/* Default tokens. Themes in assets/themes/*.css override the :root block. */
  3 +:root {
  4 + --bg: #ffffff;
  5 + --bg-soft: #f7f7f8;
  6 + --surface: #ffffff;
  7 + --surface-2: #f2f2f4;
  8 + --border: rgba(0,0,0,.08);
  9 + --border-strong: rgba(0,0,0,.16);
  10 + --text-1: #111216;
  11 + --text-2: #55596a;
  12 + --text-3: #8a8f9e;
  13 + --accent: #3b6cff;
  14 + --accent-2: #7a5cff;
  15 + --accent-3: #ff5c8a;
  16 + --good: #1aaf6c;
  17 + --warn: #f5a524;
  18 + --bad: #e0445a;
  19 + --grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
  20 + --grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
  21 + --radius: 18px;
  22 + --radius-sm: 12px;
  23 + --radius-lg: 26px;
  24 + --shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
  25 + --shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
  26 + --font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
  27 + --font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
  28 + --font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
  29 + --font-display: var(--font-sans);
  30 + --letter-tight: -.03em;
  31 + --letter-normal: -.01em;
  32 + --ease: cubic-bezier(.4,0,.2,1);
  33 +}
  34 +
  35 +*,*::before,*::after{box-sizing:border-box}
  36 +html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
  37 + font-family:var(--font-sans);font-weight:400;line-height:1.6;
  38 + -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
  39 + letter-spacing:var(--letter-normal)}
  40 +img,svg,video{max-width:100%;display:block}
  41 +a{color:var(--accent);text-decoration:none}
  42 +a:hover{text-decoration:underline}
  43 +code,kbd,pre,samp{font-family:var(--font-mono)}
  44 +
  45 +/* ================= SLIDE SYSTEM ================= */
  46 +.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
  47 +.slide{
  48 + position:absolute;inset:0;
  49 + display:flex;flex-direction:column;justify-content:center;
  50 + padding:72px 96px;
  51 + box-sizing:border-box;
  52 + opacity:0;pointer-events:none;
  53 + transition:opacity .5s var(--ease), transform .5s var(--ease);
  54 + transform:translateX(30px);
  55 + overflow:hidden;
  56 +}
  57 +.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
  58 +.slide.is-prev{transform:translateX(-30px)}
  59 +
  60 +/* single-page standalone (used when a layout file is opened directly) */
  61 +body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
  62 +
  63 +/* ================= TYPOGRAPHY ================= */
  64 +.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
  65 +.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
  66 +h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
  67 +h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
  68 +h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
  69 +h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
  70 +.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
  71 +.dim{color:var(--text-2)}
  72 +.dim2{color:var(--text-3)}
  73 +.mono{font-family:var(--font-mono)}
  74 +.serif{font-family:var(--font-serif)}
  75 +.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
  76 +
  77 +/* ================= LAYOUT PRIMITIVES ================= */
  78 +.stack>*+*{margin-top:14px}
  79 +.row{display:flex;gap:24px;align-items:center}
  80 +.row.wrap{flex-wrap:wrap}
  81 +.grid{display:grid;gap:24px}
  82 +.g2{grid-template-columns:repeat(2,1fr)}
  83 +.g3{grid-template-columns:repeat(3,1fr)}
  84 +.g4{grid-template-columns:repeat(4,1fr)}
  85 +.center{display:flex;align-items:center;justify-content:center;text-align:center}
  86 +.fill{flex:1}
  87 +.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
  88 +.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
  89 +.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
  90 +
  91 +/* ================= CARDS ================= */
  92 +.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
  93 + padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
  94 +.card-soft{background:var(--surface-2);border:1px solid var(--border)}
  95 +.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
  96 +.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
  97 +.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
  98 +.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
  99 +
  100 +.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
  101 + background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
  102 +.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
  103 +
  104 +/* ================= BARS / DIVIDERS ================= */
  105 +.divider{height:1px;background:var(--border);width:100%}
  106 +.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
  107 +
  108 +/* ================= CHROME (header/footer/progress) ================= */
  109 +.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
  110 + font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
  111 +.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
  112 + font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
  113 +.slide-number::before{content:attr(data-current)}
  114 +.slide-number::after{content:" / " attr(data-total)}
  115 +.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
  116 +.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
  117 +
  118 +/* ================= PRESENTER / OVERVIEW ================= */
  119 +.notes{display:none!important}
  120 +.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
  121 + padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
  122 + transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
  123 +.notes-overlay.open{transform:translateY(0)}
  124 +.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
  125 + display:none;padding:40px;overflow:auto}
  126 +.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
  127 +.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
  128 + aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
  129 + font-size:11px;transition:transform .2s var(--ease)}
  130 +.overview .thumb:hover{transform:scale(1.04)}
  131 +.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
  132 +.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
  133 +
  134 +/* ================= PRESENTER VIEW ================= */
  135 +/* Presenter view opens in a separate popup window (S key).
  136 + * All presenter styles are self-contained in the popup HTML generated by runtime.js.
  137 + * The audience window (this file) is NOT affected — it stays as normal deck view.
  138 + * Only the .notes class below is needed to hide speaker notes from audience. */
  139 +
  140 +/* ================= UTILITY ================= */
  141 +.hidden{display:none!important}
  142 +.nowrap{white-space:nowrap}
  143 +.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
  144 +.uppercase{text-transform:uppercase;letter-spacing:.12em}
  145 +
  146 +/* ================= PRINT ================= */
  147 +@media print{
  148 + .slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
  149 + .deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
  150 +}
docs/ppt-assets/fonts.css 0 → 100644
  1 +/* html-ppt :: shared webfonts */
  2 +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
  3 +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
  4 +@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
  5 +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
  6 +@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
  7 +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
  8 +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
  9 +@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
docs/ppt-assets/runtime.js 0 → 100644
  1 +/* html-ppt :: runtime.js
  2 + * Keyboard-driven deck runtime. Zero dependencies.
  3 + *
  4 + * Features:
  5 + * ← → / space / PgUp PgDn / Home End navigation
  6 + * F fullscreen
  7 + * S presenter mode (opens a NEW WINDOW with current/next slide preview + notes + timer)
  8 + * The original window stays as audience view, synced via BroadcastChannel.
  9 + * Slide previews use CSS transform:scale() at design resolution for pixel-perfect layout.
  10 + * N quick notes overlay (bottom drawer)
  11 + * O slide overview grid
  12 + * T cycle themes (reads data-themes on <html> or <body>)
  13 + * A cycle demo animation on current slide
  14 + * URL hash #/N deep-link to slide N (1-based)
  15 + * Progress bar auto-managed
  16 + */
  17 +(function () {
  18 + 'use strict';
  19 +
  20 + const ANIMS = ['fade-up','fade-down','fade-left','fade-right','rise-in','drop-in',
  21 + 'zoom-pop','blur-in','glitch-in','typewriter','neon-glow','shimmer-sweep',
  22 + 'gradient-flow','stagger-list','counter-up','path-draw','parallax-tilt',
  23 + 'card-flip-3d','cube-rotate-3d','page-turn-3d','perspective-zoom',
  24 + 'marquee-scroll','kenburns','confetti-burst','spotlight','morph-shape','ripple-reveal'];
  25 +
  26 + function ready(fn){ if(document.readyState!='loading')fn(); else document.addEventListener('DOMContentLoaded',fn);}
  27 +
  28 + /* ========== Parse URL for preview-only mode ==========
  29 + * When loaded as iframe.src = "index.html?preview=3", runtime enters a
  30 + * locked single-slide mode: only slide N is visible, no chrome, no keys,
  31 + * no hash updates. This is how the presenter window shows pixel-perfect
  32 + * previews — by loading the actual deck file in an iframe and telling it
  33 + * to display only a specific slide.
  34 + */
  35 + function getPreviewIdx() {
  36 + const m = /[?&]preview=(\d+)/.exec(location.search || '');
  37 + return m ? parseInt(m[1], 10) - 1 : -1;
  38 + }
  39 +
  40 + ready(function () {
  41 + const deck = document.querySelector('.deck');
  42 + if (!deck) return;
  43 + const slides = Array.from(deck.querySelectorAll('.slide'));
  44 + if (!slides.length) return;
  45 +
  46 + const previewOnlyIdx = getPreviewIdx();
  47 + const isPreviewMode = previewOnlyIdx >= 0 && previewOnlyIdx < slides.length;
  48 +
  49 + /* ===== Preview-only mode: show one slide, hide everything else ===== */
  50 + if (isPreviewMode) {
  51 + function showSlide(i) {
  52 + slides.forEach((s, j) => {
  53 + const active = (j === i);
  54 + s.classList.toggle('is-active', active);
  55 + s.style.display = active ? '' : 'none';
  56 + if (active) {
  57 + s.style.opacity = '1';
  58 + s.style.transform = 'none';
  59 + s.style.pointerEvents = 'auto';
  60 + }
  61 + });
  62 + }
  63 + showSlide(previewOnlyIdx);
  64 + /* Hide chrome that the presenter shouldn't see in preview */
  65 + const hideSel = '.progress-bar, .notes-overlay, .overview, .notes, aside.notes, .speaker-notes';
  66 + document.querySelectorAll(hideSel).forEach(el => { el.style.display = 'none'; });
  67 + document.documentElement.setAttribute('data-preview', '1');
  68 + document.body.setAttribute('data-preview', '1');
  69 + /* Auto-detect theme base path for theme switching in preview mode */
  70 + function getPreviewThemeBase() {
  71 + const base = document.documentElement.getAttribute('data-theme-base');
  72 + if (base) return base;
  73 + const tl = document.getElementById('theme-link');
  74 + if (tl) {
  75 + const raw = tl.getAttribute('href') || '';
  76 + const ls = raw.lastIndexOf('/');
  77 + if (ls >= 0) return raw.substring(0, ls + 1);
  78 + }
  79 + return 'assets/themes/';
  80 + }
  81 + const previewThemeBase = getPreviewThemeBase();
  82 +
  83 + /* Listen for postMessage from parent presenter window:
  84 + * - preview-goto: switch visible slide WITHOUT reloading
  85 + * - preview-theme: switch theme CSS link to match audience window */
  86 + window.addEventListener('message', function(e) {
  87 + if (!e.data) return;
  88 + if (e.data.type === 'preview-goto') {
  89 + const n = parseInt(e.data.idx, 10);
  90 + if (n >= 0 && n < slides.length) showSlide(n);
  91 + } else if (e.data.type === 'preview-theme' && e.data.name) {
  92 + let link = document.getElementById('theme-link');
  93 + if (!link) {
  94 + link = document.createElement('link');
  95 + link.rel = 'stylesheet';
  96 + link.id = 'theme-link';
  97 + document.head.appendChild(link);
  98 + }
  99 + link.href = previewThemeBase + e.data.name + '.css';
  100 + document.documentElement.setAttribute('data-theme', e.data.name);
  101 + }
  102 + });
  103 + /* Signal to parent that preview iframe is ready */
  104 + try { window.parent && window.parent.postMessage({ type: 'preview-ready' }, '*'); } catch(e) {}
  105 + return;
  106 + }
  107 +
  108 + let idx = 0;
  109 + const total = slides.length;
  110 +
  111 + /* ===== BroadcastChannel for presenter sync ===== */
  112 + const CHANNEL_NAME = 'html-ppt-presenter-' + location.pathname;
  113 + let bc;
  114 + try { bc = new BroadcastChannel(CHANNEL_NAME); } catch(e) { bc = null; }
  115 +
  116 + // Are we running inside the presenter popup? (legacy flag, now unused)
  117 + const isPresenterWindow = false;
  118 +
  119 + /* ===== progress bar ===== */
  120 + let bar = document.querySelector('.progress-bar');
  121 + if (!bar) {
  122 + bar = document.createElement('div');
  123 + bar.className = 'progress-bar';
  124 + bar.innerHTML = '<span></span>';
  125 + document.body.appendChild(bar);
  126 + }
  127 + const barFill = bar.querySelector('span');
  128 +
  129 + /* ===== notes overlay (N key) ===== */
  130 + let notes = document.querySelector('.notes-overlay');
  131 + if (!notes) {
  132 + notes = document.createElement('div');
  133 + notes.className = 'notes-overlay';
  134 + document.body.appendChild(notes);
  135 + }
  136 +
  137 + /* ===== overview grid (O key) ===== */
  138 + let overview = document.querySelector('.overview');
  139 + if (!overview) {
  140 + overview = document.createElement('div');
  141 + overview.className = 'overview';
  142 + slides.forEach((s, i) => {
  143 + const t = document.createElement('div');
  144 + t.className = 'thumb';
  145 + // Force 16:9 aspect ratio robustly
  146 + t.style.padding = '0 0 56.25% 0';
  147 + t.style.height = '0';
  148 + t.style.position = 'relative';
  149 + t.style.overflow = 'hidden';
  150 +
  151 + const title = s.getAttribute('data-title') ||
  152 + (s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1));
  153 +
  154 + // Create a container for the mini-slide
  155 + const mini = document.createElement('div');
  156 + mini.className = 'mini-slide';
  157 + mini.style.position = 'absolute';
  158 + mini.style.top = '0';
  159 + mini.style.left = '0';
  160 + mini.style.width = '1920px';
  161 + mini.style.height = '1080px';
  162 + mini.style.transformOrigin = 'top left';
  163 + mini.style.pointerEvents = 'none';
  164 + mini.style.background = 'var(--bg)';
  165 +
  166 + // Clone the slide content
  167 + const clone = s.cloneNode(true);
  168 + clone.className = 'slide is-active'; // force active styles
  169 + clone.style.position = 'absolute';
  170 + clone.style.inset = '0';
  171 + clone.style.transform = 'none';
  172 + clone.style.opacity = '1';
  173 + clone.style.padding = '72px 96px'; // ensure padding is kept
  174 +
  175 + mini.appendChild(clone);
  176 + t.appendChild(mini);
  177 +
  178 + // Add the number and title overlay
  179 + const overlay = document.createElement('div');
  180 + overlay.style.position = 'absolute';
  181 + overlay.style.inset = '0';
  182 + overlay.style.background = 'linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, transparent 40%, transparent 60%, rgba(0,0,0,0.8) 100%)';
  183 + overlay.style.color = '#fff';
  184 + overlay.style.zIndex = '10';
  185 + overlay.style.pointerEvents = 'none';
  186 +
  187 + const n = document.createElement('div');
  188 + n.className = 'n';
  189 + n.textContent = i + 1;
  190 + n.style.position = 'absolute';
  191 + n.style.top = '12px';
  192 + n.style.left = '16px';
  193 + n.style.fontWeight = '700';
  194 + n.style.fontSize = '16px';
  195 + n.style.color = '#fff';
  196 + n.style.textShadow = '0 1px 4px rgba(0,0,0,0.8)';
  197 +
  198 + const text = document.createElement('div');
  199 + text.className = 't';
  200 + text.textContent = title.trim().slice(0,80);
  201 + text.style.position = 'absolute';
  202 + text.style.bottom = '12px';
  203 + text.style.left = '16px';
  204 + text.style.right = '16px';
  205 + text.style.fontWeight = '600';
  206 + text.style.fontSize = '14px';
  207 + text.style.color = '#fff';
  208 + text.style.textShadow = '0 1px 4px rgba(0,0,0,0.8)';
  209 +
  210 + overlay.appendChild(n);
  211 + overlay.appendChild(text);
  212 + t.appendChild(overlay);
  213 +
  214 + t.addEventListener('click', () => { go(i); toggleOverview(false); });
  215 + overview.appendChild(t);
  216 + });
  217 + document.body.appendChild(overview);
  218 + }
  219 +
  220 + /* ===== navigation ===== */
  221 + function go(n, fromRemote){
  222 + n = Math.max(0, Math.min(total-1, n));
  223 + slides.forEach((s,i) => {
  224 + s.classList.toggle('is-active', i===n);
  225 + s.classList.toggle('is-prev', i<n);
  226 + });
  227 + idx = n;
  228 + barFill.style.width = ((n+1)/total*100)+'%';
  229 + const numEl = document.querySelector('.slide-number');
  230 + if (numEl) { numEl.setAttribute('data-current', n+1); numEl.setAttribute('data-total', total); }
  231 +
  232 + // notes (bottom overlay)
  233 + const note = slides[n].querySelector('.notes, aside.notes, .speaker-notes');
  234 + notes.innerHTML = note ? note.innerHTML : '';
  235 +
  236 + // hash
  237 + const hashTarget = '#/'+(n+1);
  238 + if (location.hash !== hashTarget && !isPresenterWindow) {
  239 + history.replaceState(null,'', hashTarget);
  240 + }
  241 +
  242 + // re-trigger entry animations
  243 + slides[n].querySelectorAll('[data-anim]').forEach(el => {
  244 + const a = el.getAttribute('data-anim');
  245 + el.classList.remove('anim-'+a);
  246 + void el.offsetWidth;
  247 + el.classList.add('anim-'+a);
  248 + });
  249 +
  250 + // counter-up
  251 + slides[n].querySelectorAll('.counter').forEach(el => {
  252 + const target = parseFloat(el.getAttribute('data-to')||el.textContent);
  253 + const dur = parseInt(el.getAttribute('data-dur')||'1200',10);
  254 + const start = performance.now();
  255 + const from = 0;
  256 + function tick(now){
  257 + const t = Math.min(1,(now-start)/dur);
  258 + const v = from + (target-from)*(1-Math.pow(1-t,3));
  259 + el.textContent = (target % 1 === 0) ? Math.round(v) : v.toFixed(1);
  260 + if (t<1) requestAnimationFrame(tick);
  261 + }
  262 + requestAnimationFrame(tick);
  263 + });
  264 +
  265 + // Broadcast to other window (audience ↔ presenter)
  266 + if (!fromRemote && bc) {
  267 + bc.postMessage({ type: 'go', idx: n });
  268 + }
  269 + }
  270 +
  271 + /* ===== listen for remote navigation / theme changes ===== */
  272 + if (bc) {
  273 + bc.onmessage = function(e) {
  274 + if (!e.data) return;
  275 + if (e.data.type === 'go' && typeof e.data.idx === 'number') {
  276 + go(e.data.idx, true);
  277 + } else if (e.data.type === 'theme' && e.data.name) {
  278 + /* Sync theme across windows */
  279 + const i = themes.indexOf(e.data.name);
  280 + if (i >= 0) themeIdx = i;
  281 + applyTheme(e.data.name);
  282 + }
  283 + };
  284 + }
  285 +
  286 + function toggleNotes(force){ notes.classList.toggle('open', force!==undefined?force:!notes.classList.contains('open')); }
  287 + function toggleOverview(force){
  288 + const isOpen = force!==undefined ? force : !overview.classList.contains('open');
  289 + overview.classList.toggle('open', isOpen);
  290 + if (isOpen) {
  291 + requestAnimationFrame(() => {
  292 + const thumbs = overview.querySelectorAll('.thumb');
  293 + if (thumbs.length) {
  294 + const scale = thumbs[0].clientWidth / 1920;
  295 + overview.querySelectorAll('.mini-slide').forEach(m => {
  296 + m.style.transform = 'scale(' + scale + ')';
  297 + });
  298 + }
  299 + });
  300 + }
  301 + }
  302 +
  303 + /* ========== PRESENTER MODE — Magnetic-card popup window ========== */
  304 + /* Opens a new window with 4 draggable, resizable cards:
  305 + * CURRENT — iframe(?preview=N) pixel-perfect preview of current slide
  306 + * NEXT — iframe(?preview=N+1) pixel-perfect preview of next slide
  307 + * SCRIPT — large speaker notes (逐字稿)
  308 + * TIMER — elapsed timer + page counter + controls
  309 + * Cards remember position/size in localStorage.
  310 + * Two windows sync via BroadcastChannel.
  311 + */
  312 + let presenterWin = null;
  313 +
  314 + function openPresenterWindow() {
  315 + if (presenterWin && !presenterWin.closed) {
  316 + presenterWin.focus();
  317 + return;
  318 + }
  319 +
  320 + // Build absolute URL of THIS deck file (without hash/query)
  321 + const deckUrl = location.protocol + '//' + location.host + location.pathname;
  322 +
  323 + // Collect slide titles + notes (HTML strings)
  324 + const slideMeta = slides.map((s, i) => {
  325 + const note = s.querySelector('.notes, aside.notes, .speaker-notes');
  326 + return {
  327 + title: s.getAttribute('data-title') ||
  328 + (s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)),
  329 + notes: note ? note.innerHTML : ''
  330 + };
  331 + });
  332 +
  333 + /* Capture current theme so presenter previews match the audience */
  334 + const currentTheme = root.getAttribute('data-theme') || (themes[themeIdx] || '');
  335 + const presenterHTML = buildPresenterHTML(deckUrl, slideMeta, total, idx, CHANNEL_NAME, currentTheme);
  336 +
  337 + presenterWin = window.open('', 'html-ppt-presenter', 'width=1280,height=820,menubar=no,toolbar=no');
  338 + if (!presenterWin) {
  339 + alert('请允许弹出窗口以使用演讲者视图');
  340 + return;
  341 + }
  342 + presenterWin.document.open();
  343 + presenterWin.document.write(presenterHTML);
  344 + presenterWin.document.close();
  345 + }
  346 +
  347 + function buildPresenterHTML(deckUrl, slideMeta, total, startIdx, channelName, currentTheme) {
  348 + const metaJSON = JSON.stringify(slideMeta);
  349 + const deckUrlJSON = JSON.stringify(deckUrl);
  350 + const channelJSON = JSON.stringify(channelName);
  351 + const themeJSON = JSON.stringify(currentTheme || '');
  352 + const storageKey = 'html-ppt-presenter:' + location.pathname;
  353 +
  354 + // Build the document as a single template string for clarity
  355 + return `<!DOCTYPE html>
  356 +<html lang="zh-CN">
  357 +<head>
  358 +<meta charset="utf-8">
  359 +<title>Presenter View</title>
  360 +<style>
  361 + * { margin: 0; padding: 0; box-sizing: border-box; }
  362 + html, body {
  363 + width: 100%; height: 100%; overflow: hidden;
  364 + background: #1a1d24;
  365 + background-image:
  366 + radial-gradient(circle at 20% 30%, rgba(88,166,255,.04), transparent 50%),
  367 + radial-gradient(circle at 80% 70%, rgba(188,140,255,.04), transparent 50%);
  368 + color: #e6edf3;
  369 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
  370 + }
  371 + /* Stage: positioned area where cards live */
  372 + #stage { position: absolute; inset: 0; overflow: hidden; }
  373 +
  374 + /* Magnetic card */
  375 + .pcard {
  376 + position: absolute;
  377 + background: #0d1117;
  378 + border: 1px solid rgba(255,255,255,.1);
  379 + border-radius: 12px;
  380 + box-shadow: 0 8px 32px rgba(0,0,0,.45), 0 0 0 1px rgba(255,255,255,.02);
  381 + display: flex; flex-direction: column;
  382 + overflow: hidden;
  383 + min-width: 180px; min-height: 100px;
  384 + transition: box-shadow .2s, border-color .2s;
  385 + }
  386 + .pcard.dragging { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(88,166,255,.5); border-color: #58a6ff; transition: none; z-index: 9999; }
  387 + .pcard.resizing { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(63,185,80,.5); border-color: #3fb950; transition: none; z-index: 9999; }
  388 + .pcard:hover { border-color: rgba(88,166,255,.3); }
  389 +
  390 + /* Card header (drag handle) */
  391 + .pcard-head {
  392 + display: flex; align-items: center; gap: 10px;
  393 + padding: 8px 12px;
  394 + background: rgba(255,255,255,.04);
  395 + border-bottom: 1px solid rgba(255,255,255,.06);
  396 + cursor: move;
  397 + user-select: none;
  398 + flex-shrink: 0;
  399 + }
  400 + .pcard-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--dot-color, #58a6ff); flex-shrink: 0; }
  401 + .pcard-title {
  402 + font-size: 11px; letter-spacing: .15em; text-transform: uppercase;
  403 + font-weight: 700; color: #8b949e; flex: 1;
  404 + }
  405 + .pcard-meta { font-size: 11px; color: #6e7681; }
  406 +
  407 + /* Card body */
  408 + .pcard-body { flex: 1; position: relative; overflow: hidden; min-height: 0; }
  409 +
  410 + /* Preview cards (CURRENT/NEXT) — iframe-based pixel-perfect render */
  411 + .pcard-preview .pcard-body { background: #000; }
  412 + .pcard-preview iframe {
  413 + position: absolute; top: 0; left: 0;
  414 + width: 1920px; height: 1080px;
  415 + border: none;
  416 + transform-origin: top left;
  417 + pointer-events: none;
  418 + background: transparent;
  419 + }
  420 + .pcard-preview .preview-end {
  421 + position: absolute; inset: 0;
  422 + display: flex; align-items: center; justify-content: center;
  423 + color: #484f58; font-size: 14px; letter-spacing: .12em;
  424 + }
  425 +
  426 + /* Notes card */
  427 + .pcard-notes .pcard-body {
  428 + padding: 14px 18px;
  429 + overflow-y: auto;
  430 + font-size: 18px; line-height: 1.75;
  431 + color: #d0d7de;
  432 + font-family: "Noto Sans SC", -apple-system, sans-serif;
  433 + }
  434 + .pcard-notes .pcard-body p { margin: 0 0 .7em 0; }
  435 + .pcard-notes .pcard-body strong { color: #f0883e; }
  436 + .pcard-notes .pcard-body em { color: #58a6ff; font-style: normal; }
  437 + .pcard-notes .pcard-body code {
  438 + font-family: "SF Mono", monospace; font-size: .9em;
  439 + background: rgba(255,255,255,.08); padding: 1px 6px; border-radius: 4px;
  440 + }
  441 + .pcard-notes .empty { color: #484f58; font-style: italic; }
  442 +
  443 + /* Timer card */
  444 + .pcard-timer .pcard-body {
  445 + display: flex; flex-direction: column; gap: 14px;
  446 + padding: 18px 20px; justify-content: center;
  447 + }
  448 + .timer-display {
  449 + font-family: "SF Mono", "JetBrains Mono", monospace;
  450 + font-size: 42px; font-weight: 700;
  451 + color: #3fb950;
  452 + letter-spacing: .04em;
  453 + line-height: 1;
  454 + }
  455 + .timer-row {
  456 + display: flex; align-items: center; gap: 12px;
  457 + font-size: 14px; color: #8b949e;
  458 + }
  459 + .timer-row .label { font-size: 10px; letter-spacing: .15em; text-transform: uppercase; color: #6e7681; }
  460 + .timer-row .val { color: #e6edf3; font-weight: 600; font-family: "SF Mono", monospace; }
  461 + .timer-controls { display: flex; gap: 8px; flex-wrap: wrap; }
  462 + .timer-btn {
  463 + background: rgba(255,255,255,.06);
  464 + border: 1px solid rgba(255,255,255,.1);
  465 + color: #e6edf3;
  466 + padding: 6px 12px;
  467 + border-radius: 6px;
  468 + font-size: 12px;
  469 + cursor: pointer;
  470 + font-family: inherit;
  471 + }
  472 + .timer-btn:hover { background: rgba(88,166,255,.15); border-color: #58a6ff; }
  473 + .timer-btn:active { transform: translateY(1px); }
  474 +
  475 + /* Resize handle */
  476 + .pcard-resize {
  477 + position: absolute; right: 0; bottom: 0;
  478 + width: 18px; height: 18px;
  479 + cursor: nwse-resize;
  480 + background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,.25) 50%, rgba(255,255,255,.25) 60%, transparent 60%, transparent 70%, rgba(255,255,255,.25) 70%, rgba(255,255,255,.25) 80%, transparent 80%);
  481 + z-index: 5;
  482 + }
  483 + .pcard-resize:hover { background: linear-gradient(135deg, transparent 50%, #58a6ff 50%, #58a6ff 60%, transparent 60%, transparent 70%, #58a6ff 70%, #58a6ff 80%, transparent 80%); }
  484 +
  485 + /* Bottom hint bar */
  486 + .hint-bar {
  487 + position: fixed; bottom: 0; left: 0; right: 0;
  488 + background: rgba(0,0,0,.6);
  489 + backdrop-filter: blur(10px);
  490 + border-top: 1px solid rgba(255,255,255,.08);
  491 + padding: 6px 16px;
  492 + font-size: 11px; color: #8b949e;
  493 + display: flex; gap: 18px; align-items: center;
  494 + z-index: 1000;
  495 + }
  496 + .hint-bar kbd {
  497 + background: rgba(255,255,255,.08);
  498 + padding: 1px 6px; border-radius: 3px;
  499 + font-family: "SF Mono", monospace;
  500 + font-size: 10px;
  501 + border: 1px solid rgba(255,255,255,.1);
  502 + color: #e6edf3;
  503 + }
  504 + .hint-bar .reset-layout {
  505 + margin-left: auto;
  506 + background: transparent; border: 1px solid rgba(255,255,255,.15);
  507 + color: #8b949e; padding: 3px 10px; border-radius: 4px;
  508 + font-size: 11px; cursor: pointer; font-family: inherit;
  509 + }
  510 + .hint-bar .reset-layout:hover { background: rgba(248,81,73,.15); border-color: #f85149; color: #f85149; }
  511 +
  512 + body.is-dragging-card * { user-select: none !important; }
  513 + body.is-dragging-card iframe { pointer-events: none !important; }
  514 +</style>
  515 +</head>
  516 +<body>
  517 +
  518 +<div id="stage">
  519 + <div class="pcard pcard-preview" id="card-cur" style="--dot-color:#58a6ff">
  520 + <div class="pcard-head" data-drag>
  521 + <span class="pcard-dot"></span>
  522 + <span class="pcard-title">CURRENT</span>
  523 + <span class="pcard-meta" id="cur-meta">—</span>
  524 + </div>
  525 + <div class="pcard-body"><iframe id="iframe-cur"></iframe></div>
  526 + <div class="pcard-resize" data-resize></div>
  527 + </div>
  528 +
  529 + <div class="pcard pcard-preview" id="card-nxt" style="--dot-color:#bc8cff">
  530 + <div class="pcard-head" data-drag>
  531 + <span class="pcard-dot"></span>
  532 + <span class="pcard-title">NEXT</span>
  533 + <span class="pcard-meta" id="nxt-meta">—</span>
  534 + </div>
  535 + <div class="pcard-body"><iframe id="iframe-nxt"></iframe></div>
  536 + <div class="pcard-resize" data-resize></div>
  537 + </div>
  538 +
  539 + <div class="pcard pcard-notes" id="card-notes" style="--dot-color:#f0883e">
  540 + <div class="pcard-head" data-drag>
  541 + <span class="pcard-dot"></span>
  542 + <span class="pcard-title">SPEAKER SCRIPT · 逐字稿</span>
  543 + </div>
  544 + <div class="pcard-body" id="notes-body"></div>
  545 + <div class="pcard-resize" data-resize></div>
  546 + </div>
  547 +
  548 + <div class="pcard pcard-timer" id="card-timer" style="--dot-color:#3fb950">
  549 + <div class="pcard-head" data-drag>
  550 + <span class="pcard-dot"></span>
  551 + <span class="pcard-title">TIMER</span>
  552 + </div>
  553 + <div class="pcard-body">
  554 + <div class="timer-display" id="timer-display">00:00</div>
  555 + <div class="timer-row">
  556 + <span class="label">Slide</span>
  557 + <span class="val" id="timer-count">1 / ${total}</span>
  558 + </div>
  559 + <div class="timer-controls">
  560 + <button class="timer-btn" id="btn-prev">← Prev</button>
  561 + <button class="timer-btn" id="btn-next">Next →</button>
  562 + <button class="timer-btn" id="btn-reset">⏱ Reset</button>
  563 + </div>
  564 + </div>
  565 + <div class="pcard-resize" data-resize></div>
  566 + </div>
  567 +</div>
  568 +
  569 +<div class="hint-bar">
  570 + <span><kbd>← →</kbd> 翻页</span>
  571 + <span><kbd>R</kbd> 重置计时</span>
  572 + <span><kbd>Esc</kbd> 关闭</span>
  573 + <span style="color:#6e7681">拖动卡片头部移动 · 拖动右下角调整大小</span>
  574 + <button class="reset-layout" id="reset-layout">重置布局</button>
  575 +</div>
  576 +
  577 +<script>
  578 +(function(){
  579 + var slideMeta = ${metaJSON};
  580 + var total = ${total};
  581 + var idx = ${startIdx};
  582 + var deckUrl = ${deckUrlJSON};
  583 + var STORAGE_KEY = ${JSON.stringify(storageKey)};
  584 + var bc;
  585 + try { bc = new BroadcastChannel(${channelJSON}); } catch(e) {}
  586 +
  587 + var iframeCur = document.getElementById('iframe-cur');
  588 + var iframeNxt = document.getElementById('iframe-nxt');
  589 + var notesBody = document.getElementById('notes-body');
  590 + var curMeta = document.getElementById('cur-meta');
  591 + var nxtMeta = document.getElementById('nxt-meta');
  592 + var timerDisplay = document.getElementById('timer-display');
  593 + var timerCount = document.getElementById('timer-count');
  594 +
  595 + /* ===== Default card layout ===== */
  596 + function defaultLayout() {
  597 + var w = window.innerWidth;
  598 + var h = window.innerHeight - 36; /* leave room for hint bar */
  599 + return {
  600 + 'card-cur': { x: 16, y: 16, w: Math.round(w*0.55) - 24, h: Math.round(h*0.62) - 16 },
  601 + 'card-nxt': { x: Math.round(w*0.55) + 8, y: 16, w: w - Math.round(w*0.55) - 24, h: Math.round(h*0.42) - 16 },
  602 + 'card-notes': { x: Math.round(w*0.55) + 8, y: Math.round(h*0.42) + 8, w: w - Math.round(w*0.55) - 24, h: h - Math.round(h*0.42) - 16 },
  603 + 'card-timer': { x: 16, y: Math.round(h*0.62) + 8, w: Math.round(w*0.55) - 24, h: h - Math.round(h*0.62) - 16 }
  604 + };
  605 + }
  606 +
  607 + /* ===== Apply / save / restore layout ===== */
  608 + function applyLayout(layout) {
  609 + Object.keys(layout).forEach(function(id){
  610 + var el = document.getElementById(id);
  611 + var l = layout[id];
  612 + if (el && l) {
  613 + el.style.left = l.x + 'px';
  614 + el.style.top = l.y + 'px';
  615 + el.style.width = l.w + 'px';
  616 + el.style.height = l.h + 'px';
  617 + }
  618 + });
  619 + rescaleAll();
  620 + }
  621 + function readLayout() {
  622 + try {
  623 + var saved = localStorage.getItem(STORAGE_KEY);
  624 + if (saved) return JSON.parse(saved);
  625 + } catch(e) {}
  626 + return defaultLayout();
  627 + }
  628 + function saveLayout() {
  629 + var layout = {};
  630 + ['card-cur','card-nxt','card-notes','card-timer'].forEach(function(id){
  631 + var el = document.getElementById(id);
  632 + if (el) {
  633 + layout[id] = {
  634 + x: parseInt(el.style.left,10) || 0,
  635 + y: parseInt(el.style.top,10) || 0,
  636 + w: parseInt(el.style.width,10) || 300,
  637 + h: parseInt(el.style.height,10) || 200
  638 + };
  639 + }
  640 + });
  641 + try { localStorage.setItem(STORAGE_KEY, JSON.stringify(layout)); } catch(e) {}
  642 + }
  643 +
  644 + /* ===== iframe rescale to fit card body ===== */
  645 + function rescaleIframe(iframe) {
  646 + if (!iframe || iframe.style.display === 'none') return;
  647 + var body = iframe.parentElement;
  648 + var cw = body.clientWidth, ch = body.clientHeight;
  649 + if (!cw || !ch) return;
  650 + var s = Math.min(cw / 1920, ch / 1080);
  651 + iframe.style.transform = 'scale(' + s + ')';
  652 + /* Center the scaled iframe in the body */
  653 + var sw = 1920 * s, sh = 1080 * s;
  654 + iframe.style.left = Math.max(0, (cw - sw) / 2) + 'px';
  655 + iframe.style.top = Math.max(0, (ch - sh) / 2) + 'px';
  656 + }
  657 + function rescaleAll() {
  658 + rescaleIframe(iframeCur);
  659 + rescaleIframe(iframeNxt);
  660 + }
  661 + window.addEventListener('resize', rescaleAll);
  662 +
  663 + /* ===== Drag (move card by header) ===== */
  664 + document.querySelectorAll('[data-drag]').forEach(function(handle){
  665 + handle.addEventListener('mousedown', function(e){
  666 + if (e.button !== 0) return;
  667 + var card = handle.closest('.pcard');
  668 + if (!card) return;
  669 + e.preventDefault();
  670 + card.classList.add('dragging');
  671 + document.body.classList.add('is-dragging-card');
  672 + var startX = e.clientX, startY = e.clientY;
  673 + var startL = parseInt(card.style.left,10) || 0;
  674 + var startT = parseInt(card.style.top,10) || 0;
  675 + function onMove(ev){
  676 + var nx = Math.max(0, Math.min(window.innerWidth - 100, startL + ev.clientX - startX));
  677 + var ny = Math.max(0, Math.min(window.innerHeight - 50, startT + ev.clientY - startY));
  678 + card.style.left = nx + 'px';
  679 + card.style.top = ny + 'px';
  680 + }
  681 + function onUp(){
  682 + card.classList.remove('dragging');
  683 + document.body.classList.remove('is-dragging-card');
  684 + document.removeEventListener('mousemove', onMove);
  685 + document.removeEventListener('mouseup', onUp);
  686 + saveLayout();
  687 + }
  688 + document.addEventListener('mousemove', onMove);
  689 + document.addEventListener('mouseup', onUp);
  690 + });
  691 + });
  692 +
  693 + /* ===== Resize (drag bottom-right corner) ===== */
  694 + document.querySelectorAll('[data-resize]').forEach(function(handle){
  695 + handle.addEventListener('mousedown', function(e){
  696 + if (e.button !== 0) return;
  697 + var card = handle.closest('.pcard');
  698 + if (!card) return;
  699 + e.preventDefault(); e.stopPropagation();
  700 + card.classList.add('resizing');
  701 + document.body.classList.add('is-dragging-card');
  702 + var startX = e.clientX, startY = e.clientY;
  703 + var startW = parseInt(card.style.width,10) || card.offsetWidth;
  704 + var startH = parseInt(card.style.height,10) || card.offsetHeight;
  705 + function onMove(ev){
  706 + var nw = Math.max(180, startW + ev.clientX - startX);
  707 + var nh = Math.max(100, startH + ev.clientY - startY);
  708 + card.style.width = nw + 'px';
  709 + card.style.height = nh + 'px';
  710 + if (card.querySelector('iframe')) rescaleIframe(card.querySelector('iframe'));
  711 + }
  712 + function onUp(){
  713 + card.classList.remove('resizing');
  714 + document.body.classList.remove('is-dragging-card');
  715 + document.removeEventListener('mousemove', onMove);
  716 + document.removeEventListener('mouseup', onUp);
  717 + rescaleAll();
  718 + saveLayout();
  719 + }
  720 + document.addEventListener('mousemove', onMove);
  721 + document.addEventListener('mouseup', onUp);
  722 + });
  723 + });
  724 +
  725 + /* ===== Preview iframe ready tracking =====
  726 + * Each iframe loads the deck ONCE with ?preview=1 on init. Subsequent
  727 + * slide changes are sent via postMessage('preview-goto') so the iframe
  728 + * just toggles visibility of a different .slide — no reload, no flicker.
  729 + */
  730 + var iframeReady = { cur: false, nxt: false };
  731 + var currentTheme = ${themeJSON};
  732 + window.addEventListener('message', function(e) {
  733 + if (!e.data || e.data.type !== 'preview-ready') return;
  734 + var iframe = null;
  735 + if (e.source === iframeCur.contentWindow) {
  736 + iframeReady.cur = true;
  737 + iframe = iframeCur;
  738 + postPreviewGoto(iframeCur, idx);
  739 + } else if (e.source === iframeNxt.contentWindow) {
  740 + iframeReady.nxt = true;
  741 + iframe = iframeNxt;
  742 + postPreviewGoto(iframeNxt, idx + 1 < total ? idx + 1 : idx);
  743 + }
  744 + /* Sync current theme to the iframe */
  745 + if (iframe && currentTheme) {
  746 + try { iframe.contentWindow.postMessage({ type: 'preview-theme', name: currentTheme }, '*'); } catch(err) {}
  747 + }
  748 + if (iframe) rescaleIframe(iframe);
  749 + });
  750 +
  751 + function postPreviewGoto(iframe, n) {
  752 + try {
  753 + iframe.contentWindow.postMessage({ type: 'preview-goto', idx: n }, '*');
  754 + } catch(e) {}
  755 + }
  756 +
  757 + /* ===== Update content =====
  758 + * Smooth (no-reload) navigation: send postMessage to iframes instead of
  759 + * resetting src. Iframes stay loaded, just switch visible .slide.
  760 + */
  761 + function update(n) {
  762 + n = Math.max(0, Math.min(total - 1, n));
  763 + idx = n;
  764 +
  765 + /* Current preview — postMessage (smooth) */
  766 + if (iframeReady.cur) postPreviewGoto(iframeCur, n);
  767 + curMeta.textContent = (n + 1) + '/' + total;
  768 +
  769 + /* Next preview */
  770 + if (n + 1 < total) {
  771 + iframeNxt.style.display = '';
  772 + var endEl = document.querySelector('#card-nxt .preview-end');
  773 + if (endEl) endEl.remove();
  774 + if (iframeReady.nxt) postPreviewGoto(iframeNxt, n + 1);
  775 + nxtMeta.textContent = (n + 2) + '/' + total;
  776 + } else {
  777 + iframeNxt.style.display = 'none';
  778 + var body = document.querySelector('#card-nxt .pcard-body');
  779 + if (body && !body.querySelector('.preview-end')) {
  780 + var end = document.createElement('div');
  781 + end.className = 'preview-end';
  782 + end.textContent = '— END OF DECK —';
  783 + body.appendChild(end);
  784 + }
  785 + nxtMeta.textContent = 'END';
  786 + }
  787 +
  788 + /* Notes */
  789 + var note = slideMeta[n].notes;
  790 + notesBody.innerHTML = note || '<span class="empty">(这一页还没有逐字稿)</span>';
  791 +
  792 + /* Timer count */
  793 + timerCount.textContent = (n + 1) + ' / ' + total;
  794 + }
  795 +
  796 + /* ===== Timer ===== */
  797 + var tStart = Date.now();
  798 + setInterval(function(){
  799 + var s = Math.floor((Date.now() - tStart) / 1000);
  800 + var mm = String(Math.floor(s/60)).padStart(2,'0');
  801 + var ss = String(s%60).padStart(2,'0');
  802 + timerDisplay.textContent = mm + ':' + ss;
  803 + }, 1000);
  804 + function resetTimer(){ tStart = Date.now(); timerDisplay.textContent = '00:00'; }
  805 +
  806 + /* ===== BroadcastChannel sync ===== */
  807 + if (bc) {
  808 + bc.onmessage = function(e){
  809 + if (!e.data) return;
  810 + if (e.data.type === 'go') update(e.data.idx);
  811 + else if (e.data.type === 'theme' && e.data.name) {
  812 + currentTheme = e.data.name;
  813 + /* Forward theme change to preview iframes */
  814 + [iframeCur, iframeNxt].forEach(function(iframe){
  815 + try {
  816 + iframe.contentWindow.postMessage({ type: 'preview-theme', name: e.data.name }, '*');
  817 + } catch(err) {}
  818 + });
  819 + }
  820 + };
  821 + }
  822 + function go(n) {
  823 + update(n);
  824 + if (bc) bc.postMessage({ type: 'go', idx: idx });
  825 + }
  826 +
  827 + /* ===== Buttons ===== */
  828 + document.getElementById('btn-prev').addEventListener('click', function(){ go(idx - 1); });
  829 + document.getElementById('btn-next').addEventListener('click', function(){ go(idx + 1); });
  830 + document.getElementById('btn-reset').addEventListener('click', resetTimer);
  831 + document.getElementById('reset-layout').addEventListener('click', function(){
  832 + if (confirm('恢复默认卡片布局?')) {
  833 + try { localStorage.removeItem(STORAGE_KEY); } catch(e){}
  834 + applyLayout(defaultLayout());
  835 + }
  836 + });
  837 +
  838 + /* ===== Keyboard ===== */
  839 + document.addEventListener('keydown', function(e){
  840 + if (e.metaKey || e.ctrlKey || e.altKey) return;
  841 + switch(e.key) {
  842 + case 'ArrowRight': case ' ': case 'PageDown': go(idx + 1); e.preventDefault(); break;
  843 + case 'ArrowLeft': case 'PageUp': go(idx - 1); e.preventDefault(); break;
  844 + case 'Home': go(0); break;
  845 + case 'End': go(total - 1); break;
  846 + case 'r': case 'R': resetTimer(); break;
  847 + case 'Escape': window.close(); break;
  848 + }
  849 + });
  850 +
  851 + /* ===== Iframe load → rescale (catches initial size) ===== */
  852 + iframeCur.addEventListener('load', function(){ rescaleIframe(iframeCur); });
  853 + iframeNxt.addEventListener('load', function(){ rescaleIframe(iframeNxt); });
  854 +
  855 + /* ===== Init =====
  856 + * Load each iframe ONCE with the deck file. After they post
  857 + * 'preview-ready', all subsequent navigation is via postMessage
  858 + * (smooth, no reload, no flicker).
  859 + */
  860 + applyLayout(readLayout());
  861 + iframeCur.src = deckUrl + '?preview=' + (idx + 1);
  862 + if (idx + 1 < total) iframeNxt.src = deckUrl + '?preview=' + (idx + 2);
  863 + /* Initialize notes/timer/count without touching iframes */
  864 + notesBody.innerHTML = slideMeta[idx].notes || '<span class="empty">(这一页还没有逐字稿)</span>';
  865 + curMeta.textContent = (idx + 1) + '/' + total;
  866 + nxtMeta.textContent = (idx + 2) + '/' + total;
  867 + timerCount.textContent = (idx + 1) + ' / ' + total;
  868 +})();
  869 +</` + `script>
  870 +</body></html>`;
  871 + }
  872 +
  873 + function fullscreen(){ const el=document.documentElement;
  874 + if (!document.fullscreenElement) el.requestFullscreen&&el.requestFullscreen();
  875 + else document.exitFullscreen&&document.exitFullscreen();
  876 + }
  877 +
  878 + // theme cycling
  879 + const root = document.documentElement;
  880 + const themesAttr = root.getAttribute('data-themes') || document.body.getAttribute('data-themes');
  881 + const themes = themesAttr ? themesAttr.split(',').map(s=>s.trim()).filter(Boolean) : [];
  882 + let themeIdx = 0;
  883 +
  884 + // Auto-detect theme base path from existing <link id="theme-link">
  885 + let themeBase = root.getAttribute('data-theme-base');
  886 + if (!themeBase) {
  887 + const existingLink = document.getElementById('theme-link');
  888 + if (existingLink) {
  889 + // el.getAttribute('href') gives the raw relative path written in HTML
  890 + const rawHref = existingLink.getAttribute('href') || '';
  891 + const lastSlash = rawHref.lastIndexOf('/');
  892 + themeBase = lastSlash >= 0 ? rawHref.substring(0, lastSlash + 1) : 'assets/themes/';
  893 + } else {
  894 + themeBase = 'assets/themes/';
  895 + }
  896 + }
  897 +
  898 + function applyTheme(name) {
  899 + let link = document.getElementById('theme-link');
  900 + if (!link) {
  901 + link = document.createElement('link');
  902 + link.rel = 'stylesheet';
  903 + link.id = 'theme-link';
  904 + document.head.appendChild(link);
  905 + }
  906 + link.href = themeBase + name + '.css';
  907 + root.setAttribute('data-theme', name);
  908 + const ind = document.querySelector('.theme-indicator');
  909 + if (ind) ind.textContent = name;
  910 + }
  911 + function cycleTheme(fromRemote){
  912 + if (!themes.length) return;
  913 + themeIdx = (themeIdx+1) % themes.length;
  914 + const name = themes[themeIdx];
  915 + applyTheme(name);
  916 + /* Broadcast to other window (audience ↔ presenter) */
  917 + if (!fromRemote && bc) bc.postMessage({ type: 'theme', name: name });
  918 + }
  919 +
  920 + // animation cycling on current slide
  921 + let animIdx = 0;
  922 + function cycleAnim(){
  923 + animIdx = (animIdx+1) % ANIMS.length;
  924 + const a = ANIMS[animIdx];
  925 + const target = slides[idx].querySelector('[data-anim-target]') || slides[idx];
  926 + ANIMS.forEach(x => target.classList.remove('anim-'+x));
  927 + void target.offsetWidth;
  928 + target.classList.add('anim-'+a);
  929 + target.setAttribute('data-anim', a);
  930 + const ind = document.querySelector('.anim-indicator');
  931 + if (ind) ind.textContent = a;
  932 + }
  933 +
  934 + document.addEventListener('keydown', function (e) {
  935 + if (e.metaKey||e.ctrlKey||e.altKey) return;
  936 + switch (e.key) {
  937 + case 'ArrowRight': case ' ': case 'PageDown': case 'Enter': go(idx+1); e.preventDefault(); break;
  938 + case 'ArrowLeft': case 'PageUp': case 'Backspace': go(idx-1); e.preventDefault(); break;
  939 + case 'Home': go(0); break;
  940 + case 'End': go(total-1); break;
  941 + case 'f': case 'F': fullscreen(); break;
  942 + case 's': case 'S': openPresenterWindow(); break;
  943 + case 'n': case 'N': toggleNotes(); break;
  944 + case 'o': case 'O': toggleOverview(); break;
  945 + case 't': case 'T': cycleTheme(); break;
  946 + case 'a': case 'A': cycleAnim(); break;
  947 + case 'Escape': toggleOverview(false); toggleNotes(false); break;
  948 + }
  949 + });
  950 +
  951 + // hash deep-link
  952 + function fromHash(){
  953 + const m = /^#\/(\d+)/.exec(location.hash||'');
  954 + if (m) go(Math.max(0, parseInt(m[1],10)-1));
  955 + }
  956 + window.addEventListener('hashchange', fromHash);
  957 + fromHash();
  958 + go(idx);
  959 + });
  960 +})();
docs/ppt-assets/style.css 0 → 100644
  1 +/* tech-sharing — 技术分享 dark, code-forward */
  2 +.tpl-tech-sharing{
  3 + --bg:#0d1117;--bg-soft:#161b22;--surface:#161b22;--surface-2:#1c2230;
  4 + --border:rgba(139,148,158,.22);--border-strong:rgba(139,148,158,.4);
  5 + --text-1:#e6edf3;--text-2:#8b949e;--text-3:#6e7681;
  6 + --accent:#7ee787;--accent-2:#79c0ff;--accent-3:#ff7b72;
  7 + --grad:linear-gradient(120deg,#7ee787 0%,#79c0ff 60%,#d2a8ff 100%);
  8 + --radius:14px;--radius-lg:20px;
  9 + --shadow:0 20px 60px rgba(0,0,0,.5);
  10 + font-family:'Inter','Noto Sans SC',sans-serif;
  11 +}
  12 +.tpl-tech-sharing{background:#0d1117;color:var(--text-1)}
  13 +.tpl-tech-sharing .slide{padding:72px 96px;background:#0d1117;color:var(--text-1)}
  14 +.tpl-tech-sharing .slide::before{content:"";position:absolute;inset:0;background:
  15 + radial-gradient(60% 50% at 90% 10%,rgba(121,192,255,.12),transparent 60%),
  16 + radial-gradient(50% 50% at 10% 90%,rgba(126,231,135,.08),transparent 60%);
  17 + pointer-events:none;z-index:0}
  18 +.tpl-tech-sharing .slide>*{position:relative;z-index:1}
  19 +.tpl-tech-sharing .h1{font-size:78px;line-height:1.03;font-weight:800;letter-spacing:-.03em;color:#fff}
  20 +.tpl-tech-sharing .h2{font-size:54px;font-weight:700;letter-spacing:-.025em;color:#fff}
  21 +.tpl-tech-sharing h3,.tpl-tech-sharing h4{color:#fff}
  22 +.tpl-tech-sharing .kicker{color:var(--accent);font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:600;text-transform:none;letter-spacing:.02em}
  23 +.tpl-tech-sharing .kicker::before{content:"> "}
  24 +.tpl-tech-sharing .mono{font-family:'JetBrains Mono','IBM Plex Mono',monospace}
  25 +.tpl-tech-sharing .terminal{background:#010409;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:0 30px 80px rgba(0,0,0,.6);font-family:'JetBrains Mono',monospace;font-size:15px;line-height:1.65}
  26 +.tpl-tech-sharing .terminal .bar{display:flex;align-items:center;gap:8px;padding:12px 16px;background:#161b22;border-bottom:1px solid var(--border);font-size:12px;color:var(--text-3)}
  27 +.tpl-tech-sharing .terminal .dot{width:12px;height:12px;border-radius:50%;background:#ff5f56}
  28 +.tpl-tech-sharing .terminal .dot:nth-child(2){background:#ffbd2e}
  29 +.tpl-tech-sharing .terminal .dot:nth-child(3){background:#27c93f}
  30 +.tpl-tech-sharing .terminal pre{margin:0;padding:24px 28px;color:#e6edf3;overflow:auto;max-height:440px}
  31 +.tpl-tech-sharing .kw{color:#ff7b72}
  32 +.tpl-tech-sharing .fn{color:#d2a8ff}
  33 +.tpl-tech-sharing .str{color:#a5d6ff}
  34 +.tpl-tech-sharing .cmt{color:#8b949e;font-style:italic}
  35 +.tpl-tech-sharing .num{color:#79c0ff}
  36 +.tpl-tech-sharing .card{background:var(--surface);border:1px solid var(--border);box-shadow:none}
  37 +.tpl-tech-sharing .card-accent{border-top:3px solid var(--accent)}
  38 +.tpl-tech-sharing .pill{background:var(--surface-2);color:var(--text-2);border-color:var(--border)}
  39 +.tpl-tech-sharing .pill-accent{background:rgba(126,231,135,.12);color:var(--accent);border-color:rgba(126,231,135,.35)}
  40 +.tpl-tech-sharing .tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--surface-2);border:1px solid var(--border);color:var(--text-2)}
  41 +.tpl-tech-sharing .agenda-row{display:flex;align-items:baseline;gap:24px;padding:18px 0;border-bottom:1px dashed var(--border);font-family:'JetBrains Mono',monospace}
  42 +.tpl-tech-sharing .agenda-row .num{color:var(--accent);flex:none;width:48px}
  43 +.tpl-tech-sharing .agenda-row .t{color:#fff;font-size:24px;flex:1;font-family:'Inter',sans-serif;font-weight:600}
  44 +.tpl-tech-sharing .agenda-row .d{color:var(--text-3);font-size:13px}
  45 +.tpl-tech-sharing .speaker{display:flex;align-items:center;gap:14px;margin-top:28px}
  46 +.tpl-tech-sharing .speaker .av{width:56px;height:56px;border-radius:50%;background:var(--grad)}
  47 +.tpl-tech-sharing .speaker b{display:block;color:#fff;font-size:18px}
  48 +.tpl-tech-sharing .speaker span{color:var(--text-3);font-size:13px;font-family:'JetBrains Mono',monospace}
  49 +.tpl-tech-sharing .lede{color:var(--text-2)}
docs/urbanops-architecture-ppt.html 0 → 100644
  1 +<!DOCTYPE html>
  2 +<html lang="zh-CN">
  3 +<head>
  4 +<meta charset="UTF-8">
  5 +<meta name="viewport" content="width=device-width, initial-scale=1.0">
  6 +<title>全域运营平台 — 微服务架构技术分享</title>
  7 +<style>
  8 +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
  9 +
  10 +:root {
  11 + --bg: #0a0e17;
  12 + --bg-card: #111827;
  13 + --bg-card2: #1a2332;
  14 + --border: #1e2d45;
  15 + --text: #e2e8f0;
  16 + --text-dim: #8899aa;
  17 + --accent: #38bdf8;
  18 + --accent2: #818cf8;
  19 + --accent3: #34d399;
  20 + --accent4: #f472b6;
  21 + --accent5: #fbbf24;
  22 + --red: #ef4444;
  23 + --orange: #f97316;
  24 + --green: #22c55e;
  25 + --blue: #3b82f6;
  26 +}
  27 +
  28 +* { margin: 0; padding: 0; box-sizing: border-box; }
  29 +
  30 +body {
  31 + font-family: 'Inter', 'Noto Sans SC', -apple-system, sans-serif;
  32 + background: var(--bg);
  33 + color: var(--text);
  34 + overflow: hidden;
  35 + height: 100vh;
  36 + width: 100vw;
  37 +}
  38 +
  39 +.slides-container { width: 100%; height: 100%; position: relative; }
  40 +
  41 +.slide {
  42 + position: absolute;
  43 + inset: 0;
  44 + display: flex;
  45 + flex-direction: column;
  46 + align-items: center;
  47 + justify-content: center;
  48 + padding: 60px 80px;
  49 + opacity: 0;
  50 + transform: scale(0.95);
  51 + transition: opacity .5s, transform .5s;
  52 + pointer-events: none;
  53 +}
  54 +.slide.active {
  55 + opacity: 1;
  56 + transform: scale(1);
  57 + pointer-events: all;
  58 +}
  59 +
  60 +/* Navigation */
  61 +.nav {
  62 + position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
  63 + display: flex; gap: 12px; z-index: 100; align-items: center;
  64 +}
  65 +.nav button {
  66 + width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--border);
  67 + background: var(--bg-card); color: var(--text); cursor: pointer;
  68 + font-size: 18px; transition: all .2s; display: flex; align-items: center; justify-content: center;
  69 +}
  70 +.nav button:hover { background: var(--accent); color: #000; border-color: var(--accent); }
  71 +.nav .page-indicator {
  72 + color: var(--text-dim); font-size: 14px; min-width: 80px; text-align: center;
  73 + font-variant-numeric: tabular-nums; font-family: 'Inter', monospace;
  74 +}
  75 +.nav .progress {
  76 + position: absolute; bottom: -8px; left: 0; height: 2px;
  77 + background: var(--accent); transition: width .4s; border-radius: 1px;
  78 +}
  79 +
  80 +/* Typography */
  81 +h1 { font-size: 3.2em; font-weight: 900; letter-spacing: -1px; line-height: 1.2; }
  82 +h2 { font-size: 2.4em; font-weight: 700; margin-bottom: 40px; letter-spacing: -.5px; }
  83 +h3 { font-size: 1.3em; font-weight: 600; margin-bottom: 12px; }
  84 +.subtitle { font-size: 1.3em; color: var(--text-dim); margin-top: 16px; font-weight: 300; }
  85 +.gradient-text {
  86 + background: linear-gradient(135deg, var(--accent), var(--accent2), var(--accent4));
  87 + -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  88 + background-clip: text;
  89 +}
  90 +
  91 +/* Cards */
  92 +.card-grid { display: grid; gap: 20px; width: 100%; max-width: 1100px; }
  93 +.card-grid.cols-2 { grid-template-columns: 1fr 1fr; }
  94 +.card-grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
  95 +.card-grid.cols-4 { grid-template-columns: repeat(4, 1fr); }
  96 +.card {
  97 + background: var(--bg-card); border: 1px solid var(--border); border-radius: 16px;
  98 + padding: 28px; transition: all .3s;
  99 +}
  100 +.card:hover { border-color: var(--accent); transform: translateY(-2px); }
  101 +.card h3 { color: var(--accent); }
  102 +.card p, .card li { color: var(--text-dim); font-size: .92em; line-height: 1.6; }
  103 +.card ul { list-style: none; padding: 0; }
  104 +.card ul li::before { content: "▸ "; color: var(--accent); }
  105 +
  106 +/* Tech tags */
  107 +.tag {
  108 + display: inline-block; padding: 4px 12px; border-radius: 20px;
  109 + font-size: .78em; font-weight: 600; margin: 3px;
  110 +}
  111 +.tag.blue { background: rgba(56,189,248,.15); color: var(--accent); }
  112 +.tag.purple { background: rgba(129,140,248,.15); color: var(--accent2); }
  113 +.tag.green { background: rgba(52,211,153,.15); color: var(--accent3); }
  114 +.tag.pink { background: rgba(244,114,182,.15); color: var(--accent4); }
  115 +.tag.yellow { background: rgba(251,191,36,.15); color: var(--accent5); }
  116 +.tag.red { background: rgba(239,68,68,.15); color: var(--red); }
  117 +
  118 +/* SVG area */
  119 +.diagram-area { width: 100%; max-width: 1100px; flex: 1; display: flex; align-items: center; justify-content: center; }
  120 +.diagram-area svg { width: 100%; height: 100%; max-height: 500px; }
  121 +
  122 +/* Flow arrows */
  123 +.flow-grid { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; justify-content: center; }
  124 +.flow-box {
  125 + background: var(--bg-card); border: 2px solid var(--border); border-radius: 14px;
  126 + padding: 20px 30px; text-align: center; font-weight: 600; min-width: 140px;
  127 + transition: all .3s;
  128 +}
  129 +.flow-box.accent { border-color: var(--accent); background: rgba(56,189,248,.08); }
  130 +.flow-box.purple { border-color: var(--accent2); background: rgba(129,140,248,.08); }
  131 +.flow-box.green { border-color: var(--accent3); background: rgba(52,211,153,.08); }
  132 +.flow-arrow { font-size: 1.8em; color: var(--text-dim); }
  133 +
  134 +/* Slide specific */
  135 +.cover { text-align: center; }
  136 +.cover .logo-area { margin-bottom: 40px; }
  137 +.cover .badges { margin-top: 30px; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
  138 +.cover .line { width: 80px; height: 4px; background: linear-gradient(90deg, var(--accent), var(--accent4)); border-radius: 2px; margin: 30px auto; }
  139 +
  140 +.section-header { text-align: center; }
  141 +.section-header .line { width: 60px; height: 3px; background: var(--accent); margin: 20px auto; border-radius: 2px; }
  142 +
  143 +/* Layer diagram */
  144 +.layer-stack { display: flex; flex-direction: column; gap: 12px; width: 100%; max-width: 1000px; }
  145 +.layer {
  146 + display: flex; align-items: center; gap: 20px; padding: 16px 24px;
  147 + border-radius: 12px; background: var(--bg-card); border: 1px solid var(--border);
  148 +}
  149 +.layer-label {
  150 + font-weight: 700; font-size: .9em; min-width: 100px; text-align: right;
  151 + color: var(--accent);
  152 +}
  153 +.layer-content { display: flex; gap: 8px; flex-wrap: wrap; }
  154 +
  155 +/* Stats */
  156 +.stat-row { display: flex; gap: 40px; justify-content: center; }
  157 +.stat { text-align: center; }
  158 +.stat .num { font-size: 3em; font-weight: 900; background: linear-gradient(135deg, var(--accent), var(--accent2));
  159 + -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
  160 +.stat .label { color: var(--text-dim); font-size: .9em; margin-top: 4px; }
  161 +
  162 +/* Tree */
  163 +.tree { font-family: 'SF Mono', 'Fira Code', monospace; font-size: .85em; line-height: 1.8; }
  164 +.tree .root { color: var(--accent); font-weight: 700; }
  165 +.tree .branch { color: var(--accent2); }
  166 +.tree .leaf { color: var(--text-dim); }
  167 +.tree .highlight { color: var(--accent3); font-weight: 600; }
  168 +
  169 +/* Mermaid-like diagram */
  170 +.arch-diagram { position: relative; width: 100%; max-width: 1000px; height: 440px; }
  171 +.arch-node {
  172 + position: absolute; padding: 14px 20px; border-radius: 10px;
  173 + font-size: .82em; font-weight: 600; text-align: center;
  174 + border: 2px solid; transition: all .3s;
  175 +}
  176 +.arch-node:hover { z-index: 10; transform: scale(1.05); }
  177 +
  178 +@media (max-width: 1024px) {
  179 + .slide { padding: 40px 30px; }
  180 + h1 { font-size: 2em; } h2 { font-size: 1.6em; }
  181 + .card-grid.cols-3, .card-grid.cols-4 { grid-template-columns: 1fr 1fr; }
  182 +}
  183 +</style>
  184 +</head>
  185 +<body>
  186 +
  187 +<div class="slides-container" id="slides">
  188 +
  189 +<!-- ==================== SLIDE 1: COVER ==================== -->
  190 +<div class="slide active" data-slide="0">
  191 + <div class="cover">
  192 + <div class="logo-area">
  193 + <div style="font-size:5em;margin-bottom:10px">&#x1f3d9;</div>
  194 + </div>
  195 + <h1 class="gradient-text">全域运营平台</h1>
  196 + <h1 style="margin-top:4px">微服务架构技术分享</h1>
  197 + <div class="line"></div>
  198 + <p class="subtitle">模块化架构设计 · 技术栈全景 · 最佳实践</p>
  199 + <div class="badges">
  200 + <span class="tag blue">Spring Boot 3.5</span>
  201 + <span class="tag purple">JDK 17</span>
  202 + <span class="tag green">Maven 多模块</span>
  203 + <span class="tag pink">Flowable 7</span>
  204 + <span class="tag yellow">IoT 网关</span>
  205 + <span class="tag blue">Docker</span>
  206 + </div>
  207 + <p style="margin-top:40px;color:var(--text-dim);font-size:.85em">2026年5月 · 技术团队内部分享</p>
  208 + </div>
  209 +</div>
  210 +
  211 +<!-- ==================== SLIDE 2: PROJECT OVERVIEW ==================== -->
  212 +<div class="slide" data-slide="1">
  213 + <h2>项目概览</h2>
  214 + <div class="card-grid cols-2" style="max-width:1000px">
  215 + <div class="card">
  216 + <h3>定位</h3>
  217 + <p>面向城市运营管理的综合业务平台,覆盖园林养护、工单调度、IoT 设备管理、BPM 流程引擎等核心业务场景。</p>
  218 + </div>
  219 + <div class="card">
  220 + <h3>规模</h3>
  221 + <div class="stat-row" style="margin-top:8px">
  222 + <div class="stat"><div class="num">4,525+</div><div class="label">Java 源文件</div></div>
  223 + <div class="stat"><div class="num">15</div><div class="label">框架 Starter</div></div>
  224 + <div class="stat"><div class="num">10+</div><div class="label">业务模块</div></div>
  225 + </div>
  226 + </div>
  227 + <div class="card">
  228 + <h3>架构风格</h3>
  229 + <p><strong>模块化单体</strong> 向微服务演进中 — 通过 Maven 多模块 + API 契约层实现模块解耦,IoT 网关已独立部署。</p>
  230 + </div>
  231 + <div class="card">
  232 + <h3>核心能力</h3>
  233 + <ul>
  234 + <li>多租户 SaaS 架构</li>
  235 + <li>Flowable 工作流引擎</li>
  236 + <li>IoT 设备接入网关 (MQTT/HTTP/TCP)</li>
  237 + <li>分布式消息 & 定时任务</li>
  238 + <li>Spring AI 多模型接入</li>
  239 + </ul>
  240 + </div>
  241 + </div>
  242 +</div>
  243 +
  244 +<!-- ==================== SLIDE 3: TECH STACK ==================== -->
  245 +<div class="slide" data-slide="2">
  246 + <h2>技术栈全景图</h2>
  247 + <div class="layer-stack">
  248 + <div class="layer">
  249 + <div class="layer-label">前端</div>
  250 + <div class="layer-content">
  251 + <span class="tag blue">Vue 3</span><span class="tag blue">Element Plus</span>
  252 + <span class="tag purple">Vben Admin</span><span class="tag blue">Vue 2</span>
  253 + <span class="tag green">uni-app</span><span class="tag pink">TypeScript</span>
  254 + </div>
  255 + </div>
  256 + <div class="layer">
  257 + <div class="layer-label">网关 & 接入</div>
  258 + <div class="layer-content">
  259 + <span class="tag blue">Vert.x</span><span class="tag green">MQTT</span>
  260 + <span class="tag purple">HTTP/2</span><span class="tag pink">TCP</span>
  261 + <span class="tag blue">WebSocket</span>
  262 + </div>
  263 + </div>
  264 + <div class="layer">
  265 + <div class="layer-label">业务框架</div>
  266 + <div class="layer-content">
  267 + <span class="tag blue">Spring Boot 3.5</span><span class="tag purple">Spring Security</span>
  268 + <span class="tag green">Flowable 7</span><span class="tag pink">Quartz</span>
  269 + <span class="tag yellow">Spring AI</span><span class="tag blue">PF4J</span>
  270 + </div>
  271 + </div>
  272 + <div class="layer">
  273 + <div class="layer-label">数据层</div>
  274 + <div class="layer-content">
  275 + <span class="tag blue">MyBatis Plus</span><span class="tag purple">Druid</span>
  276 + <span class="tag green">MySQL 8</span><span class="tag pink">Redis Sentinel</span>
  277 + <span class="tag yellow">Redisson</span><span class="tag blue">Dynamic DS</span>
  278 + </div>
  279 + </div>
  280 + <div class="layer">
  281 + <div class="layer-label">消息 & 集成</div>
  282 + <div class="layer-content">
  283 + <span class="tag green">RocketMQ</span><span class="tag blue">Kafka</span>
  284 + <span class="tag purple">RabbitMQ</span><span class="tag pink">JustAuth</span>
  285 + <span class="tag yellow">UAA SSO</span><span class="tag blue">S3</span>
  286 + </div>
  287 + </div>
  288 + <div class="layer">
  289 + <div class="layer-label">可观测性</div>
  290 + <div class="layer-content">
  291 + <span class="tag blue">SkyWalking</span><span class="tag purple">SBA Admin</span>
  292 + <span class="tag green">OpenTracing</span><span class="tag pink">Actuator</span>
  293 + <span class="tag yellow">Knife4j</span>
  294 + </div>
  295 + </div>
  296 + <div class="layer">
  297 + <div class="layer-label">部署 & DevOps</div>
  298 + <div class="layer-content">
  299 + <span class="tag blue">Docker</span><span class="tag green">docker-compose</span>
  300 + <span class="tag purple">Eclipse Temurin 21</span><span class="tag pink">Maven</span>
  301 + <span class="tag yellow">Git</span>
  302 + </div>
  303 + </div>
  304 + </div>
  305 +</div>
  306 +
  307 +<!-- ==================== SLIDE 4: ARCHITECTURE OVERVIEW ==================== -->
  308 +<div class="slide" data-slide="3">
  309 + <h2>系统架构总览</h2>
  310 + <div class="diagram-area">
  311 + <svg viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
  312 + <defs>
  313 + <linearGradient id="gAccent" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#38bdf8"/><stop offset="100%" stop-color="#818cf8"/></linearGradient>
  314 + <linearGradient id="gGreen" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#34d399"/><stop offset="100%" stop-color="#22c55e"/></linearGradient>
  315 + <linearGradient id="gPink" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f472b6"/><stop offset="100%" stop-color="#e879f9"/></linearGradient>
  316 + <linearGradient id="gYellow" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#fbbf24"/><stop offset="100%" stop-color="#f59e0b"/></linearGradient>
  317 + <filter id="shadow"><feDropShadow dx="0" dy="2" stdDeviation="4" flood-opacity="0.3"/></filter>
  318 + </defs>
  319 +
  320 + <!-- Top: Client Layer -->
  321 + <rect x="30" y="10" width="940" height="60" rx="10" fill="#111827" stroke="#1e3a5f" stroke-width="1.5"/>
  322 + <text x="500" y="35" text-anchor="middle" fill="#8899aa" font-size="12" font-weight="600">接入层 CLIENT LAYER</text>
  323 + <rect x="60" y="42" width="100" height="22" rx="6" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="1"/>
  324 + <text x="110" y="57" text-anchor="middle" fill="#38bdf8" font-size="10">Vue 3 Web</text>
  325 + <rect x="180" y="42" width="100" height="22" rx="6" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="1"/>
  326 + <text x="230" y="57" text-anchor="middle" fill="#38bdf8" font-size="10">uni-app 小程序</text>
  327 + <rect x="300" y="42" width="100" height="22" rx="6" fill="rgba(129,140,248,.12)" stroke="#818cf8" stroke-width="1"/>
  328 + <text x="350" y="57" text-anchor="middle" fill="#818cf8" font-size="10">IoT 设备</text>
  329 + <rect x="420" y="42" width="100" height="22" rx="6" fill="rgba(129,140,248,.12)" stroke="#818cf8" stroke-width="1"/>
  330 + <text x="470" y="57" text-anchor="middle" fill="#818cf8" font-size="10">第三方系统</text>
  331 +
  332 + <!-- Arrow Client -> Gateway -->
  333 + <line x1="500" y1="70" x2="500" y2="90" stroke="#8899aa" stroke-width="1.5" stroke-dasharray="4,3"/>
  334 +
  335 + <!-- Gateway Layer -->
  336 + <rect x="30" y="90" width="940" height="80" rx="10" fill="#111827" stroke="#1e3a5f" stroke-width="1.5"/>
  337 + <text x="500" y="115" text-anchor="middle" fill="#8899aa" font-size="12" font-weight="600">网关与接入 GATEWAY LAYER</text>
  338 + <rect x="60" y="127" width="200" height="32" rx="8" fill="rgba(56,189,248,.1)" stroke="#38bdf8" stroke-width="1.5" filter="url(#shadow)"/>
  339 + <text x="160" y="147" text-anchor="middle" fill="#38bdf8" font-size="11" font-weight="600">UrbanOps Server :48081</text>
  340 + <rect x="300" y="127" width="200" height="32" rx="8" fill="rgba(52,211,153,.1)" stroke="#34d399" stroke-width="1.5" filter="url(#shadow)"/>
  341 + <text x="400" y="147" text-anchor="middle" fill="#34d399" font-size="11" font-weight="600">IoT 网关 :8092 (独立部署)</text>
  342 + <rect x="540" y="127" width="180" height="32" rx="8" fill="rgba(129,140,248,.1)" stroke="#818cf8" stroke-width="1.5" filter="url(#shadow)"/>
  343 + <text x="630" y="147" text-anchor="middle" fill="#818cf8" font-size="11" font-weight="600">UAA SSO :28201</text>
  344 +
  345 + <!-- Arrow Gateway -> Business -->
  346 + <line x1="500" y1="170" x2="500" y2="190" stroke="#8899aa" stroke-width="1.5" stroke-dasharray="4,3"/>
  347 +
  348 + <!-- Business Module Layer -->
  349 + <rect x="30" y="190" width="940" height="130" rx="10" fill="#111827" stroke="#1e3a5f" stroke-width="1.5"/>
  350 + <text x="500" y="215" text-anchor="middle" fill="#8899aa" font-size="12" font-weight="600">业务模块层 BUSINESS MODULES</text>
  351 +
  352 + <rect x="50" y="228" width="145" height="42" rx="8" fill="rgba(56,189,248,.1)" stroke="#38bdf8" stroke-width="1.5"/>
  353 + <text x="122" y="246" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">system</text>
  354 + <text x="122" y="260" text-anchor="middle" fill="#8899aa" font-size="9">用户·权限·租户</text>
  355 +
  356 + <rect x="210" y="228" width="145" height="42" rx="8" fill="rgba(129,140,248,.1)" stroke="#818cf8" stroke-width="1.5"/>
  357 + <text x="282" y="246" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">garden</text>
  358 + <text x="282" y="260" text-anchor="middle" fill="#8899aa" font-size="9">园林养护·巡检</text>
  359 +
  360 + <rect x="370" y="228" width="145" height="42" rx="8" fill="rgba(52,211,153,.1)" stroke="#34d399" stroke-width="1.5"/>
  361 + <text x="442" y="246" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">workorder</text>
  362 + <text x="442" y="260" text-anchor="middle" fill="#8899aa" font-size="9">工单·调度</text>
  363 +
  364 + <rect x="530" y="228" width="145" height="42" rx="8" fill="rgba(244,114,182,.1)" stroke="#f472b6" stroke-width="1.5"/>
  365 + <text x="602" y="246" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">bpm</text>
  366 + <text x="602" y="260" text-anchor="middle" fill="#8899aa" font-size="9">Flowable 流程</text>
  367 +
  368 + <rect x="690" y="228" width="145" height="42" rx="8" fill="rgba(251,191,36,.1)" stroke="#fbbf24" stroke-width="1.5"/>
  369 + <text x="762" y="246" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">gloabl</text>
  370 + <text x="762" y="260" text-anchor="middle" fill="#8899aa" font-size="9">全域运营·微信</text>
  371 +
  372 + <!-- API Contract row -->
  373 + <rect x="250" y="280" width="500" height="30" rx="8" fill="rgba(255,255,255,.03)" stroke="#334155" stroke-width="1" stroke-dasharray="4,3"/>
  374 + <text x="500" y="299" text-anchor="middle" fill="#8899aa" font-size="10">API 契约层: garden-api · workorder-api (接口定义 + Feign 风格调用)</text>
  375 +
  376 + <!-- Arrow Business -> Infra -->
  377 + <line x1="500" y1="320" x2="500" y2="340" stroke="#8899aa" stroke-width="1.5" stroke-dasharray="4,3"/>
  378 +
  379 + <!-- Infrastructure Layer -->
  380 + <rect x="30" y="340" width="940" height="50" rx="10" fill="#111827" stroke="#1e3a5f" stroke-width="1.5"/>
  381 + <text x="500" y="370" text-anchor="middle" fill="#8899aa" font-size="12" font-weight="600">基础设施层 INFRASTRUCTURE: 15个Spring Boot Starter (Web · Security · MyBatis · Redis · MQ · Job · Excel · WebSocket · Monitor · Protection · Tenant · DataPermission · IP · Test)</text>
  382 +
  383 + <!-- Arrow Infra -> Data -->
  384 + <line x1="500" y1="390" x2="500" y2="408" stroke="#8899aa" stroke-width="1.5" stroke-dasharray="4,3"/>
  385 +
  386 + <!-- Data Layer -->
  387 + <rect x="30" y="408" width="940" height="45" rx="10" fill="#0a0e17" stroke="#1e2e45" stroke-width="1.5"/>
  388 + <text x="500" y="435" text-anchor="middle" fill="#8899aa" font-size="12" font-weight="600">数据层: MySQL 8 · Redis Sentinel · RocketMQ · Kafka · RabbitMQ · S3 · Druid · Redisson</text>
  389 + </svg>
  390 + </div>
  391 +</div>
  392 +
  393 +<!-- ==================== SLIDE 5: MODULE DEPENDENCIES ==================== -->
  394 +<div class="slide" data-slide="4">
  395 + <h2>模块依赖关系图</h2>
  396 + <div class="diagram-area">
  397 + <svg viewBox="0 0 1000 440" xmlns="http://www.w3.org/2000/svg">
  398 + <defs>
  399 + <marker id="arrowBlue" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#38bdf8"/></marker>
  400 + <marker id="arrowGray" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#64748b"/></marker>
  401 + </defs>
  402 +
  403 + <!-- Top level: dependencies BOM -->
  404 + <rect x="350" y="8" width="300" height="38" rx="10" fill="rgba(251,191,36,.1)" stroke="#fbbf24" stroke-width="2"/>
  405 + <text x="500" y="32" text-anchor="middle" fill="#fbbf24" font-size="13" font-weight="700">urbanops-dependencies (BOM)</text>
  406 +
  407 + <line x1="500" y1="46" x2="500" y2="70" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#arrowBlue)"/>
  408 +
  409 + <!-- Framework layer -->
  410 + <rect x="80" y="72" width="840" height="58" rx="10" fill="#111827" stroke="#1e3a5f" stroke-width="1.5"/>
  411 + <text x="500" y="94" text-anchor="middle" fill="#8899aa" font-size="11" font-weight="600">urbanops-framework</text>
  412 + <text x="500" y="113" text-anchor="middle" fill="#64748b" font-size="9">common · web · security · mybatis · redis · mq · job · excel · websocket · monitor · protection · tenant · data-permission · ip · test</text>
  413 +
  414 + <line x1="300" y1="130" x2="300" y2="155" stroke="#64748b" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  415 + <line x1="500" y1="130" x2="500" y2="155" stroke="#64748b" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  416 + <line x1="700" y1="130" x2="700" y2="155" stroke="#64748b" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  417 +
  418 + <!-- infra -->
  419 + <rect x="210" y="158" width="180" height="40" rx="8" fill="rgba(99,102,241,.12)" stroke="#6366f1" stroke-width="1.5"/>
  420 + <text x="300" y="183" text-anchor="middle" fill="#a5b4fc" font-size="12" font-weight="600">module-infra</text>
  421 +
  422 + <!-- system -->
  423 + <rect x="410" y="158" width="180" height="40" rx="8" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="1.5"/>
  424 + <text x="500" y="183" text-anchor="middle" fill="#38bdf8" font-size="12" font-weight="600">module-system</text>
  425 +
  426 + <!-- global -->
  427 + <rect x="610" y="158" width="180" height="40" rx="8" fill="rgba(251,191,36,.12)" stroke="#fbbf24" stroke-width="1.5"/>
  428 + <text x="700" y="183" text-anchor="middle" fill="#fbbf24" font-size="12" font-weight="600">module-gloabl</text>
  429 +
  430 + <!-- Arrows down from infra and system -->
  431 + <line x1="300" y1="198" x2="300" y2="230" stroke="#64748b" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  432 + <line x1="500" y1="198" x2="360" y2="230" stroke="#38bdf8" stroke-width="1.2"/>
  433 + <line x1="500" y1="198" x2="640" y2="230" stroke="#38bdf8" stroke-width="1.2"/>
  434 +
  435 + <!-- bpm -->
  436 + <rect x="210" y="233" width="180" height="40" rx="8" fill="rgba(244,114,182,.12)" stroke="#f472b6" stroke-width="1.5"/>
  437 + <text x="300" y="258" text-anchor="middle" fill="#f472b6" font-size="12" font-weight="600">module-bpm</text>
  438 +
  439 + <!-- garden -->
  440 + <rect x="85" y="308" width="200" height="40" rx="8" fill="rgba(52,211,153,.12)" stroke="#34d399" stroke-width="2"/>
  441 + <text x="185" y="333" text-anchor="middle" fill="#34d399" font-size="12" font-weight="600">module-garden</text>
  442 +
  443 + <!-- workorder -->
  444 + <rect x="420" y="308" width="200" height="40" rx="8" fill="rgba(52,211,153,.12)" stroke="#34d399" stroke-width="2"/>
  445 + <text x="520" y="333" text-anchor="middle" fill="#34d399" font-size="12" font-weight="600">module-workorder</text>
  446 +
  447 + <!-- API modules -->
  448 + <rect x="680" y="308" width="220" height="40" rx="8" fill="rgba(129,140,248,.08)" stroke="#818cf8" stroke-width="1" stroke-dasharray="5,3"/>
  449 + <text x="790" y="333" text-anchor="middle" fill="#818cf8" font-size="11" font-weight="600">module-api (契约层)</text>
  450 +
  451 + <!-- Arrows from bpm -->
  452 + <line x1="240" y1="273" x2="185" y2="307" stroke="#f472b6" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  453 + <line x1="360" y1="273" x2="520" y2="307" stroke="#f472b6" stroke-width="1.2" marker-end="url(#arrowGray)"/>
  454 +
  455 + <!-- API arrows -->
  456 + <line x1="285" y1="330" x2="420" y2="330" stroke="#818cf8" stroke-width="1" stroke-dasharray="5,3" marker-end="url(#arrowGray)"/>
  457 + <line x1="620" y1="330" x2="680" y2="330" stroke="#818cf8" stroke-width="1" stroke-dasharray="5,3" marker-end="url(#arrowGray)"/>
  458 +
  459 + <!-- IoT (separate) -->
  460 + <rect x="420" y="380" width="220" height="40" rx="8" fill="rgba(239,68,68,.12)" stroke="#ef4444" stroke-width="1.5" stroke-dasharray="6,3"/>
  461 + <text x="530" y="405" text-anchor="middle" fill="#ef4444" font-size="12" font-weight="600">IoT 网关 (独立进程 :8092)</text>
  462 + <text x="530" y="395" text-anchor="middle" fill="#f87171" font-size="8" font-weight="400">Vert.x · MQTT · HTTP · TCP</text>
  463 +
  464 + <line x1="620" y1="348" x2="560" y2="379" stroke="#ef4444" stroke-width="1" stroke-dasharray="5,3"/>
  465 +
  466 + <!-- Legend -->
  467 + <rect x="30" y="415" width="12" height="12" rx="2" fill="none" stroke="#34d399" stroke-width="1.5"/>
  468 + <text x="48" y="425" fill="#64748b" font-size="9">核心业务域</text>
  469 + <rect x="150" y="415" width="12" height="12" rx="2" fill="none" stroke="#38bdf8" stroke-width="1.5"/>
  470 + <text x="168" y="425" fill="#64748b" font-size="9">基础服务</text>
  471 + <rect x="260" y="415" width="12" height="12" rx="2" fill="none" stroke="#ef4444" stroke-width="1.5" stroke-dasharray="3,2"/>
  472 + <text x="278" y="425" fill="#64748b" font-size="9">独立部署</text>
  473 + <line x1="370" y1="421" x2="400" y2="421" stroke="#818cf8" stroke-width="1" stroke-dasharray="5,3"/>
  474 + <text x="408" y="425" fill="#64748b" font-size="9">API 契约调用</text>
  475 + </svg>
  476 + </div>
  477 +</div>
  478 +
  479 +<!-- ==================== SLIDE 6: FRAMEWORK ARCHITECTURE ==================== -->
  480 +<div class="slide" data-slide="5">
  481 + <h2>框架层 — 15 个 Spring Boot Starter</h2>
  482 + <div class="card-grid cols-4" style="max-width:1100px">
  483 + <div class="card"><h3>web</h3><p>全局异常处理、API 访问日志、敏感数据脱敏、错误码体系</p></div>
  484 + <div class="card"><h3>security</h3><p>Spring Security + Token 认证、操作日志(bizlog)、权限校验</p></div>
  485 + <div class="card"><h3>mybatis</h3><p>MyBatis Plus + Join、动态数据源、多数据库适配</p></div>
  486 + <div class="card"><h3>redis</h3><p>Redis Sentinel 集群、Redisson 分布式锁、Spring Cache</p></div>
  487 + <div class="card"><h3>mq</h3><p>消息队列抽象层 — RocketMQ / Kafka / RabbitMQ 统一接口</p></div>
  488 + <div class="card"><h3>job</h3><p>Quartz 分布式定时任务、JDBC 持久化、集群调度</p></div>
  489 + <div class="card"><h3>excel</h3><p>FastExcel 导入导出、模板下载、大数据量流式处理</p></div>
  490 + <div class="card"><h3>websocket</h3><p>WebSocket 集群广播,支持 Redis/MQ 跨节点消息同步</p></div>
  491 + <div class="card"><h3>monitor</h3><p>Spring Boot Admin、SkyWalking 链路追踪、OpenTracing</p></div>
  492 + <div class="card"><h3>protection</h3><p>分布式锁(Lock4j)、幂等性、限流、API 签名校验</p></div>
  493 + <div class="card"><h3>tenant</h3><p>多租户数据隔离、租户上下文传递、SaaS 计费</p></div>
  494 + <div class="card"><h3>data-permission</h3><p>数据权限控制 — 部门/用户级数据隔离</p></div>
  495 + <div class="card"><h3>ip</h3><p>IP 归属地解析、地区库自动更新</p></div>
  496 + <div class="card"><h3>test</h3><p>单元测试工具、Mock 工具集</p></div>
  497 + <div class="card"><h3>common</h3><p>通用工具、枚举、基础类、Hutool + Guava 集成</p></div>
  498 + </div>
  499 +</div>
  500 +
  501 +<!-- ==================== SLIDE 7: DATA FLOW & COMMUNICATION ==================== -->
  502 +<div class="slide" data-slide="6">
  503 + <h2>数据流与通信架构</h2>
  504 + <div class="diagram-area">
  505 + <svg viewBox="0 0 1000 420" xmlns="http://www.w3.org/2000/svg">
  506 + <defs>
  507 + <marker id="arrGreen" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#34d399"/></marker>
  508 + <marker id="arrBlue" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#38bdf8"/></marker>
  509 + <marker id="arrPink" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#f472b6"/></marker>
  510 + <marker id="arrYellow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#fbbf24"/></marker>
  511 + </defs>
  512 +
  513 + <!-- Central: UrbanOps Server -->
  514 + <rect x="350" y="160" width="300" height="60" rx="14" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="2.5" filter="url(#shadow)"/>
  515 + <text x="500" y="187" text-anchor="middle" fill="#e2e8f0" font-size="15" font-weight="700">UrbanOps Server</text>
  516 + <text x="500" y="205" text-anchor="middle" fill="#8899aa" font-size="10">核心业务处理引擎</text>
  517 +
  518 + <!-- SQL -->
  519 + <rect x="60" y="60" width="160" height="50" rx="10" fill="rgba(59,130,246,.12)" stroke="#3b82f6" stroke-width="1.5"/>
  520 + <text x="140" y="82" text-anchor="middle" fill="#60a5fa" font-size="12" font-weight="600">MySQL 8</text>
  521 + <text x="140" y="98" text-anchor="middle" fill="#8899aa" font-size="9">主从 + Druid 连接池</text>
  522 + <line x1="220" y1="85" x2="348" y2="175" stroke="#3b82f6" stroke-width="1.2" stroke-dasharray="6,3" marker-end="url(#arrBlue)"/>
  523 +
  524 + <!-- Redis -->
  525 + <rect x="60" y="150" width="160" height="50" rx="10" fill="rgba(239,68,68,.12)" stroke="#ef4444" stroke-width="1.5"/>
  526 + <text x="140" y="172" text-anchor="middle" fill="#f87171" font-size="12" font-weight="600">Redis Sentinel</text>
  527 + <text x="140" y="188" text-anchor="middle" fill="#8899aa" font-size="9">缓存 · 分布式锁 · Token</text>
  528 + <line x1="220" y1="175" x2="348" y2="180" stroke="#ef4444" stroke-width="1.2" stroke-dasharray="6,3" marker-end="url(#arrBlue)"/>
  529 +
  530 + <!-- MQ -->
  531 + <rect x="60" y="240" width="160" height="50" rx="10" fill="rgba(52,211,153,.12)" stroke="#34d399" stroke-width="1.5"/>
  532 + <text x="140" y="262" text-anchor="middle" fill="#4ade80" font-size="12" font-weight="600">MQ 消息队列</text>
  533 + <text x="140" y="278" text-anchor="middle" fill="#8899aa" font-size="9">RocketMQ · Kafka · RabbitMQ</text>
  534 + <line x1="220" y1="265" x2="348" y2="195" stroke="#34d399" stroke-width="1.2" stroke-dasharray="6,3" marker-end="url(#arrGreen)"/>
  535 +
  536 + <!-- IoT Gateway -->
  537 + <rect x="740" y="60" width="200" height="50" rx="10" fill="rgba(244,114,182,.12)" stroke="#f472b6" stroke-width="1.5"/>
  538 + <text x="840" y="82" text-anchor="middle" fill="#f472b6" font-size="12" font-weight="600">IoT 网关 :8092</text>
  539 + <text x="840" y="98" text-anchor="middle" fill="#8899aa" font-size="9">Vert.x · MQTT · TCP</text>
  540 + <line x1="740" y1="85" x2="652" y2="175" stroke="#f472b6" stroke-width="1.2" stroke-dasharray="6,3" marker-end="url(#arrPink)"/>
  541 +
  542 + <!-- Devices -->
  543 + <rect x="740" y="160" width="200" height="50" rx="10" fill="rgba(251,191,36,.12)" stroke="#fbbf24" stroke-width="1.5"/>
  544 + <text x="840" y="182" text-anchor="middle" fill="#fbbf24" font-size="12" font-weight="600">IoT 设备层</text>
  545 + <text x="840" y="198" text-anchor="middle" fill="#8899aa" font-size="9">传感器 · GPS · 控制器</text>
  546 + <line x1="740" y1="185" x2="940" y2="185" stroke="#fbbf24" stroke-width="1.2" stroke-dasharray="4,3"/>
  547 + <line x1="840" y1="210" x2="840" y2="350" stroke="#fbbf24" stroke-width="1" stroke-dasharray="4,3"/>
  548 +
  549 + <!-- S3 -->
  550 + <rect x="740" y="240" width="200" height="50" rx="10" fill="rgba(129,140,248,.12)" stroke="#818cf8" stroke-width="1.5"/>
  551 + <text x="840" y="262" text-anchor="middle" fill="#a5b4fc" font-size="12" font-weight="600">对象存储 S3</text>
  552 + <text x="840" y="278" text-anchor="middle" fill="#8899aa" font-size="9">文件 · 图片 · 附件</text>
  553 + <line x1="740" y1="265" x2="652" y2="205" stroke="#818cf8" stroke-width="1.2" stroke-dasharray="6,3" marker-end="url(#arrBlue)"/>
  554 +
  555 + <!-- Flowable -->
  556 + <rect x="60" y="330" width="160" height="50" rx="10" fill="rgba(244,114,182,.12)" stroke="#f472b6" stroke-width="1.5"/>
  557 + <text x="140" y="352" text-anchor="middle" fill="#f472b6" font-size="12" font-weight="600">Flowable 7</text>
  558 + <text x="140" y="368" text-anchor="middle" fill="#8899aa" font-size="9">工作流引擎</text>
  559 + <line x1="220" y1="355" x2="348" y2="220" stroke="#f472b6" stroke-width="1.2" stroke-dasharray="6,3"/>
  560 +
  561 + <!-- Quartz -->
  562 + <rect x="320" y="340" width="160" height="50" rx="10" fill="rgba(251,191,36,.12)" stroke="#fbbf24" stroke-width="1.5"/>
  563 + <text x="400" y="362" text-anchor="middle" fill="#fbbf24" font-size="12" font-weight="600">Quartz 调度</text>
  564 + <text x="400" y="378" text-anchor="middle" fill="#8899aa" font-size="9">定时任务集群</text>
  565 + <line x1="480" y1="340" x2="500" y2="222" stroke="#fbbf24" stroke-width="1.2" stroke-dasharray="6,3"/>
  566 +
  567 + <!-- WebSocket -->
  568 + <rect x="560" y="340" width="190" height="50" rx="10" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="1.5"/>
  569 + <text x="655" y="362" text-anchor="middle" fill="#38bdf8" font-size="12" font-weight="600">WebSocket</text>
  570 + <text x="655" y="378" text-anchor="middle" fill="#8899aa" font-size="9">实时消息推送</text>
  571 + <line x1="560" y1="365" x2="502" y2="222" stroke="#38bdf8" stroke-width="1.2" stroke-dasharray="6,3"/>
  572 + </svg>
  573 + </div>
  574 +</div>
  575 +
  576 +<!-- ==================== SLIDE 8: SECURITY ==================== -->
  577 +<div class="slide" data-slide="7">
  578 + <h2>安全与认证架构</h2>
  579 + <div class="diagram-area">
  580 + <svg viewBox="0 0 1000 400" xmlns="http://www.w3.org/2000/svg">
  581 + <!-- Client -->
  582 + <rect x="30" y="30" width="120" height="60" rx="10" fill="rgba(56,189,248,.1)" stroke="#38bdf8" stroke-width="1.5"/>
  583 + <text x="90" y="55" text-anchor="middle" fill="#38bdf8" font-size="11" font-weight="600">客户端</text>
  584 + <text x="90" y="73" text-anchor="middle" fill="#8899aa" font-size="9">Web / 小程序 / App</text>
  585 +
  586 + <!-- Filter Chain -->
  587 + <rect x="200" y="30" width="760" height="60" rx="10" fill="#111827" stroke="#1e3a5f" stroke-width="1.5"/>
  588 + <text x="580" y="52" text-anchor="middle" fill="#8899aa" font-size="11" font-weight="600">Security Filter Chain</text>
  589 +
  590 + <rect x="215" y="58" width="130" height="24" rx="6" fill="rgba(239,68,68,.12)" stroke="#ef4444" stroke-width="1"/>
  591 + <text x="280" y="74" text-anchor="middle" fill="#f87171" font-size="9">API 签名校验</text>
  592 +
  593 + <rect x="355" y="58" width="120" height="24" rx="6" fill="rgba(251,191,36,.12)" stroke="#fbbf24" stroke-width="1"/>
  594 + <text x="415" y="74" text-anchor="middle" fill="#fbbf24" font-size="9">限流 & 幂等</text>
  595 +
  596 + <rect x="485" y="58" width="120" height="24" rx="6" fill="rgba(56,189,248,.12)" stroke="#38bdf8" stroke-width="1"/>
  597 + <text x="545" y="74" text-anchor="middle" fill="#38bdf8" font-size="9">Token 认证</text>
  598 +
  599 + <rect x="615" y="58" width="100" height="24" rx="6" fill="rgba(129,140,248,.12)" stroke="#818cf8" stroke-width="1"/>
  600 + <text x="665" y="74" text-anchor="middle" fill="#a5b4fc" font-size="9">权限校验</text>
  601 +
  602 + <rect x="725" y="58" width="100" height="24" rx="6" fill="rgba(52,211,153,.12)" stroke="#34d399" stroke-width="1"/>
  603 + <text x="775" y="74" text-anchor="middle" fill="#4ade80" font-size="9">数据脱敏</text>
  604 +
  605 + <rect x="835" y="58" width="110" height="24" rx="6" fill="rgba(244,114,182,.12)" stroke="#f472b6" stroke-width="1"/>
  606 + <text x="890" y="74" text-anchor="middle" fill="#f472b6" font-size="9">操作日志</text>
  607 +
  608 + <!-- Arrow -->
  609 + <line x1="150" y1="60" x2="198" y2="60" stroke="#38bdf8" stroke-width="2" marker-end="url(#arrBlue)"/>
  610 +
  611 + <!-- Business Layer -->
  612 + <rect x="200" y="120" width="760" height="110" rx="10" fill="#111827" stroke="#1e3a5f" stroke-width="1.5"/>
  613 + <text x="580" y="145" text-anchor="middle" fill="#8899aa" font-size="11" font-weight="600">认证与授权中心</text>
  614 +
  615 + <!-- JWT -->
  616 + <rect x="220" y="155" width="200" height="42" rx="8" fill="rgba(56,189,248,.1)" stroke="#38bdf8" stroke-width="1.5"/>
  617 + <text x="320" y="174" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">JWT Token</text>
  618 + <text x="320" y="189" text-anchor="middle" fill="#8899aa" font-size="9">Redis 存储 · 自动续期</text>
  619 +
  620 + <!-- SSO -->
  621 + <rect x="450" y="155" width="200" height="42" rx="8" fill="rgba(129,140,248,.1)" stroke="#818cf8" stroke-width="1.5"/>
  622 + <text x="550" y="174" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">UAA SSO</text>
  623 + <text x="550" y="189" text-anchor="middle" fill="#8899aa" font-size="9">OAuth2 · 单点登录</text>
  624 +
  625 + <!-- Social -->
  626 + <rect x="680" y="155" width="250" height="42" rx="8" fill="rgba(52,211,153,.1)" stroke="#34d399" stroke-width="1.5"/>
  627 + <text x="805" y="174" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">JustAuth 社交登录</text>
  628 + <text x="805" y="189" text-anchor="middle" fill="#8899aa" font-size="9">钉钉 · 企业微信 · 小程序 · 公众号</text>
  629 +
  630 + <!-- Tenant + Data Permission -->
  631 + <rect x="220" y="205" width="340" height="20" rx="5" fill="rgba(251,191,36,.08)" stroke="#fbbf24" stroke-width="0.8"/>
  632 + <text x="390" y="219" text-anchor="middle" fill="#fbbf24" font-size="9">多租户数据隔离 (SaaS)</text>
  633 +
  634 + <rect x="580" y="205" width="350" height="20" rx="5" fill="rgba(244,114,182,.08)" stroke="#f472b6" stroke-width="0.8"/>
  635 + <text x="755" y="219" text-anchor="middle" fill="#f472b6" font-size="9">部门/用户级数据权限 (RBAC + Data Scope)</text>
  636 +
  637 + <!-- Arrow to Business -->
  638 + <line x1="580" y1="230" x2="580" y2="260" stroke="#8899aa" stroke-width="1.5" stroke-dasharray="5,3"/>
  639 + <text x="600" y="250" fill="#64748b" font-size="9">授权通过</text>
  640 +
  641 + <!-- Business modules -->
  642 + <rect x="200" y="260" width="760" height="50" rx="10" fill="#0a0e17" stroke="#1e2e45" stroke-width="1.5"/>
  643 + <text x="580" y="290" text-anchor="middle" fill="#8899aa" font-size="11" font-weight="600">业务模块: garden · workorder · bpm · gloabl · infra · system</text>
  644 +
  645 + <!-- Captcha -->
  646 + <rect x="30" y="320" width="120" height="50" rx="10" fill="rgba(251,191,36,.08)" stroke="#fbbf24" stroke-width="1"/>
  647 + <text x="90" y="342" text-anchor="middle" fill="#fbbf24" font-size="11" font-weight="600">AJ Captcha</text>
  648 + <text x="90" y="358" text-anchor="middle" fill="#8899aa" font-size="9">滑块验证</text>
  649 + <line x1="150" y1="345" x2="198" y2="285" stroke="#fbbf24" stroke-width="0.8" stroke-dasharray="4,3"/>
  650 +
  651 + <!-- AES -->
  652 + <rect x="30" y="200" width="120" height="50" rx="10" fill="rgba(239,68,68,.08)" stroke="#ef4444" stroke-width="1"/>
  653 + <text x="90" y="222" text-anchor="middle" fill="#f87171" font-size="11" font-weight="600">API 加密</text>
  654 + <text x="90" y="238" text-anchor="middle" fill="#8899aa" font-size="9">AES 算法</text>
  655 + </svg>
  656 + </div>
  657 +</div>
  658 +
  659 +<!-- ==================== SLIDE 9: DEPLOYMENT ==================== -->
  660 +<div class="slide" data-slide="8">
  661 + <h2>部署架构</h2>
  662 + <div class="diagram-area">
  663 + <svg viewBox="0 0 1000 400" xmlns="http://www.w3.org/2000/svg">
  664 + <!-- Docker Host -->
  665 + <rect x="20" y="20" width="960" height="360" rx="16" fill="#111827" stroke="#1e3a5f" stroke-width="2"/>
  666 + <text x="500" y="48" text-anchor="middle" fill="#64748b" font-size="13" font-weight="600">Docker Host</text>
  667 +
  668 + <!-- urbanops-server -->
  669 + <rect x="50" y="65" width="290" height="160" rx="12" fill="rgba(56,189,248,.08)" stroke="#38bdf8" stroke-width="2"/>
  670 + <text x="195" y="92" text-anchor="middle" fill="#38bdf8" font-size="14" font-weight="700">urbanops-server</text>
  671 + <text x="195" y="110" text-anchor="middle" fill="#8899aa" font-size="10">Eclipse Temurin 21 JRE</text>
  672 + <text x="195" y="126" text-anchor="middle" fill="#64748b" font-size="9">-Xms512m -Xmx512m</text>
  673 + <rect x="70" y="137" width="120" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/>
  674 + <text x="130" y="152" text-anchor="middle" fill="#8899aa" font-size="9">Port 48081</text>
  675 + <rect x="200" y="137" width="120" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/>
  676 + <text x="260" y="152" text-anchor="middle" fill="#8899aa" font-size="9">Actuator /actuator</text>
  677 + <rect x="70" y="165" width="250" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/>
  678 + <text x="195" y="180" text-anchor="middle" fill="#8899aa" font-size="9">包含所有业务模块 (fat JAR)</text>
  679 + <rect x="70" y="193" width="250" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/>
  680 + <text x="195" y="208" text-anchor="middle" fill="#8899aa" font-size="9">SpringDoc /v3/api-docs · Knife4j</text>
  681 +
  682 + <!-- IoT Gateway -->
  683 + <rect x="370" y="65" width="260" height="160" rx="12" fill="rgba(239,68,68,.08)" stroke="#ef4444" stroke-width="2"/>
  684 + <text x="500" y="92" text-anchor="middle" fill="#f87171" font-size="14" font-weight="700">iot-gateway-server</text>
  685 + <text x="500" y="110" text-anchor="middle" fill="#8899aa" font-size="10">独立部署 · Vert.x 引擎</text>
  686 + <rect x="390" y="122" width="220" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/>
  687 + <text x="500" y="137" text-anchor="middle" fill="#8899aa" font-size="9">Port 8092</text>
  688 + <rect x="390" y="150" width="220" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/>
  689 + <text x="500" y="165" text-anchor="middle" fill="#8899aa" font-size="9">协议: MQTT · HTTP · TCP</text>
  690 + <rect x="390" y="178" width="220" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/>
  691 + <text x="500" y="193" text-anchor="middle" fill="#8899aa" font-size="9">设备接入 · 数据采集</text>
  692 + <rect x="390" y="206" width="220" height="22" rx="5" fill="rgba(255,255,255,.04)" stroke="#334155" stroke-width="0.8"/>
  693 + <text x="500" y="221" text-anchor="middle" fill="#8899aa" font-size="9">HTTP RPC → Server :48080</text>
  694 +
  695 + <!-- MySQL -->
  696 + <rect x="660" y="65" width="180" height="72" rx="10" fill="rgba(59,130,246,.08)" stroke="#3b82f6" stroke-width="1.5"/>
  697 + <text x="750" y="92" text-anchor="middle" fill="#60a5fa" font-size="12" font-weight="600">MySQL 8</text>
  698 + <text x="750" y="110" text-anchor="middle" fill="#8899aa" font-size="9">:3306</text>
  699 + <text x="750" y="125" text-anchor="middle" fill="#64748b" font-size="8">主从读写分离</text>
  700 +
  701 + <!-- Redis -->
  702 + <rect x="660" y="150" width="180" height="72" rx="10" fill="rgba(239,68,68,.08)" stroke="#ef4444" stroke-width="1.5"/>
  703 + <text x="750" y="177" text-anchor="middle" fill="#f87171" font-size="12" font-weight="600">Redis Sentinel</text>
  704 + <text x="750" y="195" text-anchor="middle" fill="#8899aa" font-size="9">:26379</text>
  705 + <text x="750" y="210" text-anchor="middle" fill="#64748b" font-size="8">高可用集群</text>
  706 +
  707 + <!-- MQ -->
  708 + <rect x="860" y="65" width="110" height="72" rx="10" fill="rgba(52,211,153,.08)" stroke="#34d399" stroke-width="1.5"/>
  709 + <text x="915" y="92" text-anchor="middle" fill="#4ade80" font-size="11" font-weight="600">RocketMQ</text>
  710 + <text x="915" y="110" text-anchor="middle" fill="#8899aa" font-size="9">:9876</text>
  711 + <text x="915" y="125" text-anchor="middle" fill="#64748b" font-size="8">消息驱动</text>
  712 +
  713 + <rect x="860" y="150" width="110" height="72" rx="10" fill="rgba(129,140,248,.08)" stroke="#818cf8" stroke-width="1.5"/>
  714 + <text x="915" y="177" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="600">Kafka</text>
  715 + <text x="915" y="195" text-anchor="middle" fill="#8899aa" font-size="9">:9092</text>
  716 + <text x="915" y="210" text-anchor="middle" fill="#64748b" font-size="8">流式处理</text>
  717 +
  718 + <!-- Frontend -->
  719 + <rect x="660" y="240" width="310" height="40" rx="10" fill="rgba(251,191,36,.08)" stroke="#fbbf24" stroke-width="1.5"/>
  720 + <text x="815" y="265" text-anchor="middle" fill="#fbbf24" font-size="11" font-weight="600">urbanops-admin (前端 Nginx) :8080</text>
  721 +
  722 + <!-- Monitoring -->
  723 + <rect x="50" y="245" width="580" height="40" rx="10" fill="rgba(129,140,248,.06)" stroke="#6366f1" stroke-width="1.5"/>
  724 + <text x="340" y="270" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="600">可观测性: SkyWalking Agent · Spring Boot Admin · OpenTracing · Actuator · 日志采集</text>
  725 +
  726 + <!-- PF4J Plugins -->
  727 + <rect x="50" y="298" width="580" height="32" rx="8" fill="rgba(244,114,182,.06)" stroke="#f472b6" stroke-width="1" stroke-dasharray="5,3"/>
  728 + <text x="340" y="319" text-anchor="middle" fill="#f472b6" font-size="10">PF4J 插件目录: ../plugins (动态扩展)</text>
  729 +
  730 + <!-- Labels -->
  731 + <rect x="50" y="350" width="12" height="12" rx="2" fill="rgba(56,189,248,.3)" stroke="#38bdf8" stroke-width="1"/>
  732 + <text x="68" y="360" fill="#64748b" font-size="9">fat JAR</text>
  733 + <rect x="140" y="350" width="12" height="12" rx="2" fill="rgba(239,68,68,.3)" stroke="#ef4444" stroke-width="1"/>
  734 + <text x="158" y="360" fill="#64748b" font-size="9">独立进程</text>
  735 + <rect x="240" y="350" width="12" height="12" rx="2" fill="rgba(59,130,246,.3)" stroke="#3b82f6" stroke-width="1"/>
  736 + <text x="258" y="360" fill="#64748b" font-size="9">数据服务</text>
  737 + <rect x="340" y="350" width="12" height="12" rx="2" fill="rgba(52,211,153,.3)" stroke="#34d399" stroke-width="1"/>
  738 + <text x="358" y="360" fill="#64748b" font-size="9">消息中间件</text>
  739 + </svg>
  740 + </div>
  741 +</div>
  742 +
  743 +<!-- ==================== SLIDE 10: SUMMARY & ROADMAP ==================== -->
  744 +<div class="slide" data-slide="9">
  745 + <h2>总结与架构演进</h2>
  746 + <div class="card-grid cols-2" style="max-width:1050px">
  747 + <div class="card">
  748 + <h3>当前架构优势</h3>
  749 + <ul>
  750 + <li>Maven 多模块清晰分层,业务边界明确</li>
  751 + <li>15 个框架 Starter 封装最佳实践</li>
  752 + <li>API 契约层实现模块间松耦合</li>
  753 + <li>IoT 网关已实现独立部署验证</li>
  754 + <li>多租户 + 数据权限完备</li>
  755 + <li>消息队列抽象层支持多中间件切换</li>
  756 + </ul>
  757 + </div>
  758 +
  759 + <div class="card">
  760 + <h3>演进方向</h3>
  761 + <ul>
  762 + <li><strong>服务拆分</strong>: garden / workorder -> 独立微服务</li>
  763 + <li><strong>注册中心</strong>: 引入 Nacos 实现服务发现与配置管理</li>
  764 + <li><strong>API 网关</strong>: Spring Cloud Gateway 统一入口</li>
  765 + <li><strong>流量治理</strong>: Sentinel 限流熔断</li>
  766 + <li><strong>分布式事务</strong>: Seata 保障跨服务数据一致性</li>
  767 + <li><strong>容器编排</strong>: docker-compose -> Kubernetes</li>
  768 + </ul>
  769 + </div>
  770 +
  771 + <div class="card" style="grid-column: 1 / -1">
  772 + <h3>目标微服务架构</h3>
  773 + <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;justify-content:center;margin-top:12px">
  774 + <span class="flow-box accent" style="font-size:.8em;padding:10px 16px">Nginx</span>
  775 + <span class="flow-arrow">→</span>
  776 + <span class="flow-box purple" style="font-size:.8em;padding:10px 16px">Gateway</span>
  777 + <span class="flow-arrow">→</span>
  778 + <span class="flow-box green" style="font-size:.8em;padding:10px 16px">Nacos</span>
  779 + <span class="flow-arrow">⇢</span>
  780 + <div style="display:flex;flex-direction:column;gap:6px">
  781 + <span class="tag blue">garden-service</span>
  782 + <span class="tag purple">workorder-service</span>
  783 + <span class="tag green">bpm-service</span>
  784 + <span class="tag pink">system-service</span>
  785 + <span class="tag yellow">iot-gateway (已有)</span>
  786 + </div>
  787 + <span class="flow-arrow">⇢</span>
  788 + <div style="display:flex;flex-direction:column;gap:6px">
  789 + <span class="tag red">Sentinel</span>
  790 + <span class="tag blue">Seata</span>
  791 + <span class="tag green">SkyWalking</span>
  792 + </div>
  793 + </div>
  794 + </div>
  795 +
  796 + <div class="card" style="grid-column: 1 / -1; text-align:center">
  797 + <p style="font-size:1.1em;color:var(--text);margin-bottom:8px"><strong>关键原则</strong></p>
  798 + <p style="color:var(--text-dim)">
  799 + 模块化先行 → API 契约驱动 → 渐进式拆分 → 独立部署验证 → 全链路可观测
  800 + </p>
  801 + </div>
  802 + </div>
  803 +</div>
  804 +
  805 +</div>
  806 +
  807 +<!-- Navigation -->
  808 +<div class="nav" id="nav">
  809 + <button onclick="prevSlide()" title="上一页">&#x2190;</button>
  810 + <span class="page-indicator" id="pageIndicator">1 / 10</span>
  811 + <button onclick="nextSlide()" title="下一页">&#x2192;</button>
  812 + <div class="progress" id="progress" style="width:10%"></div>
  813 +</div>
  814 +
  815 +<script>
  816 +let current = 0;
  817 +const slides = document.querySelectorAll('.slide');
  818 +const total = slides.length;
  819 +const indicator = document.getElementById('pageIndicator');
  820 +const progress = document.getElementById('progress');
  821 +
  822 +function showSlide(n) {
  823 + slides[current].classList.remove('active');
  824 + current = (n + total) % total;
  825 + slides[current].classList.add('active');
  826 + indicator.textContent = (current + 1) + ' / ' + total;
  827 + progress.style.width = ((current + 1) / total * 100) + '%';
  828 +}
  829 +
  830 +function nextSlide() { showSlide(current + 1); }
  831 +function prevSlide() { showSlide(current - 1); }
  832 +
  833 +document.addEventListener('keydown', e => {
  834 + if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') {
  835 + e.preventDefault(); nextSlide();
  836 + } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
  837 + e.preventDefault(); prevSlide();
  838 + } else if (e.key === 'Home') { e.preventDefault(); showSlide(0); }
  839 + else if (e.key === 'End') { e.preventDefault(); showSlide(total - 1); }
  840 +});
  841 +
  842 +// Touch support
  843 +let touchStartX = 0;
  844 +document.addEventListener('touchstart', e => { touchStartX = e.touches[0].clientX; });
  845 +document.addEventListener('touchend', e => {
  846 + const diff = touchStartX - e.changedTouches[0].clientX;
  847 + if (Math.abs(diff) > 50) { diff > 0 ? nextSlide() : prevSlide(); }
  848 +});
  849 +
  850 +// Click on edges
  851 +document.addEventListener('click', e => {
  852 + if (e.target.closest('.nav') || e.target.closest('a') || e.target.closest('button')) return;
  853 + if (e.clientX > window.innerWidth * 0.85) nextSlide();
  854 + else if (e.clientX < window.innerWidth * 0.15) prevSlide();
  855 +});
  856 +</script>
  857 +</body>
  858 +</html>
docs/urbanops-tech-sharing.html 0 → 100644
  1 +<!DOCTYPE html>
  2 +<html lang="zh-CN">
  3 +<head>
  4 +<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
  5 +<title>城市运营全域平台 · 技术架构分享</title>
  6 +<link rel="stylesheet" href="ppt-assets/fonts.css">
  7 +<link rel="stylesheet" href="ppt-assets/base.css">
  8 +<link rel="stylesheet" href="ppt-assets/animations.css">
  9 +<link rel="stylesheet" href="ppt-assets/style.css">
  10 +<style>
  11 +/* Override for 20+ slides */
  12 +.tpl-tech-sharing .h1{font-size:68px;line-height:1.05}
  13 +.tpl-tech-sharing .h2{font-size:46px}
  14 +.tpl-tech-sharing .slide{padding:60px 80px}
  15 +
  16 +/* Architecture diagram layers */
  17 +.arch-layers{margin-top:18px;display:grid;grid-template-rows:auto auto auto auto;gap:12px}
  18 +.arch-layers .al{display:grid;grid-template-columns:110px 1fr;align-items:stretch;gap:12px}
  19 +.arch-layers .al .al-name{display:flex;align-items:center;justify-content:center;background:var(--surface-2);border-radius:var(--radius);padding:12px 10px;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:600;color:var(--accent);text-align:center;border:1px solid var(--border)}
  20 +.arch-layers .al .al-cells{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px}
  21 +.arch-layers .al .acell{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 10px;text-align:center}
  22 +.arch-layers .al .acell .aci{font-size:20px;margin-bottom:2px}
  23 +.arch-layers .al .acell h4{font-size:13px;color:#fff;margin:3px 0 1px}
  24 +.arch-layers .al .acell p{font-size:10px;color:var(--text-3);margin:0}
  25 +.arch-layers .al.hl .acell{border-top:3px solid var(--accent);border-color:rgba(126,231,135,.35)}
  26 +.arch-layers .al.fw .acell{border-top:3px solid var(--accent-2);border-color:rgba(121,192,255,.35)}
  27 +
  28 +/* Biz domain grid */
  29 +.biz-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:16px}
  30 +.biz-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:22px 18px}
  31 +.biz-card .biz-icon{font-size:32px;margin-bottom:8px}
  32 +.biz-card h4{font-size:17px;color:#fff;margin-bottom:6px}
  33 +.biz-card p{font-size:12px;color:var(--text-2);line-height:1.6;margin:0}
  34 +.biz-card .biz-tags{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}
  35 +.biz-card .biz-tags span{font-family:'JetBrains Mono',monospace;font-size:10px;padding:2px 8px;border-radius:4px;background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
  36 +.biz-card.ac{border-top:3px solid var(--accent)}
  37 +.biz-card.blue{border-top:3px solid var(--accent-2)}
  38 +.biz-card.purple{border-top:3px solid #d2a8ff}
  39 +
  40 +/* Two column custom */
  41 +.col2{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:16px}
  42 +.col2 .box{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:22px}
  43 +
  44 +/* Flow steps */
  45 +.flow-steps{display:flex;align-items:center;gap:8px;margin-top:18px}
  46 +.flow-steps .fs{flex:1;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 12px;text-align:center}
  47 +.flow-steps .fs .fs-icon{font-size:26px;margin-bottom:4px}
  48 +.flow-steps .fs h4{font-size:13px;color:#fff;margin:3px 0}
  49 +.flow-steps .fs p{font-size:10px;color:var(--text-3);margin:0}
  50 +.flow-steps .fs-arr{color:var(--accent);font-size:20px;font-family:'JetBrains Mono',monospace;flex-shrink:0}
  51 +.flow-steps .fs.hl{border-top:3px solid var(--accent)}
  52 +
  53 +/* Tech grid mini */
  54 +.tg{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:14px}
  55 +.tg .tgc{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 12px;text-align:center}
  56 +.tg .tgc .tgci{font-size:28px;margin-bottom:4px}
  57 +.tg .tgc h4{font-size:14px;color:#fff;margin:4px 0}
  58 +.tg .tgc .tgt{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;margin-top:8px}
  59 +.tg .tgc .tgt span{font-family:'JetBrains Mono',monospace;font-size:10px;padding:2px 7px;border-radius:4px;background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
  60 +
  61 +/* Timeline */
  62 +.timeline{margin-top:18px}
  63 +.timeline .tl-item{display:grid;grid-template-columns:100px 1fr;gap:16px;padding:14px 0;border-bottom:1px dashed var(--border)}
  64 +.timeline .tl-item .tl-phase{font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent);font-weight:600}
  65 +.timeline .tl-item .tl-body h4{font-size:16px;color:#fff;margin:0 0 4px}
  66 +.timeline .tl-item .tl-body p{font-size:12px;color:var(--text-2);margin:0}
  67 +
  68 +/* Framework grid */
  69 +.fw-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:14px}
  70 +.fw-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 14px}
  71 +.fw-card .fwi{font-size:22px;margin-bottom:6px}
  72 +.fw-card h4{font-size:14px;color:#fff;margin:4px 0}
  73 +.fw-card p{font-size:11px;color:var(--text-3);margin:0}
  74 +.fw-card.hl{border-top:2px solid var(--accent-2)}
  75 +
  76 +/* Roadmap */
  77 +.roadmap-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:16px}
  78 +.roadmap-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:22px 18px}
  79 +.roadmap-card .r-period{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--accent);font-weight:600;margin-bottom:8px}
  80 +.roadmap-card h4{font-size:18px;color:#fff;margin-bottom:8px}
  81 +.roadmap-card ul{margin:0;padding-left:16px;font-size:12px;color:var(--text-2);line-height:1.8}
  82 +.roadmap-card.now{border-top:3px solid var(--accent)}
  83 +.roadmap-card.next{border-top:3px solid var(--accent-2)}
  84 +.roadmap-card.future{border-top:3px solid #d2a8ff}
  85 +
  86 +/* Stats row */
  87 +.stats-row{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-top:16px}
  88 +.stat-item{text-align:center;padding:18px 10px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}
  89 +.stat-item .stat-num{font-size:38px;font-weight:800;color:var(--accent);font-family:'JetBrains Mono',monospace}
  90 +.stat-item .stat-label{font-size:12px;color:var(--text-2);margin-top:4px}
  91 +
  92 +/* Invocation flow */
  93 +.invoc-flow{display:flex;flex-direction:column;gap:10px;margin-top:14px}
  94 +.invoc-flow .invoc-row{display:flex;align-items:center;gap:8px}
  95 +.invoc-flow .invoc-row .ir-label{width:100px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--accent);font-weight:600;flex-shrink:0}
  96 +.invoc-flow .invoc-row .ir-items{display:flex;gap:6px;flex:1;flex-wrap:wrap}
  97 +.invoc-flow .invoc-row .ir-items span{font-family:'JetBrains Mono',monospace;font-size:10px;padding:4px 10px;border-radius:5px;background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
  98 +.invoc-flow .ir-arrow{color:var(--text-3);font-size:16px;font-family:'JetBrains Mono',monospace;text-align:center;padding:2px 0}
  99 +
  100 +/* Deployment diagram */
  101 +.deploy-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:16px}
  102 +.deploy-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px 16px;text-align:center}
  103 +.deploy-card .di{font-size:34px;margin-bottom:8px}
  104 +.deploy-card h4{font-size:16px;color:#fff;margin-bottom:6px}
  105 +.deploy-card ul{margin:0;padding:0;list-style:none;font-size:11px;color:var(--text-2);line-height:1.8}
  106 +.deploy-card.ci{border-top:3px solid var(--accent)}
  107 +.deploy-card.cn{border-top:3px solid var(--accent-2)}
  108 +.deploy-card.mon{border-top:3px solid #d2a8ff}
  109 +
  110 +/* Full width code-like text */
  111 +.code-text{font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--text-2);background:var(--surface-2);padding:8px 14px;border-radius:8px;border:1px solid var(--border);display:inline-block}
  112 +</style>
  113 +</head>
  114 +<body class="tpl-tech-sharing">
  115 +<div class="deck">
  116 +
  117 + <!-- 1. Cover -->
  118 + <section class="slide" data-title="Cover">
  119 + <p class="kicker">urbanops · tech-sharing</p>
  120 + <h1 class="h1 anim-fade-up" data-anim="fade-up">城市运营全域平台<br><span style="background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent">技术架构深度解析</span></h1>
  121 + <p class="lede mt-m">从业务架构到技术架构,从单体模块到微服务演进——一文讲透全域平台。</p>
  122 + <div class="speaker"><div class="av"></div><div><b>架构组</b><span>platform infra · 50 min + Q&amp;A</span></div></div>
  123 + <div class="deck-footer"><span class="mono">#spring-boot #smart-garden #urbanops #architecture</span><span class="slide-number" data-current="1" data-total="20"></span></div>
  124 + </section>
  125 +
  126 + <!-- 2. Agenda -->
  127 + <section class="slide" data-title="Agenda">
  128 + <p class="kicker">agenda.yaml</p>
  129 + <h2 class="h2">今日路线图</h2>
  130 + <div class="stack mt-l">
  131 + <div class="agenda-row"><span class="num">01</span><span class="t">项目概述与业务背景</span><span class="d">~3min</span></div>
  132 + <div class="agenda-row"><span class="num">02</span><span class="t">业务架构全景与业务布局</span><span class="d">~8min</span></div>
  133 + <div class="agenda-row"><span class="num">03</span><span class="t">技术架构总览与分层设计</span><span class="d">~8min</span></div>
  134 + <div class="agenda-row"><span class="num">04</span><span class="t">核心技术栈与基础设施层</span><span class="d">~6min</span></div>
  135 + <div class="agenda-row"><span class="num">05</span><span class="t">数据存储与多数据库策略</span><span class="d">~5min</span></div>
  136 + <div class="agenda-row"><span class="num">06</span><span class="t">模块调用逻辑与通信机制</span><span class="d">~6min</span></div>
  137 + <div class="agenda-row"><span class="num">07</span><span class="t">安全权限与可观测性</span><span class="d">~5min</span></div>
  138 + <div class="agenda-row"><span class="num">08</span><span class="t">部署架构与后续规划</span><span class="d">~5min</span></div>
  139 + <div class="agenda-row"><span class="num">09</span><span class="t">总结 &amp; Q&amp;A</span><span class="d">~4min</span></div>
  140 + </div>
  141 + </section>
  142 +
  143 + <!-- 3. Project Overview -->
  144 + <section class="slide" data-title="Project Overview">
  145 + <p class="kicker">// overview</p>
  146 + <h2 class="h2">UrbanOps 全域平台:<br>智慧城市运营管理底座</h2>
  147 + <p class="lede">基于 RuoYi-Vue-Pro 演进而来,面向城市园林绿化、工单调度、流程协同的综合运营管理平台。</p>
  148 + <div class="stats-row mt-l">
  149 + <div class="stat-item anim-fade-up" data-anim="fade-up"><div class="stat-num">17</div><div class="stat-label">Maven 模块</div></div>
  150 + <div class="stat-item anim-fade-up" data-anim="fade-up" style="animation-delay:.05s"><div class="stat-num">7</div><div class="stat-label">激活业务模块</div></div>
  151 + <div class="stat-item anim-fade-up" data-anim="fade-up" style="animation-delay:.1s"><div class="stat-num">10+</div><div class="stat-label">数据库支持</div></div>
  152 + <div class="stat-item anim-fade-up" data-anim="fade-up" style="animation-delay:.15s"><div class="stat-num">3.5.5</div><div class="stat-label">Spring Boot</div></div>
  153 + <div class="stat-item anim-fade-up" data-anim="fade-up" style="animation-delay:.2s"><div class="stat-num">MIT</div><div class="stat-label">开源协议</div></div>
  154 + </div>
  155 + <div class="col2 mt-l">
  156 + <div class="box" style="border-top:3px solid var(--accent)">
  157 + <h4 style="color:#fff;margin:0 0 8px">后端技术基石</h4>
  158 + <p class="dim" style="font-size:13px;line-height:1.7">Java 17 · Spring Boot 3.5.5 · MyBatis Plus 3.5.14 · Spring Security 6.x · Flowable 7.0 · Redisson 3.51 · SkyWalking 9.5 · RocketMQ · Vert.x + MQTT</p>
  159 + </div>
  160 + <div class="box" style="border-top:3px solid var(--accent-2)">
  161 + <h4 style="color:#fff;margin:0 0 8px">前端生态</h4>
  162 + <p class="dim" style="font-size:13px;line-height:1.7">Vue3 + element-plus 管理后台 · Vue3 + vben(ant-design-vue) 高级后台 · uni-app 移动端 · 大屏报表设计器 · 代码生成器一键生成前后端</p>
  163 + </div>
  164 + </div>
  165 + </section>
  166 +
  167 + <!-- 4. Business Architecture Overview -->
  168 + <section class="slide" data-title="Business Architecture">
  169 + <p class="kicker">business · architecture</p>
  170 + <h2 class="h2">业务架构全景:<br>四大核心域 + 三大支撑域</h2>
  171 + <div class="biz-grid">
  172 + <div class="biz-card ac anim-fade-up" data-anim="fade-up">
  173 + <div class="biz-icon">🌳</div><h4>智慧园林管理</h4>
  174 + <p>树木资产管理、巡检计划、养护计划、绩效评估、物料出入库、机械成本核算</p>
  175 + <div class="biz-tags"><span>Garden</span><span>巡检</span><span>养护</span><span>绩效</span></div>
  176 + </div>
  177 + <div class="biz-card blue anim-fade-up" data-anim="fade-up" style="animation-delay:.05s">
  178 + <div class="biz-icon">📋</div><h4>工单调度系统</h4>
  179 + <p>事件上报、工单流转、物料明细、附件管理、地理位置编码、园林事件映射</p>
  180 + <div class="biz-tags"><span>Workorder</span><span>事件</span><span>物料</span></div>
  181 + </div>
  182 + <div class="biz-card purple anim-fade-up" data-anim="fade-up" style="animation-delay:.1s">
  183 + <div class="biz-icon">🔄</div><h4>工作流引擎</h4>
  184 + <p>Flowable 驱动,仿钉钉/飞书 + BPMN 双设计器,会签、或签、转办、委派、加签全覆盖</p>
  185 + <div class="biz-tags"><span>BPM</span><span>Flowable</span><span>审批</span></div>
  186 + </div>
  187 + <div class="biz-card ac anim-fade-up" data-anim="fade-up" style="animation-delay:.15s">
  188 + <div class="biz-icon">🌐</div><h4>全局业务管理</h4>
  189 + <p>问题上报、道路信息、工单提交详情、流程路径想法、跨模块数据聚合</p>
  190 + <div class="biz-tags"><span>Global</span><span>道路</span><span>问题</span></div>
  191 + </div>
  192 + <div class="biz-card blue anim-fade-up" data-anim="fade-up" style="animation-delay:.2s">
  193 + <div class="biz-icon">⚙️</div><h4>系统管理</h4>
  194 + <p>用户/角色/菜单/部门/岗位管理、租户与套餐、字典、通知公告、应用管理</p>
  195 + <div class="biz-tags"><span>System</span><span>RBAC</span><span>多租户</span></div>
  196 + </div>
  197 + <div class="biz-card purple anim-fade-up" data-anim="fade-up" style="animation-delay:.25s">
  198 + <div class="biz-icon">🏗️</div><h4>基础设施</h4>
  199 + <p>代码生成、文件存储、定时任务、配置管理、消息队列、API 日志、监控追踪</p>
  200 + <div class="biz-tags"><span>Infra</span><span>代码生成</span><span>监控</span></div>
  201 + </div>
  202 + </div>
  203 + </section>
  204 +
  205 + <!-- 5. Business Layout - Garden -->
  206 + <section class="slide" data-title="Business · Garden">
  207 + <p class="kicker">garden · smart-greening</p>
  208 + <h2 class="h2">核心业务:智慧园林管理</h2>
  209 + <p class="lede">覆盖园林绿化全生命周期——从资产登记到巡检养护,从绩效评估到成本核算。</p>
  210 + <div class="arch-layers">
  211 + <div class="al hl"><div class="al-name">管理端</div>
  212 + <div class="al-cells">
  213 + <div class="acell"><div class="aci">🌲</div><h4>树木资产管理</h4><p>古树名木 / 变更日志</p></div>
  214 + <div class="acell"><div class="aci">📅</div><h4>巡检计划管理</h4><p>计划制定 / 明细配置</p></div>
  215 + <div class="acell"><div class="aci">🔧</div><h4>养护计划管理</h4><p>养护任务 / 执行跟踪</p></div>
  216 + <div class="acell"><div class="aci">📊</div><h4>绩效管理</h4><p>指标定义 / 专家评定</p></div>
  217 + <div class="acell"><div class="aci">📦</div><h4>物料管理</h4><p>物料类型 / 出入库</p></div>
  218 + <div class="acell"><div class="aci">💰</div><h4>机械成本</h4><p>折旧 / 租赁核算</p></div>
  219 + </div>
  220 + </div>
  221 + <div class="al fw"><div class="al-name">移动端</div>
  222 + <div class="al-cells" style="grid-template-columns:repeat(5,1fr)">
  223 + <div class="acell"><div class="aci">📱</div><h4>首页概览</h4><p>数据汇总</p></div>
  224 + <div class="acell"><div class="aci">📍</div><h4>GPS 定位</h4><p>位置上报</p></div>
  225 + <div class="acell"><div class="aci">✅</div><h4>巡检提交</h4><p>现场打卡</p></div>
  226 + <div class="acell"><div class="aci">🛠️</div><h4>养护提交</h4><p>任务执行</p></div>
  227 + <div class="acell"><div class="aci">📝</div><h4>变更记录</h4><p>树木变更</p></div>
  228 + </div>
  229 + </div>
  230 + </div>
  231 + <div class="grid g4 mt-m">
  232 + <div class="card card-accent"><h4>定时任务</h4><p class="dim">自动生成巡检/养护计划</p></div>
  233 + <div class="card card-accent"><h4>数据权限</h4><p class="dim">按区域隔离园林数据</p></div>
  234 + <div class="card card-accent"><h4>时间区间</h4><p class="dim">灵活配置任务执行周期</p></div>
  235 + <div class="card card-accent"><h4>防重复提交</h4><p class="dim">计划明细幂等性保证</p></div>
  236 + </div>
  237 + </section>
  238 +
  239 + <!-- 6. Business Layout - Work Order -->
  240 + <section class="slide" data-title="Business · Workorder">
  241 + <p class="kicker">workorder · dispatch</p>
  242 + <h2 class="h2">工单调度:事件驱动 + 流程闭环</h2>
  243 + <div class="col2">
  244 + <div>
  245 + <div class="card card-accent" style="padding:22px">
  246 + <h4 style="color:#fff;margin:0 0 12px">工单核心实体模型</h4>
  247 + <div class="stack">
  248 + <div class="tag">📋 MainInfo — 工单主信息</div>
  249 + <div class="tag">📎 Attachment — 附件管理</div>
  250 + <div class="tag">📦 MaterialDetail — 物料明细</div>
  251 + <div class="tag">🎯 EventInfo — 事件信息</div>
  252 + <div class="tag">🔗 EventMappingInfo — 事件映射</div>
  253 + <div class="tag">🌳 GardenRef — 园林业务关联</div>
  254 + </div>
  255 + </div>
  256 + </div>
  257 + <div>
  258 + <div class="card" style="padding:22px;border-top:3px solid var(--accent-2)">
  259 + <h4 style="color:#fff;margin:0 0 12px">工单处理流程</h4>
  260 + <div class="flow-steps" style="flex-direction:column;gap:8px">
  261 + <div style="display:flex;align-items:center;gap:8px">
  262 + <div class="fs" style="flex:1"><div class="fs-icon">📝</div><h4>事件上报</h4></div>
  263 + <span class="fs-arr">→</span>
  264 + <div class="fs" style="flex:1"><div class="fs-icon">🔍</div><h4>工单生成</h4></div>
  265 + <span class="fs-arr">→</span>
  266 + <div class="fs hl" style="flex:1"><div class="fs-icon">👤</div><h4>人员指派</h4></div>
  267 + </div>
  268 + <div style="display:flex;align-items:center;gap:8px">
  269 + <div class="fs" style="flex:1"><div class="fs-icon">🔧</div><h4>现场处理</h4></div>
  270 + <span class="fs-arr">→</span>
  271 + <div class="fs" style="flex:1"><div class="fs-icon">📋</div><h4>结果反馈</h4></div>
  272 + <span class="fs-arr">→</span>
  273 + <div class="fs" style="flex:1"><div class="fs-icon">✅</div><h4>审核结单</h4></div>
  274 + </div>
  275 + </div>
  276 + </div>
  277 + </div>
  278 + </div>
  279 + <div class="code-text mt-m">WorkorderModule → GardenModule (Feign API) · EventMapping 策略 · GeoCode 位置服务 · 数据权限隔离</div>
  280 + </section>
  281 +
  282 + <!-- 7. Business Layout - BPM -->
  283 + <section class="slide" data-title="Business · BPM">
  284 + <p class="kicker">bpm · flowable-engine</p>
  285 + <h2 class="h2">工作流引擎:同时支持<br>「轻量配置」与「深度编排」</h2>
  286 + <div class="grid g2 mt-l" style="align-items:start">
  287 + <div>
  288 + <div class="card card-accent" style="padding:22px">
  289 + <h4 style="color:var(--accent);font-family:'JetBrains Mono',monospace;font-size:13px">SIMPLE 设计器</h4>
  290 + <p class="dim" style="font-size:13px">仿钉钉/飞书体验,拖拽搭建表单流程,10 分钟完成配置</p>
  291 + <div class="stack mt-s">
  292 + <div class="tag">✦ 拖拽式表单设计</div>
  293 + <div class="tag">✦ 审批节点可视化配置</div>
  294 + <div class="tag">✦ 抄送人 / 条件分支</div>
  295 + <div class="tag">✦ 超时审批 / 自动提醒</div>
  296 + </div>
  297 + </div>
  298 + </div>
  299 + <div>
  300 + <div class="card" style="padding:22px;border-top:3px solid #d2a8ff">
  301 + <h4 style="color:#d2a8ff;font-family:'JetBrains Mono',monospace;font-size:13px">BPMN 设计器</h4>
  302 + <p class="dim" style="font-size:13px">基于 BPMN 2.0 标准,适配复杂多层级审批及自动化场景</p>
  303 + <div class="stack mt-s">
  304 + <div class="tag">✦ 会签 / 或签 / 依次审批</div>
  305 + <div class="tag">✦ 转办 / 委派 / 加签 / 减签</div>
  306 + <div class="tag">✦ 并行 / 包容 / 路由分支</div>
  307 + <div class="tag">✦ 父子流程 / 触发节点</div>
  308 + </div>
  309 + </div>
  310 + </div>
  311 + </div>
  312 + <div class="grid g3 mt-m">
  313 + <div class="card"><h4 style="color:#fff;font-size:14px">Flowable 7.0</h4><p class="dim">核心流程引擎</p></div>
  314 + <div class="card"><h4 style="color:#fff;font-size:14px">信创数据库兼容</h4><p class="dim">DM8 / KingBase / OpenGauss</p></div>
  315 + <div class="card"><h4 style="color:#fff;font-size:14px">动态表单</h4><p class="dim">节点级只读/编辑/隐藏权限</p></div>
  316 + </div>
  317 + </section>
  318 +
  319 + <!-- 8. Business Layout - Global & System -->
  320 + <section class="slide" data-title="Business · Global & System">
  321 + <p class="kicker">global · system · infra</p>
  322 + <h2 class="h2">业务支撑域:<br>公共服务与系统管理</h2>
  323 + <div class="grid g2 mt-l" style="align-items:start;gap:20px">
  324 + <div>
  325 + <h4 style="color:var(--accent-2);font-family:'JetBrains Mono',monospace;font-size:13px;margin-bottom:12px">🌐 Global 全局模块</h4>
  326 + <div class="stack">
  327 + <div class="card card-accent" style="padding:14px 16px"><h4 style="font-size:14px">问题上报管理</h4><p class="dim" style="font-size:11px">公共问题发现 → 流转到工单系统</p></div>
  328 + <div class="card card-accent" style="padding:14px 16px"><h4 style="font-size:14px">道路信息管理</h4><p class="dim" style="font-size:11px">道路基础数据维护</p></div>
  329 + <div class="card card-accent" style="padding:14px 16px"><h4 style="font-size:14px">工单提交详情</h4><p class="dim" style="font-size:11px">跨模块工单数据聚合视图</p></div>
  330 + <div class="card card-accent" style="padding:14px 16px"><h4 style="font-size:14px">流程路径想法</h4><p class="dim" style="font-size:11px">流程优化建议收集</p></div>
  331 + </div>
  332 + </div>
  333 + <div>
  334 + <h4 style="color:#d2a8ff;font-family:'JetBrains Mono',monospace;font-size:13px;margin-bottom:12px">⚙️ System + Infra 模块</h4>
  335 + <div class="stack">
  336 + <div class="card" style="padding:14px 16px;border-top:2px solid var(--accent-2)"><h4 style="font-size:14px">RBAC 权限体系</h4><p class="dim" style="font-size:11px">用户 → 角色 → 菜单 → 按钮权限 · 数据权限</p></div>
  337 + <div class="card" style="padding:14px 16px;border-top:2px solid var(--accent-2)"><h4 style="font-size:14px">SaaS 多租户</h4><p class="dim" style="font-size:11px">租户管理 · 套餐配置 · 数据隔离 · 透明化封装</p></div>
  338 + <div class="card" style="padding:14px 16px;border-top:2px solid var(--accent-2)"><h4 style="font-size:14px">代码生成器</h4><p class="dim" style="font-size:11px">一键生成 Java · Vue · SQL · 接口文档</p></div>
  339 + <div class="card" style="padding:14px 16px;border-top:2px solid var(--accent-2)"><h4 style="font-size:14px">消息通知中心</h4><p class="dim" style="font-size:11px">短信 · 邮件 · 站内信 · 多通道</p></div>
  340 + </div>
  341 + </div>
  342 + </div>
  343 + </section>
  344 +
  345 + <!-- 9. Tech Architecture Overview -->
  346 + <section class="slide" data-title="Tech Architecture">
  347 + <p class="kicker">architecture · layers</p>
  348 + <h2 class="h2">技术架构五层模型</h2>
  349 + <p class="lede">从用户端到底层基础设施,清晰分层、逐级解耦。</p>
  350 + <div class="arch-layers anim-stagger-list" data-anim-target>
  351 + <div class="al fw">
  352 + <div class="al-name" style="color:var(--accent-2)">接入层</div>
  353 + <div class="al-cells" style="grid-template-columns:repeat(4,1fr)">
  354 + <div class="acell"><div class="aci">🖥️</div><h4>Web Admin</h4><p>Vue3 + Element Plus</p></div>
  355 + <div class="acell"><div class="aci">📱</div><h4>Mobile App</h4><p>uni-app 跨端</p></div>
  356 + <div class="acell"><div class="aci">📊</div><h4>大屏展示</h4><p>GoView 可视化</p></div>
  357 + <div class="acell"><div class="aci">🔗</div><h4>开放 API</h4><p>第三方对接</p></div>
  358 + </div>
  359 + </div>
  360 + <div class="al fw" style="grid-template-columns:110px 1fr">
  361 + <div class="al-name" style="color:var(--accent-2)">网关层</div>
  362 + <div class="al-cells" style="grid-template-columns:repeat(4,1fr)">
  363 + <div class="acell"><div class="aci">🔐</div><h4>认证鉴权</h4><p>Spring Security + Token</p></div>
  364 + <div class="acell"><div class="aci">🚪</div><h4>路由转发</h4><p>Nginx / Gateway</p></div>
  365 + <div class="acell"><div class="aci">⚖️</div><h4>负载均衡</h4><p>多实例分发</p></div>
  366 + <div class="acell"><div class="aci">🛡️</div><h4>安全防护</h4><p>XSS / CSRF / SQL注入</p></div>
  367 + </div>
  368 + </div>
  369 + <div class="al hl">
  370 + <div class="al-name">服务层</div>
  371 + <div class="al-cells" style="grid-template-columns:repeat(6,1fr)">
  372 + <div class="acell"><div class="aci">🌳</div><h4>Garden</h4><p>园林管理</p></div>
  373 + <div class="acell"><div class="aci">📋</div><h4>Workorder</h4><p>工单调度</p></div>
  374 + <div class="acell"><div class="aci">🔄</div><h4>BPM</h4><p>工作流</p></div>
  375 + <div class="acell"><div class="aci">🌐</div><h4>Global</h4><p>全局服务</p></div>
  376 + <div class="acell"><div class="aci">⚙️</div><h4>System</h4><p>系统管理</p></div>
  377 + <div class="acell"><div class="aci">🏗️</div><h4>Infra</h4><p>基础设施</p></div>
  378 + </div>
  379 + </div>
  380 + <div class="al">
  381 + <div class="al-name">中间件</div>
  382 + <div class="al-cells" style="grid-template-columns:repeat(5,1fr)">
  383 + <div class="acell"><div class="aci">🗄️</div><h4>MySQL</h4><p>OLTP</p></div>
  384 + <div class="acell"><div class="aci">⚡</div><h4>Redis</h4><p>缓存/锁</p></div>
  385 + <div class="acell"><div class="aci">📨</div><h4>RocketMQ</h4><p>消息队列</p></div>
  386 + <div class="acell"><div class="aci">🔍</div><h4>Elasticsearch</h4><p>全文检索</p></div>
  387 + <div class="acell"><div class="aci">📡</div><h4>TDengine</h4><p>时序数据</p></div>
  388 + </div>
  389 + </div>
  390 + </div>
  391 + </section>
  392 +
  393 + <!-- 10. Backend Layered Architecture -->
  394 + <section class="slide" data-title="Backend Architecture">
  395 + <p class="kicker">backend · layered</p>
  396 + <h2 class="h2">后端分层架构:<br>Controller → Service → DAL 三层解耦</h2>
  397 + <div class="grid g2 mt-l" style="align-items:start;gap:20px">
  398 + <div>
  399 + <div class="stack">
  400 + <div class="card card-accent" style="padding:16px">
  401 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent)">controller/</h4>
  402 + <p class="dim" style="font-size:12px;margin-bottom:6px">接收请求、参数校验、VO 转换</p>
  403 + <div class="biz-tags"><span>admin/</span><span>app/</span><span>VO 对象</span><span>@Valid</span></div>
  404 + </div>
  405 + <div class="card card-accent" style="padding:16px">
  406 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent)">service/</h4>
  407 + <p class="dim" style="font-size:12px;margin-bottom:6px">业务逻辑编排、事务管理、策略模式</p>
  408 + <div class="biz-tags"><span>接口+实现</span><span>@Transactional</span><span>策略</span><span>Listener</span></div>
  409 + </div>
  410 + <div class="card card-accent" style="padding:16px">
  411 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent)">dal/</h4>
  412 + <p class="dim" style="font-size:12px;margin-bottom:6px">数据访问层:MySQL Mapper + Redis Cache</p>
  413 + <div class="biz-tags"><span>mysql/</span><span>redis/</span><span>dataobject/</span></div>
  414 + </div>
  415 + </div>
  416 + </div>
  417 + <div>
  418 + <div class="stack">
  419 + <div class="card" style="padding:16px;border-top:2px solid var(--accent-2)">
  420 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent-2)">convert/</h4>
  421 + <p class="dim" style="font-size:12px">MapStruct 对象转换层,编译期生成高性能映射代码</p>
  422 + </div>
  423 + <div class="card" style="padding:16px;border-top:2px solid var(--accent-2)">
  424 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent-2)">api/</h4>
  425 + <p class="dim" style="font-size:12px">模块间 Feign 接口定义 + DTO,实现跨模块调用</p>
  426 + </div>
  427 + <div class="card" style="padding:16px;border-top:2px solid var(--accent-2)">
  428 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent-2)">mq/ + job/</h4>
  429 + <p class="dim" style="font-size:12px">异步消息(producer/consumer)+ 定时任务调度</p>
  430 + </div>
  431 + <div class="card" style="padding:16px;border-top:2px solid var(--accent-2)">
  432 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent-2)">framework/</h4>
  433 + <p class="dim" style="font-size:12px">模块内框架代码:数据权限配置、Web 配置、操作日志等</p>
  434 + </div>
  435 + </div>
  436 + </div>
  437 + </div>
  438 + </section>
  439 +
  440 + <!-- 11. Framework Infrastructure -->
  441 + <section class="slide" data-title="Framework Infrastructure">
  442 + <p class="kicker">framework · starters</p>
  443 + <h2 class="h2">基础设施层:<br>15 个 Spring Boot Starter</h2>
  444 + <p class="lede">将通用能力抽象为可复用的 Starter 组件,所有业务模块按需引入。</p>
  445 + <div class="fw-grid">
  446 + <div class="fw-card hl anim-fade-up" data-anim="fade-up"><div class="fwi">🌐</div><h4>web</h4><p>全局异常处理 · 统一返回 · CORS · Swagger</p></div>
  447 + <div class="fw-card hl anim-fade-up" data-anim="fade-up" style="animation-delay:.05s"><div class="fwi">🔐</div><h4>security</h4><p>Spring Security · Token 鉴权 · SSO</p></div>
  448 + <div class="fw-card hl anim-fade-up" data-anim="fade-up" style="animation-delay:.1s"><div class="fwi">🗄️</div><h4>mybatis</h4><p>MyBatis Plus · 多数据源 · 联表查询</p></div>
  449 + <div class="fw-card hl anim-fade-up" data-anim="fade-up" style="animation-delay:.15s"><div class="fwi">⚡</div><h4>redis</h4><p>Redisson · 分布式锁 · 缓存策略</p></div>
  450 + <div class="fw-card hl anim-fade-up" data-anim="fade-up" style="animation-delay:.2s"><div class="fwi">📨</div><h4>mq</h4><p>RocketMQ 集成 · 消息幂等 · 重试</p></div>
  451 + <div class="fw-card hl anim-fade-up" data-anim="fade-up" style="animation-delay:.25s"><div class="fwi">🛡️</div><h4>protection</h4><p>分布式锁 · 幂等 · 限流 · 熔断</p></div>
  452 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.3s"><div class="fwi">📊</div><h4>monitor</h4><p>SkyWalking · Admin · 日志中心</p></div>
  453 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.35s"><div class="fwi">⏰</div><h4>job</h4><p>Quartz 定时任务 · 执行日志</p></div>
  454 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.4s"><div class="fwi">📝</div><h4>excel</h4><p>FastExcel 导入导出</p></div>
  455 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.45s"><div class="fwi">🔌</div><h4>websocket</h4><p>实时通信 · Token 校验 · 集群</p></div>
  456 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.5s"><div class="fwi">🏢</div><h4>biz-tenant</h4><p>多租户数据隔离 · 透明封装</p></div>
  457 + <div class="fw-card anim-fade-up" data-anim="fade-up" style="animation-delay:.55s"><div class="fwi">🔒</div><h4>biz-data-permission</h4><p>数据权限 · 行级过滤</p></div>
  458 + </div>
  459 + </section>
  460 +
  461 + <!-- 12. Core Tech Stack -->
  462 + <section class="slide" data-title="Tech Stack">
  463 + <p class="kicker">tech-stack · ecosystem</p>
  464 + <h2 class="h2">核心技术栈全景</h2>
  465 + <div class="tg">
  466 + <div class="tgc anim-fade-up" data-anim="fade-up"><div class="tgci">☕</div><h4>核心框架</h4><div class="tgt"><span>Spring Boot 3.5.5</span><span>Spring MVC 6.x</span><span>Spring Security 6.x</span></div></div>
  467 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.05s"><div class="tgci">🗄️</div><h4>持久层</h4><div class="tgt"><span>MyBatis Plus 3.5.14</span><span>Druid 1.2.27</span><span>Dynamic-DS 4.3.1</span></div></div>
  468 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.1s"><div class="tgci">⚡</div><h4>缓存 & 锁</h4><div class="tgt"><span>Redis 7.x</span><span>Redisson 3.51</span><span>Lock4j 2.2.7</span></div></div>
  469 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.15s"><div class="tgci">📨</div><h4>消息队列</h4><div class="tgt"><span>RocketMQ</span><span>Spring-RocketMQ</span><span>Redis Stream</span></div></div>
  470 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.2s"><div class="tgci">🔄</div><h4>工作流</h4><div class="tgt"><span>Flowable 7.0.1</span><span>BPMN 2.0</span><span>SIMPLE 设计器</span></div></div>
  471 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.25s"><div class="tgci">📡</div><h4>IoT & 通信</h4><div class="tgt"><span>Vert.x 4.5.13</span><span>Netty 4.2.4</span><span>MQTT 1.2.5</span></div></div>
  472 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.3s"><div class="tgci">🔍</div><h4>可观测性</h4><div class="tgt"><span>SkyWalking 9.5</span><span>Admin 3.5.2</span><span>OpenTracing</span></div></div>
  473 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.35s"><div class="tgci">📋</div><h4>API 文档</h4><div class="tgt"><span>Springdoc 2.8.11</span><span>Knife4j 4.5.0</span><span>Swagger 3</span></div></div>
  474 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.4s"><div class="tgci">🔌</div><h4>插件 & 工具</h4><div class="tgt"><span>PF4J 0.9.0</span><span>MapStruct 1.6.3</span><span>Hutool 5.8+6.0</span></div></div>
  475 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.45s"><div class="tgci">🧪</div><h4>测试</h4><div class="tgt"><span>JUnit 5</span><span>Mockito 5.x</span><span>Jedis-Mock</span></div></div>
  476 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.5s"><div class="tgci">🗄️</div><h4>多数据库</h4><div class="tgt"><span>MySQL 8.x</span><span>DM8</span><span>KingBase</span><span>OpenGauss</span></div></div>
  477 + <div class="tgc anim-fade-up" data-anim="fade-up" style="animation-delay:.55s"><div class="tgci">🐳</div><h4>部署运维</h4><div class="tgt"><span>Docker</span><span>K8s</span><span>Jenkins</span><span>Harbor</span></div></div>
  478 + </div>
  479 + </section>
  480 +
  481 + <!-- 13. Data Storage Strategy -->
  482 + <section class="slide" data-title="Data Storage">
  483 + <p class="kicker">database · strategy</p>
  484 + <h2 class="h2">数据存储策略:<br>多数据源 + 多数据库 + 读写分离</h2>
  485 + <div class="grid g2 mt-l" style="align-items:start;gap:20px">
  486 + <div>
  487 + <div class="card card-accent" style="padding:22px">
  488 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:14px;color:var(--accent)">Dynamic-Datasource 多数据源</h4>
  489 + <div class="stack mt-m">
  490 + <div class="tag"><span style="color:var(--accent)">●</span> 一主多从读写分离</div>
  491 + <div class="tag"><span style="color:var(--accent)">●</span> @DS 注解切换数据源</div>
  492 + <div class="tag"><span style="color:var(--accent)">●</span> 多库事务(本地事务 + 补偿)</div>
  493 + <div class="tag"><span style="color:var(--accent)">●</span> Druid 连接池监控</div>
  494 + </div>
  495 + </div>
  496 + <div class="card" style="padding:22px;border-top:3px solid var(--accent-2);margin-top:12px">
  497 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:14px;color:var(--accent-2)">Redis 缓存体系</h4>
  498 + <div class="stack mt-m">
  499 + <div class="tag">○ Redisson 分布式锁 · 幂等键</div>
  500 + <div class="tag">○ Redis Stream 轻量 MQ</div>
  501 + <div class="tag">○ 菜单/字典/租户权限缓存</div>
  502 + <div class="tag">○ Token 存储 & 在线用户管理</div>
  503 + </div>
  504 + </div>
  505 + </div>
  506 + <div>
  507 + <div class="card" style="padding:22px;border-top:3px solid #d2a8ff">
  508 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:14px;color:#d2a8ff">多数据库兼容矩阵</h4>
  509 + <div class="stack mt-m">
  510 + <div class="tag"><span style="color:#d2a8ff">■</span> MySQL 5.7 / 8.0+ — 主力库</div>
  511 + <div class="tag"><span style="color:#d2a8ff">■</span> 达梦 DM8 — 国产信创</div>
  512 + <div class="tag"><span style="color:#d2a8ff">■</span> 人大金仓 KingBase 8.6 — 信创</div>
  513 + <div class="tag"><span style="color:#d2a8ff">■</span> OpenGauss 5.1 — 华为开源</div>
  514 + <div class="tag"><span style="color:#d2a8ff">■</span> Oracle / PostgreSQL / SQL Server</div>
  515 + <div class="tag"><span style="color:#d2a8ff">■</span> TDengine 3.7 — IoT 时序数据</div>
  516 + </div>
  517 + </div>
  518 + <div class="card card-accent" style="padding:22px;margin-top:12px">
  519 + <h4 style="font-size:14px;color:#fff">数据隔离策略</h4>
  520 + <p class="dim" style="font-size:12px">SaaS 多租户:共享数据表 + tenant_id 字段隔离,biz-tenant Starter 透明拦截 SQL,自动添加租户条件</p>
  521 + </div>
  522 + </div>
  523 + </div>
  524 + </section>
  525 +
  526 + <!-- 14. Module Invocation Logic -->
  527 + <section class="slide" data-title="Invocation Logic">
  528 + <p class="kicker">invocation · communication</p>
  529 + <h2 class="h2">模块调用逻辑:<br>三种通信通道</h2>
  530 + <div class="invoc-flow mt-l">
  531 + <div class="invoc-row">
  532 + <div class="ir-label" style="color:var(--accent)">Feign API</div>
  533 + <div class="ir-items"><span>System → Dept API</span><span>System → User API</span><span>System → Dict API</span><span>Workorder → Garden API</span><span>Garden → System API</span></div>
  534 + </div>
  535 + <div class="ir-arrow">▼ 同步调用 · 模块间 RPC · 请求/响应模式 ▼</div>
  536 + <div class="invoc-row">
  537 + <div class="ir-label" style="color:var(--accent-2)">MQ 消息</div>
  538 + <div class="ir-items"><span>SMS Producer → Consumer</span><span>Mail Producer → Consumer</span><span>Event → 多消费者</span><span>RocketMQ Topic</span><span>Redis Stream</span></div>
  539 + </div>
  540 + <div class="ir-arrow">▼ 异步解耦 · 事件驱动 · 最终一致性 ▼</div>
  541 + <div class="invoc-row">
  542 + <div class="ir-label" style="color:#d2a8ff">直接依赖</div>
  543 + <div class="ir-items"><span>Server → All Modules</span><span>Workorder → Garden Service</span><span>BPM → System User</span><span>Garden → Infra File</span></div>
  544 + </div>
  545 + </div>
  546 + <div class="grid g3 mt-l">
  547 + <div class="card card-accent"><h4 style="font-size:14px">api/ 包设计</h4><p class="dim" style="font-size:12px">每个模块暴露 Feign 接口 + DTO,其他模块引入依赖即可调用,编译期类型安全。</p></div>
  548 + <div class="card card-accent"><h4 style="font-size:14px">mq/ 包设计</h4><p class="dim" style="font-size:12px">Producer 发送消息,Consumer 消费处理。SMS / Mail 异步发送,不解耦主流程。</p></div>
  549 + <div class="card card-accent"><h4 style="font-size:14px">service/ 直接注入</h4><p class="dim" style="font-size:12px">同模块内 Service 互相注入。跨模块通过 api 包或直接依赖。</p></div>
  550 + </div>
  551 + </section>
  552 +
  553 + <!-- 15. Message & Async -->
  554 + <section class="slide" data-title="Message & Async">
  555 + <p class="kicker">async · event-driven</p>
  556 + <h2 class="h2">异步消息与事件驱动</h2>
  557 + <div class="col2">
  558 + <div>
  559 + <div class="card card-accent" style="padding:20px">
  560 + <h4 style="color:#fff;margin:0 0 10px">RocketMQ 消息架构</h4>
  561 + <div class="stack">
  562 + <div class="tag">📤 Producer: 业务服务发送消息到 Topic</div>
  563 + <div class="tag">📥 Consumer: 独立消费组拉取并处理</div>
  564 + <div class="tag">🔄 重试机制: 消费失败自动重试</div>
  565 + <div class="tag">⚡ 顺序消息: 全局/分区有序</div>
  566 + <div class="tag">🔒 事务消息: 分布式事务最终一致性</div>
  567 + </div>
  568 + </div>
  569 + </div>
  570 + <div>
  571 + <div class="card" style="padding:20px;border-top:3px solid var(--accent-2)">
  572 + <h4 style="color:#fff;margin:0 0 10px">典型异步场景</h4>
  573 + <div class="stack">
  574 + <div class="tag">📧 SMS 发送 → MQ 异步 → 消费者推送</div>
  575 + <div class="tag">📨 邮件发送 → MQ 异步 → 消费者投递</div>
  576 + <div class="tag">🔔 站内信通知 → Redis Stream 广播</div>
  577 + <div class="tag">📋 操作日志 → 异步记录不影响主流程</div>
  578 + <div class="tag">🔄 工单事件 → MQ 通知园林模块更新</div>
  579 + </div>
  580 + </div>
  581 + </div>
  582 + </div>
  583 + <div class="grid g2 mt-m">
  584 + <div class="card card-accent"><h4 style="font-size:14px">定时任务 (Quartz)</h4><p class="dim" style="font-size:12px">巡检计划自动生成 · 养护计划定时创建 · 数据统计报表 · 超时任务自动处理</p></div>
  585 + <div class="card card-accent"><h4 style="font-size:14px">服务保障 (Protection)</h4><p class="dim" style="font-size:12px">@Lock4j 分布式锁防并发 · @Idempotent 幂等性 · @RateLimiter 限流保护 · 熔断降级</p></div>
  586 + </div>
  587 + </section>
  588 +
  589 + <!-- 16. Security & Auth -->
  590 + <section class="slide" data-title="Security & Auth">
  591 + <p class="kicker">security · auth · tenant</p>
  592 + <h2 class="h2">安全与权限体系</h2>
  593 + <div class="grid g3 mt-l">
  594 + <div class="card card-accent anim-fade-up" data-anim="fade-up" style="padding:22px">
  595 + <h4 style="color:#fff;margin-bottom:8px">🔐 认证体系</h4>
  596 + <ul style="font-size:12px;color:var(--text-2);padding-left:16px;line-height:1.8;margin:0">
  597 + <li>Spring Security 6.x 核心</li>
  598 + <li>Token + Redis 无状态会话</li>
  599 + <li>OAuth2 授权码模式</li>
  600 + <li>多终端(Web / App / 小程序)</li>
  601 + <li>SSO 单点登录</li>
  602 + <li>社交登录(JustAuth)</li>
  603 + </ul>
  604 + </div>
  605 + <div class="card card-accent anim-fade-up" data-anim="fade-up" style="padding:22px;animation-delay:.1s">
  606 + <h4 style="color:#fff;margin-bottom:8px">🛡️ 授权体系</h4>
  607 + <ul style="font-size:12px;color:var(--text-2);padding-left:16px;line-height:1.8;margin:0">
  608 + <li>RBAC:用户 → 角色 → 菜单</li>
  609 + <li>按钮级别权限控制</li>
  610 + <li>动态菜单加载</li>
  611 + <li>Redis 缓存权限信息</li>
  612 + <li>数据权限:行级过滤</li>
  613 + <li>部门数据范围隔离</li>
  614 + </ul>
  615 + </div>
  616 + <div class="card card-accent anim-fade-up" data-anim="fade-up" style="padding:22px;animation-delay:.2s">
  617 + <h4 style="color:#fff;margin-bottom:8px">🏢 多租户</h4>
  618 + <ul style="font-size:12px;color:var(--text-2);padding-left:16px;line-height:1.8;margin:0">
  619 + <li>tenant_id 字段隔离</li>
  620 + <li>MyBatis 拦截器自动注入</li>
  621 + <li>租户套餐权限定制</li>
  622 + <li>每个租户独立菜单/角色</li>
  623 + <li>透明化封装无侵入</li>
  624 + <li>支持租户禁用/过期</li>
  625 + </ul>
  626 + </div>
  627 + </div>
  628 + <div class="code-text mt-l">SystemModule → Security Starter → Token + Redis → @PreAuthorize → DataPermission AOP → SQL Interceptor</div>
  629 + </section>
  630 +
  631 + <!-- 17. Observability -->
  632 + <section class="slide" data-title="Observability">
  633 + <p class="kicker">observability · monitoring</p>
  634 + <h2 class="h2">可观测性与监控体系</h2>
  635 + <div class="grid g3 mt-l">
  636 + <div class="card" style="padding:22px;border-top:3px solid var(--accent)">
  637 + <h4 style="color:#fff;font-size:16px;margin-bottom:8px">📝 日志体系</h4>
  638 + <p class="dim" style="font-size:12px;line-height:1.7">Logback + SkyWalking Logback Appender,TraceId 自动注入,一键关联链路</p>
  639 + <div class="biz-tags mt-s"><span>操作日志</span><span>登录日志</span><span>API 访问日志</span><span>错误日志</span></div>
  640 + </div>
  641 + <div class="card" style="padding:22px;border-top:3px solid var(--accent-2)">
  642 + <h4 style="color:#fff;font-size:16px;margin-bottom:8px">📊 指标监控</h4>
  643 + <p class="dim" style="font-size:12px;line-height:1.7">Spring Boot Admin 应用监控 + Druid SQL 监控 + Redis 监控面板</p>
  644 + <div class="biz-tags mt-s"><span>JVM 指标</span><span>连接池</span><span>QPS/RT</span><span>慢 SQL</span></div>
  645 + </div>
  646 + <div class="card" style="padding:22px;border-top:3px solid #d2a8ff">
  647 + <h4 style="color:#fff;font-size:16px;margin-bottom:8px">🔍 链路追踪</h4>
  648 + <p class="dim" style="font-size:12px;line-height:1.7">Apache SkyWalking 9.5 分布式追踪,服务拓扑自动发现</p>
  649 + <div class="biz-tags mt-s"><span>调用链</span><span>拓扑图</span><span>慢端点</span><span>告警</span></div>
  650 + </div>
  651 + </div>
  652 + <div class="grid g2 mt-m">
  653 + <div class="card card-accent"><h4 style="font-size:14px">Monitor Starter 集成</h4><p class="dim" style="font-size:12px">SkyWalking · Admin Client · OpenTracing · 日志中心 · 一键接入</p></div>
  654 + <div class="card card-accent"><h4 style="font-size:14px">保护机制</h4><p class="dim" style="font-size:12px">分布式锁 (Lock4j) · 幂等性保证 · 限流降级 · 异常全局处理 · 错误码统一管理</p></div>
  655 + </div>
  656 + </section>
  657 +
  658 + <!-- 18. Deployment Architecture -->
  659 + <section class="slide" data-title="Deployment Architecture">
  660 + <p class="kicker">deployment · ci-cd</p>
  661 + <h2 class="h2">部署架构与 CI/CD 流水线</h2>
  662 + <div class="deploy-grid mt-l">
  663 + <div class="deploy-card ci anim-fade-up" data-anim="fade-up">
  664 + <div class="di">🚀</div><h4>CI 持续集成</h4>
  665 + <ul><li>Jenkins Pipeline</li><li>Maven 多模块构建</li><li>单元测试 + 代码扫描</li><li>Docker 镜像构建</li><li>推送 Harbor 私有仓库</li></ul>
  666 + </div>
  667 + <div class="deploy-card cn anim-fade-up" data-anim="fade-up" style="animation-delay:.1s">
  668 + <div class="di">☸️</div><h4>容器编排</h4>
  669 + <ul><li>Kubernetes 集群</li><li>Deployment 滚动更新</li><li>Service 服务发现</li><li>ConfigMap 配置管理</li><li>Ingress 流量入口</li></ul>
  670 + </div>
  671 + <div class="deploy-card mon anim-fade-up" data-anim="fade-up" style="animation-delay:.2s">
  672 + <div class="di">📊</div><h4>运维监控</h4>
  673 + <ul><li>Prometheus + Grafana</li><li>SkyWalking 链路追踪</li><li>ELK 日志收集</li><li>AlertManager 告警</li><li>Admin 应用监控</li></ul>
  674 + </div>
  675 + </div>
  676 + <div class="card card-accent mt-l" style="padding:22px">
  677 + <h4 style="font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent);margin-bottom:12px">Dockerfile — 多阶段构建</h4>
  678 + <div class="terminal">
  679 + <div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span>urbanops-server/Dockerfile</span></div>
  680 + <pre><span class="cmt"># Stage 1: 构建</span>
  681 +<span class="kw">FROM</span> maven:3.9-eclipse-temurin-17 <span class="kw">AS</span> builder
  682 +<span class="kw">COPY</span> . /app && <span class="kw">WORKDIR</span> /app
  683 +<span class="kw">RUN</span> mvn clean package -DskipTests
  684 +
  685 +<span class="cmt"># Stage 2: 运行</span>
  686 +<span class="kw">FROM</span> eclipse-temurin:17-jre
  687 +<span class="kw">COPY</span> --from=builder /app/urbanops-server/target/*.jar app.jar
  688 +<span class="kw">ENTRYPOINT</span> ["java","-jar","/app.jar"]</pre>
  689 + </div>
  690 + </div>
  691 + </section>
  692 +
  693 + <!-- 19. Future Planning & Roadmap -->
  694 + <section class="slide" data-title="Future Planning">
  695 + <p class="kicker">roadmap · planning</p>
  696 + <h2 class="h2">后续扩展规划</h2>
  697 + <div class="roadmap-grid">
  698 + <div class="roadmap-card now anim-fade-up" data-anim="fade-up">
  699 + <div class="r-period">Phase 1 · 近期</div>
  700 + <h4>🏗️ 架构夯实</h4>
  701 + <ul>
  702 + <li>完善 IoT 设备接入能力 (MQTT + Vert.x)</li>
  703 + <li>会员中心模块激活 (Member)</li>
  704 + <li>支付系统集成 (Pay)</li>
  705 + <li>微信公众号接入 (MP)</li>
  706 + <li>数据报表与大屏 (Report)</li>
  707 + <li>完善单元测试覆盖率</li>
  708 + </ul>
  709 + </div>
  710 + <div class="roadmap-card next anim-fade-up" data-anim="fade-up" style="animation-delay:.1s">
  711 + <div class="r-period">Phase 2 · 中期</div>
  712 + <h4>🚀 业务扩展</h4>
  713 + <ul>
  714 + <li>ERP 企业资源管理</li>
  715 + <li>CRM 客户关系管理</li>
  716 + <li>商城系统搭建 (Mall)</li>
  717 + <li>AI 大模型集成 (AI)</li>
  718 + <li>微服务拆分:Spring Cloud</li>
  719 + <li>服务网格:Istio 引入</li>
  720 + </ul>
  721 + </div>
  722 + <div class="roadmap-card future anim-fade-up" data-anim="fade-up" style="animation-delay:.2s">
  723 + <div class="r-period">Phase 3 · 远期</div>
  724 + <h4>🌐 平台化</h4>
  725 + <ul>
  726 + <li>多租户 SaaS 化运营</li>
  727 + <li>开放平台 & API 市场</li>
  728 + <li>低代码/无代码搭建</li>
  729 + <li>PF4J 插件生态</li>
  730 + <li>边缘计算 + IoT 规模化</li>
  731 + <li>数据中台 & 智能决策</li>
  732 + </ul>
  733 + </div>
  734 + </div>
  735 + <div class="timeline mt-l">
  736 + <div class="tl-item"><div class="tl-phase">Q2 2025</div><div class="tl-body"><h4>物联网接入打通</h4><p>MQTT 协议适配 · 设备影子 · 数据上行 · TDengine 时序存储</p></div></div>
  737 + <div class="tl-item"><div class="tl-phase">Q3 2025</div><div class="tl-body"><h4>业务模块激活</h4><p>Member + Pay + MP + Report 模块上线,补齐业务闭环</p></div></div>
  738 + <div class="tl-item"><div class="tl-phase">Q4 2025</div><div class="tl-body"><h4>微服务演进</h4><p>核心域拆分 · Spring Cloud 迁移 · 服务独立部署</p></div></div>
  739 + </div>
  740 + </section>
  741 +
  742 + <!-- 20. Summary & Q&A -->
  743 + <section class="slide center tc" data-title="Summary & QA">
  744 + <div>
  745 + <p class="kicker" style="text-align:center">// key-takeaways</p>
  746 + <h2 class="h2">五个核心要点</h2>
  747 + <div class="grid g3 mt-l" style="max-width:960px;margin-left:auto;margin-right:auto;gap:14px">
  748 + <div class="card card-accent"><h4>1 · 业务驱动架构</h4><p class="dim">智慧园林 + 工单调度 + 流程协同,三大核心域支撑城市运营。</p></div>
  749 + <div class="card card-accent"><h4>2 · 分层清晰可扩展</h4><p class="dim">15 个 Starter 组件化,模块间 Feign API + MQ 双通道解耦。</p></div>
  750 + <div class="card card-accent"><h4>3 · 信创全栈兼容</h4><p class="dim">MySQL / DM8 / KingBase / OpenGauss 多数据库无缝切换。</p></div>
  751 + <div class="card card-accent"><h4>4 · 可观测性内置</h4><p class="dim">SkyWalking + Admin + 日志中心,三大支柱保障系统可靠。</p></div>
  752 + <div class="card card-accent"><h4>5 · 渐进式演进</h4><p class="dim">从模块化到微服务化,从单应用到平台化,路径清晰可落地。</p></div>
  753 + </div>
  754 + <div style="margin-top:36px">
  755 + <div class="mono" style="font-size:72px;color:var(--accent);font-weight:800;letter-spacing:-.04em">?</div>
  756 + <h2 class="h2">Questions &amp; Discussion</h2>
  757 + <p class="lede" style="margin:14px auto">感谢聆听 · 欢迎交流</p>
  758 + <div class="row mt-m" style="justify-content:center">
  759 + <span class="tag">#urbanops</span>
  760 + <span class="tag">#spring-boot</span>
  761 + <span class="tag">#microservices</span>
  762 + <span class="tag">#smart-garden</span>
  763 + <span class="tag">#cloud-native</span>
  764 + </div>
  765 + </div>
  766 + </div>
  767 + </section>
  768 +
  769 +</div>
  770 +<script src="ppt-assets/runtime.js"></script>
  771 +</body></html>
docs/开放API对接接口文档.html 0 → 100644
  1 +# UrbanOps 开放 API 对接接口文档
  2 +
  3 +> **版本**: v1.1
  4 +> **更新日期**: 2026-06-25
  5 +> **适用对象**: 第三方系统对接开发人员 / Co-Claw AI 助手
  6 +> **鉴权方式**: HMAC-SHA256 API 签名
  7 +
  8 +---
  9 +
  10 +## 目录
  11 +
  12 +1. [概述](#1-概述)
  13 +2. [签名鉴权机制](#2-签名鉴权机制)
  14 +3. [通用规范](#3-通用规范)
  15 +4. [接口 1:团队考核查询(Co-Claw AI 专用)](#4-接口-1团队考核查询Co-Claw-ai-专用)
  16 +5. [接口 2:人员查询](#5-接口-2人员查询)
  17 +6. [接口 3:班组查询](#6-接口-3班组查询)
  18 +7. [接口 4:考勤查询](#7-接口-4考勤查询)
  19 +8. [接口 5:工单查询](#8-接口-5工单查询)
  20 +9. [接口 6:物料查询](#9-接口-6物料查询)
  21 +10. [接口 7:全量工单统计](#10-接口-7全量工单统计)
  22 +11. [错误码说明](#11-错误码说明)
  23 +12. [附录:客户端签名代码示例](#12-附录客户端签名代码示例)
  24 +
  25 +---
  26 +
  27 +## 1. 概述
  28 +
  29 +### 1.1 基本信息
  30 +
  31 +| 项目 | 说明 |
  32 +|------|------|
  33 +| 接口协议 | HTTPS |
  34 +| 接口地址 | `https://{host}/open-api/` |
  35 +| 鉴权方式 | HMAC-SHA256 API 签名(详见第2节) |
  36 +| 响应格式 | JSON |
  37 +| 字符编码 | UTF-8 |
  38 +
  39 +### 1.2 接口列表
  40 +
  41 +| # | 接口名称 | 路径 | 方法 | 说明 |
  42 +|---|---------|------|------|------|
  43 +| 1 | **团队考核查询** ⭐ | `/open-api/team-assessment/query` | GET | **Co-Claw AI 专用**:班组/人员考核聚合接口,含考勤/工单/耗材三维评分 |
  44 +| 2 | 人员查询 | `/open-api/staff/query` | GET | 获取人员档案,支持按部门筛选 |
  45 +| 3 | 班组查询 | `/open-api/teams/query` | GET | 获取班组信息,含班组长和人员数量 |
  46 +| 4 | 考勤查询 | `/open-api/attendance/query` | GET | 获取考勤打卡数据 |
  47 +| 5 | 工单查询 | `/open-api/tasks/query` | GET | 获取工单全生命周期数据 |
  48 +| 6 | 物料查询 | `/open-api/materials/query` | GET | 获取物料消耗与照片数据 |
  49 +| 7 | 全量工单统计 | `/open-api/full-order-statistics/query` | GET | 获取公司/班组工单/巡查/养护完成数量 |
  50 +
  51 +### 1.3 接入前提
  52 +
  53 +接入前需向 UrbanOps 平台申请以下凭证(由运维人员配置到 Redis):
  54 +
  55 +| 参数 | 说明 |
  56 +|------|------|
  57 +| `appId` | 应用唯一标识(由平台分配) |
  58 +| `appSecret` | 应用密钥(妥善保管,不可泄露) |
  59 +
  60 +---
  61 +
  62 +## 2. 签名鉴权机制
  63 +
  64 +### 2.1 签名参数
  65 +
  66 +每个请求必须携带以下 **4 个请求头**:
  67 +
  68 +| 请求头 | 类型 | 必填 | 说明 |
  69 +|--------|------|------|------|
  70 +| `appId` | String | ✅ | 平台分配的应用 ID |
  71 +| `timestamp` | String | ✅ | 当前 Unix 时间戳(**毫秒**),与服务器时间误差不能超过 60 秒 |
  72 +| `nonce` | String | ✅ | 随机字符串,**长度 ≥ 10**,每次请求必须不同(防重放) |
  73 +| `sign` | String | ✅ | 签名字符串(SHA256 计算结果,**小写十六进制**) |
  74 +
  75 +### 2.2 签名算法
  76 +
  77 +#### 签名字符串构造规则
  78 +
  79 +```
  80 +签名字符串 = queryParams(按key字母序排列的value拼接)
  81 + + headerParams(按key字母序排列的value拼接)
  82 + + appSecret
  83 +```
  84 +
  85 +**具体步骤:**
  86 +
  87 +1. **排序 Query 参数值**:将 URL 中所有 query 参数按 key 字母升序排列,拼接所有 value
  88 +2. **拼接 Header 参数值**:按 key 字母升序(即 `appId` → `nonce` → `timestamp`),依次拼接它们的值
  89 +3. **追加 appSecret**:在最后拼接 `appSecret`
  90 +4. **计算 SHA256**:对上述完整字符串计算 SHA256 哈希,转为**小写十六进制**字符串
  91 +
  92 +#### 签名公式
  93 +
  94 +```
  95 +sign = SHA256(
  96 + sortedQueryParamValues
  97 + + appId值 + nonce值 + timestamp值
  98 + + appSecret
  99 +)
  100 +```
  101 +
  102 +> **注意**:GET 请求无 body,body 部分为空字符串(不参与签名)。
  103 +
  104 +### 2.3 签名示例
  105 +
  106 +假设:
  107 +- `appId` = `my_app_001`
  108 +- `appSecret` = `abc123xyz`
  109 +- 请求 URL:`/open-api/staff/query?deptId=100`
  110 +
  111 +**步骤拆解:**
  112 +
  113 +```
  114 +queryParams(sorted by key):
  115 + deptId=100
  116 + → values = "100"
  117 +
  118 +headerParams(sorted by key):
  119 + appId=my_app_001, nonce=a1b2c3d4e5f6, timestamp=1749696000000
  120 + → values = "my_app_001" + "a1b2c3d4e5f6" + "1749696000000"
  121 +
  122 +signStr = "100" + "my_app_001" + "a1b2c3d4e5f6" + "1749696000000" + "abc123xyz"
  123 +
  124 +sign = SHA256(signStr)
  125 +```
  126 +
  127 +### 2.4 签名校验规则
  128 +
  129 +| 校验项 | 规则 | 失败返回 |
  130 +|--------|------|----------|
  131 +| appId 非空 | 必须传递 | `400 签名不正确` |
  132 +| timestamp 时效 | `|serverTime - timestamp| ≤ 60s` | `400 签名不正确` |
  133 +| nonce 长度 | `len(nonce) ≥ 10` | `400 签名不正确` |
  134 +| sign 非空 | 必须传递 | `400 签名不正确` |
  135 +| 签名比对 | `serverSign == clientSign` | `400 签名不正确` |
  136 +| nonce 防重放 | 同一 appId + nonce 组合只能使用一次 | `900 存在重复请求` |
  137 +
  138 +---
  139 +
  140 +## 3. 通用规范
  141 +
  142 +### 3.1 通用响应格式
  143 +
  144 +```json
  145 +{
  146 + "code": 0,
  147 + "msg": "成功",
  148 + "data": [ ... ]
  149 +}
  150 +```
  151 +
  152 +| 字段 | 类型 | 说明 |
  153 +|------|------|------|
  154 +| `code` | Integer | `0` 表示成功,非 `0` 表示失败 |
  155 +| `msg` | String | 提示信息 |
  156 +| `data` | Array | 业务数据列表(根据查询条件返回所有匹配记录) |
  157 +
  158 +### 3.2 日期时间格式
  159 +
  160 +所有日期时间字段统一使用格式:`yyyy-MM-dd HH:mm:ss`
  161 +
  162 +示例:`2026-06-11 09:00:00`
  163 +
  164 +---
  165 +
  166 +## 4. 接口 1:团队考核查询(Co-Claw AI 专用)⭐
  167 +
  168 +> **设计定位**:本接口专为 Co-Claw AI 助手设计,提供班组/人员考核数据的**一站式聚合查询**。
  169 +> 接口内部完成考勤、工单、耗材三维度评分计算,Co-Claw 可直接使用返回的得分和扣分明细进行考核分析,
  170 +> 无需自行计算。默认只返回汇总指标(`includeDetail=false`),Token 友好。
  171 +
  172 +### 4.1 考核背景
  173 +
  174 +本接口基于《房山区园林所养护队智慧化管理制度》设计,考核满分 100 分,三大维度:
  175 +
  176 +| 维度 | 满分 | 主要扣分规则 |
  177 +|------|------|-------------|
  178 +| 工单上线率与及时率 | **40分** | 超24h补录 -1分/次 · 超时>2h -1分/次 |
  179 +| 人员考勤管理 | **30分** | 缺勤/未打卡 -1分/次 · 缺共同处理人 -1分/次 · 未佩戴工卡 -1分/次 · 班组长记录仪未开 -1分/次 |
  180 +| 人机材录入 | **30分** | 工单完成24h内未上报耗材 -2分/次 |
  181 +
  182 +### 4.2 基本信息
  183 +
  184 +| 项目 | 说明 |
  185 +|------|------|
  186 +| 路径 | `/open-api/team-assessment/query` |
  187 +| 方法 | `GET` |
  188 +| 说明 | 团队考核聚合查询。支持 **team 模式**(班组→人员+工单→考勤+耗材,三层结构)和 **staff 模式**(人员→考勤+工单→耗材) |
  189 +
  190 +### 4.3 请求参数
  191 +
  192 +| 参数 | 类型 | 必填 | 默认值 | 说明 |
  193 +|------|------|------|--------|------|
  194 +| `keyword` | String | **是** | — | 查询关键字:班组名称或人员姓名,**支持模糊匹配** |
  195 +| `queryMode` | String | 否 | `team` | 查询模式:`team` 按班组查询 / `staff` 按人员查询 |
  196 +| `startDate` | String | 否 | 当月1日 | 考核开始日期(格式:yyyy-MM-dd) |
  197 +| `endDate` | String | 否 | 今天 | 考核结束日期(格式:yyyy-MM-dd) |
  198 +| `includeDetail` | Boolean | 否 | `false` | 是否返回明细数据。`false` 仅返回汇总指标(推荐),`true` 同时返回考勤明细和工单耗材明细 |
  199 +
  200 +### 4.4 响应字段(queryMode=team)
  201 +
  202 +#### 顶层结构
  203 +
  204 +| 字段 | 类型 | 说明 |
  205 +|------|------|------|
  206 +| `queryMode` | String | 查询模式(`team` 或 `staff`) |
  207 +| `period` | PeriodVO | 考核周期 |
  208 +| `teams` | Array\<TeamVO\> | 班组考核列表(team 模式) |
  209 +| `staffList` | Array\<StaffVO\> | 人员考核列表(staff 模式,与 teams 互斥) |
  210 +
  211 +#### TeamVO — 班组考核(第一层)
  212 +
  213 +| 字段 | 类型 | 说明 |
  214 +|------|------|------|
  215 +| `teamId` | Long | 班组ID |
  216 +| `teamName` | String | 班组名称 |
  217 +| `parentTeamName` | String | 上级部门名称(所属养护所) |
  218 +| `leaderName` | String | 班组长姓名 |
  219 +| `totalMembers` | Long | 班组人数 |
  220 +| `score` | ScoreVO | 考核得分(详见下方) |
  221 +| `orderStats` | OrderStatsVO | 工单统计 |
  222 +| `attendanceStats` | AttendanceStatsVO | 考勤统计 |
  223 +| `materialStats` | MaterialStatsVO | 耗材统计 |
  224 +| `members` | Array\<MemberVO\> | 人员列表(**第二层**,含个人考核指标) |
  225 +| `orders` | Array\<OrderVO\> | 工单列表(**第二层**,含耗材) |
  226 +
  227 +#### ScoreVO — 考核得分
  228 +
  229 +| 字段 | 类型 | 说明 |
  230 +|------|------|------|
  231 +| `total` | Integer | 总分(满分100) |
  232 +| `orderScore` | Integer | 工单得分(满分40) |
  233 +| `attendanceScore` | Integer | 考勤得分(满分30) |
  234 +| `materialScore` | Integer | 人机材得分(满分30) |
  235 +| `deductions` | Array\<DeductionVO\> | 扣分明细列表 |
  236 +
  237 +**DeductionVO:**
  238 +
  239 +| 字段 | 类型 | 说明 |
  240 +|------|------|------|
  241 +| `rule` | String | 扣分规则描述 |
  242 +| `count` | Integer | 触发次数 |
  243 +| `points` | Integer | 扣分(负数) |
  244 +
  245 +#### OrderStatsVO — 工单统计
  246 +
  247 +| 字段 | 类型 | 说明 |
  248 +|------|------|------|
  249 +| `totalOrders` | Long | 总工单数 |
  250 +| `completedOrders` | Long | 已完成工单数 |
  251 +| `completionRate` | BigDecimal | 完成率(%) |
  252 +| `onlineRate` | BigDecimal | 线上化率(%) |
  253 +| `responseWithin30Min` | Long | 30分钟内响应数 |
  254 +| `responseRate30Min` | BigDecimal | 30分钟响应率(%) |
  255 +| `overdueOrders` | Long | 超期工单数 |
  256 +| `lateSupplementOrders` | Long | 超24小时补录工单数 |
  257 +
  258 +#### AttendanceStatsVO — 考勤统计
  259 +
  260 +| 字段 | 类型 | 说明 |
  261 +|------|------|------|
  262 +| `totalCheckins` | Long | 总打卡次数 |
  263 +| `absentCount` | Long | 缺勤人次 |
  264 +| `missedCheckinCount` | Long | 未触发电子围栏打卡次数 |
  265 +| `trajectoryLowCount` | Long | 轨迹覆盖率不达标次数 |
  266 +| `missingCoHandlerCount` | Long | 缺共同处理人次数 |
  267 +| `missingCardCount` | Long | 未佩戴工卡次数 |
  268 +| `multiCardViolation` | Boolean | 是否存在一人多卡违规 |
  269 +| `leaderRecorderOffCount` | Long | 班组长记录仪未开机次数 |
  270 +
  271 +#### MaterialStatsVO — 耗材统计
  272 +
  273 +| 字段 | 类型 | 说明 |
  274 +|------|------|------|
  275 +| `ordersWithMaterial` | Long | 已上报耗材的工单数 |
  276 +| `ordersMissingMaterial` | Long | 缺耗材上报的工单数 |
  277 +| `totalMaterialItems` | Long | 耗材总用量 |
  278 +| `materialTypes` | Long | 耗材种类数 |
  279 +
  280 +#### MemberVO — 人员信息(第二层)
  281 +
  282 +| 字段 | 类型 | 说明 |
  283 +|------|------|------|
  284 +| `userId` | Long | 人员ID |
  285 +| `userName` | String | 姓名 |
  286 +| `mobile` | String | 手机号 |
  287 +| `postNames` | String | 岗位名称(逗号分隔) |
  288 +| `cardNumber` | String | 工卡号 |
  289 +| `isInner` | Integer | 是否内部人员(1=内部,2=外部) |
  290 +| `personalScore` | PersonalScoreVO | 个人考核指标 |
  291 +| `attendances` | Array\<AttendanceRecordVO\> | 考勤明细(**第三层**,`includeDetail=true` 时返回) |
  292 +| `orders` | Array\<OrderVO\> | 个人工单(**第三层**,`includeDetail=true` 时返回) |
  293 +
  294 +#### PersonalScoreVO — 个人考核指标
  295 +
  296 +| 字段 | 类型 | 说明 |
  297 +|------|------|------|
  298 +| `attendanceDays` | Long | 出勤天数 |
  299 +| `lateCount` | Long | 迟到次数 |
  300 +| `absentCount` | Long | 缺勤次数 |
  301 +| `assignedOrders` | Long | 承担工单数 |
  302 +| `completedOrders` | Long | 完成工单数 |
  303 +| `completionRate` | BigDecimal | 完成率(%) |
  304 +| `overdueOrders` | Long | 超期工单数 |
  305 +| `materialReported` | Long | 已上报耗材工单数 |
  306 +| `materialMissing` | Long | 缺耗材上报工单数 |
  307 +
  308 +#### OrderVO — 工单信息(第二层/第三层)
  309 +
  310 +| 字段 | 类型 | 说明 |
  311 +|------|------|------|
  312 +| `orderNo` | String | 工单号 |
  313 +| `orderName` | String | 工单名称 |
  314 +| `orderType` | String | 工单类型:Q=快速,C=普通,O=其他 |
  315 +| `workerName` | String | 接单人姓名 |
  316 +| `status` | Integer | 审批状态 |
  317 +| `buzStatus` | String | 业务状态(completed=已完成) |
  318 +| `commitDate` | String | 发布时间 |
  319 +| `finishDate` | String | 完成时间 |
  320 +| `expectedFinishDate` | String | 期望完成时间 |
  321 +| `responseMinutes` | Long | 响应时长(分钟) |
  322 +| `isOverdue` | Boolean | 是否超期(`finishDate > expectedFinishDate`) |
  323 +| `isLateSupplement` | Boolean | 是否超24小时补录 |
  324 +| `hasMaterialReported` | Boolean | 是否已上报耗材 |
  325 +| `coHandlers` | Array\<String\> | 共同处理人姓名 |
  326 +| `materials` | Array\<MaterialVO\> | 耗材明细(**第三层**,`includeDetail=true` 时返回) |
  327 +
  328 +#### 其他嵌套 VO
  329 +
  330 +**AttendanceRecordVO:**
  331 +
  332 +| 字段 | 类型 | 说明 |
  333 +|------|------|------|
  334 +| `punchType` | String | 打卡类型(上班/下班) |
  335 +| `punchTime` | String | 打卡时间 |
  336 +| `address` | String | 打卡地址 |
  337 +
  338 +**MaterialVO:**
  339 +
  340 +| 字段 | 类型 | 说明 |
  341 +|------|------|------|
  342 +| `materialName` | String | 耗材名称 |
  343 +| `specifications` | String | 规格 |
  344 +| `userCount` | Long | 使用数量 |
  345 +| `unitName` | String | 单位 |
  346 +| `classifyName` | String | 分类 |
  347 +
  348 +**PeriodVO:**
  349 +
  350 +| 字段 | 类型 | 说明 |
  351 +|------|------|------|
  352 +| `start` | String | 考核开始日期(yyyy-MM-dd) |
  353 +| `end` | String | 考核结束日期(yyyy-MM-dd) |
  354 +
  355 +### 4.5 请求示例
  356 +
  357 +```bash
  358 +# team 模式:按班组名称查询考核数据(仅汇总)
  359 +curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/team-assessment/query?keyword=养护一班&queryMode=team&startDate=2026-06-01&endDate=2026-06-30" \
  360 + -H "appId: my_app_001" \
  361 + -H "timestamp: 1749696000000" \
  362 + -H "nonce: c3d4e5f6g7h8i9j0" \
  363 + -H "sign: ..."
  364 +
  365 +# staff 模式:按人员姓名查询考核数据(含考勤明细和工单耗材)
  366 +curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/team-assessment/query?keyword=张三&queryMode=staff&includeDetail=true" \
  367 + -H "appId: my_app_001" \
  368 + -H "timestamp: 1749696000000" \
  369 + -H "nonce: d4e5f6g7h8i9j0k1" \
  370 + -H "sign: ..."
  371 +```
  372 +
  373 +### 4.6 响应示例(team 模式,includeDetail=false)
  374 +
  375 +```json
  376 +{
  377 + "code": 0,
  378 + "msg": "成功",
  379 + "data": {
  380 + "queryMode": "team",
  381 + "period": {
  382 + "start": "2026-06-01",
  383 + "end": "2026-06-30"
  384 + },
  385 + "teams": [
  386 + {
  387 + "teamId": 100,
  388 + "teamName": "养护一班",
  389 + "parentTeamName": "房山区园林所",
  390 + "leaderName": "张三",
  391 + "totalMembers": 15,
  392 + "score": {
  393 + "total": 85,
  394 + "orderScore": 34,
  395 + "attendanceScore": 25,
  396 + "materialScore": 26,
  397 + "deductions": [
  398 + { "rule": "工单超24小时补录", "count": 1, "points": -1 },
  399 + { "rule": "工单超时>2小时", "count": 2, "points": -2 },
  400 + { "rule": "缺勤/未打卡", "count": 5, "points": -5 },
  401 + { "rule": "人机材24h内未上报", "count": 1, "points": -2 }
  402 + ]
  403 + },
  404 + "orderStats": {
  405 + "totalOrders": 45,
  406 + "completedOrders": 42,
  407 + "completionRate": 93.3,
  408 + "onlineRate": 97.8,
  409 + "responseWithin30Min": 40,
  410 + "responseRate30Min": 88.9,
  411 + "overdueOrders": 2,
  412 + "lateSupplementOrders": 1
  413 + },
  414 + "attendanceStats": {
  415 + "totalCheckins": 570,
  416 + "absentCount": 5,
  417 + "missedCheckinCount": 5,
  418 + "trajectoryLowCount": 1,
  419 + "missingCoHandlerCount": 0,
  420 + "missingCardCount": 0,
  421 + "multiCardViolation": false,
  422 + "leaderRecorderOffCount": 0
  423 + },
  424 + "materialStats": {
  425 + "ordersWithMaterial": 42,
  426 + "ordersMissingMaterial": 1,
  427 + "totalMaterialItems": 230,
  428 + "materialTypes": 15
  429 + },
  430 + "members": [
  431 + {
  432 + "userId": 1001,
  433 + "userName": "张三",
  434 + "mobile": "13800138001",
  435 + "postNames": "班组长,技术员",
  436 + "cardNumber": "DEV001",
  437 + "isInner": 1,
  438 + "personalScore": {
  439 + "attendanceDays": 28,
  440 + "lateCount": 0,
  441 + "absentCount": 0,
  442 + "assignedOrders": 8,
  443 + "completedOrders": 8,
  444 + "completionRate": 100.0,
  445 + "overdueOrders": 0,
  446 + "materialReported": 8,
  447 + "materialMissing": 0
  448 + }
  449 + }
  450 + ],
  451 + "orders": [
  452 + {
  453 + "orderNo": "WO202606250001",
  454 + "orderName": "树木修剪",
  455 + "orderType": "C",
  456 + "workerName": "张三",
  457 + "status": 1,
  458 + "buzStatus": "completed",
  459 + "commitDate": "2026-06-25 09:00:00",
  460 + "finishDate": "2026-06-25 16:30:00",
  461 + "expectedFinishDate": "2026-06-25 18:00:00",
  462 + "responseMinutes": 15,
  463 + "isOverdue": false,
  464 + "isLateSupplement": false,
  465 + "hasMaterialReported": true,
  466 + "coHandlers": ["李四"]
  467 + }
  468 + ]
  469 + }
  470 + ]
  471 + }
  472 +}
  473 +```
  474 +
  475 +### 4.7 响应示例(staff 模式,includeDetail=true)
  476 +
  477 +```json
  478 +{
  479 + "code": 0,
  480 + "msg": "成功",
  481 + "data": {
  482 + "queryMode": "staff",
  483 + "period": {
  484 + "start": "2026-06-01",
  485 + "end": "2026-06-25"
  486 + },
  487 + "staffList": [
  488 + {
  489 + "userId": 1001,
  490 + "userName": "张三",
  491 + "mobile": "13800138001",
  492 + "deptId": 100,
  493 + "deptName": "养护一班",
  494 + "postNames": "班组长",
  495 + "cardNumber": "DEV001",
  496 + "isInner": 1,
  497 + "assessment": {
  498 + "attendanceDays": 28,
  499 + "lateCount": 0,
  500 + "absentCount": 0,
  501 + "assignedOrders": 8,
  502 + "completedOrders": 8,
  503 + "completionRate": 100.0,
  504 + "overdueOrders": 0,
  505 + "materialReported": 8,
  506 + "materialMissing": 0
  507 + },
  508 + "attendances": [
  509 + { "punchType": "上班", "punchTime": "2026-06-25 08:00:00", "address": "北京市房山区xx路" },
  510 + { "punchType": "下班", "punchTime": "2026-06-25 17:30:00", "address": "北京市房山区xx路" }
  511 + ],
  512 + "orders": [
  513 + {
  514 + "orderNo": "WO202606250001",
  515 + "orderName": "树木修剪",
  516 + "orderType": "C",
  517 + "workerName": "张三",
  518 + "status": 1,
  519 + "buzStatus": "completed",
  520 + "commitDate": "2026-06-25 09:00:00",
  521 + "finishDate": "2026-06-25 16:30:00",
  522 + "expectedFinishDate": "2026-06-25 18:00:00",
  523 + "responseMinutes": 15,
  524 + "isOverdue": false,
  525 + "isLateSupplement": false,
  526 + "hasMaterialReported": true,
  527 + "coHandlers": ["李四"],
  528 + "materials": [
  529 + {
  530 + "materialName": "高效氯氰菊酯",
  531 + "specifications": "500ml/瓶",
  532 + "userCount": 2,
  533 + "unitName": "瓶",
  534 + "classifyName": "药品"
  535 + }
  536 + ]
  537 + }
  538 + ]
  539 + }
  540 + ]
  541 + }
  542 +}
  543 +```
  544 +
  545 +### 4.8 使用建议(Co-ClawCo-Claw)
  546 +
  547 +| 场景 | 推荐参数 | 说明 |
  548 +|------|----------|------|
  549 +| 月底考核排名 | `keyword=养护&queryMode=team` | 模糊查询所有养护班组,返回考核得分用于排名 |
  550 +| 单班组详情分析 | `keyword=养护一班&queryMode=team&includeDetail=true` | 查看特定班组全部明细,分析扣分原因 |
  551 +| 个人绩效查询 | `keyword=张三&queryMode=staff&includeDetail=true` | 查看某人出勤、工单、耗材完整记录 |
  552 +| 日常快速巡检 | `keyword=养护&includeDetail=false` | 只看汇总指标,Token 最少 |
  553 +
  554 +---
  555 +
  556 +## 5. 接口 2:人员查询
  557 +
  558 +### 4.1 基本信息
  559 +
  560 +| 项目 | 说明 |
  561 +|------|------|
  562 +| 路径 | `/open-api/staff/query` |
  563 +| 方法 | `GET` |
  564 +| 说明 | 获取人员基础档案信息,支持按部门、姓名、手机号等条件筛选 |
  565 +
  566 +### 4.2 请求参数
  567 +
  568 +| 参数 | 类型 | 必填 | 说明 |
  569 +|------|------|------|------|
  570 +| `name` | String | 否 | 姓名,模糊匹配 |
  571 +| `mobile` | String | 否 | 手机号,模糊匹配 |
  572 +| `deptId` | Long | 否 | 部门ID,精确匹配 |
  573 +| `status` | Integer | 否 | 状态:0=启用,1=禁用 |
  574 +| `busiLine` | String | 否 | 业务线:yl=园林,wy=物业,sz=市政 |
  575 +
  576 +### 4.3 响应字段
  577 +
  578 +| 字段 | 类型 | 说明 |
  579 +|------|------|------|
  580 +| `id` | Long | 人员ID |
  581 +| `username` | String | 用户名 |
  582 +| `nickname` | String | 姓名 |
  583 +| `mobile` | String | 手机号 |
  584 +| `deptId` | Long | 部门ID |
  585 +| `deptName` | String | 部门名称(如"养护一班") |
  586 +| `postIds` | Array\<Long\> | 岗位ID列表 |
  587 +| `postNames` | String | 岗位名称(逗号分隔,如"班组长,技术员") |
  588 +| `busiLine` | String | 业务线 |
  589 +| `status` | Integer | 状态(0=启用,1=禁用) |
  590 +| `cardNumber` | String | 工卡号(设备编号,用于考勤打卡) |
  591 +| `isInner` | Integer | 是否内部人员(1=内部,2=外部) |
  592 +| `createTime` | String | 入职日期(格式:yyyy-MM-dd HH:mm:ss) |
  593 +
  594 +### 4.4 请求示例
  595 +
  596 +```bash
  597 +curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/staff/query?deptId=100" \
  598 + -H "appId: my_app_001" \
  599 + -H "timestamp: 1749696000000" \
  600 + -H "nonce: a1b2c3d4e5f6g7h8" \
  601 + -H "sign: 3a5f8c9e2b1d4f6a8c0e2b4d6f8a0c2e4b6d8f0a"
  602 +```
  603 +
  604 +### 4.5 响应示例
  605 +
  606 +```json
  607 +{
  608 + "code": 0,
  609 + "msg": "成功",
  610 + "data": [
  611 + {
  612 + "id": 1001,
  613 + "username": "zhangsan",
  614 + "nickname": "张三",
  615 + "mobile": "13800138001",
  616 + "deptId": 100,
  617 + "deptName": "养护一班",
  618 + "postIds": [10, 20],
  619 + "postNames": "班组长,技术员",
  620 + "busiLine": "yl",
  621 + "status": 0,
  622 + "cardNumber": "DEV001",
  623 + "isInner": 1,
  624 + "createTime": "2024-01-15 00:00:00"
  625 + }
  626 + ],
  627 + "total": 150
  628 + }
  629 +```
  630 +
  631 +---
  632 +
  633 +## 6. 接口 3:班组查询
  634 +
  635 +### 5.1 基本信息
  636 +
  637 +| 项目 | 说明 |
  638 +|------|------|
  639 +| 路径 | `/open-api/teams/query` |
  640 +| 方法 | `GET` |
  641 +| 说明 | 获取组织架构班组信息,含班组长、所属养护所、人员数量 |
  642 +
  643 +### 5.2 请求参数
  644 +
  645 +| 参数 | 类型 | 必填 | 说明 |
  646 +|------|------|------|------|
  647 +| `name` | String | 否 | 班组名称,模糊匹配 |
  648 +| `parentId` | Long | 否 | 父部门ID(上级组织),精确匹配 |
  649 +| `status` | Integer | 否 | 状态:0=启用,1=禁用 |
  650 +
  651 +### 5.3 响应字段
  652 +
  653 +| 字段 | 类型 | 说明 |
  654 +|------|------|------|
  655 +| `id` | Long | 班组ID |
  656 +| `name` | String | 班组名称 |
  657 +| `parentId` | Long | 父部门ID |
  658 +| `parentName` | String | 父部门名称(所属养护所) |
  659 +| `leaderUserId` | Long | 班组长人员ID(Staff.id) |
  660 +| `leaderUserName` | String | 班组长姓名 |
  661 +| `memberCount` | Long | 人员数量 |
  662 +| `sort` | Integer | 排序 |
  663 +| `status` | Integer | 状态(0=启用,1=禁用) |
  664 +
  665 +### 5.4 请求示例
  666 +
  667 +```bash
  668 +curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/teams/query" \
  669 + -H "appId: my_app_001" \
  670 + -H "timestamp: 1749696000000" \
  671 + -H "nonce: b2c3d4e5f6g7h8i9" \
  672 + -H "sign: ..."
  673 +```
  674 +
  675 +### 5.5 响应示例
  676 +
  677 +```json
  678 +{
  679 + "code": 0,
  680 + "msg": "成功",
  681 + "data": [
  682 + {
  683 + "id": 100,
  684 + "name": "养护一班",
  685 + "parentId": 1,
  686 + "parentName": "蓟城山水集团",
  687 + "leaderUserId": 1001,
  688 + "leaderUserName": "张三",
  689 + "memberCount": 15,
  690 + "sort": 1,
  691 + "status": 0
  692 + }
  693 + ],
  694 + "total": 12
  695 + }
  696 +}
  697 +```
  698 +
  699 +---
  700 +
  701 +## 7. 接口 4:考勤查询
  702 +
  703 +### 6.1 基本信息
  704 +
  705 +| 项目 | 说明 |
  706 +|------|------|
  707 +| 路径 | `/open-api/attendance/query` |
  708 +| 方法 | `GET` |
  709 +| 说明 | 获取电子围栏打卡与位置数据,支持按班组、人员、时间范围筛选 |
  710 +
  711 +### 6.2 请求参数
  712 +
  713 +| 参数 | 类型 | 必填 | 说明 |
  714 +|------|------|------|------|
  715 +| `userId` | Long | 否 | 人员ID,精确匹配 |
  716 +| `deptId` | Long | 否 | 部门ID(班组),精确匹配 |
  717 +| `busiLine` | String | 否 | 业务线 |
  718 +| `punchType` | String | 否 | 打卡类型(上班/下班) |
  719 +| `startTime` | String | 否 | 开始时间(格式:yyyy-MM-dd HH:mm:ss) |
  720 +| `endTime` | String | 否 | 结束时间(格式:yyyy-MM-dd HH:mm:ss) |
  721 +
  722 +### 6.3 响应字段
  723 +
  724 +| 字段 | 类型 | 说明 |
  725 +|------|------|------|
  726 +| `id` | Integer | 记录ID |
  727 +| `deviceCode` | String | 设备编码 |
  728 +| `deviceName` | String | 设备名称 |
  729 +| `punchType` | String | 打卡类型 |
  730 +| `userId` | Long | 人员ID |
  731 +| `nickname` | String | 人员姓名 |
  732 +| `deptId` | Long | 部门ID |
  733 +| `deptName` | String | 部门名称 |
  734 +| `address` | String | 打卡地址 |
  735 +| `latGcj02` | BigDecimal | 高德纬度(GCJ-02坐标系) |
  736 +| `lngGcj02` | BigDecimal | 高德经度(GCJ-02坐标系) |
  737 +| `lat` | BigDecimal | WGS84纬度 |
  738 +| `lng` | BigDecimal | WGS84经度 |
  739 +| `createTime` | String | 打卡时间 |
  740 +
  741 +> **注意**:当前版本暂不支持"轨迹点集""是否佩戴工卡""记录仪状态"字段(数据库无对应存储)。待业务系统确认后将补充。
  742 +
  743 +### 6.4 响应示例
  744 +
  745 +```json
  746 +{
  747 + "code": 0,
  748 + "msg": "成功",
  749 + "data": [
  750 + {
  751 + "id": 1,
  752 + "deviceCode": "DEV001",
  753 + "deviceName": "打卡机01",
  754 + "punchType": "上班",
  755 + "userId": 1001,
  756 + "nickname": "张三",
  757 + "deptId": 100,
  758 + "deptName": "养护一班",
  759 + "address": "北京市西城区xx路xx号",
  760 + "latGcj02": 39.9042,
  761 + "lngGcj02": 116.4074,
  762 + "lat": 39.9010,
  763 + "lng": 116.4030,
  764 + "createTime": "2026-06-11 08:30:00"
  765 + }
  766 + ],
  767 + "total": 320
  768 + }
  769 +}
  770 +```
  771 +
  772 +---
  773 +
  774 +## 8. 接口 5:工单查询
  775 +
  776 +### 7.1 基本信息
  777 +
  778 +| 项目 | 说明 |
  779 +|------|------|
  780 +| 路径 | `/open-api/tasks/query` |
  781 +| 方法 | `GET` |
  782 +| 说明 | 获取工单全生命周期数据,含发布时间、状态、接单渠道、共同处理人 |
  783 +
  784 +### 7.2 请求参数
  785 +
  786 +| 参数 | 类型 | 必填 | 说明 |
  787 +|------|------|------|------|
  788 +| `orderNo` | String | 否 | 工单号,模糊匹配 |
  789 +| `orderName` | String | 否 | 工单名称,模糊匹配 |
  790 +| `busiLine` | String | 否 | 业务线 |
  791 +| `status` | Integer | 否 | 审批状态 |
  792 +| `buzStatus` | String | 否 | 业务状态 |
  793 +| `workerId` | Long | 否 | 接单人ID |
  794 +| `startDate` | String | 否 | 发布时间起始(yyyy-MM-dd HH:mm:ss) |
  795 +| `endDate` | String | 否 | 发布时间截止(yyyy-MM-dd HH:mm:ss) |
  796 +
  797 +### 7.3 响应字段
  798 +
  799 +| 字段 | 类型 | 说明 |
  800 +|------|------|------|
  801 +| `id` | Long | 工单ID |
  802 +| `orderNo` | String | 工单号 |
  803 +| `orderName` | String | 工单名称 |
  804 +| `orderType` | String | 工单类型:Q=快速,C=普通,O=其他 |
  805 +| `busiLine` | String | 业务线 |
  806 +| `commitDate` | String | 发布时间(工单派发时间) |
  807 +| `finishDate` | String | 完成时间 |
  808 +| `userId` | Long | 发布人ID |
  809 +| `userName` | String | 发布人姓名 |
  810 +| `workerId` | Long | 接单人ID |
  811 +| `workerName` | String | 接单人姓名 |
  812 +| `coHandlers` | Array\<String\> | 共同处理人姓名列表 |
  813 +| `woSourceName` | String | 接单渠道(小程序/人工派单等) |
  814 +| `status` | Integer | 审批状态 |
  815 +| `buzStatus` | String | 业务状态 |
  816 +| `remark` | String | 备注 |
  817 +| `photos` | Array\<PhotoVO\> | 工单附件照片 |
  818 +
  819 +**PhotoVO 结构:**
  820 +
  821 +| 字段 | 类型 | 说明 |
  822 +|------|------|------|
  823 +| `busiType` | String | 业务类型:`01`=问题照片,`02`=处理中,`03`=完成 |
  824 +| `fileName` | String | 文件名 |
  825 +| `url` | String | 文件URL |
  826 +
  827 +### 7.4 响应示例
  828 +
  829 +```json
  830 +{
  831 + "code": 0,
  832 + "msg": "成功",
  833 + "data": [
  834 + {
  835 + "id": 5001,
  836 + "orderNo": "WO202606110001",
  837 + "orderName": "树木修剪",
  838 + "orderType": "C",
  839 + "busiLine": "yl",
  840 + "commitDate": "2026-06-11 09:00:00",
  841 + "finishDate": null,
  842 + "userId": 1,
  843 + "userName": "管理员",
  844 + "workerId": 1001,
  845 + "workerName": "张三",
  846 + "coHandlers": ["李四", "王五"],
  847 + "woSourceName": "小程序",
  848 + "status": 1,
  849 + "buzStatus": "processing",
  850 + "remark": "紧急处理",
  851 + "photos": [
  852 + {
  853 + "busiType": "01",
  854 + "fileName": "problem.jpg",
  855 + "url": "https://xxx.com/file/abc.jpg"
  856 + }
  857 + ]
  858 + }
  859 + ],
  860 + "total": 85
  861 + }
  862 +}
  863 +```
  864 +
  865 +---
  866 +
  867 +## 9. 接口 6:物料查询
  868 +
  869 +### 8.1 基本信息
  870 +
  871 +| 项目 | 说明 |
  872 +|------|------|
  873 +| 路径 | `/open-api/materials/query` |
  874 +| 方法 | `GET` |
  875 +| 说明 | 获取物料消耗记录与人机材照片数据,支持按工单筛选 |
  876 +
  877 +### 8.2 请求参数
  878 +
  879 +| 参数 | 类型 | 必填 | 说明 |
  880 +|------|------|------|------|
  881 +| `orderNo` | String | 否 | 工单号,精确匹配 |
  882 +| `classifyName` | String | 否 | 物料分类名,模糊匹配 |
  883 +| `typeName` | String | 否 | 物料类型名,模糊匹配 |
  884 +| `userId` | Long | 否 | 使用人ID |
  885 +| `startDate` | String | 否 | 开始时间(yyyy-MM-dd HH:mm:ss) |
  886 +| `endDate` | String | 否 | 结束时间(yyyy-MM-dd HH:mm:ss) |
  887 +
  888 +### 8.3 响应字段
  889 +
  890 +| 字段 | 类型 | 说明 |
  891 +|------|------|------|
  892 +| `id` | Long | 记录ID |
  893 +| `orderNo` | String | 关联工单号 |
  894 +| `materialName` | String | 物料名称 |
  895 +| `specifications` | String | 规格 |
  896 +| `userCount` | Long | 使用数量 |
  897 +| `unitName` | String | 单位 |
  898 +| `classifyName` | String | 物料分类 |
  899 +| `typeName` | String | 物料类型 |
  900 +| `typeDetailName` | String | 物料明细类型 |
  901 +| `userId` | Long | 使用人ID |
  902 +| `userName` | String | 使用人姓名 |
  903 +| `source` | Integer | 来源:1=工单,2=巡检,3=养护 |
  904 +| `createTime` | String | 上传时间 |
  905 +| `photos` | Array\<PhotoVO\> | 人机材照片(结构同工单接口) |
  906 +
  907 +### 8.4 响应示例
  908 +
  909 +```json
  910 +{
  911 + "code": 0,
  912 + "msg": "成功",
  913 + "data": [
  914 + {
  915 + "id": 3001,
  916 + "orderNo": "WO202606110001",
  917 + "materialName": "高效氯氰菊酯",
  918 + "specifications": "500ml/瓶",
  919 + "userCount": 5,
  920 + "unitName": "瓶",
  921 + "classifyName": "药品",
  922 + "typeName": "杀虫剂",
  923 + "typeDetailName": "有机磷",
  924 + "userId": 1001,
  925 + "userName": "张三",
  926 + "source": 1,
  927 + "createTime": "2026-06-11 14:30:00",
  928 + "photos": [
  929 + {
  930 + "busiType": "02",
  931 + "fileName": "material.jpg",
  932 + "url": "https://xxx.com/file/def.jpg"
  933 + }
  934 + ]
  935 + }
  936 + ],
  937 + "total": 45
  938 + }
  939 +}
  940 +```
  941 +
  942 +---
  943 +
  944 +## 10. 接口 7:全量工单统计
  945 +
  946 +### 9.1 基本信息
  947 +
  948 +| 项目 | 说明 |
  949 +|------|------|
  950 +| 路径 | `/open-api/full-order-statistics/query` |
  951 +| 方法 | `GET` |
  952 +| 说明 | 获取各公司/班组在一段时间内实际完成的工单数量、巡查记录数量、养护记录数量 |
  953 +
  954 +### 9.2 请求参数
  955 +
  956 +| 参数 | 类型 | 必填 | 说明 |
  957 +|------|------|------|------|
  958 +| `companyId` | Long | 否 | 公司/班组ID,精确匹配 |
  959 +| `companyName` | String | 否 | 公司/班组名称,模糊匹配 |
  960 +| `type` | Integer | 否 | 类型:1=按公司,2=按班组 |
  961 +| `startDate` | String | 否 | 统计开始日期(格式:yyyy-MM-dd,返回开始日期>=该值的记录) |
  962 +| `endDate` | String | 否 | 统计结束日期(格式:yyyy-MM-dd,返回结束日期<=该值的记录) |
  963 +
  964 +### 9.3 响应字段
  965 +
  966 +| 字段 | 类型 | 说明 |
  967 +|------|------|------|
  968 +| `id` | Long | 记录ID |
  969 +| `companyId` | Long | 公司/班组ID |
  970 +| `companyName` | String | 公司/班组名称 |
  971 +| `type` | Integer | 类型:1=按公司,2=按班组 |
  972 +| `startDate` | String | 统计开始日期 |
  973 +| `endDate` | String | 统计结束日期 |
  974 +| `workOrderCount` | Integer | 工单完成数量 |
  975 +| `inspectionCount` | Integer | 巡查记录数量 |
  976 +| `maintenanceCount` | Integer | 养护记录数量 |
  977 +| `remark` | String | 备注 |
  978 +| `createTime` | String | 创建时间 |
  979 +
  980 +### 9.4 请求示例
  981 +
  982 +```bash
  983 +curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/full-order-statistics/query?type=1&startDate=2026-06-01&endDate=2026-06-30" \
  984 + -H "appId: my_app_001" \
  985 + -H "timestamp: 1749696000000" \
  986 + -H "nonce: f6g7h8i9j0k1l2m3" \
  987 + -H "sign: ..."
  988 +```
  989 +
  990 +### 9.5 响应示例
  991 +
  992 +```json
  993 +{
  994 + "code": 0,
  995 + "msg": "成功",
  996 + "data": [
  997 + {
  998 + "id": 1,
  999 + "companyId": 100,
  1000 + "companyName": "养护一班",
  1001 + "type": 1,
  1002 + "startDate": "2026-06-01",
  1003 + "endDate": "2026-06-30",
  1004 + "workOrderCount": 85,
  1005 + "inspectionCount": 120,
  1006 + "maintenanceCount": 60,
  1007 + "remark": "二季度统计",
  1008 + "createTime": "2026-06-11 10:00:00"
  1009 + }
  1010 + ]
  1011 +}
  1012 +```
  1013 +
  1014 +---
  1015 +
  1016 +## 11. 错误码说明
  1017 +
  1018 +### 11.1 签名相关错误
  1019 +
  1020 +| code | msg | 说明 |
  1021 +|------|-----|------|
  1022 +| 400 | 签名不正确 | 签名验证失败(appId不存在/timestamp超时/nonce长度不足/签名不匹配) |
  1023 +| 900 | 存在重复请求 | nonce 已被使用(重放攻击检测) |
  1024 +
  1025 +### 11.2 业务接口错误
  1026 +
  1027 +| code | msg | 说明 |
  1028 +|------|-----|------|
  1029 +| 1-100-020-001 | 人员查询失败 | 人员查询内部错误 |
  1030 +| 1-100-020-002 | 班组查询失败 | 班组查询内部错误 |
  1031 +| 1-100-020-003 | 考勤查询失败 | 考勤查询内部错误 |
  1032 +| 1-100-020-004 | 工单查询失败 | 工单查询内部错误 |
  1033 +| 1-100-020-005 | 物料查询失败 | 物料查询内部错误 |
  1034 +| 1-100-020-006 | 团队考核查询失败 | 团队考核聚合查询内部错误 |
  1035 +| 1-900-005-001 | 全量工单统计不存在 | 记录不存在 |
  1036 +| 1-900-005-002 | 相同公司/班组同时间段统计已存在 | 重复数据校验 |
  1037 +
  1038 +### 11.3 通用错误
  1039 +
  1040 +| code | msg | 说明 |
  1041 +|------|-----|------|
  1042 +| 400 | 参数校验失败 | 请求参数格式或值不正确 |
  1043 +| 500 | 系统内部错误 | 服务器内部异常 |
  1044 +
  1045 +---
  1046 +
  1047 +## 12. 附录:客户端签名代码示例
  1048 +
  1049 +### 12.1 Java 示例
  1050 +
  1051 +```java
  1052 +import cn.hutool.crypto.digest.DigestUtil;
  1053 +import cn.hutool.http.HttpRequest;
  1054 +import cn.hutool.http.HttpResponse;
  1055 +import cn.hutool.core.util.RandomUtil;
  1056 +
  1057 +import java.util.Map;
  1058 +import java.util.TreeMap;
  1059 +
  1060 +public class OpenApiClient {
  1061 +
  1062 + private static final String BASE_URL = "https://test.jichengshanshui.com.cn:28302";
  1063 + private static final String APP_ID = "your_app_id";
  1064 + private static final String APP_SECRET = "your_app_secret";
  1065 +
  1066 + public static String call(String path, Map<String, Object> params) {
  1067 + String timestamp = String.valueOf(System.currentTimeMillis());
  1068 + String nonce = RandomUtil.randomString(16);
  1069 + String sign = buildSign(params, timestamp, nonce);
  1070 +
  1071 + String url = BASE_URL + path;
  1072 + HttpResponse response = HttpRequest.get(url)
  1073 + .form(params)
  1074 + .header("appId", APP_ID)
  1075 + .header("timestamp", timestamp)
  1076 + .header("nonce", nonce)
  1077 + .header("sign", sign)
  1078 + .execute();
  1079 + return response.body();
  1080 + }
  1081 +
  1082 + private static String buildSign(Map<String, Object> params,
  1083 + String timestamp, String nonce) {
  1084 + StringBuilder sb = new StringBuilder();
  1085 +
  1086 + // 1. 排序 query 参数值
  1087 + if (params != null && !params.isEmpty()) {
  1088 + new TreeMap<>(params).values().stream()
  1089 + .map(String::valueOf).forEach(sb::append);
  1090 + }
  1091 +
  1092 + // 2. header 参数值(appId, nonce, timestamp 字母序)
  1093 + sb.append(APP_ID);
  1094 + sb.append(nonce);
  1095 + sb.append(timestamp);
  1096 +
  1097 + // 3. 追加 appSecret + SHA256
  1098 + sb.append(APP_SECRET);
  1099 + return DigestUtil.sha256Hex(sb.toString());
  1100 + }
  1101 +}
  1102 +```
  1103 +
  1104 +### 12.2 Python 示例
  1105 +
  1106 +```python
  1107 +import hashlib
  1108 +import time
  1109 +import random
  1110 +import string
  1111 +import requests
  1112 +
  1113 +BASE_URL = "https://test.jichengshanshui.com.cn:28302"
  1114 +APP_ID = "your_app_id"
  1115 +APP_SECRET = "your_app_secret"
  1116 +
  1117 +def build_sign(params, timestamp, nonce):
  1118 + parts = []
  1119 + if params:
  1120 + for key in sorted(params.keys()):
  1121 + parts.append(str(params[key]))
  1122 + parts.append(APP_ID)
  1123 + parts.append(nonce)
  1124 + parts.append(timestamp)
  1125 + parts.append(APP_SECRET)
  1126 + sign_str = "".join(parts)
  1127 + return hashlib.sha256(sign_str.encode()).hexdigest()
  1128 +
  1129 +def call(path, params=None):
  1130 + if params is None:
  1131 + params = {}
  1132 + timestamp = str(int(time.time() * 1000))
  1133 + nonce = "".join(random.choices(string.ascii_lowercase + string.digits, k=16))
  1134 + sign = build_sign(params, timestamp, nonce)
  1135 + headers = {
  1136 + "appId": APP_ID,
  1137 + "timestamp": timestamp,
  1138 + "nonce": nonce,
  1139 + "sign": sign
  1140 + }
  1141 + response = requests.get(BASE_URL + path, params=params, headers=headers)
  1142 + return response.json()
  1143 +```
  1144 +
  1145 +### 12.3 Shell (cURL) 示例
  1146 +
  1147 +```bash
  1148 +#!/bin/bash
  1149 +APP_ID="your_app_id"
  1150 +APP_SECRET="your_app_secret"
  1151 +TIMESTAMP=$(date +%s%3N)
  1152 +NONCE=$(openssl rand -hex 16)
  1153 +
  1154 +# 示例:/open-api/staff/query?deptId=100
  1155 +# query params 排序后 values: deptId=100 → "100"
  1156 +QUERY_VALUES="100"
  1157 +HEADER_VALUES="${APP_ID}${NONCE}${TIMESTAMP}"
  1158 +SIGN_STR="${QUERY_VALUES}${HEADER_VALUES}${APP_SECRET}"
  1159 +SIGN=$(echo -n "$SIGN_STR" | openssl dgst -sha256 -hex | awk '{print $2}')
  1160 +
  1161 +curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/staff/query?deptId=100" \
  1162 + -H "appId: ${APP_ID}" \
  1163 + -H "timestamp: ${TIMESTAMP}" \
  1164 + -H "nonce: ${NONCE}" \
  1165 + -H "sign: ${SIGN}"
  1166 +```
  1167 +
  1168 +---
  1169 +
  1170 +> **本文档基于 2026-06-11 代码生成,如有接口变更请同步更新。**
docs/开放API对接接口文档.md
1 # UrbanOps 开放 API 对接接口文档 1 # UrbanOps 开放 API 对接接口文档
2 2
3 -> **版本**: v1.0  
4 -> **更新日期**: 2026-06-11  
5 -> **适用对象**: 第三方系统对接开发人员 3 +> **版本**: v1.1
  4 +> **更新日期**: 2026-06-25
  5 +> **适用对象**: 第三方系统对接开发人员 / Co-Claw AI 助手
6 > **鉴权方式**: HMAC-SHA256 API 签名 6 > **鉴权方式**: HMAC-SHA256 API 签名
7 7
8 --- 8 ---
@@ -12,14 +12,15 @@ @@ -12,14 +12,15 @@
12 1. [概述](#1-概述) 12 1. [概述](#1-概述)
13 2. [签名鉴权机制](#2-签名鉴权机制) 13 2. [签名鉴权机制](#2-签名鉴权机制)
14 3. [通用规范](#3-通用规范) 14 3. [通用规范](#3-通用规范)
15 -4. [接口 1:人员查询](#4-接口-1人员查询)  
16 -5. [接口 2:班组查询](#5-接口-2班组查询)  
17 -6. [接口 3:考勤查询](#6-接口-3考勤查询)  
18 -7. [接口 4:工单查询](#7-接口-4工单查询)  
19 -8. [接口 5:物料查询](#8-接口-5物料查询)  
20 -9. [接口 6:全量工单统计](#9-接口-6全量工单统计)  
21 -10. [错误码说明](#10-错误码说明)  
22 -11. [附录:客户端签名代码示例](#11-附录客户端签名代码示例) 15 +4. [接口 1:团队考核查询(Co-Claw AI 专用)](#4-接口-1团队考核查询Co-Claw-ai-专用)
  16 +5. [接口 2:人员查询](#5-接口-2人员查询)
  17 +6. [接口 3:班组查询](#6-接口-3班组查询)
  18 +7. [接口 4:考勤查询](#7-接口-4考勤查询)
  19 +8. [接口 5:工单查询](#8-接口-5工单查询)
  20 +9. [接口 6:物料查询](#9-接口-6物料查询)
  21 +10. [接口 7:全量工单统计](#10-接口-7全量工单统计)
  22 +11. [错误码说明](#11-错误码说明)
  23 +12. [附录:客户端签名代码示例](#12-附录客户端签名代码示例)
23 24
24 --- 25 ---
25 26
@@ -39,12 +40,13 @@ @@ -39,12 +40,13 @@
39 40
40 | # | 接口名称 | 路径 | 方法 | 说明 | 41 | # | 接口名称 | 路径 | 方法 | 说明 |
41 |---|---------|------|------|------| 42 |---|---------|------|------|------|
42 -| 1 | 人员查询 | `/open-api/staff/query` | GET | 获取人员档案,支持按部门筛选 |  
43 -| 2 | 班组查询 | `/open-api/teams/query` | GET | 获取班组信息,含班组长和人员数量 |  
44 -| 3 | 考勤查询 | `/open-api/attendance/query` | GET | 获取考勤打卡数据 |  
45 -| 4 | 工单查询 | `/open-api/tasks/query` | GET | 获取工单全生命周期数据 |  
46 -| 5 | 物料查询 | `/open-api/materials/query` | GET | 获取物料消耗与照片数据 |  
47 -| 6 | 全量工单统计 | `/open-api/full-order-statistics/query` | GET | 获取公司/班组工单/巡查/养护完成数量 | 43 +| 1 | **团队考核查询** ⭐ | `/open-api/team-assessment/query` | GET | **Co-Claw AI 专用**:班组/人员考核聚合接口,含考勤/工单/耗材三维评分 |
  44 +| 2 | 人员查询 | `/open-api/staff/query` | GET | 获取人员档案,支持按部门筛选 |
  45 +| 3 | 班组查询 | `/open-api/teams/query` | GET | 获取班组信息,含班组长和人员数量 |
  46 +| 4 | 考勤查询 | `/open-api/attendance/query` | GET | 获取考勤打卡数据 |
  47 +| 5 | 工单查询 | `/open-api/tasks/query` | GET | 获取工单全生命周期数据 |
  48 +| 6 | 物料查询 | `/open-api/materials/query` | GET | 获取物料消耗与照片数据 |
  49 +| 7 | 全量工单统计 | `/open-api/full-order-statistics/query` | GET | 获取公司/班组工单/巡查/养护完成数量 |
48 50
49 ### 1.3 接入前提 51 ### 1.3 接入前提
50 52
@@ -161,7 +163,397 @@ sign = SHA256(signStr) @@ -161,7 +163,397 @@ sign = SHA256(signStr)
161 163
162 --- 164 ---
163 165
164 -## 4. 接口 1:人员查询 166 +## 4. 接口 1:团队考核查询(Co-Claw AI 专用)⭐
  167 +
  168 +> **设计定位**:本接口专为 Co-Claw AI 助手设计,提供班组/人员考核数据的**一站式聚合查询**。
  169 +> 接口内部完成考勤、工单、耗材三维度评分计算,Co-Claw 可直接使用返回的得分和扣分明细进行考核分析,
  170 +> 无需自行计算。默认只返回汇总指标(`includeDetail=false`),Token 友好。
  171 +
  172 +### 4.1 考核背景
  173 +
  174 +本接口基于《房山区园林所养护队智慧化管理制度》设计,考核满分 100 分,三大维度:
  175 +
  176 +| 维度 | 满分 | 主要扣分规则 |
  177 +|------|------|-------------|
  178 +| 工单上线率与及时率 | **40分** | 超24h补录 -1分/次 · 超时>2h -1分/次 |
  179 +| 人员考勤管理 | **30分** | 缺勤/未打卡 -1分/次 · 缺共同处理人 -1分/次 · 未佩戴工卡 -1分/次 · 班组长记录仪未开 -1分/次 |
  180 +| 人机材录入 | **30分** | 工单完成24h内未上报耗材 -2分/次 |
  181 +
  182 +### 4.2 基本信息
  183 +
  184 +| 项目 | 说明 |
  185 +|------|------|
  186 +| 路径 | `/open-api/team-assessment/query` |
  187 +| 方法 | `GET` |
  188 +| 说明 | 团队考核聚合查询。支持 **team 模式**(班组→人员+工单→考勤+耗材,三层结构)和 **staff 模式**(人员→考勤+工单→耗材) |
  189 +
  190 +### 4.3 请求参数
  191 +
  192 +| 参数 | 类型 | 必填 | 默认值 | 说明 |
  193 +|------|------|------|--------|------|
  194 +| `keyword` | String | **是** | — | 查询关键字:班组名称或人员姓名,**支持模糊匹配** |
  195 +| `queryMode` | String | 否 | `team` | 查询模式:`team` 按班组查询 / `staff` 按人员查询 |
  196 +| `startDate` | String | 否 | 当月1日 | 考核开始日期(格式:yyyy-MM-dd) |
  197 +| `endDate` | String | 否 | 今天 | 考核结束日期(格式:yyyy-MM-dd) |
  198 +| `includeDetail` | Boolean | 否 | `false` | 是否返回明细数据。`false` 仅返回汇总指标(推荐),`true` 同时返回考勤明细和工单耗材明细 |
  199 +
  200 +### 4.4 响应字段(queryMode=team)
  201 +
  202 +#### 顶层结构
  203 +
  204 +| 字段 | 类型 | 说明 |
  205 +|------|------|------|
  206 +| `queryMode` | String | 查询模式(`team` 或 `staff`) |
  207 +| `period` | PeriodVO | 考核周期 |
  208 +| `teams` | Array\<TeamVO\> | 班组考核列表(team 模式) |
  209 +| `staffList` | Array\<StaffVO\> | 人员考核列表(staff 模式,与 teams 互斥) |
  210 +
  211 +#### TeamVO — 班组考核(第一层)
  212 +
  213 +| 字段 | 类型 | 说明 |
  214 +|------|------|------|
  215 +| `teamId` | Long | 班组ID |
  216 +| `teamName` | String | 班组名称 |
  217 +| `parentTeamName` | String | 上级部门名称(所属养护所) |
  218 +| `leaderName` | String | 班组长姓名 |
  219 +| `totalMembers` | Long | 班组人数 |
  220 +| `score` | ScoreVO | 考核得分(详见下方) |
  221 +| `orderStats` | OrderStatsVO | 工单统计 |
  222 +| `attendanceStats` | AttendanceStatsVO | 考勤统计 |
  223 +| `materialStats` | MaterialStatsVO | 耗材统计 |
  224 +| `members` | Array\<MemberVO\> | 人员列表(**第二层**,含个人考核指标) |
  225 +| `orders` | Array\<OrderVO\> | 工单列表(**第二层**,含耗材) |
  226 +
  227 +#### ScoreVO — 考核得分
  228 +
  229 +| 字段 | 类型 | 说明 |
  230 +|------|------|------|
  231 +| `total` | Integer | 总分(满分100) |
  232 +| `orderScore` | Integer | 工单得分(满分40) |
  233 +| `attendanceScore` | Integer | 考勤得分(满分30) |
  234 +| `materialScore` | Integer | 人机材得分(满分30) |
  235 +| `deductions` | Array\<DeductionVO\> | 扣分明细列表 |
  236 +
  237 +**DeductionVO:**
  238 +
  239 +| 字段 | 类型 | 说明 |
  240 +|------|------|------|
  241 +| `rule` | String | 扣分规则描述 |
  242 +| `count` | Integer | 触发次数 |
  243 +| `points` | Integer | 扣分(负数) |
  244 +
  245 +#### OrderStatsVO — 工单统计
  246 +
  247 +| 字段 | 类型 | 说明 |
  248 +|------|------|------|
  249 +| `totalOrders` | Long | 总工单数 |
  250 +| `completedOrders` | Long | 已完成工单数 |
  251 +| `completionRate` | BigDecimal | 完成率(%) |
  252 +| `onlineRate` | BigDecimal | 线上化率(%) |
  253 +| `responseWithin30Min` | Long | 30分钟内响应数 |
  254 +| `responseRate30Min` | BigDecimal | 30分钟响应率(%) |
  255 +| `overdueOrders` | Long | 超期工单数 |
  256 +| `lateSupplementOrders` | Long | 超24小时补录工单数 |
  257 +
  258 +#### AttendanceStatsVO — 考勤统计
  259 +
  260 +| 字段 | 类型 | 说明 |
  261 +|------|------|------|
  262 +| `totalCheckins` | Long | 总打卡次数 |
  263 +| `absentCount` | Long | 缺勤人次 |
  264 +| `missedCheckinCount` | Long | 未触发电子围栏打卡次数 |
  265 +| `trajectoryLowCount` | Long | 轨迹覆盖率不达标次数 |
  266 +| `missingCoHandlerCount` | Long | 缺共同处理人次数 |
  267 +| `missingCardCount` | Long | 未佩戴工卡次数 |
  268 +| `multiCardViolation` | Boolean | 是否存在一人多卡违规 |
  269 +| `leaderRecorderOffCount` | Long | 班组长记录仪未开机次数 |
  270 +
  271 +#### MaterialStatsVO — 耗材统计
  272 +
  273 +| 字段 | 类型 | 说明 |
  274 +|------|------|------|
  275 +| `ordersWithMaterial` | Long | 已上报耗材的工单数 |
  276 +| `ordersMissingMaterial` | Long | 缺耗材上报的工单数 |
  277 +| `totalMaterialItems` | Long | 耗材总用量 |
  278 +| `materialTypes` | Long | 耗材种类数 |
  279 +
  280 +#### MemberVO — 人员信息(第二层)
  281 +
  282 +| 字段 | 类型 | 说明 |
  283 +|------|------|------|
  284 +| `userId` | Long | 人员ID |
  285 +| `userName` | String | 姓名 |
  286 +| `mobile` | String | 手机号 |
  287 +| `postNames` | String | 岗位名称(逗号分隔) |
  288 +| `cardNumber` | String | 工卡号 |
  289 +| `isInner` | Integer | 是否内部人员(1=内部,2=外部) |
  290 +| `personalScore` | PersonalScoreVO | 个人考核指标 |
  291 +| `attendances` | Array\<AttendanceRecordVO\> | 考勤明细(**第三层**,`includeDetail=true` 时返回) |
  292 +| `orders` | Array\<OrderVO\> | 个人工单(**第三层**,`includeDetail=true` 时返回) |
  293 +
  294 +#### PersonalScoreVO — 个人考核指标
  295 +
  296 +| 字段 | 类型 | 说明 |
  297 +|------|------|------|
  298 +| `attendanceDays` | Long | 出勤天数 |
  299 +| `lateCount` | Long | 迟到次数 |
  300 +| `absentCount` | Long | 缺勤次数 |
  301 +| `assignedOrders` | Long | 承担工单数 |
  302 +| `completedOrders` | Long | 完成工单数 |
  303 +| `completionRate` | BigDecimal | 完成率(%) |
  304 +| `overdueOrders` | Long | 超期工单数 |
  305 +| `materialReported` | Long | 已上报耗材工单数 |
  306 +| `materialMissing` | Long | 缺耗材上报工单数 |
  307 +
  308 +#### OrderVO — 工单信息(第二层/第三层)
  309 +
  310 +| 字段 | 类型 | 说明 |
  311 +|------|------|------|
  312 +| `orderNo` | String | 工单号 |
  313 +| `orderName` | String | 工单名称 |
  314 +| `orderType` | String | 工单类型:Q=快速,C=普通,O=其他 |
  315 +| `workerName` | String | 接单人姓名 |
  316 +| `status` | Integer | 审批状态 |
  317 +| `buzStatus` | String | 业务状态(completed=已完成) |
  318 +| `commitDate` | String | 发布时间 |
  319 +| `finishDate` | String | 完成时间 |
  320 +| `expectedFinishDate` | String | 期望完成时间 |
  321 +| `responseMinutes` | Long | 响应时长(分钟) |
  322 +| `isOverdue` | Boolean | 是否超期(`finishDate > expectedFinishDate`) |
  323 +| `isLateSupplement` | Boolean | 是否超24小时补录 |
  324 +| `hasMaterialReported` | Boolean | 是否已上报耗材 |
  325 +| `coHandlers` | Array\<String\> | 共同处理人姓名 |
  326 +| `materials` | Array\<MaterialVO\> | 耗材明细(**第三层**,`includeDetail=true` 时返回) |
  327 +
  328 +#### 其他嵌套 VO
  329 +
  330 +**AttendanceRecordVO:**
  331 +
  332 +| 字段 | 类型 | 说明 |
  333 +|------|------|------|
  334 +| `punchType` | String | 打卡类型(上班/下班) |
  335 +| `punchTime` | String | 打卡时间 |
  336 +| `address` | String | 打卡地址 |
  337 +
  338 +**MaterialVO:**
  339 +
  340 +| 字段 | 类型 | 说明 |
  341 +|------|------|------|
  342 +| `materialName` | String | 耗材名称 |
  343 +| `specifications` | String | 规格 |
  344 +| `userCount` | Long | 使用数量 |
  345 +| `unitName` | String | 单位 |
  346 +| `classifyName` | String | 分类 |
  347 +
  348 +**PeriodVO:**
  349 +
  350 +| 字段 | 类型 | 说明 |
  351 +|------|------|------|
  352 +| `start` | String | 考核开始日期(yyyy-MM-dd) |
  353 +| `end` | String | 考核结束日期(yyyy-MM-dd) |
  354 +
  355 +### 4.5 请求示例
  356 +
  357 +```bash
  358 +# team 模式:按班组名称查询考核数据(仅汇总)
  359 +curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/team-assessment/query?keyword=养护一班&queryMode=team&startDate=2026-06-01&endDate=2026-06-30" \
  360 + -H "appId: my_app_001" \
  361 + -H "timestamp: 1749696000000" \
  362 + -H "nonce: c3d4e5f6g7h8i9j0" \
  363 + -H "sign: ..."
  364 +
  365 +# staff 模式:按人员姓名查询考核数据(含考勤明细和工单耗材)
  366 +curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/team-assessment/query?keyword=张三&queryMode=staff&includeDetail=true" \
  367 + -H "appId: my_app_001" \
  368 + -H "timestamp: 1749696000000" \
  369 + -H "nonce: d4e5f6g7h8i9j0k1" \
  370 + -H "sign: ..."
  371 +```
  372 +
  373 +### 4.6 响应示例(team 模式,includeDetail=false)
  374 +
  375 +```json
  376 +{
  377 + "code": 0,
  378 + "msg": "成功",
  379 + "data": {
  380 + "queryMode": "team",
  381 + "period": {
  382 + "start": "2026-06-01",
  383 + "end": "2026-06-30"
  384 + },
  385 + "teams": [
  386 + {
  387 + "teamId": 100,
  388 + "teamName": "养护一班",
  389 + "parentTeamName": "房山区园林所",
  390 + "leaderName": "张三",
  391 + "totalMembers": 15,
  392 + "score": {
  393 + "total": 85,
  394 + "orderScore": 34,
  395 + "attendanceScore": 25,
  396 + "materialScore": 26,
  397 + "deductions": [
  398 + { "rule": "工单超24小时补录", "count": 1, "points": -1 },
  399 + { "rule": "工单超时>2小时", "count": 2, "points": -2 },
  400 + { "rule": "缺勤/未打卡", "count": 5, "points": -5 },
  401 + { "rule": "人机材24h内未上报", "count": 1, "points": -2 }
  402 + ]
  403 + },
  404 + "orderStats": {
  405 + "totalOrders": 45,
  406 + "completedOrders": 42,
  407 + "completionRate": 93.3,
  408 + "onlineRate": 97.8,
  409 + "responseWithin30Min": 40,
  410 + "responseRate30Min": 88.9,
  411 + "overdueOrders": 2,
  412 + "lateSupplementOrders": 1
  413 + },
  414 + "attendanceStats": {
  415 + "totalCheckins": 570,
  416 + "absentCount": 5,
  417 + "missedCheckinCount": 5,
  418 + "trajectoryLowCount": 1,
  419 + "missingCoHandlerCount": 0,
  420 + "missingCardCount": 0,
  421 + "multiCardViolation": false,
  422 + "leaderRecorderOffCount": 0
  423 + },
  424 + "materialStats": {
  425 + "ordersWithMaterial": 42,
  426 + "ordersMissingMaterial": 1,
  427 + "totalMaterialItems": 230,
  428 + "materialTypes": 15
  429 + },
  430 + "members": [
  431 + {
  432 + "userId": 1001,
  433 + "userName": "张三",
  434 + "mobile": "13800138001",
  435 + "postNames": "班组长,技术员",
  436 + "cardNumber": "DEV001",
  437 + "isInner": 1,
  438 + "personalScore": {
  439 + "attendanceDays": 28,
  440 + "lateCount": 0,
  441 + "absentCount": 0,
  442 + "assignedOrders": 8,
  443 + "completedOrders": 8,
  444 + "completionRate": 100.0,
  445 + "overdueOrders": 0,
  446 + "materialReported": 8,
  447 + "materialMissing": 0
  448 + }
  449 + }
  450 + ],
  451 + "orders": [
  452 + {
  453 + "orderNo": "WO202606250001",
  454 + "orderName": "树木修剪",
  455 + "orderType": "C",
  456 + "workerName": "张三",
  457 + "status": 1,
  458 + "buzStatus": "completed",
  459 + "commitDate": "2026-06-25 09:00:00",
  460 + "finishDate": "2026-06-25 16:30:00",
  461 + "expectedFinishDate": "2026-06-25 18:00:00",
  462 + "responseMinutes": 15,
  463 + "isOverdue": false,
  464 + "isLateSupplement": false,
  465 + "hasMaterialReported": true,
  466 + "coHandlers": ["李四"]
  467 + }
  468 + ]
  469 + }
  470 + ]
  471 + }
  472 +}
  473 +```
  474 +
  475 +### 4.7 响应示例(staff 模式,includeDetail=true)
  476 +
  477 +```json
  478 +{
  479 + "code": 0,
  480 + "msg": "成功",
  481 + "data": {
  482 + "queryMode": "staff",
  483 + "period": {
  484 + "start": "2026-06-01",
  485 + "end": "2026-06-25"
  486 + },
  487 + "staffList": [
  488 + {
  489 + "userId": 1001,
  490 + "userName": "张三",
  491 + "mobile": "13800138001",
  492 + "deptId": 100,
  493 + "deptName": "养护一班",
  494 + "postNames": "班组长",
  495 + "cardNumber": "DEV001",
  496 + "isInner": 1,
  497 + "assessment": {
  498 + "attendanceDays": 28,
  499 + "lateCount": 0,
  500 + "absentCount": 0,
  501 + "assignedOrders": 8,
  502 + "completedOrders": 8,
  503 + "completionRate": 100.0,
  504 + "overdueOrders": 0,
  505 + "materialReported": 8,
  506 + "materialMissing": 0
  507 + },
  508 + "attendances": [
  509 + { "punchType": "上班", "punchTime": "2026-06-25 08:00:00", "address": "北京市房山区xx路" },
  510 + { "punchType": "下班", "punchTime": "2026-06-25 17:30:00", "address": "北京市房山区xx路" }
  511 + ],
  512 + "orders": [
  513 + {
  514 + "orderNo": "WO202606250001",
  515 + "orderName": "树木修剪",
  516 + "orderType": "C",
  517 + "workerName": "张三",
  518 + "status": 1,
  519 + "buzStatus": "completed",
  520 + "commitDate": "2026-06-25 09:00:00",
  521 + "finishDate": "2026-06-25 16:30:00",
  522 + "expectedFinishDate": "2026-06-25 18:00:00",
  523 + "responseMinutes": 15,
  524 + "isOverdue": false,
  525 + "isLateSupplement": false,
  526 + "hasMaterialReported": true,
  527 + "coHandlers": ["李四"],
  528 + "materials": [
  529 + {
  530 + "materialName": "高效氯氰菊酯",
  531 + "specifications": "500ml/瓶",
  532 + "userCount": 2,
  533 + "unitName": "瓶",
  534 + "classifyName": "药品"
  535 + }
  536 + ]
  537 + }
  538 + ]
  539 + }
  540 + ]
  541 + }
  542 +}
  543 +```
  544 +
  545 +### 4.8 使用建议(Co-ClawCo-Claw)
  546 +
  547 +| 场景 | 推荐参数 | 说明 |
  548 +|------|----------|------|
  549 +| 月底考核排名 | `keyword=养护&queryMode=team` | 模糊查询所有养护班组,返回考核得分用于排名 |
  550 +| 单班组详情分析 | `keyword=养护一班&queryMode=team&includeDetail=true` | 查看特定班组全部明细,分析扣分原因 |
  551 +| 个人绩效查询 | `keyword=张三&queryMode=staff&includeDetail=true` | 查看某人出勤、工单、耗材完整记录 |
  552 +| 日常快速巡检 | `keyword=养护&includeDetail=false` | 只看汇总指标,Token 最少 |
  553 +
  554 +---
  555 +
  556 +## 5. 接口 2:人员查询
165 557
166 ### 4.1 基本信息 558 ### 4.1 基本信息
167 559
@@ -238,7 +630,7 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/staff/query?dept @@ -238,7 +630,7 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/staff/query?dept
238 630
239 --- 631 ---
240 632
241 -## 5. 接口 2:班组查询 633 +## 6. 接口 3:班组查询
242 634
243 ### 5.1 基本信息 635 ### 5.1 基本信息
244 636
@@ -306,7 +698,7 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/teams/query&quot; \ @@ -306,7 +698,7 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/teams/query&quot; \
306 698
307 --- 699 ---
308 700
309 -## 6. 接口 3:考勤查询 701 +## 7. 接口 4:考勤查询
310 702
311 ### 6.1 基本信息 703 ### 6.1 基本信息
312 704
@@ -379,7 +771,7 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/teams/query&quot; \ @@ -379,7 +771,7 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/teams/query&quot; \
379 771
380 --- 772 ---
381 773
382 -## 7. 接口 4:工单查询 774 +## 8. 接口 5:工单查询
383 775
384 ### 7.1 基本信息 776 ### 7.1 基本信息
385 777
@@ -472,7 +864,7 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/teams/query&quot; \ @@ -472,7 +864,7 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/teams/query&quot; \
472 864
473 --- 865 ---
474 866
475 -## 8. 接口 5:物料查询 867 +## 9. 接口 6:物料查询
476 868
477 ### 8.1 基本信息 869 ### 8.1 基本信息
478 870
@@ -549,7 +941,7 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/teams/query&quot; \ @@ -549,7 +941,7 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/teams/query&quot; \
549 941
550 --- 942 ---
551 943
552 -## 9. 接口 6:全量工单统计 944 +## 10. 接口 7:全量工单统计
553 945
554 ### 9.1 基本信息 946 ### 9.1 基本信息
555 947
@@ -621,18 +1013,16 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/full-order-stati @@ -621,18 +1013,16 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/full-order-stati
621 1013
622 --- 1014 ---
623 1015
624 -## 10. 错误码说明 1016 +## 11. 错误码说明
625 1017
626 -### 10.1 签名相关错误  
627 -  
628 -### 10.2 业务相关错误 1018 +### 11.1 签名相关错误
629 1019
630 | code | msg | 说明 | 1020 | code | msg | 说明 |
631 |------|-----|------| 1021 |------|-----|------|
632 | 400 | 签名不正确 | 签名验证失败(appId不存在/timestamp超时/nonce长度不足/签名不匹配) | 1022 | 400 | 签名不正确 | 签名验证失败(appId不存在/timestamp超时/nonce长度不足/签名不匹配) |
633 | 900 | 存在重复请求 | nonce 已被使用(重放攻击检测) | 1023 | 900 | 存在重复请求 | nonce 已被使用(重放攻击检测) |
634 1024
635 -### 10.3 通用错误 1025 +### 11.2 业务接口错误
636 1026
637 | code | msg | 说明 | 1027 | code | msg | 说明 |
638 |------|-----|------| 1028 |------|-----|------|
@@ -641,11 +1031,11 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/full-order-stati @@ -641,11 +1031,11 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/full-order-stati
641 | 1-100-020-003 | 考勤查询失败 | 考勤查询内部错误 | 1031 | 1-100-020-003 | 考勤查询失败 | 考勤查询内部错误 |
642 | 1-100-020-004 | 工单查询失败 | 工单查询内部错误 | 1032 | 1-100-020-004 | 工单查询失败 | 工单查询内部错误 |
643 | 1-100-020-005 | 物料查询失败 | 物料查询内部错误 | 1033 | 1-100-020-005 | 物料查询失败 | 物料查询内部错误 |
644 - 1034 +| 1-100-020-006 | 团队考核查询失败 | 团队考核聚合查询内部错误 |
645 | 1-900-005-001 | 全量工单统计不存在 | 记录不存在 | 1035 | 1-900-005-001 | 全量工单统计不存在 | 记录不存在 |
646 | 1-900-005-002 | 相同公司/班组同时间段统计已存在 | 重复数据校验 | 1036 | 1-900-005-002 | 相同公司/班组同时间段统计已存在 | 重复数据校验 |
647 1037
648 -### 10.3 通用错误 1038 +### 11.3 通用错误
649 1039
650 | code | msg | 说明 | 1040 | code | msg | 说明 |
651 |------|-----|------| 1041 |------|-----|------|
@@ -654,9 +1044,9 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/full-order-stati @@ -654,9 +1044,9 @@ curl -X GET &quot;https://test.jichengshanshui.com.cn:28302/open-api/full-order-stati
654 1044
655 --- 1045 ---
656 1046
657 -## 11. 附录:客户端签名代码示例 1047 +## 12. 附录:客户端签名代码示例
658 1048
659 -### 10.1 Java 示例 1049 +### 12.1 Java 示例
660 1050
661 ```java 1051 ```java
662 import cn.hutool.crypto.digest.DigestUtil; 1052 import cn.hutool.crypto.digest.DigestUtil;
@@ -711,7 +1101,7 @@ public class OpenApiClient { @@ -711,7 +1101,7 @@ public class OpenApiClient {
711 } 1101 }
712 ``` 1102 ```
713 1103
714 -### 10.2 Python 示例 1104 +### 12.2 Python 示例
715 1105
716 ```python 1106 ```python
717 import hashlib 1107 import hashlib
@@ -752,7 +1142,7 @@ def call(path, params=None): @@ -752,7 +1142,7 @@ def call(path, params=None):
752 return response.json() 1142 return response.json()
753 ``` 1143 ```
754 1144
755 -### 10.3 Shell (cURL) 示例 1145 +### 12.3 Shell (cURL) 示例
756 1146
757 ```bash 1147 ```bash
758 #!/bin/bash 1148 #!/bin/bash
docs/开放API对接接口文档.pdf
No preview for this file type
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/open/assessment/TeamAssessmentOpenController.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.controller.open.assessment;
  2 +
  3 +import com.zteits.urbanops.framework.common.pojo.CommonResult;
  4 +import com.zteits.urbanops.framework.signature.core.annotation.ApiSignature;
  5 +import com.zteits.urbanops.module.garden.controller.open.assessment.vo.TeamAssessmentReqVO;
  6 +import com.zteits.urbanops.module.garden.controller.open.assessment.vo.TeamAssessmentRespVO;
  7 +import com.zteits.urbanops.module.garden.controller.open.service.OpenApiService;
  8 +import io.swagger.v3.oas.annotations.Operation;
  9 +import io.swagger.v3.oas.annotations.tags.Tag;
  10 +import jakarta.annotation.Resource;
  11 +import jakarta.validation.Valid;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +import org.springframework.web.bind.annotation.GetMapping;
  14 +import org.springframework.web.bind.annotation.RequestMapping;
  15 +import org.springframework.web.bind.annotation.RestController;
  16 +
  17 +import java.util.concurrent.TimeUnit;
  18 +
  19 +import static com.zteits.urbanops.framework.common.pojo.CommonResult.success;
  20 +
  21 +@Tag(name = "开放API - 团队考核查询")
  22 +@RestController
  23 +@RequestMapping("/open-api/team-assessment")
  24 +@Slf4j
  25 +@jakarta.annotation.security.PermitAll
  26 +@ApiSignature(timeout = 60, timeUnit = TimeUnit.SECONDS)
  27 +public class TeamAssessmentOpenController {
  28 +
  29 + @Resource
  30 + private OpenApiService openApiService;
  31 +
  32 + @GetMapping("/query")
  33 + @Operation(summary = "查询班组/人员考核数据(供 OpenClaw AI 助手使用)")
  34 + public CommonResult<TeamAssessmentRespVO> queryTeamAssessment(
  35 + @Valid TeamAssessmentReqVO reqVO) {
  36 + return success(openApiService.queryTeamAssessment(reqVO));
  37 + }
  38 +}
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/open/assessment/vo/TeamAssessmentReqVO.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.controller.open.assessment.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +import org.springframework.format.annotation.DateTimeFormat;
  6 +
  7 +import java.io.Serializable;
  8 +import java.time.LocalDate;
  9 +
  10 +import static com.zteits.urbanops.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
  11 +
  12 +@Schema(description = "开放API - 团队考核查询 Request VO")
  13 +@Data
  14 +public class TeamAssessmentReqVO implements Serializable {
  15 +
  16 + @Schema(description = "查询关键字:班组名称或人员姓名(支持模糊匹配)", example = "养护一班", requiredMode = Schema.RequiredMode.REQUIRED)
  17 + private String keyword;
  18 +
  19 + @Schema(description = "查询模式:team 按班组查询 / staff 按人员查询", example = "team", defaultValue = "team")
  20 + private String queryMode;
  21 +
  22 + @Schema(description = "考核开始日期(默认当月1日)", example = "2026-06-01")
  23 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
  24 + private LocalDate startDate;
  25 +
  26 + @Schema(description = "考核结束日期(默认今天)", example = "2026-06-25")
  27 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
  28 + private LocalDate endDate;
  29 +
  30 + @Schema(description = "是否返回明细数据(考勤明细、工单耗材明细)", example = "false", defaultValue = "false")
  31 + private Boolean includeDetail;
  32 +}
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/open/assessment/vo/TeamAssessmentRespVO.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.controller.open.assessment.vo;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonFormat;
  4 +import com.fasterxml.jackson.annotation.JsonInclude;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import lombok.Data;
  7 +
  8 +import java.math.BigDecimal;
  9 +import java.time.LocalDate;
  10 +import java.time.LocalDateTime;
  11 +import java.util.List;
  12 +
  13 +import static com.zteits.urbanops.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
  14 +import static com.zteits.urbanops.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  15 +import static com.zteits.urbanops.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
  16 +
  17 +@Schema(description = "开放API - 团队考核查询 Response VO")
  18 +@Data
  19 +@JsonInclude(JsonInclude.Include.NON_NULL)
  20 +public class TeamAssessmentRespVO {
  21 +
  22 + @Schema(description = "查询模式", example = "team")
  23 + private String queryMode;
  24 +
  25 + @Schema(description = "考核周期")
  26 + private PeriodVO period;
  27 +
  28 + @Schema(description = "班组考核列表(queryMode=team 时返回)")
  29 + private List<TeamVO> teams;
  30 +
  31 + @Schema(description = "人员考核列表(queryMode=staff 时返回)")
  32 + private List<StaffVO> staffList;
  33 +
  34 + // ==================== 考核周期 ====================
  35 +
  36 + @Data
  37 + public static class PeriodVO {
  38 + @Schema(description = "开始日期", example = "2026-06-01")
  39 + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY, timezone = TIME_ZONE_DEFAULT)
  40 + private LocalDate start;
  41 +
  42 + @Schema(description = "结束日期", example = "2026-06-25")
  43 + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY, timezone = TIME_ZONE_DEFAULT)
  44 + private LocalDate end;
  45 + }
  46 +
  47 + // ==================== 班组(第一层) ====================
  48 +
  49 + @Data
  50 + public static class TeamVO {
  51 + @Schema(description = "班组ID", example = "100")
  52 + private Long teamId;
  53 +
  54 + @Schema(description = "班组名称", example = "养护一班")
  55 + private String teamName;
  56 +
  57 + @Schema(description = "上级部门名称", example = "房山区园林所")
  58 + private String parentTeamName;
  59 +
  60 + @Schema(description = "班组长姓名", example = "张三")
  61 + private String leaderName;
  62 +
  63 + @Schema(description = "班组人数", example = "15")
  64 + private Long totalMembers;
  65 +
  66 + @Schema(description = "考核得分")
  67 + private ScoreVO score;
  68 +
  69 + @Schema(description = "工单统计")
  70 + private OrderStatsVO orderStats;
  71 +
  72 + @Schema(description = "考勤统计")
  73 + private AttendanceStatsVO attendanceStats;
  74 +
  75 + @Schema(description = "耗材统计")
  76 + private MaterialStatsVO materialStats;
  77 +
  78 + @Schema(description = "人员列表(第二层)")
  79 + private List<MemberVO> members;
  80 +
  81 + @Schema(description = "工单列表(第二层)")
  82 + private List<OrderVO> orders;
  83 + }
  84 +
  85 + // ==================== 考核得分 ====================
  86 +
  87 + @Data
  88 + public static class ScoreVO {
  89 + @Schema(description = "总分", example = "85")
  90 + private Integer total;
  91 +
  92 + @Schema(description = "工单得分(满分40)", example = "34")
  93 + private Integer orderScore;
  94 +
  95 + @Schema(description = "考勤得分(满分30)", example = "25")
  96 + private Integer attendanceScore;
  97 +
  98 + @Schema(description = "人机材得分(满分30)", example = "26")
  99 + private Integer materialScore;
  100 +
  101 + @Schema(description = "扣分明细")
  102 + private List<DeductionVO> deductions;
  103 + }
  104 +
  105 + @Data
  106 + public static class DeductionVO {
  107 + @Schema(description = "扣分规则", example = "工单超时>2小时")
  108 + private String rule;
  109 +
  110 + @Schema(description = "触发次数", example = "2")
  111 + private Integer count;
  112 +
  113 + @Schema(description = "扣分", example = "-2")
  114 + private Integer points;
  115 + }
  116 +
  117 + // ==================== 工单统计 ====================
  118 +
  119 + @Data
  120 + public static class OrderStatsVO {
  121 + @Schema(description = "总工单数", example = "45")
  122 + private Long totalOrders;
  123 +
  124 + @Schema(description = "已完成工单数", example = "42")
  125 + private Long completedOrders;
  126 +
  127 + @Schema(description = "完成率(%)", example = "93.3")
  128 + private BigDecimal completionRate;
  129 +
  130 + @Schema(description = "线上化率(%)", example = "97.8")
  131 + private BigDecimal onlineRate;
  132 +
  133 + @Schema(description = "30分钟内响应数", example = "40")
  134 + private Long responseWithin30Min;
  135 +
  136 + @Schema(description = "30分钟响应率(%)", example = "88.9")
  137 + private BigDecimal responseRate30Min;
  138 +
  139 + @Schema(description = "超期工单数", example = "2")
  140 + private Long overdueOrders;
  141 +
  142 + @Schema(description = "超24小时补录工单数", example = "1")
  143 + private Long lateSupplementOrders;
  144 + }
  145 +
  146 + // ==================== 考勤统计 ====================
  147 +
  148 + @Data
  149 + public static class AttendanceStatsVO {
  150 + @Schema(description = "总打卡次数", example = "570")
  151 + private Long totalCheckins;
  152 +
  153 + @Schema(description = "缺勤人次", example = "5")
  154 + private Long absentCount;
  155 +
  156 + @Schema(description = "未触发电子围栏打卡次数", example = "3")
  157 + private Long missedCheckinCount;
  158 +
  159 + @Schema(description = "轨迹覆盖率不达标次数", example = "1")
  160 + private Long trajectoryLowCount;
  161 +
  162 + @Schema(description = "缺共同处理人次数", example = "0")
  163 + private Long missingCoHandlerCount;
  164 +
  165 + @Schema(description = "未佩戴工卡次数", example = "0")
  166 + private Long missingCardCount;
  167 +
  168 + @Schema(description = "是否存在一人多卡违规", example = "false")
  169 + private Boolean multiCardViolation;
  170 +
  171 + @Schema(description = "班组长记录仪未开机次数", example = "0")
  172 + private Long leaderRecorderOffCount;
  173 + }
  174 +
  175 + // ==================== 耗材统计 ====================
  176 +
  177 + @Data
  178 + public static class MaterialStatsVO {
  179 + @Schema(description = "已上报耗材的工单数", example = "42")
  180 + private Long ordersWithMaterial;
  181 +
  182 + @Schema(description = "缺耗材上报的工单数", example = "1")
  183 + private Long ordersMissingMaterial;
  184 +
  185 + @Schema(description = "耗材总用量", example = "230")
  186 + private Long totalMaterialItems;
  187 +
  188 + @Schema(description = "耗材种类数", example = "15")
  189 + private Long materialTypes;
  190 + }
  191 +
  192 + // ==================== 人员(第二层) ====================
  193 +
  194 + @Data
  195 + public static class MemberVO {
  196 + @Schema(description = "人员ID", example = "1001")
  197 + private Long userId;
  198 +
  199 + @Schema(description = "姓名", example = "张三")
  200 + private String userName;
  201 +
  202 + @Schema(description = "手机号", example = "13800138000")
  203 + private String mobile;
  204 +
  205 + @Schema(description = "岗位名称", example = "班组长,技术员")
  206 + private String postNames;
  207 +
  208 + @Schema(description = "工卡号", example = "DEV001")
  209 + private String cardNumber;
  210 +
  211 + @Schema(description = "是否内部人员", example = "1")
  212 + private Integer isInner;
  213 +
  214 + @Schema(description = "个人考核指标")
  215 + private PersonalScoreVO personalScore;
  216 +
  217 + @Schema(description = "考勤明细 / 第三层(includeDetail=true 时返回)")
  218 + private List<AttendanceRecordVO> attendances;
  219 +
  220 + @Schema(description = "个人工单 / 第三层(includeDetail=true 时返回)")
  221 + private List<OrderVO> orders;
  222 + }
  223 +
  224 + @Data
  225 + public static class PersonalScoreVO {
  226 + @Schema(description = "出勤天数", example = "28")
  227 + private Long attendanceDays;
  228 +
  229 + @Schema(description = "迟到次数", example = "0")
  230 + private Long lateCount;
  231 +
  232 + @Schema(description = "缺勤次数", example = "0")
  233 + private Long absentCount;
  234 +
  235 + @Schema(description = "承担工单数", example = "8")
  236 + private Long assignedOrders;
  237 +
  238 + @Schema(description = "完成工单数", example = "8")
  239 + private Long completedOrders;
  240 +
  241 + @Schema(description = "完成率(%)", example = "100.0")
  242 + private BigDecimal completionRate;
  243 +
  244 + @Schema(description = "超期工单数", example = "0")
  245 + private Long overdueOrders;
  246 +
  247 + @Schema(description = "已上报耗材工单数", example = "8")
  248 + private Long materialReported;
  249 +
  250 + @Schema(description = "缺耗材上报工单数", example = "0")
  251 + private Long materialMissing;
  252 + }
  253 +
  254 + @Data
  255 + public static class AttendanceRecordVO {
  256 + @Schema(description = "打卡类型", example = "上班")
  257 + private String punchType;
  258 +
  259 + @Schema(description = "打卡时间", example = "2026-06-25 08:00:00")
  260 + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
  261 + private LocalDateTime punchTime;
  262 +
  263 + @Schema(description = "打卡地址", example = "北京市房山区xx路")
  264 + private String address;
  265 + }
  266 +
  267 + // ==================== 工单(第二层/第三层) ====================
  268 +
  269 + @Data
  270 + public static class OrderVO {
  271 + @Schema(description = "工单号", example = "WO202606250001")
  272 + private String orderNo;
  273 +
  274 + @Schema(description = "工单名称", example = "树木修剪")
  275 + private String orderName;
  276 +
  277 + @Schema(description = "工单类型", example = "C")
  278 + private String orderType;
  279 +
  280 + @Schema(description = "接单人姓名", example = "张三")
  281 + private String workerName;
  282 +
  283 + @Schema(description = "审批状态", example = "1")
  284 + private Integer status;
  285 +
  286 + @Schema(description = "业务状态", example = "completed")
  287 + private String buzStatus;
  288 +
  289 + @Schema(description = "发布时间", example = "2026-06-25 09:00:00")
  290 + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
  291 + private LocalDateTime commitDate;
  292 +
  293 + @Schema(description = "完成时间", example = "2026-06-25 16:30:00")
  294 + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
  295 + private LocalDateTime finishDate;
  296 +
  297 + @Schema(description = "期望完成时间", example = "2026-06-25 18:00:00")
  298 + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
  299 + private LocalDateTime expectedFinishDate;
  300 +
  301 + @Schema(description = "响应时长(分钟)", example = "15")
  302 + private Long responseMinutes;
  303 +
  304 + @Schema(description = "是否超期", example = "false")
  305 + private Boolean isOverdue;
  306 +
  307 + @Schema(description = "是否超24小时补录", example = "false")
  308 + private Boolean isLateSupplement;
  309 +
  310 + @Schema(description = "是否已上报耗材", example = "true")
  311 + private Boolean hasMaterialReported;
  312 +
  313 + @Schema(description = "共同处理人姓名")
  314 + private List<String> coHandlers;
  315 +
  316 + @Schema(description = "耗材明细 / 第三层(includeDetail=true 时返回)")
  317 + private List<MaterialVO> materials;
  318 + }
  319 +
  320 + @Data
  321 + public static class MaterialVO {
  322 + @Schema(description = "耗材名称", example = "高效氯氰菊酯")
  323 + private String materialName;
  324 +
  325 + @Schema(description = "规格", example = "500ml/瓶")
  326 + private String specifications;
  327 +
  328 + @Schema(description = "使用数量", example = "2")
  329 + private Long userCount;
  330 +
  331 + @Schema(description = "单位", example = "瓶")
  332 + private String unitName;
  333 +
  334 + @Schema(description = "分类", example = "药品")
  335 + private String classifyName;
  336 + }
  337 +
  338 + // ==================== 人员模式 ====================
  339 +
  340 + @Data
  341 + public static class StaffVO {
  342 + @Schema(description = "人员ID", example = "1001")
  343 + private Long userId;
  344 +
  345 + @Schema(description = "姓名", example = "张三")
  346 + private String userName;
  347 +
  348 + @Schema(description = "手机号", example = "13800138000")
  349 + private String mobile;
  350 +
  351 + @Schema(description = "部门ID", example = "100")
  352 + private Long deptId;
  353 +
  354 + @Schema(description = "部门名称", example = "养护一班")
  355 + private String deptName;
  356 +
  357 + @Schema(description = "岗位名称", example = "班组长")
  358 + private String postNames;
  359 +
  360 + @Schema(description = "工卡号", example = "DEV001")
  361 + private String cardNumber;
  362 +
  363 + @Schema(description = "是否内部人员", example = "1")
  364 + private Integer isInner;
  365 +
  366 + @Schema(description = "个人考核汇总")
  367 + private PersonalScoreVO assessment;
  368 +
  369 + @Schema(description = "考勤明细")
  370 + private List<AttendanceRecordVO> attendances;
  371 +
  372 + @Schema(description = "工单列表(含耗材)")
  373 + private List<OrderVO> orders;
  374 + }
  375 +}
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/open/service/OpenApiService.java
@@ -8,6 +8,8 @@ import com.zteits.urbanops.module.garden.controller.open.staff.vo.StaffQueryReqV @@ -8,6 +8,8 @@ import com.zteits.urbanops.module.garden.controller.open.staff.vo.StaffQueryReqV
8 import com.zteits.urbanops.module.garden.controller.open.staff.vo.StaffQueryRespVO; 8 import com.zteits.urbanops.module.garden.controller.open.staff.vo.StaffQueryRespVO;
9 import com.zteits.urbanops.module.garden.controller.open.task.vo.TaskQueryReqVO; 9 import com.zteits.urbanops.module.garden.controller.open.task.vo.TaskQueryReqVO;
10 import com.zteits.urbanops.module.garden.controller.open.task.vo.TaskQueryRespVO; 10 import com.zteits.urbanops.module.garden.controller.open.task.vo.TaskQueryRespVO;
  11 +import com.zteits.urbanops.module.garden.controller.open.assessment.vo.TeamAssessmentReqVO;
  12 +import com.zteits.urbanops.module.garden.controller.open.assessment.vo.TeamAssessmentRespVO;
11 import com.zteits.urbanops.module.garden.controller.open.team.vo.TeamQueryReqVO; 13 import com.zteits.urbanops.module.garden.controller.open.team.vo.TeamQueryReqVO;
12 import com.zteits.urbanops.module.garden.controller.open.team.vo.TeamQueryRespVO; 14 import com.zteits.urbanops.module.garden.controller.open.team.vo.TeamQueryRespVO;
13 15
@@ -15,6 +17,8 @@ import java.util.List; @@ -15,6 +17,8 @@ import java.util.List;
15 17
16 public interface OpenApiService { 18 public interface OpenApiService {
17 19
  20 + TeamAssessmentRespVO queryTeamAssessment(TeamAssessmentReqVO reqVO);
  21 +
18 List<StaffQueryRespVO> queryStaffList(StaffQueryReqVO reqVO); 22 List<StaffQueryRespVO> queryStaffList(StaffQueryReqVO reqVO);
19 23
20 List<TeamQueryRespVO> queryTeamList(TeamQueryReqVO reqVO); 24 List<TeamQueryRespVO> queryTeamList(TeamQueryReqVO reqVO);
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/open/service/OpenApiServiceImpl.java
@@ -3,6 +3,8 @@ package com.zteits.urbanops.module.garden.controller.open.service; @@ -3,6 +3,8 @@ package com.zteits.urbanops.module.garden.controller.open.service;
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
5 import com.zteits.urbanops.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import com.zteits.urbanops.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import com.zteits.urbanops.module.garden.controller.open.assessment.vo.TeamAssessmentReqVO;
  7 +import com.zteits.urbanops.module.garden.controller.open.assessment.vo.TeamAssessmentRespVO;
6 import com.zteits.urbanops.module.garden.controller.open.attendance.vo.AttendanceQueryReqVO; 8 import com.zteits.urbanops.module.garden.controller.open.attendance.vo.AttendanceQueryReqVO;
7 import com.zteits.urbanops.module.garden.controller.open.attendance.vo.AttendanceQueryRespVO; 9 import com.zteits.urbanops.module.garden.controller.open.attendance.vo.AttendanceQueryRespVO;
8 import com.zteits.urbanops.module.garden.controller.open.material.vo.MaterialQueryReqVO; 10 import com.zteits.urbanops.module.garden.controller.open.material.vo.MaterialQueryReqVO;
@@ -33,6 +35,12 @@ import jakarta.annotation.Resource; @@ -33,6 +35,12 @@ import jakarta.annotation.Resource;
33 import lombok.extern.slf4j.Slf4j; 35 import lombok.extern.slf4j.Slf4j;
34 import org.springframework.stereotype.Service; 36 import org.springframework.stereotype.Service;
35 37
  38 +import java.math.BigDecimal;
  39 +import java.math.RoundingMode;
  40 +import java.time.Duration;
  41 +import java.time.LocalDate;
  42 +import java.time.LocalDateTime;
  43 +import java.time.LocalTime;
36 import java.util.*; 44 import java.util.*;
37 import java.util.stream.Collectors; 45 import java.util.stream.Collectors;
38 46
@@ -379,4 +387,569 @@ public class OpenApiServiceImpl implements OpenApiService { @@ -379,4 +387,569 @@ public class OpenApiServiceImpl implements OpenApiService {
379 } 387 }
380 return voList; 388 return voList;
381 } 389 }
  390 +
  391 + // ==================== 团队考核聚合查询 ====================
  392 +
  393 + @Override
  394 + public TeamAssessmentRespVO queryTeamAssessment(TeamAssessmentReqVO reqVO) {
  395 + String queryMode = StrUtil.blankToDefault(reqVO.getQueryMode(), "team");
  396 + LocalDate startDate = reqVO.getStartDate() != null ? reqVO.getStartDate()
  397 + : LocalDate.now().withDayOfMonth(1);
  398 + LocalDate endDate = reqVO.getEndDate() != null ? reqVO.getEndDate() : LocalDate.now();
  399 + boolean includeDetail = Boolean.TRUE.equals(reqVO.getIncludeDetail());
  400 + LocalDateTime startDateTime = startDate.atStartOfDay();
  401 + LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
  402 +
  403 + TeamAssessmentRespVO resp = new TeamAssessmentRespVO();
  404 + resp.setQueryMode(queryMode);
  405 + TeamAssessmentRespVO.PeriodVO period = new TeamAssessmentRespVO.PeriodVO();
  406 + period.setStart(startDate);
  407 + period.setEnd(endDate);
  408 + resp.setPeriod(period);
  409 +
  410 + String keyword = reqVO.getKeyword();
  411 + if (StrUtil.isBlank(keyword)) {
  412 + return resp;
  413 + }
  414 +
  415 + if ("staff".equals(queryMode)) {
  416 + resp.setStaffList(buildStaffAssessment(keyword, startDateTime, endDateTime, includeDetail));
  417 + } else {
  418 + resp.setTeams(buildTeamAssessment(keyword, startDateTime, endDateTime, includeDetail));
  419 + }
  420 + return resp;
  421 + }
  422 +
  423 + // ==================== 班组考核构建 ====================
  424 +
  425 + private List<TeamAssessmentRespVO.TeamVO> buildTeamAssessment(
  426 + String keyword, LocalDateTime startDateTime, LocalDateTime endDateTime, boolean includeDetail) {
  427 +
  428 + // 1. 按班组名称模糊查询
  429 + List<DeptDO> deptList = deptMapper.selectList(
  430 + new LambdaQueryWrapperX<DeptDO>()
  431 + .like(DeptDO::getName, keyword)
  432 + .eq(DeptDO::getStatus, 0));
  433 + if (CollUtil.isEmpty(deptList)) {
  434 + return Collections.emptyList();
  435 + }
  436 +
  437 + Set<Long> deptIds = deptList.stream().map(DeptDO::getId).collect(Collectors.toSet());
  438 +
  439 + // 2. 批量查父部门名、班组长名
  440 + Set<Long> parentIds = deptList.stream().map(DeptDO::getParentId)
  441 + .filter(Objects::nonNull).filter(id -> id > 0).collect(Collectors.toSet());
  442 + Map<Long, String> parentNameMap = CollUtil.isNotEmpty(parentIds)
  443 + ? deptMapper.selectList(DeptDO::getId, parentIds).stream()
  444 + .collect(Collectors.toMap(DeptDO::getId, DeptDO::getName, (a, b) -> a))
  445 + : Collections.emptyMap();
  446 + Set<Long> leaderIds = deptList.stream().map(DeptDO::getLeaderUserId)
  447 + .filter(Objects::nonNull).collect(Collectors.toSet());
  448 + Map<Long, String> leaderNameMap = CollUtil.isNotEmpty(leaderIds)
  449 + ? adminUserMapper.selectList(AdminUserDO::getId, leaderIds).stream()
  450 + .collect(Collectors.toMap(AdminUserDO::getId, AdminUserDO::getNickname, (a, b) -> a))
  451 + : Collections.emptyMap();
  452 +
  453 + // 3. 批量查所有成员
  454 + List<AdminUserDO> allMembers = adminUserMapper.selectList(
  455 + new LambdaQueryWrapperX<AdminUserDO>().in(AdminUserDO::getDeptId, deptIds));
  456 + Map<Long, List<AdminUserDO>> deptMemberMap = allMembers.stream()
  457 + .collect(Collectors.groupingBy(AdminUserDO::getDeptId));
  458 + Set<Long> allUserIds = allMembers.stream().map(AdminUserDO::getId).collect(Collectors.toSet());
  459 +
  460 + // 4. 批量查岗位名、工卡号
  461 + Map<Long, String> postNameMap = buildPostNameMap(allMembers);
  462 + Map<Long, String> cardNumberMap = buildCardNumberMap(allUserIds);
  463 +
  464 + // 5. 批量查考勤记录
  465 + List<DeviceClockRecordDO> allClockRecords;
  466 + Map<Long, List<DeviceClockRecordDO>> userClockMap;
  467 + if (CollUtil.isNotEmpty(allUserIds)) {
  468 + allClockRecords = deviceClockRecordMapper.selectList(
  469 + new LambdaQueryWrapperX<DeviceClockRecordDO>()
  470 + .in(DeviceClockRecordDO::getUserId, allUserIds)
  471 + .between(DeviceClockRecordDO::getCreateTime, startDateTime, endDateTime));
  472 + userClockMap = allClockRecords.stream()
  473 + .collect(Collectors.groupingBy(DeviceClockRecordDO::getUserId));
  474 + } else {
  475 + allClockRecords = Collections.emptyList();
  476 + userClockMap = Collections.emptyMap();
  477 + }
  478 +
  479 + // 6. 批量查工单(按实施人员ID或部门ID)
  480 + List<MainInfoDO> allOrders = mainInfoMapper.selectList(
  481 + new LambdaQueryWrapperX<MainInfoDO>()
  482 + .in(MainInfoDO::getDeptId, deptIds)
  483 + .between(MainInfoDO::getCommitDate, startDateTime, endDateTime));
  484 + Map<Long, List<MainInfoDO>> deptOrderMap = allOrders.stream()
  485 + .collect(Collectors.groupingBy(MainInfoDO::getDeptId));
  486 +
  487 + // 7. 批量查耗材
  488 + Set<String> allOrderNos = allOrders.stream().map(MainInfoDO::getOrderNo)
  489 + .filter(Objects::nonNull).collect(Collectors.toSet());
  490 + Map<String, List<MaterialDetailDO>> orderMaterialMap;
  491 + if (CollUtil.isNotEmpty(allOrderNos)) {
  492 + List<MaterialDetailDO> allMaterials = materialDetailMapper.selectList(
  493 + new LambdaQueryWrapperX<MaterialDetailDO>().in(MaterialDetailDO::getOrderNo, allOrderNos));
  494 + orderMaterialMap = allMaterials.stream()
  495 + .collect(Collectors.groupingBy(MaterialDetailDO::getOrderNo));
  496 + } else {
  497 + orderMaterialMap = Collections.emptyMap();
  498 + }
  499 +
  500 + // 8. 构建每个班组的考核数据
  501 + List<TeamAssessmentRespVO.TeamVO> teams = new ArrayList<>();
  502 + for (DeptDO dept : deptList) {
  503 + TeamAssessmentRespVO.TeamVO team = new TeamAssessmentRespVO.TeamVO();
  504 + team.setTeamId(dept.getId());
  505 + team.setTeamName(dept.getName());
  506 + team.setParentTeamName(dept.getParentId() != null && dept.getParentId() > 0
  507 + ? parentNameMap.getOrDefault(dept.getParentId(), "") : "");
  508 + team.setLeaderName(leaderNameMap.getOrDefault(dept.getLeaderUserId(), ""));
  509 +
  510 + List<AdminUserDO> members = deptMemberMap.getOrDefault(dept.getId(), Collections.emptyList());
  511 + team.setTotalMembers((long) members.size());
  512 +
  513 + // 班组工单
  514 + List<MainInfoDO> deptOrders = deptOrderMap.getOrDefault(dept.getId(), Collections.emptyList());
  515 + // 班组考勤(所有成员的打卡记录)
  516 + List<DeviceClockRecordDO> deptClocks = members.stream()
  517 + .flatMap(m -> userClockMap.getOrDefault(m.getId(), Collections.emptyList()).stream())
  518 + .collect(Collectors.toList());
  519 +
  520 + // 计算考核得分和统计
  521 + computeTeamScore(team, deptOrders, deptClocks, members, orderMaterialMap,
  522 + userClockMap, cardNumberMap, startDateTime, endDateTime);
  523 +
  524 + // 构建成员列表
  525 + team.setMembers(buildMemberList(members, userClockMap, deptOrders, orderMaterialMap,
  526 + postNameMap, cardNumberMap, startDateTime, endDateTime, includeDetail));
  527 +
  528 + // 构建工单列表
  529 + team.setOrders(buildOrderList(deptOrders, orderMaterialMap, includeDetail));
  530 +
  531 + teams.add(team);
  532 + }
  533 + return teams;
  534 + }
  535 +
  536 + // ==================== 人员考核构建 ====================
  537 +
  538 + private List<TeamAssessmentRespVO.StaffVO> buildStaffAssessment(
  539 + String keyword, LocalDateTime startDateTime, LocalDateTime endDateTime, boolean includeDetail) {
  540 +
  541 + // 1. 按姓名或手机号模糊查询
  542 + List<AdminUserDO> userList = adminUserMapper.selectList(
  543 + new LambdaQueryWrapperX<AdminUserDO>()
  544 + .like(AdminUserDO::getNickname, keyword)
  545 + .or(w -> w.like(AdminUserDO::getMobile, keyword)));
  546 + if (CollUtil.isEmpty(userList)) {
  547 + return Collections.emptyList();
  548 + }
  549 +
  550 + Set<Long> userIds = userList.stream().map(AdminUserDO::getId).collect(Collectors.toSet());
  551 +
  552 + // 2. 批量查部门名、岗位名、工卡号
  553 + Set<Long> deptIds = userList.stream().map(AdminUserDO::getDeptId)
  554 + .filter(Objects::nonNull).collect(Collectors.toSet());
  555 + Map<Long, String> deptNameMap = CollUtil.isNotEmpty(deptIds)
  556 + ? deptMapper.selectList(DeptDO::getId, deptIds).stream()
  557 + .collect(Collectors.toMap(DeptDO::getId, DeptDO::getName, (a, b) -> a))
  558 + : Collections.emptyMap();
  559 + Map<Long, String> postNameMap = buildPostNameMap(userList);
  560 + Map<Long, String> cardNumberMap = buildCardNumberMap(userIds);
  561 +
  562 + // 3. 批量查考勤
  563 + List<DeviceClockRecordDO> allClocks = deviceClockRecordMapper.selectList(
  564 + new LambdaQueryWrapperX<DeviceClockRecordDO>()
  565 + .in(DeviceClockRecordDO::getUserId, userIds)
  566 + .between(DeviceClockRecordDO::getCreateTime, startDateTime, endDateTime));
  567 + Map<Long, List<DeviceClockRecordDO>> userClockMap = allClocks.stream()
  568 + .collect(Collectors.groupingBy(DeviceClockRecordDO::getUserId));
  569 +
  570 + // 4. 批量查工单
  571 + List<MainInfoDO> allOrders = mainInfoMapper.selectList(
  572 + new LambdaQueryWrapperX<MainInfoDO>()
  573 + .in(MainInfoDO::getWorkerId, userIds)
  574 + .between(MainInfoDO::getCommitDate, startDateTime, endDateTime));
  575 + Map<Long, List<MainInfoDO>> userOrderMap = allOrders.stream()
  576 + .collect(Collectors.groupingBy(MainInfoDO::getWorkerId));
  577 +
  578 + // 5. 批量查耗材
  579 + Set<String> allOrderNos2 = allOrders.stream().map(MainInfoDO::getOrderNo)
  580 + .filter(Objects::nonNull).collect(Collectors.toSet());
  581 + Map<String, List<MaterialDetailDO>> orderMaterialMap2;
  582 + if (CollUtil.isNotEmpty(allOrderNos2)) {
  583 + List<MaterialDetailDO> allMaterials = materialDetailMapper.selectList(
  584 + new LambdaQueryWrapperX<MaterialDetailDO>().in(MaterialDetailDO::getOrderNo, allOrderNos2));
  585 + orderMaterialMap2 = allMaterials.stream()
  586 + .collect(Collectors.groupingBy(MaterialDetailDO::getOrderNo));
  587 + } else {
  588 + orderMaterialMap2 = Collections.emptyMap();
  589 + }
  590 +
  591 + // 6. 构建人员考核VO
  592 + List<TeamAssessmentRespVO.StaffVO> staffList = new ArrayList<>();
  593 + for (AdminUserDO user : userList) {
  594 + TeamAssessmentRespVO.StaffVO staff = new TeamAssessmentRespVO.StaffVO();
  595 + staff.setUserId(user.getId());
  596 + staff.setUserName(user.getNickname());
  597 + staff.setMobile(user.getMobile());
  598 + staff.setDeptId(user.getDeptId());
  599 + staff.setDeptName(deptNameMap.getOrDefault(user.getDeptId(), ""));
  600 + staff.setPostNames(postNameMap.getOrDefault(user.getId(), ""));
  601 + staff.setCardNumber(cardNumberMap.getOrDefault(user.getId(), ""));
  602 + staff.setIsInner(user.getIsInner());
  603 +
  604 + List<DeviceClockRecordDO> clocks = userClockMap.getOrDefault(user.getId(), Collections.emptyList());
  605 + List<MainInfoDO> orders = userOrderMap.getOrDefault(user.getId(), Collections.emptyList());
  606 +
  607 + // 个人考核指标
  608 + staff.setAssessment(buildPersonalScore(clocks, orders, orderMaterialMap2,
  609 + startDateTime, endDateTime));
  610 +
  611 + // 考勤明细
  612 + if (includeDetail) {
  613 + staff.setAttendances(buildAttendanceRecords(clocks));
  614 + staff.setOrders(buildOrderList(orders, orderMaterialMap2, true));
  615 + }
  616 +
  617 + staffList.add(staff);
  618 + }
  619 + return staffList;
  620 + }
  621 +
  622 + // ==================== 辅助方法 ====================
  623 +
  624 + private void computeTeamScore(TeamAssessmentRespVO.TeamVO team,
  625 + List<MainInfoDO> orders, List<DeviceClockRecordDO> clocks,
  626 + List<AdminUserDO> members,
  627 + Map<String, List<MaterialDetailDO>> orderMaterialMap,
  628 + Map<Long, List<DeviceClockRecordDO>> userClockMap,
  629 + Map<Long, String> cardNumberMap,
  630 + LocalDateTime startDateTime, LocalDateTime endDateTime) {
  631 +
  632 + int totalDays = (int) Math.max(1,
  633 + java.time.Duration.between(startDateTime, endDateTime).toDays());
  634 +
  635 + // --- 工单统计 ---
  636 + long totalOrders = orders.size();
  637 + long completedOrders = orders.stream()
  638 + .filter(o -> "completed".equals(o.getBuzStatus())).count();
  639 + long overdueOrders = orders.stream()
  640 + .filter(o -> o.getExpectedFinishDate() != null && o.getFinishDate() != null
  641 + && o.getFinishDate().isAfter(o.getExpectedFinishDate()))
  642 + .count();
  643 + long lateSupplementOrders = orders.stream()
  644 + .filter(o -> o.getCommitDate() != null && o.getCreateTime() != null
  645 + && java.time.Duration.between(o.getCommitDate(), o.getCreateTime()).toHours() > 24)
  646 + .count();
  647 + long responseWithin30Min = orders.stream()
  648 + .filter(o -> o.getCommitDate() != null && o.getCreateTime() != null
  649 + && java.time.Duration.between(o.getCreateTime(), o.getCommitDate()).toMinutes() <= 30)
  650 + .count();
  651 +
  652 + TeamAssessmentRespVO.OrderStatsVO orderStats = new TeamAssessmentRespVO.OrderStatsVO();
  653 + orderStats.setTotalOrders(totalOrders);
  654 + orderStats.setCompletedOrders(completedOrders);
  655 + orderStats.setCompletionRate(totalOrders > 0
  656 + ? BigDecimal.valueOf(completedOrders * 100.0 / totalOrders).setScale(1, java.math.RoundingMode.HALF_UP)
  657 + : BigDecimal.ZERO);
  658 + orderStats.setOnlineRate(totalOrders > 0
  659 + ? BigDecimal.valueOf((totalOrders - lateSupplementOrders) * 100.0 / totalOrders)
  660 + .setScale(1, java.math.RoundingMode.HALF_UP)
  661 + : BigDecimal.ZERO);
  662 + orderStats.setResponseWithin30Min(responseWithin30Min);
  663 + orderStats.setResponseRate30Min(totalOrders > 0
  664 + ? BigDecimal.valueOf(responseWithin30Min * 100.0 / totalOrders)
  665 + .setScale(1, java.math.RoundingMode.HALF_UP)
  666 + : BigDecimal.ZERO);
  667 + orderStats.setOverdueOrders(overdueOrders);
  668 + orderStats.setLateSupplementOrders(lateSupplementOrders);
  669 + team.setOrderStats(orderStats);
  670 +
  671 + // --- 考勤统计 ---
  672 + int totalMembers = members.size();
  673 + long totalCheckins = clocks.size();
  674 + // 缺勤:每个成员应有2次/天,缺少的算缺勤
  675 + int expectedCheckinsPerPerson = totalDays * 2;
  676 + long absentCount = Math.max(0,
  677 + (long) totalMembers * expectedCheckinsPerPerson - totalCheckins) / 2;
  678 + // 未打卡:无工卡绑定的成员
  679 + long missingCardCount = members.stream()
  680 + .filter(m -> StrUtil.isBlank(cardNumberMap.getOrDefault(m.getId(), "")))
  681 + .count();
  682 + // 缺共同处理人:>2人任务但co_handlers为空
  683 + long missingCoHandlerCount = orders.stream()
  684 + .filter(o -> CollUtil.isEmpty(o.getCoHandlers()))
  685 + .count();
  686 + // 轨迹覆盖率不达标(暂用打卡次数<天数的用户数近似)
  687 + long trajectoryLowCount = members.stream()
  688 + .filter(m -> {
  689 + List<DeviceClockRecordDO> uc = userClockMap.getOrDefault(m.getId(), Collections.emptyList());
  690 + return uc.size() < totalDays;
  691 + })
  692 + .count();
  693 + // 一人多卡违规(同一天同一用户多张卡打卡)
  694 + boolean multiCardViolation = false;
  695 + for (List<DeviceClockRecordDO> uc : userClockMap.values()) {
  696 + Map<java.time.LocalDate, List<DeviceClockRecordDO>> byDay = uc.stream()
  697 + .collect(Collectors.groupingBy(c -> c.getCreateTime().toLocalDate()));
  698 + for (List<DeviceClockRecordDO> dayRecords : byDay.values()) {
  699 + Set<String> devices = dayRecords.stream()
  700 + .map(DeviceClockRecordDO::getDeviceCode)
  701 + .filter(Objects::nonNull).collect(Collectors.toSet());
  702 + if (devices.size() > 2) { multiCardViolation = true; break; }
  703 + }
  704 + if (multiCardViolation) break;
  705 + }
  706 +
  707 + TeamAssessmentRespVO.AttendanceStatsVO attStats = new TeamAssessmentRespVO.AttendanceStatsVO();
  708 + attStats.setTotalCheckins(totalCheckins);
  709 + attStats.setAbsentCount(absentCount);
  710 + attStats.setMissedCheckinCount(absentCount); // 缺勤=未打卡近似
  711 + attStats.setTrajectoryLowCount(trajectoryLowCount);
  712 + attStats.setMissingCoHandlerCount(missingCoHandlerCount);
  713 + attStats.setMissingCardCount(missingCardCount);
  714 + attStats.setMultiCardViolation(multiCardViolation);
  715 + attStats.setLeaderRecorderOffCount(0L); // 记录仪数据暂不可用
  716 + team.setAttendanceStats(attStats);
  717 +
  718 + // --- 耗材统计 ---
  719 + long ordersWithMaterial = orders.stream()
  720 + .filter(o -> orderMaterialMap.containsKey(o.getOrderNo())).count();
  721 + long ordersMissingMaterial = totalOrders - ordersWithMaterial;
  722 + long totalMaterialItems = orderMaterialMap.values().stream()
  723 + .flatMap(List::stream).mapToLong(m -> m.getUserCount() != null ? m.getUserCount() : 0).sum();
  724 + long materialTypes = orderMaterialMap.values().stream()
  725 + .flatMap(List::stream).map(MaterialDetailDO::getTypeName)
  726 + .filter(Objects::nonNull).distinct().count();
  727 +
  728 + TeamAssessmentRespVO.MaterialStatsVO matStats = new TeamAssessmentRespVO.MaterialStatsVO();
  729 + matStats.setOrdersWithMaterial(ordersWithMaterial);
  730 + matStats.setOrdersMissingMaterial(ordersMissingMaterial);
  731 + matStats.setTotalMaterialItems(totalMaterialItems);
  732 + matStats.setMaterialTypes(materialTypes);
  733 + team.setMaterialStats(matStats);
  734 +
  735 + // --- 考核得分计算 ---
  736 + // 工单得分(40分)
  737 + int orderDeduction = (int) (lateSupplementOrders * 1 + overdueOrders * 1);
  738 + int orderScore = Math.max(0, 40 - orderDeduction);
  739 +
  740 + // 考勤得分(30分)
  741 + int attDeduction = (int) (absentCount * 1 + missingCoHandlerCount * 1
  742 + + missingCardCount * 1 + attStats.getLeaderRecorderOffCount() * 1);
  743 + int attScore = Math.max(0, 30 - attDeduction);
  744 +
  745 + // 人机材得分(30分)
  746 + int matDeduction = (int) (ordersMissingMaterial * 2);
  747 + int matScore = Math.max(0, 30 - matDeduction);
  748 +
  749 + int totalScore = orderScore + attScore + matScore;
  750 +
  751 + TeamAssessmentRespVO.ScoreVO score = new TeamAssessmentRespVO.ScoreVO();
  752 + score.setTotal(totalScore);
  753 + score.setOrderScore(orderScore);
  754 + score.setAttendanceScore(attScore);
  755 + score.setMaterialScore(matScore);
  756 +
  757 + List<TeamAssessmentRespVO.DeductionVO> deductions = new ArrayList<>();
  758 + if (lateSupplementOrders > 0) {
  759 + TeamAssessmentRespVO.DeductionVO d = new TeamAssessmentRespVO.DeductionVO();
  760 + d.setRule("工单超24小时补录"); d.setCount((int) lateSupplementOrders);
  761 + d.setPoints((int) -lateSupplementOrders); deductions.add(d);
  762 + }
  763 + if (overdueOrders > 0) {
  764 + TeamAssessmentRespVO.DeductionVO d = new TeamAssessmentRespVO.DeductionVO();
  765 + d.setRule("工单超时>2小时"); d.setCount((int) overdueOrders);
  766 + d.setPoints((int) -overdueOrders); deductions.add(d);
  767 + }
  768 + if (absentCount > 0) {
  769 + TeamAssessmentRespVO.DeductionVO d = new TeamAssessmentRespVO.DeductionVO();
  770 + d.setRule("缺勤/未打卡"); d.setCount((int) absentCount);
  771 + d.setPoints((int) -absentCount); deductions.add(d);
  772 + }
  773 + if (missingCoHandlerCount > 0) {
  774 + TeamAssessmentRespVO.DeductionVO d = new TeamAssessmentRespVO.DeductionVO();
  775 + d.setRule(">2人任务缺共同处理人"); d.setCount((int) missingCoHandlerCount);
  776 + d.setPoints((int) -missingCoHandlerCount); deductions.add(d);
  777 + }
  778 + if (missingCardCount > 0) {
  779 + TeamAssessmentRespVO.DeductionVO d = new TeamAssessmentRespVO.DeductionVO();
  780 + d.setRule("未佩戴工卡"); d.setCount((int) missingCardCount);
  781 + d.setPoints((int) -missingCardCount); deductions.add(d);
  782 + }
  783 + if (ordersMissingMaterial > 0) {
  784 + TeamAssessmentRespVO.DeductionVO d = new TeamAssessmentRespVO.DeductionVO();
  785 + d.setRule("人机材24h内未上报"); d.setCount((int) ordersMissingMaterial);
  786 + d.setPoints((int) (-ordersMissingMaterial * 2)); deductions.add(d);
  787 + }
  788 + score.setDeductions(deductions);
  789 + team.setScore(score);
  790 + }
  791 +
  792 + private List<TeamAssessmentRespVO.MemberVO> buildMemberList(
  793 + List<AdminUserDO> members,
  794 + Map<Long, List<DeviceClockRecordDO>> userClockMap,
  795 + List<MainInfoDO> deptOrders,
  796 + Map<String, List<MaterialDetailDO>> orderMaterialMap,
  797 + Map<Long, String> postNameMap, Map<Long, String> cardNumberMap,
  798 + LocalDateTime startDateTime, LocalDateTime endDateTime,
  799 + boolean includeDetail) {
  800 +
  801 + Map<Long, List<MainInfoDO>> userOrderMap = deptOrders.stream()
  802 + .collect(Collectors.groupingBy(MainInfoDO::getWorkerId));
  803 +
  804 + List<TeamAssessmentRespVO.MemberVO> voList = new ArrayList<>();
  805 + for (AdminUserDO user : members) {
  806 + TeamAssessmentRespVO.MemberVO vo = new TeamAssessmentRespVO.MemberVO();
  807 + vo.setUserId(user.getId());
  808 + vo.setUserName(user.getNickname());
  809 + vo.setMobile(user.getMobile());
  810 + vo.setPostNames(postNameMap.getOrDefault(user.getId(), ""));
  811 + vo.setCardNumber(cardNumberMap.getOrDefault(user.getId(), ""));
  812 + vo.setIsInner(user.getIsInner());
  813 +
  814 + List<DeviceClockRecordDO> clocks = userClockMap.getOrDefault(user.getId(), Collections.emptyList());
  815 + List<MainInfoDO> orders = userOrderMap.getOrDefault(user.getId(), Collections.emptyList());
  816 +
  817 + // 个人考核指标
  818 + vo.setPersonalScore(buildPersonalScore(clocks, orders, orderMaterialMap,
  819 + startDateTime, endDateTime));
  820 +
  821 + // 明细
  822 + if (includeDetail) {
  823 + vo.setAttendances(buildAttendanceRecords(clocks));
  824 + vo.setOrders(buildOrderList(orders, orderMaterialMap, true));
  825 + }
  826 +
  827 + voList.add(vo);
  828 + }
  829 + return voList;
  830 + }
  831 +
  832 + private TeamAssessmentRespVO.PersonalScoreVO buildPersonalScore(
  833 + List<DeviceClockRecordDO> clocks, List<MainInfoDO> orders,
  834 + Map<String, List<MaterialDetailDO>> orderMaterialMap,
  835 + LocalDateTime startDateTime, LocalDateTime endDateTime) {
  836 +
  837 + int totalDays = (int) Math.max(1,
  838 + java.time.Duration.between(startDateTime, endDateTime).toDays());
  839 +
  840 + // 出勤天数(有打卡记录的天数)
  841 + long attendanceDays = clocks.stream()
  842 + .map(c -> c.getCreateTime().toLocalDate()).distinct().count();
  843 + // 迟到次数(上班打卡晚于9点的次数)
  844 + long lateCount = clocks.stream()
  845 + .filter(c -> "上班".equals(c.getPunchType()))
  846 + .filter(c -> c.getCreateTime().toLocalTime().isAfter(java.time.LocalTime.of(9, 0)))
  847 + .count();
  848 + long absentCount = Math.max(0, totalDays - attendanceDays);
  849 +
  850 + long assignedOrders = orders.size();
  851 + long completedOrders = orders.stream()
  852 + .filter(o -> "completed".equals(o.getBuzStatus())).count();
  853 + long overdueOrders = orders.stream()
  854 + .filter(o -> o.getExpectedFinishDate() != null && o.getFinishDate() != null
  855 + && o.getFinishDate().isAfter(o.getExpectedFinishDate()))
  856 + .count();
  857 + long materialReported = orders.stream()
  858 + .filter(o -> orderMaterialMap.containsKey(o.getOrderNo())).count();
  859 + long materialMissing = assignedOrders - materialReported;
  860 +
  861 + TeamAssessmentRespVO.PersonalScoreVO score = new TeamAssessmentRespVO.PersonalScoreVO();
  862 + score.setAttendanceDays(attendanceDays);
  863 + score.setLateCount(lateCount);
  864 + score.setAbsentCount(absentCount);
  865 + score.setAssignedOrders(assignedOrders);
  866 + score.setCompletedOrders(completedOrders);
  867 + score.setCompletionRate(assignedOrders > 0
  868 + ? BigDecimal.valueOf(completedOrders * 100.0 / assignedOrders)
  869 + .setScale(1, java.math.RoundingMode.HALF_UP)
  870 + : BigDecimal.ZERO);
  871 + score.setOverdueOrders(overdueOrders);
  872 + score.setMaterialReported(materialReported);
  873 + score.setMaterialMissing(materialMissing);
  874 + return score;
  875 + }
  876 +
  877 + private List<TeamAssessmentRespVO.AttendanceRecordVO> buildAttendanceRecords(
  878 + List<DeviceClockRecordDO> clocks) {
  879 + return clocks.stream().map(c -> {
  880 + TeamAssessmentRespVO.AttendanceRecordVO vo = new TeamAssessmentRespVO.AttendanceRecordVO();
  881 + vo.setPunchType(c.getPunchType());
  882 + vo.setPunchTime(c.getCreateTime());
  883 + vo.setAddress(c.getAddress());
  884 + return vo;
  885 + }).collect(Collectors.toList());
  886 + }
  887 +
  888 + private List<TeamAssessmentRespVO.OrderVO> buildOrderList(
  889 + List<MainInfoDO> orders,
  890 + Map<String, List<MaterialDetailDO>> orderMaterialMap,
  891 + boolean includeMaterial) {
  892 +
  893 + return orders.stream().map(o -> {
  894 + TeamAssessmentRespVO.OrderVO vo = new TeamAssessmentRespVO.OrderVO();
  895 + vo.setOrderNo(o.getOrderNo());
  896 + vo.setOrderName(o.getOrderName());
  897 + vo.setOrderType(o.getOrderType());
  898 + vo.setWorkerName(o.getWorkerName());
  899 + vo.setStatus(o.getStatus());
  900 + vo.setBuzStatus(o.getBuzStatus());
  901 + vo.setCommitDate(o.getCommitDate());
  902 + vo.setFinishDate(o.getFinishDate());
  903 + vo.setExpectedFinishDate(o.getExpectedFinishDate());
  904 + // 响应时长
  905 + if (o.getCommitDate() != null && o.getCreateTime() != null) {
  906 + vo.setResponseMinutes(java.time.Duration.between(
  907 + o.getCreateTime(), o.getCommitDate()).toMinutes());
  908 + }
  909 + vo.setIsOverdue(o.getExpectedFinishDate() != null && o.getFinishDate() != null
  910 + && o.getFinishDate().isAfter(o.getExpectedFinishDate()));
  911 + vo.setIsLateSupplement(o.getCommitDate() != null && o.getCreateTime() != null
  912 + && java.time.Duration.between(o.getCommitDate(), o.getCreateTime()).toHours() > 24);
  913 + vo.setHasMaterialReported(orderMaterialMap.containsKey(o.getOrderNo()));
  914 + if (CollUtil.isNotEmpty(o.getCoHandlers())) {
  915 + vo.setCoHandlers(new ArrayList<>(o.getCoHandlers().stream()
  916 + .map(String::valueOf).collect(Collectors.toList())));
  917 + }
  918 + // 耗材
  919 + if (includeMaterial) {
  920 + List<MaterialDetailDO> mats = orderMaterialMap.getOrDefault(
  921 + o.getOrderNo(), Collections.emptyList());
  922 + vo.setMaterials(mats.stream().map(m -> {
  923 + TeamAssessmentRespVO.MaterialVO mv = new TeamAssessmentRespVO.MaterialVO();
  924 + mv.setMaterialName(m.getMaterialName());
  925 + mv.setSpecifications(m.getSpecifications());
  926 + mv.setUserCount(m.getUserCount());
  927 + mv.setUnitName(m.getUnitName());
  928 + mv.setClassifyName(m.getClassifyName());
  929 + return mv;
  930 + }).collect(Collectors.toList()));
  931 + }
  932 + return vo;
  933 + }).collect(Collectors.toList());
  934 + }
  935 +
  936 + private Map<Long, String> buildPostNameMap(List<AdminUserDO> users) {
  937 + Set<Long> allPostIds = users.stream()
  938 + .filter(u -> CollUtil.isNotEmpty(u.getPostIds()))
  939 + .flatMap(u -> u.getPostIds().stream())
  940 + .collect(Collectors.toSet());
  941 + return CollUtil.isNotEmpty(allPostIds)
  942 + ? postMapper.selectList(PostDO::getId, allPostIds).stream()
  943 + .collect(Collectors.toMap(PostDO::getId, PostDO::getName, (a, b) -> a))
  944 + : Collections.emptyMap();
  945 + }
  946 +
  947 + private Map<Long, String> buildCardNumberMap(Set<Long> userIds) {
  948 + return CollUtil.isNotEmpty(userIds)
  949 + ? deviceUserMapper.selectList(
  950 + new LambdaQueryWrapperX<DeviceUserDO>().in(DeviceUserDO::getUserId, userIds))
  951 + .stream().collect(Collectors.toMap(DeviceUserDO::getUserId,
  952 + d -> StrUtil.blankToDefault(d.getDeviceNo(), ""), (a, b) -> a))
  953 + : Collections.emptyMap();
  954 + }
382 } 955 }