Commit 70a63b781a80532fbe994946d6dc7b870d7e6144
1 parent
116f6bb0
feat(garden): 添加团队考核查询功能
- 新增 TeamAssessmentReqVO 和 TeamAssessmentRespVO 数据传输对象 - 在 OpenApiService 中添加 queryTeamAssessment 接口定义 - 实现团队考核查询逻辑,支持按班组和人员两种模式查询 - 添加考勤统计、工单统计、耗材统计等考核指标计算 - 创建 TeamAssessmentOpenController 提供开放 API 接口 - 实现个人考核指标和明细数据查询功能 - 添加扣分规则和考核得分计算机制
Showing
16 changed files
with
6392 additions
and
34 deletions
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&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">总结 & Q&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 & 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">🏙</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="上一页">←</button> | ||
| 810 | + <span class="page-indicator" id="pageIndicator">1 / 10</span> | ||
| 811 | + <button onclick="nextSlide()" title="下一页">→</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&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">总结 & Q&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 & 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 "https://test.jichengshanshui.com.cn:28302/open-api/staff/query?dept | @@ -238,7 +630,7 @@ curl -X GET "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 "https://test.jichengshanshui.com.cn:28302/open-api/teams/query" \ | @@ -306,7 +698,7 @@ curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/teams/query" \ | ||
| 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 "https://test.jichengshanshui.com.cn:28302/open-api/teams/query" \ | @@ -379,7 +771,7 @@ curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/teams/query" \ | ||
| 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 "https://test.jichengshanshui.com.cn:28302/open-api/teams/query" \ | @@ -472,7 +864,7 @@ curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/teams/query" \ | ||
| 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 "https://test.jichengshanshui.com.cn:28302/open-api/teams/query" \ | @@ -549,7 +941,7 @@ curl -X GET "https://test.jichengshanshui.com.cn:28302/open-api/teams/query" \ | ||
| 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 "https://test.jichengshanshui.com.cn:28302/open-api/full-order-stati | @@ -621,18 +1013,16 @@ curl -X GET "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 "https://test.jichengshanshui.com.cn:28302/open-api/full-order-stati | @@ -641,11 +1031,11 @@ curl -X GET "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 "https://test.jichengshanshui.com.cn:28302/open-api/full-order-stati | @@ -654,9 +1044,9 @@ curl -X GET "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 | } |