Commit c3fda6943b2e1741dc729b94e1dd00e8d4c36a7e

Authored by 王彪总
1 parent 604bda05

1

Showing 60 changed files with 2550 additions and 68 deletions
MIGRATION.md
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 - [x] 新建前端仓库(官方最新) 16 - [x] 新建前端仓库(官方最新)
17 - [x] 设计稿落盘 17 - [x] 设计稿落盘
18 - [x] 数据库拷贝 + 上游表准备(`parking_cloud_yudao`,重名旧表改 `pcbak_system_*`) 18 - [x] 数据库拷贝 + 上游表准备(`parking_cloud_yudao`,重名旧表改 `pcbak_system_*`)
19 -- [x] 上游模块裁剪(只保留 system / infra / iot) 19 +- [x] 上游模块裁剪(只保留 system / infra / iot;2026-11-04 进一步关闭 iot,见 §8.0)
20 - [x] parking 业务模块搬迁(10 个模块编译通过) 20 - [x] parking 业务模块搬迁(10 个模块编译通过)
21 - [x] 系统兼容桥(`LoginUserBridge` 打通 yudao 登录态 → 存量 `UserInfo`) 21 - [x] 系统兼容桥(`LoginUserBridge` 打通 yudao 登录态 → 存量 `UserInfo`)
22 - [x] 前端业务页面搬迁(park/order/finance/report/ops/... 已进 `src/views`,构建通过) 22 - [x] 前端业务页面搬迁(park/order/finance/report/ops/... 已进 `src/views`,构建通过)
@@ -25,10 +25,38 @@ @@ -25,10 +25,38 @@
25 - [x] **与老环境隔离:新服务 Redis 用 db1(老服务 db0)+ `scripts/warm-cache.sh` 缓存预热**(2026-11-02) 25 - [x] **与老环境隔离:新服务 Redis 用 db1(老服务 db0)+ `scripts/warm-cache.sh` 缓存预热**(2026-11-02)
26 - [x] **菜单 path 归一化:子页改相对路径,侧边栏不再出现 `/park/park/park-lot` 404**(2026-11-02,见 §7.4) 26 - [x] **菜单 path 归一化:子页改相对路径,侧边栏不再出现 `/park/park/park-lot` 404**(2026-11-02,见 §7.4)
27 - [x] **业务页全局样式回流:令牌/版式类映射到芋道主题(`src/styles/parking-pages.scss`)**(2026-11-02,见 §7.3 #5) 27 - [x] **业务页全局样式回流:令牌/版式类映射到芋道主题(`src/styles/parking-pages.scss`)**(2026-11-02,见 §7.3 #5)
28 -- [ ] 菜单/权限收口(462 个 `@PreAuthorize` 权限串尚无菜单记录;admin 不受影响)  
29 -- [ ] 调度去重(48082 与 48083 同时跑存量 `@Scheduled`)  
30 -- [ ] 组织 id 空间打通(`system_org` ↔ 老 `sys_org`)  
31 -- [ ] 视频接入(WVP-PRO + ZLMediaKit)与 IoT 时序存储(TDengine) 28 +- [x] **存量定时任务去重:`parking.legacy.scheduler.enabled` 总开关,默认 false,6 个活跃任务全部停跑**(2026-11-02,见 §7.5)
  29 +- [x] **指挥中心:按「赤峰市智慧停车数据中心」大屏重做(通栏 7 KPI;左:供给结构/联网接入/重点车场/时长/收入排行;中:全市 12 旗县区地图(有实时数据的区按占用率着色 + 四角标注卡引线)+ 24h 车流;右:紧凑进出场列表/用户与支付(图标 + 环形图)/运行预警(图标);1280–4K 分辨率自适应),旧版大屏保留入口**(2026-11-02 版式,见 §7.6)
  30 +- [x] **指挥中心接真实数据**:新增 `POST /park/command-center/overview` 单接口聚合(`ParkCommandCenterController/Mapper/ServiceImpl`,权限沿用 `park:dashboard:query`,不新增权限点、不授权角色),前端 `demo.ts` 删除、改 `mapper.ts` + `api/park/commandCenter`,60 秒轮询;支付构成取在线表 `pay_order`(**不是** `td_b_pay_order`),实时泊位/用户/重点车场无数据时整块「暂无数据」不补零(2026-11-06,见 §7.6)
  31 +- [x] **菜单/权限收口:672 个权限串全覆盖,3 个角色模板 + 3 个联调账号,接口矩阵验证通过**(2026-11-02,见 `scripts/README-permissions.md`)
  32 +- [x] **组织 id 空间统一:`sys_org` 真实 id 迁入 `system_org`,用户指向真实组织,去掉 `org-id-mapping` 换算**(2026-11-02,见 §7.7 与 `scripts/README-org.md`)
  33 +- [x] **首页监管总览:`POST /park/dashboard/overview` 单接口聚合 8 个区块,前端按 `01-dashboard.html` 重做首页**(2026-11-02,见 §7.8)
  34 +- [x] **合同缺口页:监管中心(磁贴卡片导航)、重点车场占用率监测(`/park/keyLotMonitor`)、实时停车流量监控(`/park/flowMonitor`)**;后者新增 `POST /park/flow-monitor/overview` 分钟级聚合接口(权限点 `park:flow-monitor:query`,菜单 100505/100506),窗口无数据时 `hasData=false` + `dataThrough` 提示数据停更(2026-11-05,见 `docs/superpowers/specs/2026-11-04-合同缺口与页面设计.md`)
  35 +- [x] **综合监测 `/park/monitor`(接入真实接口)**:新增 `ParkMonitorController`(`POST /park/monitor/realtime-overview`,请求 `{region?, period?}`,权限沿用 `park:dashboard:query`,**仍不授权角色 2001**),一次返回汇总、12 旗县区榜、车场点位(含 `coordType`)与趋势;`ParkMonitorMapper` 取 `parking_lot` 在用且 `pl_city_name='赤峰市'` 的车场(963 个),实时泊位取 `parking_lot_berth_temp_info`,趋势取 `parking_running_statistic`(`data_source=10`,整点 40 / 按日 50)。**数据诚实**:无实时数据的车场占用率为 `null` → 页面「—」,区县榜固定补齐 12 区、缺数据是「无数据」而非 0,趋势无统计行时整段空态不补零;`dataThrough` 展示最近一条运行统计时间(本地库为 `2026-01-31 23:59:59`,实时快照仅 1 个车场有行)。前端 `mapper.ts` 做响应 → 页面模型(字符串数字、12 区补齐、百度/WGS84 → GCJ02);地图可缩放、点区县下钻、点车场开抽屉、30 秒自动刷新、全屏、Excel 导出(全量),右侧列表最多渲染 200 个车场;刷新失败保留旧数据并给重试。菜单 100507 / 权限按钮 100508(2026-11-06,见设计方案 §2.2)
  36 +- [x] **车辆追溯 `/park/vehicleTrace`(接入真实接口)**:新增 `ParkVehicleTraceController`(`POST /park/vehicle-trace/query`、`POST /park/vehicle-trace/plate-suggest`,权限 `park:trace:query`,菜单 100509 / 按钮 100510,**不自动授权任何角色**);`ParkVehicleTraceMapper` 读 `order_park`(`data_state = 1`,`park_in_time` 落在所选范围),完整车牌走 `order_park_index_car_number`(≈5 ms),精确落空才做一次后缀匹配(`DISTINCT + LIMIT` 借索引提前结束,≈0.4 s),命中多辆返回 `ambiguous + candidates` 由用户选;金额取 `order_act_fee`(分)、时长按出场 − 入场现算、未出场为 `null`;车场名 / 区县取 `parking_lot`,坐标取一条有效 `parking_lot_geo`,`coordType` 由前端统一转 GCJ02。输入候选只扫范围内最近 2 万条记录(≈30–60 ms),故**只给最近入场时间、不给记录条数**;单次最多返回 5000 条(超出 `truncated = true`)。前端数据源改为 `mapper.ts`(TDD 门禁 `tests/verify-vehicle-trace-mapper.mjs`)+ `src/api/park/vehicleTrace`,默认范围改为「今天往前 30 天」,改范围 400ms 防抖自动重查,`?plate=` 深链自动查询,加载 / 失败(保留上次结果)/ 空结果 / 多车候选分别展示;`demo.ts`(车辆追溯 + 综合监测)已删除。已冒烟:蒙D72094 近 30 天 199 条 / ¥382、`2094` 唯一后缀命中、`8888` 命中 10 辆给候选、深链与改范围重查(2026-11-06,见设计方案 §2.7)。**地址栏同步坑**:`AppView.vue` 用 `route.meta.viewKey || route.fullPath` 当组件 key,页面里 `router.replace({ query })` 会换 key → 整页重挂载 → `onMounted` 再按 `?plate=` 查一次(一次点击两次查询)。车辆追溯改为 `resolve(...).href` + `window.history.replaceState` 只改地址栏,不再触发路由导航;新页面若要回写 query 需同样处理或给路由配稳定 `viewKey`。**16:9 一屏(2026-11-06)**:页头加「全屏 / 退出全屏」(`requestFullscreen` + `fullscreenchange` 里 `nextTick` 重算地图),`.vt:fullscreen` 铺满 100vw×100vh;`@media (max-height: 980px)` 改为紧凑定高(压 padding / 字号、隐藏 KPI 备注),页面始终一屏、时间轴与明细表内部滚动。**不要再把该断点改回 `height: auto`** —— 时间轴一条约 90px,200 条会把页面撑到约 19,000 px,底部明细表不可达。已实测 1366×768 / 1600×900 / 1920×1080 无页面滚动;同日复核其余 8 张大屏/监测页(监管中心、指挥中心、一张图、重点车场占用率、专题分析、实时流量、视频监控、综合监测)在同样尺寸下均已一屏适配,无需改动。**(2026-11-06 复核时该结论对监管中心不成立:当时量的是 `document.documentElement.scrollHeight`,页面实际在 `.el-scrollbar__wrap` 里溢出 182px / 83px,已修,见 §7.14。)**同日还修了 16:9 下暴露的两类版式问题:① `.stat-strip` 默认 5 列,只有 4 张卡时未加 `cols-4` 会在右侧留一格灰底空白(反馈管理、设备管理充电桩 / 设备原始数据 / 网关 / 诱导屏 / PDA 五个页签);② 表格列全部写死 `width` 时表格不铺满卡片,右侧出现无表头空白带(区域月卡、违停道路、违章记录、物联网卡流量四个表各留一列 `min-width`),四个表名下的列定义改完在 1366×768 与 1920×950 实测已铺满。规则见设计方案 §0.3.1。
  37 +- [x] **公告管理 `/park/notice`(接入真实 `sys_notice`)**:新增 `ParkNoticeController`(`/system/notice`,`park:notice:query|create|update|delete|publish`,**不自动授权任何角色**),读走存量 `SysNoticeService#queryNoticeForPage`、写走 `saveNotice`/`updateNotice`,只碰 `sys_notice` 的 `notice_big_type = 300` 行 —— 小程序 `POST /system/app-news/query-app-news-for-page` 一字未改,后台发布/下线即小程序可见/不可见。列表强制 `showAll`(含待发布、已下线),状态由 `eff_date`/`exp_date` 推导;`eff_date`/`exp_date` 在库里 NOT NULL,故**没有草稿态**:发布时间留空=立即发布、失效时间留空=发布+10 年;删除为逻辑删除 `data_state = 0`。存量表没有「置顶 / 附件」列,页面不再展示这两个概念;封面图可上传(复用 `/admin-api/infra/file/upload`)也可直接粘贴地址。类名带 `Park` 前缀是因为上游也有 `NoticeController`/`NoticeServiceImpl`,同组件扫描下同名会 `ConflictingBeanDefinitionException`。已冒烟:新增 → 小程序列表可见 → 下线不可见 → 再发布可见 → 删除后台/小程序均消失(2026-11-06,见设计方案 §2.10)
  38 +- [x] **反馈管理 `/park/feedback`(接入真实 `td_c_cust_suggest` + `td_c_cust_suggest_detail`)**:新增 `ParkFeedbackController`(`/pay/feedback`,`park:feedback:query|reply`,**不自动授权任何角色**),读走存量 `TdCCustSuggestService#queryFeedbackAndSuggestForPage`(固定 `state = 1`、按 `create_date desc`)与 `TdCCustSuggestDetailQueryService`,写走存量 `TdCCustSuggestDetailOperService#saveTdCCustSuggestDetail` + `TdCCustSuggestService#updateSuggestIsAnswer(1)` + 极光推送 —— 与老门户「客户反馈建议管理」同一条链路,后台答复后小程序「我的反馈」看到同一份数据。**状态只有两种**(`is_answer` 0 待处理 / 1 已回复,存量没有「已处理」动作),页面已删掉「标记已处理」与对应 `park:feedback:handle` 用法。存量查询会把登录组织追加进条件,而 `system_users.org_id` 指向平台根组织、历史数据 `org_id` 是业务组织,照抄会查出 0 条,故**反馈不按登录组织过滤**(全市台账)。页面保留:4 项汇总(总数 / 待处理 / 已回复 / 超时未处理,口径不受筛选影响)、待处理优先排序、超 24 小时标「超时」、手机号脱敏、最新平台答复回填、详情抽屉展示真实图片(FastDFS 前缀 + `UrlHandler`)与车主/平台往来记录、可补充答复、Excel 导出(id 按字符串返回避免雪花号精度丢失;单页上限 500,前端自动翻页)。副表 `suggest_id` 是 varchar、主表 id 是 bigint,按字符串比较;平台侧答复批量取 `user_type = 2`、`create_date asc` 后最后一条为准。已冒烟:真实 501 条列表、抽屉、答复后状态转已回复、往来记录渲染(合成行已清理)(2026-11-06,见设计方案 §2.11)
  39 +- [x] **大屏配色统一**:7 张在役大屏全部改用 `src/styles/bigscreen.scss` 的 `--bs-*` 色板与同一套「深空蓝 + 蓝色辉光 + 点阵」底纹,区县中心四区颜色对齐色板(2026-11-05,见设计方案 §0.1.1)
  40 +- [x] **本地演示数据(其余 11 个旗县区)**:`scripts/gen-demo-district-data.py` 给本地副本库补 69 个演示车场 / 12,490 个泊位 + 当日订单 / 支付 / 实时泊位 / 运行统计,`--apply` 可重复执行、`--clean` 可整批回滚;红山区只补缓存与统计表、不动真实车场与订单。指挥中心因此从「红山区单区 + 空态」变成 12 区齐全(109 车场 / 16,210 泊位 / 今日 21,710 进场 / 收入 24,643 元)(2026-11-06,见 §7.11)
  41 +- [x] **重点车场占用率监测接真实数据**:存量 `query-parkinglot-basic-facts-by-pl-nos` 的实时泊位在 Redis 快照缺失时由 `LegacyRealtimeBerthFiller` 从 `parking_lot_berth_temp_info` 兜底,今日进出改由 `order_park` 现算(Redis 有值时整段跳过,不改生产行为);页面 47 监测车场 / 11 超阈值 / 平均占用率 74.4%,Top 20、24 小时曲线、泊位周转与平均占用率均有值,1366×768 曲线不再被面板裁掉(2026-11-06,见 §7.12)
  42 +- [x] **指挥中心「各区联网接入进度」12 个旗县区一屏看全**:原来「区名 / 已接入数」两行一列,1080p 下只能露 5~6 行;改成单行 + 自动分栏(列宽 ≥120px 就两栏),并给同心环补一块中心圆盘垫住数字,1728 以下单栏滚动时底部渐隐;同页还修了 1366×768 整页多出的 50px 滚动(矮屏定高漏减 `--app-footer-height`,把页脚顶出视口)(2026-11-06,见 §7.13)
  43 +- [x] **综合监测「区县占用率」13 行一屏**:两行排压成「场数并入首行 + 进度条单独一行」,行高 47 → 29px,1080p 左栏 403=403 全部可见(原来只能看到 8 个区县,红山区等 5 个要滚动);≤960 高度加底部渐隐(2026-11-06,见 §7.17)
  44 +- [x] **一张图 / 专题分析的演示夹具补齐 11 个旗县区**:夹具原来只有 3 个区 14 个车场,一张图 9 个区县标「未接入」、专题分析下拉 11 个区县「(暂无车场)」,与读库的指挥中心 / 综合监测对不上;按演示库原样补 `districtSeeds` 67 条(名称 / 坐标 / 泊位 / 空余 / 今日进出)→ 全市 81 车场,12 个区县全部有数;视频监控设备树顺带补齐为 12 个区县 / 112 台(2026-11-06,见 §7.18)
  45 +- [x] **一张图入场动效(色调不变,3 秒加载完成)**:地图从中间「开花」铺开(`clip-path` 圆 + `scale(0.55→1)`),左右面板的卡片**从上到下一张张**滑入(左栏从左往右、右栏从右往左,每张错峰 140ms),底部卡片从下往上浮起;九宫格指标跟「资源总览」那张卡一起从 0 滚到真值,只播一次(切区县 / 刷新 / 轮询直接给值)。**地图改成铺满 `.om-body` 的整块背景**,去掉了原来的 1px 边框与底色,左右面板浮在上面(绝对定位后左右栏必须显式 `grid-column`,否则右栏会滑到中间盖住地图)。节奏全部由 `src/views/gov/shared/enter-motion.ts` 决定,`tests/verify-one-map-motion.mjs` 钉住顺序、逐张递增与 3 秒预算;实测刷新到全部完成 2992ms。配色仍是深空蓝,1920×1080 / 1366×768 均一屏(2026-11-06,见 §7.21)
  46 +- [x] **三张高德地图换蓝色底图**:一张图 / 专题分析 / 视频监控原来用 `amap://styles/darkblue`,实测地块取色 **0,0,1(纯黑)**,跟深空蓝大屏对不上;改成高德内置的 `amap://styles/blue`(同一套矢量样式里唯一的蓝底,路网 / 水系仍可见),三处同时改,页面高度不变(2026-11-06,见 §7.20)
  47 +- [x] **列表页本地分页(反馈 / 公告 / 视频设备)**:三个页面的接口都一次拉全量、页面上本地筛选,表格把全部行铺出来 —— 反馈 501 行一页 **21,483px**、视频设备 112 行溢出 428px、公告 19 行溢出 174px。新增共享 `src/utils/page.ts#pagedSlice`(越界页码夹到最后一页、每页 0 条按 1 条算),三页统一 **10 条/页 + 改筛选回第 1 页 + 条数变少夹回最后一页**,序号列跟着页码走;`tests/verify-feedback-manage.mjs` 守住切片边界。12 张演示页在 1920×1080 与 1366×768 实测:8 张大屏/监测页 `scrollHeight == clientHeight`,3 张列表页 1920 下一屏、1366 下按普通后台列表纵向滚动(2026-11-06,见 §7.19)
  48 +- [x] **实时停车流量监控区分「停更」与「归零」**:库内最后一条比现在旧 5 分钟以上时,「近 5 分钟入场/出场」显示「—」+「数据停更」芯片,不再拿 0 冒充「确实没车」;纯函数 `staleMinutesOf` 走 `tests/verify-flow-monitor.mjs` TDD 门禁(2026-11-06,见 §7.16)
  49 +- [x] **演示数据重灌流程 + 指挥中心占用率对齐**:演示订单一律写「当天」,隔天看板必空,演示前固定 `gen-demo-district-data.py --apply` + `scripts/warm-cache.sh` 两步;脚本修掉「当前小时订单挤进最后 30 分钟」造成的近 60 分钟假峰;指挥中心占用率与综合监测对齐到 **71.3%**(放宽 `freeBerth` 对 `berth_num` 的要求、分母回落到车场登记泊位,精度由 1 位小数改 3 位)(2026-11-06,见 §7.15)
  50 +- [x] **监管中心三行磁贴一屏适配**:§7.6 里「监管中心已一屏适配」是误判(量的是 `document.documentElement`,页面实际在 `.el-scrollbar__wrap` 里滚);实测 1920×1080 溢出 83px、1600×900 溢出 79px、1366×768 溢出 182px。960 档只收标签行、820 档再收紧卡内留白与图标,全局再收图标上限 / 行高 / 网格间距,卡高 251 → 226,四档均 `scrollHeight == clientHeight`(2026-11-06,见 §7.14)
  51 +- [x] **一张图 `/park-map` 接真实接口**:新增 `POST /park/resource-map/overview`(权限沿用 `park:park-lot-manage:query`,**不自动授权任何角色**),车场 / 泊位 / 台账 / 设备 / 进出场全部来自 `parking_cloud_yudao`;**台账与点位分开报**(路侧泊位台账 2,575 个无坐标,只报数不上图;地磁台账 2,097 台、能上图 673 台),60 秒静默刷新(2026-11-09,见 §7.28)
  52 +- [x] **专题分析 `/park-map/analysis` 接真实接口**:新增 `POST /park/topic-analysis/{coverage|duration|flow|heat|hotspot|price|revenue}`(权限 `park:topic-analysis:query`,菜单 100501,**不自动授权任何角色**),7 个专题全部来自 `parking_cloud_yudao`;24 小时帧一次返回、热点网格仍由前端算;前端改 `mapper.ts` + `api/park/topicAnalysis`,下拉 12 个旗县区不再按夹具标「暂无车场」;366 天区间 flow 7.38 → 2.38 s、heat 3.37 → 1.27 s(2026-11-09,见 §7.29)
  53 +- [x] **视频接入(WVP-PRO)**:WVP-PRO 3.0.0 已合并为 `parking-module-video`,随 `yudao-server` 启动,yudao 登录态即视频平台登录态(见 §8.1);前端实时预览 / 通道树 / 回放 / 云台 / 抓图 / 片段下载已移植到 `/video/monitor`;ZLMediaKit 仍需单独部署
  54 +- [x] **视频监控二级分组改读 WVP 业务分组 + 新增「通道分组」页(100532)**:左侧树二级节点由演示夹具的车场名改成通道 `gbBusinessGroupId` 翻出的分组名(区县仍按 `civilCode`,没挂分组的归「未分组」),并补上分组维护页(新建 215 / 子节点 216 / 改名 / 删除 / 挂通道);点播仍走设备→通道接口,不受分组影响(2026-11-09,见 §8.2.2)
  55 +- [x] **一张图加「重点车场」「实时流量」入口 + 去掉「未定位资源」「全屏打开」**:`PAGE_NAV` 5 → 7 项,新两项和专题分析 / 视频监控 并列、同样在弹框里打开(权限沿用 `park:dashboard:query` / `park:flow-monitor:query`,**不新增权限、不授权任何角色**);资源清单里的「未定位资源」按钮 + 弹框 + 样式整块删除;弹框右下角的「全屏打开」按钮删除,只留关闭(2026-11-09,见 §7.30)
  56 +- [x] **前端删掉上游「文档地址」广告条**:350 个 `.vue` 里 389 处 `<doc-alert>`(「【功能权限】文档地址:https://doc.iocoder.cn/…」)连同组件一起删除,登录页推广区、用户菜单「文档」项、901 错误弹窗的教程链接同步去掉;新增 `tests/verify-no-doc-links.mjs` 守着,`src/` 里再出现 `<doc-alert` 或 `doc.iocoder.cn` 即失败(2026-11-09,见 §7.31)
  57 +- [x] **组织管理只保留「赤峰市住建」+「赤峰中智」**:`system_org` 152 个有效组织里软删其余 150 个(`del_flag=1`,与 `OrgServiceImpl.deleteOrg` 的 `@TableLogic` 行为一致),`system_dept` 镜像同步软删;唯一挂在被删组织(赤峰市医院事业部 10228)下的车场 12303 改挂赤峰中智;4 个用户都在 10001/10003,不受影响(2026-11-09,见 §7.32)
  58 +- [x] **车场列表支持「修改车场 + 车场照片」**:`/park/park-lot` 行操作新增「编辑」弹窗(17 个字段,不传经纬度、组织原样回传),照片支持上传/删除;编辑与照片端点全部复用已迁好的 `ParkLotManageController`,不新增权限;本机新增磁盘存储实现(`parking.legacy.oss.mode=local` + `@PermitAll` 读取端点),生产不配则仍走阿里云 OSS / FastDFS(2026-11-10,见 §7.33)
  59 +- [x] **车场列表支持「新增车场」**:`/park/park-lot` 工具栏新增按钮,复用 `save-parking-lot-info`(`:create`,权限记录 310003 早已存在),编码由后端 `generatorParkNo(state)` 生成、组织按登录人回填;经纬度选填但必须成对,后端补了与修改接口一致的判空,避免不填坐标就 500(2026-11-10,见 §7.34)
32 60
33 ## 2. 关键决策(已定) 61 ## 2. 关键决策(已定)
34 62
@@ -38,13 +66,14 @@ @@ -38,13 +66,14 @@
38 | D2 | 业务代码保留 `com.zteits.*` 包名与 `parking-module-*` artifactId | 66 | D2 | 业务代码保留 `com.zteits.*` 包名与 `parking-module-*` artifactId |
39 | D3 | 框架层用 `yudao-framework`;只保留 `parking-common` + `parking-legacy-common` 作兼容层 | 67 | D3 | 框架层用 `yudao-framework`;只保留 `parking-common` + `parking-legacy-common` 作兼容层 |
40 | D4 | 上游 `yudao-module-system` 接管登录/权限/菜单/字典/日志;parking 系统模块按「丢弃 / 保留 / 桥接」三类拆分 | 68 | D4 | 上游 `yudao-module-system` 接管登录/权限/菜单/字典/日志;parking 系统模块按「丢弃 / 保留 / 桥接」三类拆分 |
41 -| D4.1 | **只挂上游 4 个模块**:`system / infra / iot`(`im` 暂不挂)。上游 `pay/member/report/mall/bpm/crm/erp/ai/mp/mes/wms/hrm/fms/pms/oa` 全部不挂,避免与 parking 业务路由/表冲突 | 69 +| D4.1 | **只挂上游 4 个模块**:`system / infra / iot`(`im` 暂不挂)。上游 `pay/member/report/mall/bpm/crm/erp/ai/mp/mes/wms/hrm/fms/pms/oa` 全部不挂,避免与 parking 业务路由/表冲突。**2026-11-04 修订:`iot` 也关闭,只保留 `system / infra`**,理由见 §8.0 |
42 | D5 | 新工程连 **`parking_cloud_yudao`(`parking_cloud` 的拷贝)**;原库不动 | 70 | D5 | 新工程连 **`parking_cloud_yudao`(`parking_cloud` 的拷贝)**;原库不动 |
43 | D6 | 业务菜单 id 整体 +100000 导入,避免与上游菜单 id 冲突 | 71 | D6 | 业务菜单 id 整体 +100000 导入,避免与上游菜单 id 冲突 |
44 | D7 | 前端只搬业务 `views` / `api` / 自研组件,框架壳用上游 | 72 | D7 | 前端只搬业务 `views` / `api` / 自研组件,框架壳用上游 |
45 | D8 | 业务 Controller 继续返回 `com.zteits.parking.framework.common.pojo.CommonResult`;JSON 与上游一致 | 73 | D8 | 业务 Controller 继续返回 `com.zteits.parking.framework.common.pojo.CommonResult`;JSON 与上游一致 |
46 | D9 | 保留 pagehelper 6.1.1 + jsqlparser 4.9,约 700 处 `PageHelper.startPage` 不动 | 74 | D9 | 保留 pagehelper 6.1.1 + jsqlparser 4.9,约 700 处 `PageHelper.startPage` 不动 |
47 | D10 | 启动类扫描顺序:`com.clouds.common.redis/threadpool/web/oss` → `cn.iocoder.yudao.server/.module` → `com.zteits.*` | 75 | D10 | 启动类扫描顺序:`com.clouds.common.redis/threadpool/web/oss` → `cn.iocoder.yudao.server/.module` → `com.zteits.*` |
  76 +| D11 | **组织 id 直接沿用老 `sys_org.id`**:`system_org` 为主表,`system_users.dept_id` 指向真实组织;`system_dept` 只作 yudao 用户模块的**镜像**(`OrgServiceImpl` 写穿透),不再维护两套组织数据,也不再需要 `parking.legacy.org-id-mapping` |
48 77
49 ## 3. 数据库方案(安全优先) 78 ## 3. 数据库方案(安全优先)
50 79
@@ -207,7 +236,7 @@ HTTP ≥400 的后端请求。结果:**0 个页面报错、0 个失败请求** @@ -207,7 +236,7 @@ HTTP ≥400 的后端请求。结果:**0 个页面报错、0 个失败请求**
207 | 1 | 请求变成 `/admin-api/admin-api/...` 404 | 老 API 文件 URL 自带前缀,上游 axios baseURL 也带 | `request.ts` 加 `stripApiPrefix` | 236 | 1 | 请求变成 `/admin-api/admin-api/...` 404 | 老 API 文件 URL 自带前缀,上游 axios baseURL 也带 | `request.ts` 加 `stripApiPrefix` |
208 | 2 | `hasPermi` 指令抛 `permission.hasPermission` | 老页面写字符串(198 处),上游指令只收数组 | `hasPermi.ts` 同时接受 string / array | 237 | 2 | `hasPermi` 指令抛 `permission.hasPermission` | 老页面写字符串(198 处),上游指令只收数组 | `hasPermi.ts` 同时接受 string / array |
209 | 3 | `Missing required prop: "type"` | 老页面用 `<DictTag dict-type=...>` | 迁过来的页面统一 `<DictTag type="..." :value="...">` | 238 | 3 | `Missing required prop: "type"` | 老页面用 `<DictTag dict-type=...>` | 迁过来的页面统一 `<DictTag type="..." :value="...">` |
210 -| 4 | `value.trim is not a function` | 存量接口时间字段是**毫秒数字**,`formatDateTime` 只处理字符串 | `format.ts` 用 dayjs 格式化数字,0/负数/null 显示 `-` | 239 +| 4 | `value.trim is not a function`;表格里直接显示 `1506302273000` | 存量接口时间字段是**毫秒数字**,有时又是数字字符串;`formatDateTime` 原来只处理 `yyyy-MM-dd HH:mm:ss` 字符串 | `format.ts` 的 `formatDateTime` 同时识别 10/13 位数字与数字字符串,统一格式化为 `YYYY-MM-DD HH:mm:ss`,0/负数/null 显示 `-`;停车记录、异常记录、高位异常、违法记录、白名单、语音告警等时间列都改走它 |
211 | 5 | 业务页面内容区**完全没有样式**(标题与数值竖排、表格无边框、无内边距) | 老工程 `main.ts` 全局引入了 `styles/tokens.css` + `styles/index.scss`(`--text-1`/`--fs-12`/`.stat-strip`/`.table-card`/`.search-field` 等 100+ 页共用的令牌与版式);新工程只搬了页面视图,两个全局文件没跟着搬 | 新增 `src/styles/parking-pages.scss` 并在 `src/styles/index.scss` 里 `@use`:业务令牌**映射到 Element Plus 变量**(`--text-1: var(--el-text-color-primary)`、`--brand-600: var(--el-color-primary)` …),页面随芋道主题(含暗色)走;`.stat-strip`/`.table-card`/`.search-field` 版式类原样保留,表格紧凑规则限定在 `.table-card` 内,不污染上游系统页。**没有**照搬老 tokens.css 对 `--el-*` 的整套覆盖(那会把整站改成政务蓝) | 240 | 5 | 业务页面内容区**完全没有样式**(标题与数值竖排、表格无边框、无内边距) | 老工程 `main.ts` 全局引入了 `styles/tokens.css` + `styles/index.scss`(`--text-1`/`--fs-12`/`.stat-strip`/`.table-card`/`.search-field` 等 100+ 页共用的令牌与版式);新工程只搬了页面视图,两个全局文件没跟着搬 | 新增 `src/styles/parking-pages.scss` 并在 `src/styles/index.scss` 里 `@use`:业务令牌**映射到 Element Plus 变量**(`--text-1: var(--el-text-color-primary)`、`--brand-600: var(--el-color-primary)` …),页面随芋道主题(含暗色)走;`.stat-strip`/`.table-card`/`.search-field` 版式类原样保留,表格紧凑规则限定在 `.table-card` 内,不污染上游系统页。**没有**照搬老 tokens.css 对 `--el-*` 的整套覆盖(那会把整站改成政务蓝) |
212 | 6 | 查询区还是老政务版式:label 在控件上方、控件通栏、按钮独占底部一行、空白过大 | 老 `SearchBar` 按「政务表单」设计;芋道标准是 `ContentWrap` + inline form(label 在左、控件 240px、搜索/重置跟在条件后同一行) | 重写 `components/SearchBar/index.vue` 外壳:flex 自动换行替代列数栅格、`搜索/重置` 带图标与条件同行;`styles/parking-pages.scss` 里 `.search-field` 改 inline-flex(label 90px 右对齐)、控件统一 `240px`。100+ 个页面用法不变,无需逐页改 | 241 | 6 | 查询区还是老政务版式:label 在控件上方、控件通栏、按钮独占底部一行、空白过大 | 老 `SearchBar` 按「政务表单」设计;芋道标准是 `ContentWrap` + inline form(label 在左、控件 240px、搜索/重置跟在条件后同一行) | 重写 `components/SearchBar/index.vue` 外壳:flex 自动换行替代列数栅格、`搜索/重置` 带图标与条件同行;`styles/parking-pages.scss` 里 `.search-field` 改 inline-flex(label 90px 右对齐)、控件统一 `240px`。100+ 个页面用法不变,无需逐页改 |
213 | 7 | 50 处 `userStore.userInfo?.nickname/username/orgId` 取到空值(打印/导出「操作人」为 `-`),`pnpm ts:check` 报 50 个 TS2339 | 老工程 store 暴露 `userInfo`;芋道 store 只有 `user`/`getUser()`,且 `get-permission-info` 只返回 nickname/deptId | `store/modules/user.ts` 新增兼容 getter `userInfo`(nickname 兜底 username、透出 id/deptId);**`orgId` 刻意不填**——老 orgId 是 `sys_org.id`,deptId 是另一套 id,把 deptId 当 orgId 传给存量接口会静默返回空数据,正确做法是不传、由后端 `LoginUserBridge` 换算 | 242 | 7 | 50 处 `userStore.userInfo?.nickname/username/orgId` 取到空值(打印/导出「操作人」为 `-`),`pnpm ts:check` 报 50 个 TS2339 | 老工程 store 暴露 `userInfo`;芋道 store 只有 `user`/`getUser()`,且 `get-permission-info` 只返回 nickname/deptId | `store/modules/user.ts` 新增兼容 getter `userInfo`(nickname 兜底 username、透出 id/deptId);**`orgId` 刻意不填**——老 orgId 是 `sys_org.id`,deptId 是另一套 id,把 deptId 当 orgId 传给存量接口会静默返回空数据,正确做法是不传、由后端 `LoginUserBridge` 换算 |
@@ -249,18 +278,1746 @@ return `${parentPath}${childPath}`.replace(/\/+/g, &#39;/&#39;) // /park + /park/park- @@ -249,18 +278,1746 @@ return `${parentPath}${childPath}`.replace(/\/+/g, &#39;/&#39;) // /park + /park/park-
249 验证:重新登录后展开「停车设施」点「车场列表」,`location.pathname` 278 验证:重新登录后展开「停车设施」点「车场列表」,`location.pathname`
250 = `/park/park-lot`,标题「芋道管理系统 - 车场列表」,0 条 console error。 279 = `/park/park-lot`,标题「芋道管理系统 - 车场列表」,0 条 console error。
251 280
  281 +### 7.5 存量定时任务去重(`@Scheduled` 总开关)
  282 +
  283 +老工程把不少业务逻辑挂在 Spring `@Scheduled` 上,搬进新工程后**默认会跟着启动**。
  284 +新旧服务同时在线时,同一份数据会被处理两遍(最危险的是往太极/西城等外部平台重复上报)。
  285 +
  286 +代码里共 11 个 `@Scheduled`,实际会跑的只有 **6 个**(其余 `@Component` 或 `@Scheduled`
  287 +已被注释,等于停用):
  288 +
  289 +| 模块 | 类 | 频率 | 作用 | 状态 |
  290 +|---|---|---|---|---|
  291 +| park | `DealGeoInOutParkDataSchedule` | 1s | 处理地磁进出场接口日志(`TP_P_INTERFACE_LOG`) | 活跃 → 已加开关 |
  292 +| park | `EqpOutDelayQueueMsgSchedule` | 10s | 地磁出场延迟队列(Redisson `RBlockingQueue`) | 活跃 → 已加开关 |
  293 +| park | `TaiJiEqpTokenTask` | 30min | 西城太极设备 token 刷新 | 活跃 → 已加开关 |
  294 +| park | `GetExceptionMsgSchedule` | — | 异常消息拉取 | 整类注释,未注册 |
  295 +| order | `ArrearageSMSSendConfigTask` | 60s + 动态 cron | 刷新欠费催缴短信配置并按 cron 发短信 | 活跃 → 已加开关 |
  296 +| order | `TaiJiUploadDeviceStatusTask` | 20min | 设备状态上报太极 | 活跃 → 已加开关 |
  297 +| order | `TaiJiUploadRefreshTokenTask` | 30min | 太极 token 刷新 | 活跃 → 已加开关 |
  298 +| order | `TaiJiUploadInOutParkRecordTask` | 5min | 进出场记录上报 | `@Component` 已注释 |
  299 +| order | `TaiJiUploadXJCParkFlowTask` | 14min | 巡检车流水上报 | `@Component` 已注释(改用 xxl-job) |
  300 +| order | `UserPayOutDelayQueueMsgSchedule` | — | 用户出场延迟队列 | 类在,`@Scheduled` 已注释 |
  301 +| order | `OrderDelayQueueMsgOpServiceImpl` | — | 订单延迟队列 | 类在,`@Scheduled` 已注释 |
  302 +
  303 +**开关**(6 个活跃类统一加 `@ConditionalOnProperty`):
  304 +
  305 +```java
  306 +@ConditionalOnProperty(prefix = "parking.legacy.scheduler", name = "enabled", havingValue = "true")
  307 +```
  308 +
  309 +```yaml
  310 +parking:
  311 + legacy:
  312 + scheduler:
  313 + enabled: ${PARKING_LEGACY_SCHEDULER_ENABLED:false} # 默认关
  314 +```
  315 +
  316 +**默认 false 的理由**:本地/并行期 48082 仍在跑这些任务,新服务只做读写接口;
  317 +再加上新服务连的是 `parking_cloud_yudao` 副本库、Redis 用 db1,跑起来既无收益又可能
  318 +把副本库的数据当生产数据往外部平台推。注意有些任务自身还有业务开关
  319 +(`BJXC_TAIJI_UPLOAD` 为空、`DISTRIBUTED_TASK_PROP` 未配置时会提前 return),
  320 +但那是**数据层兜底**,不能替代结构性开关——副本库配置一变就会“复活”。
  321 +
  322 +**验证(2026-11-02)**:重启前日志里 `延迟队列开始取数据` 每 10s 一条(累计 828 条)、
  323 +`欠费催缴短信…刷新` 每 60s 一条;加开关并重启(16:26:57 启动)后 70s 内上述关键字
  324 +**0 条**,其余 6 个任务也无任何调度线程日志,接口功能正常。
  325 +
  326 +**正式切换步骤**(新服务独占生产时):
  327 +
  328 +1. 停老服务(48082),确认没有其它实例还在连原库;
  329 +2. 把新服务数据源指到生产库,并把 Redis 切回生产者用的同一个 db
  330 + (当前 local 用 db1 是为了并行期隔离;**只改 db 而不开开关,延迟队列仍然消费不到消息**);
  331 +3. 设 `PARKING_LEGACY_SCHEDULER_ENABLED=true` 并重启;
  332 +4. 按上表逐个确认日志:地磁延迟队列、太极 token/设备状态、欠费催缴刷新;
  333 +5. 多实例部署时**只在一个实例开**(这些任务没有统一分布式锁;
  334 + `DealGeoInOutParkDataSchedule` 自带 `DISTRIBUTED_TASK_PROP` IP 取模,其余没有),
  335 + 后续可整体迁到 yudao Quartz / xxl-job 再按任务开关。
  336 +
  337 +### 7.6 指挥中心(真实数据聚合)
  338 +
  339 +原 iframe 壳已替换为自研页面,**菜单、路由、component 均未变**:
  340 +`100498` / `/command-center` / `gov/commandCenter/index`。右上角「旧版大屏」仍指向
  341 +存量综合监测大屏(`VITE_APP_COMMAND_CENTER_URL`,缺省
  342 +`http://139.224.115.129/.../systemFirstPage.html`),过渡期可对照。
  343 +
  344 +2026-11-02 按用户提供的「赤峰市智慧停车数据中心」大屏重做版式,随后按「站在政府角度、
  345 +重排整页比例」的要求调整为**通栏 KPI + 左(供给与规划)/ 中(城市态势)/ 右(运行与服务)**
  346 +三段式。**2026-11-06 起接真实数据**:`demo.ts` 已删除,页面只读后端聚合接口
  347 +`POST /park/command-center/overview`(一次返回整屏),60 秒轮询 + 手动「刷新」。
  348 +左上角标识由「演示数据」改为绿色「实时数据」,接口失败时变粉色「数据加载失败」并保留上次数据。
  349 +
  350 +| 项 | 内容 |
  351 +|---|---|
  352 +| 页面 | `src/views/gov/commandCenter/index.vue`(视图) + `mapper.ts`(响应 → 页面模型) |
  353 +| 接口 | `POST /admin-api/park/command-center/overview`;`ParkCommandCenterController`,权限沿用 `park:dashboard:query`(**不新增权限点、不授权任何角色**) |
  354 +| 聚合 | `ParkCommandCenterServiceImpl`;SQL 在 `ParkCommandCenterMapper` + `mybatis/mapper/commandcenter/ParkCommandCenterMapper.xml`,车场/实时/订单汇总复用 `ParkDashboardMapper` |
  355 +| 前端数据源 | `src/api/park/commandCenter/index.ts` → `queryCommandCenterOverview()`;TDD 门禁 `tests/verify-command-center-mapper.mjs` |
  356 +| 路由 | `/command-center`(顶级菜单,type=2,parent_id=0,sort=5,`keep_alive=0`) |
  357 +| 菜单 id / 种子 | `100498` / `scripts/add-command-center-menu.sql`(幂等) |
  358 +| 角色授权 | `scripts/seed-role-templates.sql` 给运营(2001)显式加了 `100498`;超级管理员天然全量 |
  359 +| 地图数据 | `src/assets/map/json/chifeng.json`(DataV 赤峰市 `150400_full`,12 个旗县区,随包发布) |
  360 +
  361 +**口径与诚实原则**(改这里之前先读):
  362 +
  363 +- 在用停车场 = `parking_lot.data_status = 1 AND state = 1`;订单 = `order_park.data_state = 1`,
  364 + 取消(`-50`)不计入收入/欠费;金额字段是**分**,后端统一转元。
  365 +- 实时泊位取 `parking_lot_berth_temp_info`。**原始导入**状态下该表为空 → 剩余泊位 / 占用率 /
  366 + 地图标注卡 / 重点车场 TOP10 一律 `null`,页面显「—」或「暂无数据」,**不写 0、不造演示值**;
  367 + 同理无用户表 → 用户三项 `null`。后端用 `unavailableModules` 把这些模块名回传给页脚。
  368 + (2026-11-06 起本地跑过演示数据脚本,该表有值,页面不再是空态,见 §7.11。)
  369 +- 支付构成取**在线支付表 `pay_order`**(`pay_order_state = 40` 成功、`order_big_type = 100`
  370 + 停车订单、`create_date` 当日)。**不要改用 `td_b_pay_order`**:那是 2017 年的历史表
  371 + (`pay_order_state = 1`),且没有 `pl_no`,按车场关联会直接 SQL 报错。
  372 + `pay_order_success` 是归档表,同样不用。`pay_type` 33 本地未知,落到「其它」。
  373 +
  374 +版式:**顶部通栏 7 个 KPI**(竖分隔线)+ 下方三栏城市态势。标题签仍为顶部居中的异形
  375 +(两端斜切)标签,面板为深空蓝半透明底 + 内描线 + 四角折线,页面底为星点纹理 + 顶部斜纹。
  376 +
  377 +| 位置 | 面板 | 真实口径 |
  378 +|---|---|---|
  379 +| 通栏 | KPI ×7 | 停车场总数 / 停车泊位总数 / 今日进场车次 / 当前剩余泊位 / 泊位占用率 / 平均周转率 / 今日收入(≥1 万元切「万元」并保留 1 位小数,否则「元」取整) |
  380 +| 左 | 泊位供给结构 | 总泊位数 / 路侧泊位 / 封闭泊位 + 「在用泊位合计」;副标题给 `dataThrough`(订单数据截止时间) |
  381 +| 左 | 各区联网接入进度 | 按区县的**同心环**(外圈向内递减)+ 中心「已接入 40」+ 右侧「已接入/计划」列表与百分比;副标题「今日有订单 40 / 40(100%)」。分母是各区在用停车场,分子是当天有订单的车场 |
  382 +| 左 | 重点区域占用率 | **TOP 10**(按占用率降序),每页 5 条、5 秒轮播,悬停暂停;无实时泊位数据时整块显示「暂无实时泊位数据」 |
  383 +| 左 | 各区停车时长分布 | ≤30 分钟 / 30–60 / 1–2 小时 / >2 小时,按区县堆叠;时长优先 `order_park.parking_duration`,缺省用出场 − 入场现算 |
  384 +| 左 | 各区今日收入排行 | 按区县今日收入降序(来源同上,含无实时数据的区县);副标题合计(万元,按千元取整) |
  385 +| 中 | 全市停车占用率分布 | **12 旗县区完整轮廓**;只有拿到实时占用率的区县才按分档着色并出四角标注卡(车场数 / 泊位数 / 占用率 / 周转率 + 引线),其余深色底、弱化标签;无任何实时数据时整块「暂无实时占用率数据」 |
  386 +| 中 | 24 小时进出场车流 | 进场(青)/ 出场(绿)柱 + 停车指数(黄)线(右轴 0–100%,无泊位数据时为空),右上「今日累计进场 … 次」 |
  387 +| 右 | 实时进出场记录 | 最近 8 条(车牌 / 车场 / 时间 / 进·出标签),取 `order_park` 按 `COALESCE(park_out_time, park_in_time)` 倒序;60 秒随整屏刷新,不再本地模拟推送 |
  388 +| 右 | 用户与支付 | 累计用户 / 今日活跃 / 昨日新增(本库无用户表 → 三项均「—」)+ 支付方式**环形图**,图例按当日笔数占比降序(本地:微信公众号 / 支付宝 / 微信 / 个人账户) |
  389 +| 右 | 运行预警 | 设备离线 / 满位车场 / 数据延迟区县 / 欠费逃逸订单 四格:常规高度 2×2,视口高 ≥1100px 改 4 行通栏;图标按后端稳定键 `device/full/stale/arrears` 取,色调固定不随数据顺序跳动 |
  390 +
  391 +三栏按 `1.05fr / 1.85fr / 1.05fr` 配比——**左右两栏等宽,地图栏才落在页面正中**;
  392 +左栏下限 300px、右栏下限 350px,保证记录列表四列不挤。左列 5 个面板按
  393 +`0.66 / 1.18 / 1.02 / 0.95 / 0.95` 分配高度,中列地图 `1.62` / 车流 `0.88`,
  394 +右列记录 `1.38` / 用户与支付 `1.15` / 预警 `0.87`。
  395 +
  396 +**视觉规格(2026-11-02 定稿)**:
  397 +
  398 +- 标题 `SCREEN_TITLE = 赤峰市智慧停车数据中心`(换城市改这一处);顶栏左侧「实时监测 +
  399 + 实时数据」、右侧日期/星期/时钟与「刷新 / 全屏 / 旧版大屏」。
  400 +- 配色:底 `#050C1C`、正文 `#DCEBF9`、弱化 `#8AA9C9`、描线 `#2D5C86`、主青 `#35D6FF`、
  401 + 蓝 `#4D8DFF`、绿 `#5BE3B3`、黄 `#FFD166`、橙 `#FF9F43`、粉 `#FF8FB1`、紫 `#B79CFF`。
  402 +- 地图为**星空深空底**(`#05101e` + SVG 星点平铺 + 两团星云径向光晕,**不放横竖网格线**),
  403 + 注册完整 12 区(`chifeng-full`,不动全局 `chifeng` 注册)。有数据的 4 区按占用率分档:
  404 + `<50%` 绿 `#2BD9A4` / `50–70%` 青 `#35D6FF` / `70–85%` 黄 `#FFC11E` / `>85%` 红 `#FF5A5A`,
  405 + 白色描边 + 同色外发光;其余 8 区 `#0C253C` 深底 + 弱化标签。红山 / 元宝山两块小区相邻,
  406 + 标签按 `FOCUS_LABEL_OFFSET` 外移(红山 `[0, 8]`、元宝山 `[30, 12]`),避免两块相邻区县的
  407 + 标签互相压字(此前红山偏移到了松山区内,地图上会看到两个「红山区」字样)。
  408 +- **地图悬停联动**:鼠标滑过任一有数据的区县 → 区域颜色提亮 + 白色描边加粗(2.6px)+
  409 + 同色外发光,区名标签放大到 13.5px/700;同时对应四角标注卡高亮(边框换成分档色、
  410 + 上浮 2px)并与引线同步加粗发光(`is-active`)。反向从标注卡悬停 → `dispatchAction`
  411 + 高亮地图区块;移出后统一 `downplay` 复原。无数据区县悬停仅轻微提亮、不弹卡。
  412 +- 四角标注卡为青边圆角卡(区名分隔线 + 2×2 指标:车场数 / 泊位数 / 占用率 / 周转率),
  413 + 引线颜色跟随占用率分档;落点不是简单连形心——按「离卡片最近的外环顶点 → 形心」取中点,
  414 + 避免红山 / 喀喇沁两条引线横穿相邻区县交叉。引线在 `ResizeObserver` / 窗口变化后
  415 + 用 `convertToPixel` 重算,全屏与 1366 下都不会错位。
  416 +- 右列「用户与支付」用 ECharts 环形图(`#08182C` 分隔线、中心「N 种方式」由实际支付方式数决定)
  417 + 替代原单条堆叠条,图例竖排;「运行预警」图标 + 数值按面板高度放大,名称在左、数值/单位在右
  418 + 撑满整格;面板高度比从 `1.45/1.02/0.78` 调为 `1.38/1.15/0.87`,与左侧面板节奏对齐。
  419 +- **分辨率自适应**:卡片文字/图标统一用
  420 + `clamp(最小值, min(vw, vh), 最大值)`(宽或高任一方向变小时先收敛,1366 用最小值、
  421 + 4K 用最大值兜底);面板内边距、进度条、圆环直径等按 `vh` 缩放;ECharts 不继承 CSS,
  422 + 用 `chartPx()` 按 `min(宽比, 高比)` 同步缩放图例/轴标签/地图标签,窗口尺寸变化后
  423 + 180ms 防抖重绘。`max-height: 820px` 保留 1366×768 紧凑规则;`max-height: 760px`
  424 + 再收紧页头/列表。高屏(`min-height: 1100px`)预警改 4 行通栏,避免 2×2 大卡留白。
  425 + 已验证 1280×720 / 1366×768 / 1600×900 / 1920×1080 / 1920×1200 / 2000×1253 /
  426 + 2560×1440 / 3440×1440 / 3840×2160 均无页面滚动、无面板内容溢出。
  427 +- **保留的动效**:① 重点区域占用率取 TOP 10 分两页(5 条/页),`translateY` 每 5 秒翻页、
  428 + 悬停暂停,右下角圆点指示当前页;② 面板进入时的数字/条形过渡;③ 60 秒整屏轮询后重绘,
  429 + 不做本地模拟推送、不做 KPI 抖动——`demo.ts` 时期的模拟记录流与 KPI 波动已删除。
  430 +
  431 +**接入结论(2026-11-06)**:
  432 +
  433 +1. `demo.ts` 已删除,页面模型改由 `mapper.ts` 从接口响应生成;旧 `useCommandCenter.ts`
  434 + 保留但本页不再引用。
  435 +2. 记录列表不展示抓拍图;如需巡检再按 `getOrderImages(orderId)`(键 `4` 进场 /
  436 + `5` 出场)补图,车牌按存量规则打码。
  437 +3. 本地缺数据的模块(实时泊位/占用率、重点车场、用户指标)不补数,后端在
  438 + `unavailableModules` 里列明、页脚展示。
  439 +
  440 +**接入时的数据陷阱(提前记录)**:存量实时接口对**没有实时缓存的车场**返回
  441 +「泊位数 + 其余全 0」,直接按 `(泊位-空闲)/泊位` 会算成 100%。新聚合接口只把
  442 +`parking_lot_berth_temp_info` 有行的车场计入占用率,无行即 `null`;改 SQL 时
  443 +不要用 `COALESCE(..., 0)` 兜底。
  444 +
  445 +**两处已知缺口(沿用旧版结论)**:
  446 +
  447 +1. 存量「本月各区车场评分排名」在本栈没有对应数据源。`ParkDashboardController`
  448 + (`/park/dashboard/overview`)能算,但权限点 `park:dashboard:query` 在
  449 + `system_menu` 里没有记录,新增菜单并授权运营属于权限变更,未经确认不做。
  450 + **新版式已把「评分排行」从页面移除**(换成「各区今日收入排行」),该权限缺口不再阻塞本页。
  451 +2. 存量「当日 APP 服务次数」没有等价字段,右侧改用「今日活跃」(`todayActiveNum`,
  452 + 按 `modfiy_date` 统计),并补「昨日新增 / 昨日活跃」。
  453 +
  454 +**本地数据现状**:原始导入只有红山区 40 个在用车场、3,720 个泊位、
  455 +`parking_lot_berth_temp_info` 为空 → 实时泊位/占用率/重点车场全部空;当日进场 4,889 次、
  456 +出场 2,899 次、收入 2,464 元;支付构成 4 种(微信公众号 670 笔占 89.5%);欠费逃逸订单 546 单;
  457 +无用户表。这是本地数据覆盖范围,不是页面缺陷。跑过 §7.11 的演示数据脚本后变成
  458 +109 个在用车场 / 16,210 个泊位 / 12 个旗县区,红山区的车场与订单仍是真实导入数据。
  459 +
  460 +**验证(2026-11-06)**:`POST /admin-api/park/command-center/overview` 返回 `code 0`,
  461 +字段与页面一致(40 / 3,720 / 4,889 / 2,464 元 / 红山区单区 / 支付 4 项 / 预警 0·0·0·546);
  462 +灌入 §7.11 演示数据后同一接口返回 109 / 16,210 / 21,710 / 4,655 / 70.0% / 1.34 / 24,643 元,
  463 +12 个旗县区齐全,预警 0·3·1·1,927,支付 5 种,1920×1080 与 1366×768 截面无串版
  464 +(**2026-11-06 复核后数字变为 71.3% / 28,349 元**:占用率口径与精度已修,见 §7.15;
  465 +收入随演示数据重灌而变化);
  466 +`tests/verify-command-center-mapper.mjs` 通过,前端 16 个 verify 脚本全绿,
  467 +`vue-tsc` 在 `commandCenter/*` 0 error,ESLint 干净;浏览器登录 admin 后实测
  468 +1920×1080 与 1366×768 均无页面滚动、控制台 0 error(仅 4 条既有 Vue Router 弃用告警),
  469 +空态显示「暂无实时泊位数据 / 暂无实时占用率数据 / —」。
  470 +
  471 +**验证(2026-11-02,版式阶段)**:admin 与 ops001 登录后页面 6 个接口全部 HTTP 200,
  472 +控制台 0 error;8 种分辨率下截图核对,面板、地图标签、车流轴均无遮挡,页面无纵向滚动;
  473 +`pnpm ts:check` 在 `commandCenter/index.vue` 0 error(仓库其他历史页面仍有既有类型告警),
  474 +`pnpm build:local` 通过。
  475 +
  476 +**权限注意**:运营角色直接拥有 `100426/100427`,接口 `@PreAuthorize` 走
  477 +`menu_role_ids` 缓存因此通过;但这两个按钮的父页面 `100424` 挂在「系统管理」下且
  478 +未授权给运营,`get-permission-info` 的权限串里不会出现 flow 项。本页未使用
  479 +`v-hasPermi`,所以不影响。以后若给页面按钮加 `v-hasPermi`,需同时授权父链。
  480 +
  481 +### 7.7 组织 id 空间统一(2026-11-02)
  482 +
  483 +**问题**:登录态 `deptId` 来自 `system_users.dept_id`(本地 103,芋道演示部门),
  484 +而存量业务表存的是老 `sys_org.id`(10003 等)。之前靠
  485 +`parking.legacy.org-id-mapping=103:10003` 在 `LoginUserBridge` 里硬换算,
  486 +切生产必须重配,配错会**静默查错组织的数据**(不报错、只是结果不对)。
  487 +
  488 +**改造**(`scripts/migrate-org-ids.sql`,幂等):
  489 +
  490 +1. `sys_org` → `system_org`,真实 id 原样保留(242 行,152 个有效;单个根 10001);
  491 +2. `system_users.dept_id` 的 `103` → `10003`(admin + 3 个联调账号);
  492 +3. `system_org` 写穿透到 `system_dept`(镜像)——yudao 用户模块仍读它;
  493 +4. 软删芋道演示部门 `100~125`,隐藏「部门管理」菜单(组织统一从「组织管理」维护);
  494 +5. `application-local.yaml` 的 `org-id-mapping` 置空 ⇒ 恒等映射,启动日志确认。
  495 +
  496 +**为什么保留 `system_dept` 镜像**:`AdminUserServiceImpl` 仍通过 `DeptService`
  497 +读 `system_dept` 做用户表单组织下拉、用户列表子树过滤、保存用户时部门校验。
  498 +替换整条链路风险大;镜像 + 写穿透是这里最小、可回退的方案。镜像只在
  499 +`OrgServiceImpl` 的 create/update/delete 里同事务维护,读路径仍以 `system_org` 为准。
  500 +
  501 +**顺带修掉的两个存量 bug**(改组织 CRUD 时暴露):
  502 +
  503 +- parking 的 `BaseDO` **没有生效的 `MetaObjectHandler`**:容器里只有 yudao 的填充器,
  504 + 只认 `cn.iocoder` 的 BaseDO,导致 `system_org` 的 INSERT 显式写
  505 + `create_time=NULL` 触发非空约束 —— 「新增组织」原来是 500。现在
  506 + `OrgServiceImpl` 显式补审计字段。
  507 +- parking `UserDO` 的两个列名写错:`orgId` 实际列是 `dept_id`、`delFlag` 实际列是
  508 + `deleted`(`system_users` 沿用的是 yudao 表)。不修的话「删除组织」检查
  509 + 「组织下是否有用户」时 SQL 直接报错。已用 `@TableField` 纠正。
  510 +
  511 +**验证**:组织树 1 根 / 10003 下 11 个子组织;`dept/simple-list` 152 条;
  512 +四个用户 `deptId=10003`;组织新增/改名/删除时 `system_dept` 同 id 同步出现/消失;
  513 +`ops001` 登录后 `get-permission-info` 的 `deptId` 为 10003,存量接口仍 `code=0`。
  514 +迁移后需清 `oauth2_access_token:*`(旧 token 里还是 103)。
  515 +
  516 +### 7.8 首页监管总览(2026-11-02)
  517 +
  518 +**页面**:`parking-cloud-yudao-ui/src/views/Home/Index.vue`(静态路由 `/index`,名称 `Index` 不变),
  519 +按老库原型 `parking-cloud-ui/docs/design/prototype/01-dashboard.html` 复刻 8 个区块。
  520 +样式全部收在 `.gov-dashboard` 下(`src/views/Home/dashboard/dashboard.scss`),
  521 +浅色用政务蓝 `#14539a`,暗色用 `--el-*` 暗色令牌 + 校验过的图表色,不影响站内其它芋道页面。
  522 +
  523 +**后端**:`parking-module-park` 新增
  524 +`POST /admin-api/park/dashboard/overview`(`ParkDashboardController`,权限 `park:dashboard:query`),
  525 +一次返回六张指标卡、24 小时/逐日车流、三级预警、区县排名、考核指标、重点车场、设备在线率、数据时效。
  526 +实现类 `ParkDashboardServiceImpl`,SQL 在 `mybatis/mapper/dashboard/ParkDashboardMapper.xml`。
  527 +
  528 +**口径**(页面底部同步展示):
  529 +
  530 +| 指标 | 来源 |
  531 +|---|---|
  532 +| 在用停车场 / 泊位 | `parking_lot`,`data_status=1 AND state=1` |
  533 +| 订单 / 欠费追缴率 | `order_park`(排除 `-50`;欠费 52、已补缴 53) |
  534 +| 工单闭环率 / 逾期 | `order_work`(`solve_state=3`;逾期 = 超 24h 未办结)。**JOIN `parking_lot` 后只统计在用车场**,与页面「接入车场」口径一致,避免把已停用车场的历史工单算进预警 |
  535 +| 设备在线率 | `parking_lot_equipment`(`equipment_status=1`) |
  536 +| 实时占用率 | `parking_lot_berth_temp_info`,无实时行返回 null(页面显示「—」) |
  537 +| 车流 / 占用率曲线 | `parking_running_statistic`,`data_source=10`;今日 `time_cycle=40` 按小时,本周/本月 `50` 按天 |
  538 +
  539 +**三级预警是实时推导,不是预警表**:红 = 车场设备离线率 ≥30%(设备 ≥5 台)或占用率 ≥95%;
  540 +橙 = 占用率 ≥85% 或工单超 24h;黄 = 设备离线率 ≥10% 或区县欠费率 ≥15%。
  541 +阈值集中在 `ParkDashboardServiceImpl` 常量区,后续接预警规则表时替换 `buildAlarms` 即可。
  542 +页面显式标注「实时推导 · 非工单流转」,不伪造处置状态与响应时长。
  543 +
  544 +**暂无数据源的模块**:数据上报及时率 / 上报记录(无对应表)。响应里用
  545 +`unavailableModules` 声明,页面显示「—」并在底部说明,**不使用模拟值**。
  546 +
  547 +**权限**:`scripts/add-dashboard-permission.sql` 幂等插入隐藏目录 + 按钮
  548 +(`park:dashboard:query`,id 310900/310901),角色管理可勾选;超管天然可见。
  549 +
  550 +**本地验证注意**:本地 `parking_cloud_yudao` 是稀疏样本(4 个在用车场、订单集中在 2017–2018),
  551 +因此「今日」指标多为 0、车流图为空态,属预期;页面必须能正常展示空态与「—」。
  552 +实测 `{}` / `{"region":"红山区","period":"month"}` / `{"period":"week"}` 三种入参均 `code=0`,
  553 +且切区县后 `regions` 仍返回全量区县(否则下拉只剩当前区县,回不到全市)。
  554 +
  555 +已修的两个运行期问题:
  556 +1. `Map.entry(lot, null)` 抛 NPE(`Map.entry` 不接受 null 值),改用 `record LotRate` 先过滤再排序;
  557 +2. 逾期工单原本只按 `pl_country_name` 过滤,会把不在用车场(如丹阳市)的 2018 年工单算进来,
  558 + 现统一 JOIN `parking_lot` + `activeLotCondition`,本地逾期数因此为 0(4 个在用车场无工单)。
  559 +
  560 +### 7.9 地图区县配色(2026-11-04)
  561 +
  562 +「停车资源一张图」「专题分析」「指挥中心」三张地图的区县底色不再写死中心四区,改为**全市 12 个旗县区各一色**,
  563 +并且颜色与开关由后端参数控制,前端只做兜底。
  564 +
  565 +| 项 | 内容 |
  566 +|---|---|
  567 +| 参数键 | `parking.map.district-theme`(基础设施 → 参数管理,`visible=1`,登录用户可读) |
  568 +| 种子脚本 | `scripts/add-district-theme-config.sql`(幂等) |
  569 +| 参数结构 | `enabled` 总开关(默认 `true`);`off` 关闭高亮的区县数组;`colors` 单区县颜色覆盖 |
  570 +| 前端 | `parking-cloud-yudao-ui/src/views/gov/shared/district-theme.ts`(纯逻辑 + 默认色表)、`district-theme-loader.ts`(读参数、单次缓存) |
  571 +| 接入页面 | 一张图 `oneMap/useOneMap.ts`、专题分析 `topicAnalysis/useTopicMap.ts`(高德 `Polygon`)、指挥中心 `commandCenter/index.vue`(ECharts `map`,用 `toRgba()` 输出带透明度 `areaColor`) |
  572 +| 兜底 | 参数缺失 / JSON 损坏 / 接口失败 → 全部高亮 + 默认配色,地图不会失色 |
  573 +
  574 +默认色(中心四区沿用智慧中心):红山区 `#FF5A5A`、松山区 `#FFC11E`、元宝山区 `#35D6FF`、
  575 +喀喇沁旗 `#2BD9A4`、阿鲁科尔沁旗 `#6C8DFF`、巴林左旗 `#B79CFF`、巴林右旗 `#FF9F43`、
  576 +林西县 `#FF7AC6`、克什克腾旗 `#A3E635`、翁牛特旗 `#2DD4BF`、宁城县 `#E879F9`、敖汉旗 `#9AA7C7`。
  577 +
  578 +填充透明度:中心四区 0.74、其余区县 0.44(街区尺度分别淡出到 0.12 / 0.08),
  579 +保证 12 块颜色区分明显又不遮挡车场点位。指挥中心有实时数据的 4 个区仍用白描边、白色区名,
  580 +其余 8 个区填充更淡、标签弱化;关闭高亮后回到指挥中心原样式(有数据的区按占用率分档、其余深色底)。
  581 +
  582 +参数改动在**下一次页面加载**生效(前端按页面生命周期缓存一次请求,避免两张地图重复取参数)。
  583 +
  584 +### 7.10 视频监控前端(演示数据版,2026-11-04)
  585 +
  586 +方案见 `docs/superpowers/specs/2026-11-04-视频监控模块设计方案.md`,计划见
  587 +`docs/superpowers/plans/2026-11-04-视频监控前端实施计划.md`。**当前只做前端**,后端 `parking-module-video`
  588 +(GB28181 / WVP-PRO + ZLMediaKit)待页面确认后再落地;接口契约、错误码、数据表已写进方案 §6,前端按契约类型消费演示数据。
  589 +
  590 +| 页面 | 路由 | 组件 | 内容 |
  591 +|---|---|---|---|
  592 +| 视频监控 | `/video/monitor` | `gov/videoMonitor/index` | 1/4/9 宫格视频墙、三级设备树(12 区县 → 车场/路段 → 112 台设备)、收藏、双击最大化、宫格右键菜单、历史回放时间轴、地图点位 + 设备卡、云台控制、视频轮巡 / 巡逻方案 |
  593 +| 视频设备 | `/video/device` | `gov/videoDevice/index` | 设备台账:区县 / 类型 / 在线状态筛选、分页、预览跳转、新增 / 编辑 / 删除 / 批量启停(内存态)、编辑抽屉 |
  594 +
  595 +- 前端目录 `parking-cloud-yudao-ui/src/views/gov/videoMonitor/`:`types.ts`(契约)、`model.ts`(纯函数:
  596 + `deviceTree` 三级树、`favoriteDevices` 收藏、`buildPatrolOrder` 巡逻分组)、`demo.ts`(确定性演示数据,112 台)、
  597 + `useVideoMonitor.ts`(页面状态)、`components/`(设备树、视频墙、播放器、回放条、地图、云台、巡逻)、
  598 + `video-monitor.scss`(非 scoped,统一 `vm-*` 类)。
  599 +- 设备树为三级:区县 → 车场/路段(车场在前)→ 设备(在线优先、中文名排序);收藏存
  600 + `localStorage['video-monitor:favorites']`,只存设备 id,后端上线后迁到表。
  601 +- 「轮巡」与「巡逻方案」是两个功能:轮巡按当前筛选结果 + 宫格大小自动翻页(工具栏,15/30/60 秒),
  602 + 巡逻方案是右栏预置设备集;两者互斥,手动播放 / 回放 / 切地图都会停止轮巡。
  603 +- 播放画面为 canvas 合成的示意监控画面(车场 / 路侧两种场景),不依赖网络与二进制样片;配置
  604 + `VITE_DEMO_VIDEO_URL` 可改用循环样片。协议无关边界已留好:`PlaySource.protocol`(`webrtc` / `flv` / `hls` /
  605 + `demo`),后续只换 `VideoPlayer` 实现。
  606 +- 菜单种子 `scripts/add-video-menu.sql`:`system_menu` 100502 `/video/monitor`、100503 `/video/device`,
  607 + `keep_alive=0`(页面带定时器与地图实例),同时授给运营角色 2001;幂等,已在 `parking_cloud_yudao` 执行。
  608 +- 验收:`parking-cloud-yudao-ui/tests/verify-video-monitor.mjs` 全绿;1920 / 1366 / 1200 宽无横向滚动;
  609 + 宫格切换、最大化 + Esc、回放暂停与空录像日、地图模式 + 设备卡实时/回放、巡逻启动、轮巡推进与停止、
  610 + 三级树折叠、收藏 + 只看收藏、宫格右键菜单、枪机云台置灰、设备台账新增/编辑/删除/批量启停均实测通过。
  611 +- 演示数据每周第 7 天(如 2026-11-05)无录像,用于验证「当日无录像」空态;设备坐标在区县锚点 ±0.15° 内展开,
  612 + 便于在地图上看出点位分布。
  613 +
  614 +### 7.11 本地演示数据:11 个旗县区(2026-11-06)
  615 +
  616 +本地副本库只导入了红山区,监管中心 / 指挥中心 / 综合监测里其余 11 个旗县区全是空白。
  617 +`scripts/gen-demo-district-data.py` 给**本地** `parking_cloud_yudao` 补一批可识别、可回滚的
  618 +演示数据,让 12 个旗县区的版式、图表、地图着色都能看到真实量级的效果。
  619 +
  620 +```bash
  621 +python3 scripts/gen-demo-district-data.py --dry-run # 只打印计划(默认)
  622 +python3 scripts/gen-demo-district-data.py --apply # 先清旧演示数据,再重建
  623 +python3 scripts/gen-demo-district-data.py --clean # 只删演示数据
  624 +```
  625 +
  626 +| 项 | 内容 |
  627 +|---|---|
  628 +| 规模 | 11 个旗县区共 **69 个演示车场 / 12,490 个泊位**;含红山区后全库 109 个在用车场、16,210 个泊位 |
  629 +| 车场 | `parking_lot`:`pl_no` 以 `P1504` 开头、`remark = 本地演示数据(脚本生成)`;约 6 成路侧(`pl_type=1`)/ 4 成封闭(`pl_type=2`),占用率 0.35–0.94,每 19 个车场有 1 个 ≥95%(让「满位车场」预警非 0) |
  630 +| 坐标 | `parking_lot_geo`,每个车场一条,取旗县区政府驻地 ±0.05° |
  631 +| 实时泊位 | `parking_lot_berth_temp_info`,**覆盖全部在用车场**(红山区按 `pl_no` 稳定哈希给 0.62/0.74 基准占用率),页面不再落空态 |
  632 +| 订单 | `order_park`,只写入演示车场、只写「今天 00:00–现在」,按 24 小时进车权重分布;状态分布 46% 完成(50) / 34% 在场(10) / 8% 待支付(20) / 10% 逃逸(52) / 2% 已补缴(53);每个车场额外补 1 条最近 25 分钟内的在场单,避免「数据延迟区县」误报 |
  633 +| 支付 | `pay_order`,已完成且有费用的订单生成支付单(`order_big_type=100`、`pay_order_state=40`),5 种 `pay_type` |
  634 +| 运行统计 | `parking_running_statistic`(`data_source=10`):`time_cycle=40` 按小时 1,526 行 + `time_cycle=50` 日汇总,由当天真实订单聚合 |
  635 +
  636 +**口径(与真实数据保持一致,改脚本前先看)**:
  637 +
  638 +- 金额单位是**分**,`order_park.parking_duration` 单位是**秒**(早期写成分钟会把时长全部落进
  639 + 「≤30 分钟」一档)。
  640 +- 收入只算 `order_state = 50/53` 的已完成订单,取消(`-50`)不计;首 30 分钟免费,
  641 + 之后按小时向上取整计费(路侧 200 分/时、封闭 300 分/时)。
  642 +- 演示车场 `state = 1`、`data_status = 1`,与真实车场同口径参与统计;
  643 + 它们的 `remark` 与 `pl_no` 前缀是唯一识别标记。
  644 +
  645 +**安全边界**:
  646 +
  647 +- 脚本只从 `application-local.yaml` 进程内解析本地库连接,不打印凭据,不连 `parking_cloud`;
  648 + 共享库 / 生产库禁止执行 `--apply`。
  649 +- `--apply` 先按演示标记删旧数据(`order_park` / `pay_order` / `parking_lot_berth_temp_info` /
  650 + `parking_lot_geo` / `parking_running_statistic` / `parking_lot`),再重建,可重复执行。
  651 +- 红山区只补 `parking_lot_berth_temp_info` 与 `parking_running_statistic` 两张
  652 + 缓存 / 统计表,**不动它的 `parking_lot` 与 `order_park`**——那里是真实导入数据。
  653 +
  654 +**已知副作用(诚实记录)**:
  655 +
  656 +1. 红山区的实时泊位、运行统计是脚本合成的(真实车场没有实时上报),所以它的占用率
  657 + 是估算值;订单、支付、车场清单仍是真实的。演示答辩时若被追问,按这个口径回答。
  658 +2. 演示订单只覆盖「今天」,**次日看板会回到接近空态**;演示前重跑一次 `--apply`。
  659 +3. 红山区真实订单流停在上次导入时刻,所以「数据延迟区县」恒为 1(红山区),这是真实缺口,
  660 + 不是脚本 bug。
  661 +4. `scripts/warm-cache.sh` 与此无关(演示数据只进 MySQL),但改完真实车场数据后仍需预热缓存。
  662 +
  663 +### 7.12 重点车场占用率监测:实时数据兜底(2026-11-06)
  664 +
  665 +`/park/keyLotMonitor` 之前是「47 监测车场 / 0 行 / 暂无可计算的占用率」。原因不在前端:
  666 +`ParkingLotQueryServiceImpl#queryParkinglotBasicFactsByPlNos` 的 `berthNum / berthFreeNum`
  667 +是从 Redis 实时快照(`PLRealTimeStatusCacheUtil`)整段 copy 的,而写这份快照的 XXL-Job
  668 +`PLRealTimeStatusCacheJob` 在新栈不跑(§7.5),Redis 里没有 key。
  669 +
  670 +处理方式:新增 `LegacyRealtimeBerthFiller`
  671 +(`com.zteits.parking.module.park.service.dashboard`),在 `IndexRealTimeController` 的
  672 +查询与导出两个端点里兜底——
  673 +
  674 +- **只在存量快照没值(`berthNum == null`)时补**;Redis 有值(生产环境)整段跳过,不改变存量行为;
  675 +- 取 `parking_lot_berth_temp_info`(`ParkDashboardMapper#selectRealtime`),与指挥中心、综合监测同源,
  676 + 同一时刻三张页面的占用率一致;
  677 +- `freeBerthsStateDesc` 逐行照搬存量公式(`1 - round2(空余/总数)`,0 停满 / <0.20 紧张 / 其余充足)。
  678 + 这个公式对全空车场会算出「停满」,但存量就是这口径,且前端只在 `caryardOverview` 的 TS 类型里
  679 + 声明过该字段、从未渲染 —— 不顺手改口径;
  680 +- 今日进出(`inCarNum / outCarNum`)同样在 Redis 快照缺失时兜底:用 `order_park`
  681 + 现算「今日 00:00 至今」进场 / 出场台次(`ParkDashboardMapper#selectTodayFlowByLot`),
  682 + 与指挥中心「今日进出场」同一张表、同一口径,**不是补零**。存量任务统计的是
  683 + `park_in_out_record`,两者在停车业务里一一对应;上线后 Redis 快照有值时不走这条兜底。
  684 +
  685 +页面上另有两块指标「泊位周转(次/日)」「平均占用率」,接口取的是 `parking_running_statistic`
  686 +里**最近 10 分钟**(`time_cycle = 10`)的聚合行,本地没有这类行 → 恒为「—」。
  687 +前端 `occupancy.ts` 新增 `summarizeDaily`,在接口返回 null 时用同一次返回的今日小时统计兜底:
  688 +平均占用率 = 各小时 `1 - freeRatio` 的均值(与存量 SQL `AVG(1 - free_ratio)` 同口径),
  689 +周转 = 最后一条小时的 `turnoverRatio`(当日累计,取平均会低估)。接口有值时优先用接口值。
  690 +TDD 门禁:`tests/verify-key-lot-occupancy.mjs`(先失败后通过)。
  691 +
  692 +验证(2026-11-06):用页面实际取到的 47 个车场请求 `query-parkinglot-basic-facts-by-pl-nos`,
  693 +47/47 都有 `berthNum / berthFreeNum`,今日进出与 `order_park` 逐车场核对一致
  694 +(如林西镇中心 317 / 166);页面变成 47 监测车场 / 11 超阈值 / 平均 74.4%,
  695 +Top 20 排名、24 小时占用曲线、两块指标全部有值。1366×768 下右侧曲线原本被面板裁成一条底边
  696 +(`.klm-body` 只剩 155px,被 `.klm-table-wrap` 的 `max-height: 38vh` 挤压),
  697 +加 `@media (max-height: 900px) { .klm-table-wrap { max-height: 26vh; } }` 把高度让给曲线,
  698 +1366×768 与 1920×1080 均已实测。
  699 +
  700 +注意:**演示数据 `--apply` 之后仍要跑一次 `scripts/warm-cache.sh`**。本页车场清单与
  701 +`plNo` 回填都走存量车场缓存(`ParkLotCacheUtil`),新加的车场不在缓存里时 DTO 的 `plNo`
  702 +为 null,接口会按存量口径返回空列表 —— 这次就是先被这个坑挡了一次,跑完预热才出数。
  703 +
  704 +### 7.13 指挥中心「各区联网接入进度」:12 个旗县区一屏看全(2026-11-06)
  705 +
  706 +补完 11 个旗县区数据后,这块面板的缺陷才显出来:`dc-access` 是「同心环 + 明细列表」两栏,
  707 +列表每行把「区名」和「已接入数」上下两行堆叠,12 行共约 317px,而 1080p 下面板内容区只有
  708 +138px —— 一屏只能看到 5~6 个区,其余要在面板内滚动;大屏没人会去滚,等于丢了 6 个区。
  709 +
  710 +改法(只动 `src/views/gov/commandCenter/index.vue` 的样式,接口与模板结构不变):
  711 +
  712 +- **行改单行**:`.dc-access-info` 由纵向 flex 改横向 baseline,「区名 3/5 60%」一行读完,行高 21px → 12px;
  713 +- **列表自动分栏**:`grid-template-columns: repeat(auto-fill, minmax(120px, 1fr))`,
  714 + 列宽够就两栏(1920×1080 起),不够自动退回一栏,不用写死断点;
  715 + 行间距用 `align-content: space-between` 铺满面板高度,与同心环上下对齐;
  716 +- **环那栏按环的实际直径取值**:`--dc-ring-size: clamp(96px, 11.7vh, 180px)`(与 `ringStyle()`
  717 + 同一套 clamp),原来固定 1.05fr 分掉一半宽度、环两侧各空 40px,现在把宽度让给列表;
  718 +- **同心环补中心圆盘**:`.dc-rings-center::before` 用同一套尺寸算出内径,盖住第 4~6 圈,
  719 + 「109 / 已接入车场」不再被 20~48px 的小环压在下面(原来字和环糊在一起);
  720 +- **1728 以下单栏滚动时底部渐隐**(`@media (max-width: 1710px)` 的 `.dc-access::after`):
  721 + 一栏装不下 12 行,硬切会像渲染坏了,渐隐提示下面还有内容。
  722 +
  723 +为什么不在窄屏也两栏:1366×768 左列只有 308px,扣掉环的 100px 后列表 190px,
  724 +两栏各 85px,「阿鲁科尔沁旗」只能显示成省略号 —— 宁可滚动也不截区名。大屏的标准尺寸是
  725 +1920×1080(及以上),那一档必须一屏看全。
  726 +
  727 +**同页补修:1366×768 整页多出 50px 滚动**(2026-11-06,量 `v-layout-content-scrollbar`
  728 +的 `scrollHeight - clientHeight` 发现)。`@media (max-height: 820px)` 里给
  729 +`.command-center` 写的是 `height: calc(100vh - --top-tool-height - --tags-view-height)`,
  730 +**漏减了 `--app-footer-height`**:页脚在内容之后是普通流,等于把 50px 的页脚顶出视口,
  731 +页面反而能滚 50px。补上 `- var(--app-footer-height)` 后与基础规则一致,页脚 718..768 完整可见、
  732 +`scrollHeight == clientHeight`;代价是矮屏面板内容区少 50px,「各区联网接入进度」由
  733 +138px 变 92px(12 行里完整可见 5~6 行 + 底部渐隐,本来就是这一档的降级形态)。
  734 +1920×1080 不受影响(列表仍 138px / 12 行全见)。
  735 +
  736 +**同页补修:「各区今日收入排行」同样只露 5 个区**(2026-11-06)。12 行单列要 244px,
  737 +面板内容区 111px,第 6~12 名(宁城县、喀喇沁旗、巴林左旗、林西县、阿鲁科尔沁旗、巴林右旗、
  738 +克什克腾旗)要面板内滚动才能看到,而表头写的是「合计 2.8 万元」、屏上只能加出 1.7 万。
  739 +改法与联网接入进度一致:`.dc-revenues` 从 flex 列改成 `grid-template-columns:
  740 +repeat(auto-fill, minmax(150px, 1fr))`(1920 两栏 6 行、2560 三栏 4 行、1366 自动退回一栏),
  741 +`row-gap` 由 `0.6vh` 收到 `0.4vh` 才塞得下 6 行;1460 以下退回一栏时用
  742 +`mask-image` 压底部渐隐 —— **不能用 `.dc-panel--revenue::after`**(已被面板四角装饰占用),
  743 +**也不能用 `.dc-revenues::after`**(滚动容器里的伪元素会跟着内容滚走)。
  744 +实测:1920×1080 2 栏 / 111=111、1600×900 2 栏 / 87=87、2560×1440 3 栏 / 159=159,
  745 +三档页面滚动均为 0;1366×768 单栏 74/216 + 底部渐隐,页面滚动 0。
  746 +
  747 +实测(`playwright-cli`,登录态):1920×1080 列表 290px / 两栏各 134px / 12 行全部在内
  748 +(`scrollHeight == clientHeight`);2560×1440 两栏各 189px、行高 16px;1366×768 单栏、
  749 +可见 7.6 行 + 底部渐隐。同期复测其余 4 个左栏面板(供给结构、重点车场、时长分布、收入排行)
  750 +高度与内容均未变 —— 本次只改面板内部布局,没动 `.dc-panel--*` 的 flex 比例。
  751 +
  752 +### 7.14 监管中心:三行磁贴的 1920×1080 / 1366×768 一屏(2026-11-06)
  753 +
  754 +§7.6 里写「监管中心也已一屏适配」,**这条是错的**。复核时发现该页两档标准尺寸都在溢出:
  755 +
  756 +| 尺寸 | 内容高 | 可视高 | 溢出 | 表现 |
  757 +|---|---|---|---|---|
  758 +| 1920×1080 | 1077 | 994 | 83 | 第三行「进入模块」压到视口边缘,数据口径脚注和页脚都看不见 |
  759 +| 1600×900 | 843 | 764 | 79 | 同上,第三行与页脚被切 |
  760 +| 1366×768 | 864 | 682 | 182 | 第三行整行 + 脚注、页脚要滚动才能看到 |
  761 +
  762 +原因是 8 张磁贴按 `span 3` / `span 6` 排成三行(01·02 宽卡一行,03·04·05、06·07·08 各一行),
  763 +每行高度由内容撑开(1080p 下 251px、768p 下 200px),三行加上页头、六格通栏指标就超了。
  764 +
  765 +改法全部是样式(模板、数据、接口未动,文件 `src/views/gov/supervisionCenter/index.vue`):
  766 +
  767 +- **960 档(`@media (max-height: 960px)`,1600×900 这一档)只去掉 `.sc-tile-tags` 标签行**
  768 + (模块说明已由 brief 承担),三行各少一行 chips 正好补上 79px;再收 1px 通栏指标留白。
  769 + 这一档**不能**并进下面的 820 紧凑块 —— 那块带 `.supervision-center { min-height: 0 }`,
  770 + 用在 900 高的窗口上内容只剩 576px,底部会空一大片;
  771 +- **820 档(`@media (max-height: 820px)`)压内部而非压字号**:同样去掉标签行、
  772 + `.sc-tile-icon` 钉到 26px(卡头 36 → 26)、`--sc-tile-pad-y` 收到 8px、
  773 + 卡内与入口条 padding、`.sc-main` / `.sc-tile-group` / `.sc-kpis` 间距各收一档;
  774 +- **1080p 也是三行,所以还要动全局值**:图标 clamp 上限 52 → 44(`2.6vw` → `2.15vw`,1366 下仍是 min 30,不受影响)、
  775 + `--sc-tile-pad-y` 上限 24 → 21、`.sc-tile-brief` 行高 1.65 → 1.55、`.sc-tile-desc` 行高 1.75 → 1.6、
  776 + 磁贴网格 gap `0.8vw` → `0.6vw`、卡内 gap `0.7vh` → `0.55vh`、入口条 padding 上限 11 → 9。
  777 + 这些值改完卡高 251 → 226,信息密度更高但不再靠省略号裁字(brief 仍是 3 行 clamp)。
  778 +
  779 +实测(`playwright-cli`,登录态,量 `scrollHeight` 与 `clientHeight`):
  780 +1920×1080 994 / 994(卡高 226,脚注底 1017);1600×900 764 / 764(卡高 179,标签行隐藏);
  781 +1366×768 682 / 682(卡高 141,脚注底 698);2560×1440 与 1280×800 同样一屏。
  782 +
  783 +**复核大屏一屏适配时不要量 `document.documentElement.scrollHeight`**:应用内容在
  784 +`.el-scrollbar__wrap` 里滚,`documentElement.scrollHeight` 恒等于视口高度,永远量不出溢出 ——
  785 +§7.6 的误判就是这么来的。正确写法:
  786 +
  787 +```js
  788 +const root = document.querySelector('.supervision-center')
  789 +const wrap = root.closest('.el-scrollbar__wrap') // 注意是「内容页」所属的那个 wrap,
  790 + // 不是侧边栏的(侧边栏 wrap 能 scrollTop 但页面不动)
  791 +wrap.scrollHeight - wrap.clientHeight // > 0 才是真的溢出
  792 +```
  793 +
  794 +### 7.15 演示数据重灌 + 指挥中心占用率口径/精度对齐(2026-11-06)
  795 +
  796 +**演示数据是「当日快照」,隔天必空**。§7.11 的订单只写「今天 00:00–现在」,第二天打开
  797 +指挥中心 / 实时流量监控就是一片「—」。演示前固定跑两步,缺一不可:
  798 +
  799 +```bash
  800 +python3 scripts/gen-demo-district-data.py --apply # 重建当天订单 / 支付 / 实时泊位 / 运行统计
  801 +./scripts/warm-cache.sh # 预热存量车场缓存(§7.12 末尾已踩过这个坑)
  802 +```
  803 +
  804 +本次现场症状:实时流量监控 KPI 全是「—」,`dataThrough` 停在 13:37:58(上一次导入的时刻),
  805 +其余图表还在画上一批数据 —— 数据截止时间与页面刷新时间差了几个小时。重灌 + 预热后变成
  806 +在停 11,641 / 近 5 分钟入场 101 / 出场 44 / 平均停放 1 小时 33 分。
  807 +
  808 +**脚本修正:当日订单的分钟分布**(`scripts/gen-demo-district-data.py`)。原来按小时权重取到
  809 +「当前这一小时」后,再在整小时里随机取分钟,`now - randrange(2, 30)` 的兜底把落在未来的记录
  810 +压回最后 30 分钟,于是「近 60 分钟进出场」图上是一段 4 倍高的假峰接断崖。现在:
  811 +
  812 +- 当前小时的权重按已过分钟数打折(`weights[-1] *= (now.minute + 1) / 60`),整小时的量不会
  813 + 全挤进已过去的那几分钟;
  814 +- 分钟只在 `[0, now.minute]` 里取,不再生成未来时间再往回压。
  815 +
  816 +合成验证(导入模块造假车场):近 60 分钟 59 个桶都有值,峰值 22、中位数 14,曲线单调自然。
  817 +
  818 +**指挥中心「泊位占用率」70.0% vs 综合监测 71.3%**:两处偏差,都在
  819 +`ParkCommandCenterServiceImpl`。
  820 +
  821 +1. **口径**:实时空闲泊位的取值要求「同一行既有 `free_berth` 又有 `berth_num`」,缺 `berth_num`
  822 + 的车场被整场跳过 —— 分子分母同时少算,分母因此比综合监测小(15,517 vs 16,210)。
  823 + 现在 `freeBerth()` 只要求 `free_berth` 有值;分母 `occupancyBerths()` 优先用实时行的
  824 + `berth_num`,缺省回落到车场登记的 `pl_berth_num`,与综合监测(`ParkMonitorServiceImpl#toLot`
  825 + 的 `firstNonNull`)完全同口径。KPI、各区分布、重点车场、满位车场预警四处全部改用它。
  826 +2. **精度**:占用率是 0~1 的比例,前端再乘 100 显示。原来复用金额的 `round1`,等于 **10% 一档**,
  827 + 65%~74% 全显示成 `70.0%`;综合监测保留 3 位小数(71.3%),两页怎么都对不上。
  828 + 新增 `round3()`(`BigDecimal.setScale(3, HALF_UP)`),只用于比例字段。
  829 +
  830 +改完 `POST /admin-api/park/command-center/overview` 返回(数值随每次 `--apply` 的随机种子浮动,
  831 +车场 / 泊位 / 进场车次是确定的,收入有几百元级差异)
  832 +`kpis { lotTotal 109, berthTotal 16,210, todayInCount 21,710, freeBerths 4,655, occupancy 0.713,
  833 +turnover 1.34, todayIncome 28,349 }`,各区 松山区 0.721 / 敖汉旗 0.705 / 元宝山区 0.813 /
  834 +翁牛特旗 0.681,重点车场 0.986 / 0.979 / 0.973 / 0.944 / 0.939 / 0.919;与
  835 +`POST /park/monitor/realtime-overview` 的 `summary.occupyRate = 0.7128` 及逐车场值一致。
  836 +页面 DOM 校验 7 个 KPI 依次为 109 个 / 16,210 个 / 21,710 次 / 4,655 个 / **71.3%** / 1.3 次/日 /
  837 +2.8 万元。
  838 +
  839 +> 同类问题排查入口:凡是「同一指标两个页面数字不同」,先比**分母口径**(哪张表给的容量、
  840 +> 缺字段的行是否被跳过),再比**舍入位数**。这次两个都中了。
  841 +
  842 +### 7.17 综合监测「区县占用率」13 行一屏(2026-11-06)
  843 +
  844 +和 §7.13、§7.15 同一类:左栏 `mon-rail-body` 装「全市 + 12 个旗县区」13 行,
  845 +原来是两行排(区名/占用率 一行 + 进度条/场数 一行),每行 47px、合计 650px,
  846 +1080p 内容区 403px —— 一屏只看到 8 个,红山区、阿鲁科尔沁旗、翁牛特旗、巴林左旗、
  847 +宁城县这 5 个要在栏内滚动才看得到,而地图上 12 个区县是全部着色的。
  848 +
  849 +改法(`src/views/park/monitor/index.vue`,样式 + 行内结构,数据与接口不动):
  850 +把「场数」并到首行,进度条单独占第二行并铺满整栏;行高 47 → 29px,13 行 377px。
  851 +实测 1920×1080 `mon-rail-body` 403 = 403,13 行全部在内、页面滚动 0。
  852 +1366×768 栏高只有 212px,仍要滚动,这档加 `.mon-rail-body` 底部 `mask-image` 渐隐;
  853 +1920 不受影响(渐隐在 `max-height: 960px` 内)。
  854 +
  855 +> 顺带核对:右侧「车场实时状态」页脚「共 109 个车场」与 KPI「车场数 109」一致,
  856 +> 接口 `lots` 也有 109 条 —— 之前的「100」是截图分辨率看错,不是缺陷。
  857 +
  858 +### 7.16 实时停车流量监控:区分「数据停更」和「流量归零」(2026-11-06)
  859 +
  860 +演示数据只写到脚本运行的那一刻。跑完 5 分钟以后再打开 `/park/flowMonitor`,
  861 +「近 5 分钟入场 / 出场」两张卡是 **0**,60 分钟曲线末端是一段贴着 0 的断崖 ——
  862 +看上去像流量真的没了。原页面只在**整个窗口都没有记录**时才提示「数据停更」,
  863 +窗口里还有历史、只是尾部停了的情况没有任何提示。
  864 +
  865 +改法:
  866 +
  867 +- `flow.ts` 新增纯函数 `staleMinutesOf(dataThrough, now)`:返回库内最后一条距今多少分钟,
  868 + 解析不了返回 `null`,时钟误差(now 早于记录)夹到 0;
  869 +- 页面 `staleMinutes >= 5`(正好是「近 5 分钟」这个窗口)时:两张卡显示「—」而不是 0,
  870 + 备注改「数据停更 · 近 5 分钟无上报」,页头「数据停更 · 最后一条 …」芯片也一并出现
  871 + (原来只在 `!hasData` 时出现)。这样「0」只表示「确实没车」,「—」才表示「没数据」;
  872 +- 10 秒轮询每次刷新 `data` 时重算,不需要额外定时器。
  873 +
  874 +TDD 门禁:`tests/verify-flow-monitor.mjs` 先加断言(`staleMinutesOf is not a function` 失败)再实现。
  875 +实测:16:06 停更时两张卡为「—」+ 停更芯片;重灌演示数据(16:26)后变成 106 / 49,芯片消失。
  876 +
  877 +**演示纪律**:`--apply` 之后 5 分钟内这两个指标才非 0,演示超过 10 分钟它们会重新变成「—」
  878 +(这是真实的数据停更状态,页面现在会明说)。要连续演示就中途再跑一次 `--apply`。
  879 +
  880 +### 7.18 一张图 / 专题分析的演示夹具补齐 11 个旗县区(2026-11-06)
  881 +
  882 +§7.11 已把 69 个车场灌进本地演示库,指挥中心、综合监测、监管中心都是读库、12 个区县都有数。
  883 +但「停车资源一张图」「专题分析」读的是前端夹具 `src/views/gov/oneMap/demo.ts`(不是接口),
  884 +里面只有 14 个手工车场、覆盖红山区 / 松山区 / 元宝山区 —— 于是同一场演示里两个页面对不上:
  885 +
  886 +- 一张图的地图上 9 个区县标「未接入」;
  887 +- 专题分析的区县下拉里 11 个区县写「(暂无车场)」且不可选。
  888 +
  889 +改法(只动夹具,接口与视图层不改):在 `curatedLotSeeds` 之后加一份 `districtSeeds`
  890 +元组表(`[名称, 区县, 经度, 纬度, 泊位, 空余, 今日进场, 今日出场, pl_type]`,67 条,
  891 +名称 / 坐标 / 泊位 / 实时空余 / 今日进出原样取自本地演示库,与综合监测同源),
  892 +再用 `districtLotSeeds` 展开成完整 `ParkingLot`:
  893 +
  894 +- id 从 101 起编、`code = PL0101…`,与手工夹具的 1–14 不撞号;
  895 +- `pl_type` 1 → `type: 'roadside'` + `plTypeProp: '1101'`,2 → `'enclosed'` + `'2102'`;
  896 +- 收费口径、营业时间、服务电话、分区、出入口按类型给合理默认值,路侧车场不给分区/出入口
  897 + (与手工夹具里路侧车场的写法一致);
  898 +- 占用率 ≥95% 记 `special`,与指挥中心「满位车场」同口径;
  899 +- 与手工夹具重名的两个(松山万达广场停车场、松山区政务服务中心停车场)不重复补。
  900 +
  901 +最后 `const lotSeeds = [...curatedLotSeeds, ...districtLotSeeds]`,原有
  902 +`parkingLots = lotSeeds.map(...)` 的建号 / 造设备逻辑不动。
  903 +
  904 +实测(1920×1080):一张图资源总览 **81 车场 / 16,220 泊位 / 4,236 空余 / 73.9% / 24,527 今日进场**,
  905 +12 个区县全部着色并带占用率;专题分析下拉 12 个区县全部可选、无「(暂无车场)」,
  906 +选阿鲁科尔沁旗得 4 车场 / 630 泊位且覆盖·流量·收费·营收四个专题都有图。
  907 +
  908 +TDD 门禁 `tests/verify-topic-analysis.mjs` 同步更新:全市 14 → **81** 条;原来的空态断言
  909 +用阿鲁科尔沁旗,该区补齐后恒不为空,改为不存在的区县名(空态分支仍在,接口返回空时也会走到),
  910 +另加一条「阿鲁科尔沁旗 > 0」守住补齐结果。16 个 `tests/verify-*.mjs` 全绿,`eslint` 无告警。
  911 +
  912 +留了两个**已知差异**,都不是缺陷,别当成 bug 去追:
  913 +
  914 +1. **一张图 / 专题分析 81 个车场,综合监测 / 指挥中心 109 个**。前者是前端演示夹具,后者读
  915 + `parking_cloud_yudao`,本来就是两套数据源(一张图页面顶部固定展示「演示数据」标识)。
  916 + 红山区库里那 40 个真实车场没有镜像进夹具:其中 2 个坐标在省外(116.36/39.92、119.55/32.00),
  917 + 放进一张图会把视野拉到北京 / 镇江,而夹具里已有 10 个精修的红山区车场。是否改成读真实接口
  918 + 由后续决定。
  919 +2. **「智能设备覆盖 100%」**。夹具给每个车场都造了设备(`buildLotDevices`),覆盖必然是 100%,
  920 + 是指标口径,不是算错。
  921 +
  922 +**附带修好的**:视频监控页的 `demo.ts` 直接 `import { parkingLots }`,夹具补齐后它的设备树
  923 +从原来只有红山区 / 元宝山区两个区县变成 **12 个区县 / 112 台设备(在线 101)**,
  924 +和页面注释里写的「12 个旗县区、约 130 台摄像头」终于对上了,不用再单独补数据;
  925 +树变长后在 `.vm-tree-list` 里内部滚动(4182px / 758px),页面本身仍一屏。
  926 +
  927 +### 7.19 三个列表页改本地分页(2026-11-06)
  928 +
  929 +「反馈管理」「公告管理」「视频设备」和别的停车列表页不一样:接口一次拉全量回来,筛选在页面上本地做,
  930 +表格就把全部行铺出来。数据一多,页面高度直接失控 —— 实测(1920×1080,量的是
  931 +`.v-layout-content-scrollbar .el-scrollbar__wrap` 的 `scrollHeight - clientHeight`):
  932 +
  933 +| 页面 | 行数 | 修复前溢出 | 修复后 |
  934 +|---|---|---|---|
  935 +| 反馈管理 `/park/feedback` | 501(本地库全部待处理) | **21,483 px** | 0(994 = 994) |
  936 +| 视频设备 `/video/device` | 112 | 428 px | 0(994 = 994) |
  937 +| 公告管理 `/park/notice` | 19 | 174 px | 0(994 = 994) |
  938 +
  939 +改法:新增共享 `src/utils/page.ts#pagedSlice(items, page, pageSize)`,把「页码夹到合法区间」
  940 +这件事收在一处 —— 筛选后条数变少、页码还停在旧值时会切出空表,用户会以为数据没了:
  941 +
  942 +- 页码 `< 1` 夹到第 1 页;大于总页数夹到最后一页;`pageSize` 为 0 按 1 算(避免除零);
  943 +- 三个页面统一 `pageSize = 10`(与其它停车列表页一致,20 条在 1920 下就会溢出 370 px 左右);
  944 +- `handleSearch()` 结尾 `page.value = 1`,换筛选条件回第一页;
  945 +- `watch(rows, …)` 在条数变少时把页码夹回最后一页 —— 答复 / 发布 / 删除都会改状态、进而改排序;
  946 +- 序号列用 `:index="rowIndexOf"`,第 2 页从 11 开始,不是每页都从 1 数;
  947 +- 分页条统一 `.table-pager` + `layout="total, sizes, prev, pager, next, jumper"`。
  948 +
  949 +TDD:`tests/verify-feedback-manage.mjs` 增加 8 条 `pagedSlice` 断言(先 RED:`pagedSlice is not a function`,
  950 +再 GREEN),覆盖首/末/越界/`page<1`/`pageSize=0`/空列表。16 个 `tests/verify-*.mjs` 全绿。
  951 +
  952 +**1366×768 的取舍**:三张列表页在这个尺寸下仍分别溢出 294 / 193 / 363 px。这是**有意保留**的 ——
  953 +它们是有搜索区 + 工具条 + 表头的普通后台列表,把 10 行再压到 768px 内只能砍到 4~5 行/页,翻页次数翻倍;
  954 +其余 8 张演示页(监管中心、指挥中心、一张图、重点车场占用率、专题分析、实时流量、视频监控、综合监测)
  955 +在该尺寸下仍是 `scrollHeight == clientHeight`,一屏要求针对的是大屏与监测页。
  956 +
  957 +页面数据与真实库一致,未造数据:反馈 501 条全部 `is_answer = 0`(库里就没有一条被答复过),
  958 +所以演示时「待处理 501 / 已回复 0 / 超时 501」是真实状态,不是算错,也没有为了好看伪造答复
  959 +(答复会触发真实极光推送)。
  960 +
  961 +### 7.20 三张高德地图的底图从近黑换成蓝色(2026-11-06)
  962 +
  963 +一张图中间的底图看上去是黑的,和周围深空蓝的面板脱节。实测原因是高德内置样式的问题,
  964 +不是页面配色写错:
  965 +
  966 +- `features: ['bg','road','building']` 下地块颜色由样式决定;
  967 +- `amap://styles/darkblue` 的地块取色 **rgb(0,0,1)**,等于纯黑,全屏裁图后几乎看不出边界;
  968 +- 试过 `normal`(浅灰)、`grey`(灰)、`dark`(#313131 灰黑),都不是大屏要的蓝。
  969 +
  970 +改成 `amap://styles/blue`:同为矢量样式,地块变成 rgb(1,69,79) 的深蓝绿,路网 / 水系仍然可见,
  971 +区县着色、气泡、标注在上面都还看得清。三处一起改(都写死同一个字符串):
  972 +
  973 +- `src/views/gov/oneMap/useOneMap.ts`(一张图)
  974 +- `src/views/gov/topicAnalysis/useTopicMap.ts`(专题分析)
  975 +- `src/views/gov/videoMonitor/components/DeviceMap.vue`(视频监控「地图」页签)
  976 +
  977 +只改 `mapStyle` 一个取值,`features`、缩放、交互都没动;三页在 1920×1080 实测仍是一屏
  978 +(`scrollHeight == clientHeight`),一张图 12 个区县标注全部带占用率、无 `NaN / undefined`。
  979 +**别再改回 `darkblue`** —— 它是黑底,不是深蓝底。
  980 +
  981 +### 7.21 一张图加入场动效(2026-11-06)
  982 +
  983 +参考浅色门户模板的 GIF(`大屏素材/01_大屏创意设计/0314_…/05.jpg`,实为 75 帧动图),
  984 +**只借动态节奏、不借配色**:页面仍是深空蓝(`--bs-*` 一套色板),没有改成浅色模板。
  985 +
  986 +**整页 3 秒加载完成**(`PAGE_ENTER_MS = 3000`),顺序按产品要求:地图先从中间
  987 +「开花」铺开,左右面板的卡片再从上到下一张张滑入,最后底部卡片浮起 ——
  988 +
  989 +| 顺序 | 区块 | 动作 | 延迟 / 时长 |
  990 +|---|---|---|---|
  991 +| 0 | 顶栏 | 淡入落下 | 0 / 500ms |
  992 +| 1 | **地图** | 从圆心**开花**:`circle(4%) → circle(150%)` + `scale(0.55) → 1` + 淡入 | 0 / 1200ms |
  993 +| 2 | **左栏第 1 张** | **从左往右**滑入(`translateX(-40px) → 0`) | 900 / 600ms |
  994 +| 3 | 左栏第 2~6 张 | 同上,每张再延后 140ms(`PANEL_STAGGER_MS`) | 1040…1600 / 600ms |
  995 +| 4 | **右栏第 1 张** | **从右往左**滑入(`translateX(40px) → 0`) | 1050 / 600ms |
  996 +| 5 | 右栏第 2~3 块 | 每块再延后 140ms | 1190 / 1330 / 600ms |
  997 +| 6 | **底部** | **从下往上**浮起(`translateY(24px) → 0`) | 1600 / 800ms |
  998 +| — | 九宫格数字 | 0 → 真值滚动(跟左栏第 2 张「资源总览」一起出来) | 1040 / 1960ms |
  999 +
  1000 +**地图改成整块背景,不再套框**:`.om-map-wrap` 从网格单元改成 `position: absolute; inset: 0`,
  1001 +铺满 `.om-body`,左右面板(`z-index: 2`)浮在地图上;同时删掉原来的
  1002 +`border: 1px solid rgba(84,140,200,.34)` 与 `background: #050c1c`。
  1003 +
  1004 +**坑(记一笔)**:地图绝对定位后就不占网格单元了,`grid` 自动排布会把右栏塞进中间那一列、
  1005 +直接盖住地图(右栏一度变成 1020px 宽)。所以 `.om-left` 必须显式 `grid-column: 1`、
  1006 +`.om-right` 必须 `grid-column: 3`。
  1007 +
  1008 +纯逻辑抽在 `src/views/gov/shared/enter-motion.ts`(`entranceDelay` / `entranceFade` /
  1009 +`panelItemDelay` / `entranceEnd` / `countUpTiming` / `easeOutCubic` / `planCountUp` /
  1010 +`formatCount`),`tests/verify-one-map-motion.mjs` 是门禁(TDD,先 RED
  1011 +`formatCount is not a function`):门禁钉住**顺序**(map ≤ left < right < foot)、
  1012 +**逐张递增**(左栏 6 张 = 900/1040/…/1600)、**预算**(各区块落位 ≤ 3000ms,
  1013 +数字延迟 + 时长恰好 = 3000ms、数字跟随「资源总览」那张卡)。
  1014 +
  1015 +几个刻意的取舍:
  1016 +
  1017 +- **数字只滚一次**:切区县、点刷新、30 秒轮询都会重算汇总,每次从 0 重滚整页永远在抖;
  1018 + 首屏滚完 `played` 置真,之后直接给值。
  1019 +- **格式口径不变**:`formatCount` 复刻原页面的 `fmtInt` / `fmtPct` —— 整数千分位四舍五入,
  1020 + 百分比默认一位小数。第一版把百分比按 0 位取整,占用率 `73.9%` 变成 `74%`,被门禁抓了。
  1021 +- **节奏值只有一份**:`entranceVars` 把 `entranceDelay()` / `entranceFade()` /
  1022 + `panelItemDelay()` 注入成 `--om-delay-*` / `--om-fade-*` / `--om-item-left-N`,
  1023 + CSS 只取值,不在样式里另写一份毫秒数。
  1024 +- **地图「开花」用 clip-path 不用滤镜**:`circle(4% → 150%)` 配合 `scale(0.55 → 1)`,
  1025 + 终态必须写成 `circle(150%)` 而不是 `none`,否则两端不可插值、会直接跳变。
  1026 +- **不碰地图多边形**:区县色块「生长」动画高德没有 API,硬做会和重绘 / 缩放打架,没做。
  1027 +
  1028 +实测(`performance.now()`,从导航开始算):476ms 开始开花(scale 0.55 / clip 4% / 透明度 0),
  1029 +1676ms 地图完全铺开,此后左栏卡片 900+140n 逐张进场,**2992ms 数字收尾** —— 刷新到全部完成约 3 秒。
  1030 +1920×1080 与 1366×768 仍一屏(`scrollHeight == clientHeight`,944 / 632),
  1031 +17 个 `tests/verify-*.mjs` 全绿。
  1032 +
  1033 +已知遗留(本次不动):1366×768 下顶栏中间的标题与右侧搜索框有横向重叠,是该页顶栏
  1034 +`grid-template-columns: 1fr auto 1fr` 在窄屏的老问题,与动效无关。
  1035 +
  1036 +### 7.22 一张图左右资源清单重排 + 底部大屏导航(2026-11-06)
  1037 +
  1038 +方案见 `docs/superpowers/specs/2026-11-06-停车资源一张图左右资源清单与底部导航设计方案.md`(方案 A)。
  1039 +一句话:左栏从「按功能凑的 6 张卡」收敛成 **资产 3 张卡**(运行总览 / 资源清单 / 图层管理),
  1040 +右栏只留 **1 张卡 4 个页签**(实时进出场 / 区县态势 / 重点车场 / 实时告警,选中对象整卡换详情),
  1041 +页脚 40px 一条:左边 5 个大屏导航按钮,右边压缩图例(**导航已于 §7.24 移到地图上,页脚只留图例**)。
  1042 +
  1043 +**资源清单(新增,7 行 + 1 行未定位)**,口径全在 `src/views/gov/oneMap/catalog.ts` 的 `buildCatalog()`:
  1044 +(2026-11-09 那 1 行「未定位资源」已整块删除,见 §7.30)
  1045 +
  1046 +| 行 | 数值 | 副信息 | 点击 |
  1047 +|---|---|---|---|
  1048 +| 停车场 | 81 个 | 占用率 73.9% | 只显示车场,右栏切「重点车场」 |
  1049 +| 泊位 | 16,220 个 | 空余 4,236 | 同车场 |
  1050 +| 路侧泊位点位 | 984 个 | 占用 620 / 空闲 282 | 只显示点位,缩放锁 16–17 |
  1051 +| 感知设备 | 961 台 | 离线 92 · 故障 17 | 只显示设备点,缩放锁 11–15 |
  1052 +| 视频设备 | 242 路 | 视频桩 121 · 摄像机 121 | 只显示视频点 |
  1053 +| 充电设施 | 4 站(后缀 36 桩) | 在线 3/4 | 只显示充电站 |
  1054 +| 实时告警 | 5 起 | 红 1 / 橙 2 / 黄 2 | 右栏切「实时告警」 |
  1055 +
  1056 +点已选中的行 = 取消筛选、恢复全部图层并 `fitPoints` 回全市。
  1057 +
  1058 +**口径坑(门禁抓到过两次)**:
  1059 +
  1060 +1. **两个「泊位」不是一回事**:16,220 是车场登记泊位数(地图不逐位画),984 是已上图的路侧泊位点位。
  1061 + 合成一个数就是对外发布错的资源量,所以清单必须两行分开。
  1062 +2. **视频设备 = 242,不是 121**:`devices.kind` 里 `video`(视频桩)与 `camera`(摄像机)各 121 台,
  1063 + 只数 `video` 一类会少一半。副信息分开写成「视频桩 121 · 摄像机 121」。
  1064 +3. 告警级别以 `demo.ts` 实际数据为准(红 1 / 橙 2 / 黄 2),方案稿里估的 2/2/1 是错的。
  1065 +
  1066 +**底部导航**:`src/views/gov/oneMap/nav.ts` 的 `PAGE_NAV` 5 项(一张图 `/park-map`、专题分析
  1067 +`/park-map/analysis`、视频监控 `/video/monitor`、综合监测 `/park/monitor`、车辆追溯
  1068 +`/park/vehicleTrace`),按权限码过滤后才渲染,**不新增权限、不调整任何角色授权**。
  1069 +
  1070 +坑:当前页高亮必须用**路径全等**(`isNavActive`),不能用 `startsWith` —— `/park-map` 是
  1071 +`/park-map/analysis` 的字符串前缀,前缀匹配会让「一张图」在专题分析页也亮着。
  1072 +
  1073 +**入场动效跟着改**(`enter-motion.ts`):左栏卡片数 6 → 3(新增 `panelItemCount()`,门禁同步断言),
  1074 +数字滚动改为跟**第 1 张卡「运行总览」**同拍(原为第 2 张),仍恰好第 3 秒收尾。
  1075 +§7.21 里那张「左栏 6 张 900 / 1040 / … / 1600」的节奏表已被本节取代:现在是 900 / 1040 / 1180。
  1076 +
  1077 +**实现取舍**:不拆 `ResourceCatalog.vue` / `PageNav.vue`。`index.vue` 的样式是 `scoped`,清单与导航大量
  1078 +复用 `.om-sec` / `.om-cat*` / `.om-legend` 这些局部类,子组件拿不到这些样式,拆了只能复制一遍面板样式或改成全局。
  1079 +纯逻辑(算数、路径判定、权限过滤)仍然抽成 `catalog.ts` / `nav.ts` 并被门禁覆盖。
  1080 +
  1081 +**一屏(≤1500px 紧凑档)**:1366×768 下左栏原本需要 763px、只有 514px。加
  1082 +`@media (max-width: 1500px)` 后把清单行压成一行(副信息右对齐、隐藏迷你条)、缩小面板 padding/gap、
  1083 +KPI、图层行、导航按钮与右栏抓拍图,字号最低 9–11px。实测 1920:整页 944/944、左 813/813、右 813/813、图例 1238/1238;
  1084 +1366:整页 632/632、左 514/514、右 514/514(4 个页签都 514/514)、图例 772/772。**左栏靠 `scrollHeight ==
  1085 +clientHeight` 验收,不靠滚动条兜底。**
  1086 +
  1087 +小坑:`.om-legend-note { display: none }` 在断点里写不生效 —— 基础规则 `.om-legend > span`(0,1,1)
  1088 +优先级高于 `.om-legend-note`(0,1,0),必须写成 `.om-legend > .om-legend-note`。
  1089 +
  1090 +门禁:新增 `tests/verify-one-map-catalog.mjs`(7 行顺序 / 口径 / 颜色 / 空数据 / 无站名兜底)、
  1091 +`tests/verify-one-map-nav.mjs`(5 入口 / 路径全等 / 权限过滤),改 `tests/verify-one-map-motion.mjs`。
  1092 +前端仓库共 **19 个 `tests/verify-*.mjs` 全绿**。
  1093 +
  1094 +### 7.23 一张图按「只有 3 个区有数据」重排:空区留白、有数据区铺满(2026-11-06)
  1095 +
  1096 +> 2026-11-07 追加喀喇沁旗为第 4 个有数据的旗县区,见 §7.25;本节其余口径不变。
  1097 +
  1098 +背景:全市 12 个旗县区里,**只有红山区 / 松山区 / 元宝山区**真正接入数据,其余 9 个
  1099 +后期也不会有数据接入。§7.18 当时把 11 个旗县区都补了演示车场,等于在大屏上对外宣称
  1100 +全域已联网;页面又按「12 个区都有」排版,一旦按真实口径只剩 3 个区,右栏会空掉一大半。
  1101 +
  1102 +**口径改成 3 个区,总量不变**:
  1103 +
  1104 +| 项 | 改前 | 改后 |
  1105 +|---|---|---|
  1106 +| 有数据的区县 | 12 | **3**(`COVERED_DISTRICTS`,`isCoveredDistrict()`) |
  1107 +| 车场 / 泊位总量 | 81 场 / 16,220 位 | **不变** |
  1108 +| 49 条补充种子 | 挂在 9 个空区 | 全部改挂到 3 个有数据的区(`relocationPlan`) |
  1109 +
  1110 +搬迁不是改数字,是**重生成名称与坐标**:名字取自三个区的地名池(不能出现「经棚」「锦山」
  1111 +这类别的旗县镇名),坐标按黄金角螺旋落在各区城区中心附近(半径 `0.01° × √(n+1)`,
  1112 +最外圈约 5 km),保证点真的落在本区行政边界内。`tests/verify-one-map-districts.mjs`
  1113 +断言 81 个车场全部落在所属区县 bbox 内(39 / 23 / 19 分布)。
  1114 +
  1115 +**空区怎么呈现**:地图上只画灰蓝底 + 虚线边界,标签只有区名,**不写任何状态或排期字样**。
  1116 +全平台新增门禁 `tests/verify-no-pending-wording.mjs`:扫 `src/` 下全部 `.vue/.ts/.mjs/.json/.scss`
  1117 +(含注释)里出现「待接入」即失败 —— 把这件事挂在页面上等于向使用方承诺一个不存在的排期。
  1118 +空态统一用「暂无数据 / 暂未开放 / 暂未迁入 / 非实时数据」。
  1119 +
  1120 +**页面怎么在只有 3 个区时不显空**(这是本节的主要工作量):
  1121 +
  1122 +1. **KPI 4 → 6**:补「今日订单」和「覆盖旗县区 3/12」,一屏 6 个数字把「全市」讲完整。
  1123 +2. **右栏区县态势**:从「12 行」改成「3 张有数据卡片 + 数据覆盖块」。卡片按 `flex: 1 1 0` 均分
  1124 + 右栏剩余高度(`justify-content: space-between` + 显式 `align-items: stretch`,否则 button 的
  1125 + UA 样式会把三行缩成一小坨),内容是大号占用率 + 泊位 / 空余 + 进度条;底部「数据覆盖 3/12」
  1126 + 进度条 + 「其余 9 个旗县区暂无数据」+ 9 个灰色 chip。
  1127 +3. **重点车场 / 实时告警**同样只有 6 / 5 条,行均分剩余高度;告警底部再加一块「告警分级」
  1128 + (红 / 橙 / 黄条数,从同一份 `alerts` 算),把版式收住。
  1129 +4. **地图取景改成数据驱动**:`fitAllDistricts()` 只按有数据的车场坐标取景(原来按区县多边形取景,
  1130 + 会把 9 个空区一起框进来,导致有数据的三区缩成一小块)。缩放取 `Math.floor` 整档 ——
  1131 + 高德按整数级渲染,四舍五入会多放大半级,最外侧的点被挤出屏幕。
  1132 +5. **区县填充透明度下调**:`CORE_DISTRICT_FILL_OPACITY` 0.74 → 0.46、`OTHER` 0.44 → 0.34
  1133 + (`gov/shared/district-theme.ts`),让底图和车场气泡透出来,否则三块色块把地图糊住。
  1134 +6. **聚合改像素网格**:`renderCluster()` 原来按经纬度距离单链聚合,车场挪密之后会串成一个大球
  1135 + (实测 39 / 60 / 62 三坨)。改成 `map.lngLatToContainer()` 换算成屏幕像素后按 60px 方格分组,
  1136 + 15 个气泡互不重叠。`render()` 的散点阈值也随之下调到 10 级。
  1137 +
  1138 +**实测一屏**:1920×1080 左 813/813、右 813/813、整页 1080/1080,右栏 4 个页签底部余量 13px;
  1139 +1366×768 左 514/514、右 514/514,区县态势 / 重点车场 / 实时告警余量 9px(实时进出场 8 条列表
  1140 +允许滚动,超出 94px)。地图 15 个聚合气泡,三区全部在左右面板之间的可视区内。
  1141 +
  1142 +门禁:新增 `tests/verify-no-pending-wording.mjs`;改 `tests/verify-one-map-districts.mjs`
  1143 +(`covered` / `uncovered` 分组、3 区 / 12 个、bbox 包含、39/23/19 分布)与
  1144 +`tests/verify-map-theme.mjs`(填充透明度新值)。前端仓库共 **21 个 `tests/verify-*.mjs` 全绿**。
  1145 +
  1146 +### 7.24 一张图:导航压到地图上、点开走弹框、车场气泡改圆润泪滴(2026-11-07)
  1147 +
  1148 +三件事,都在一张图页内完成,不新增路由、不改权限数据。
  1149 +
  1150 +**1. 5 个导航中的 4 个移到地图下沿**
  1151 +
  1152 +`一张图` 是当前页,不再出现在按钮里(`mapNavItems()` 按 `isNavActive` 过滤)。剩下
  1153 +专题分析 / 视频监控 / 综合监测 / 车辆追溯 从 40px 页脚挪进 `.om-map-wrap`,
  1154 +`.om-map-nav` 绝对定位 `bottom: 12px; left: 50%`,深色底条 + 描边。页脚只剩图例一条:
  1155 +
  1156 +- 1920×1080:按钮条 869→1251(382×42),`.om-foot` 只剩图例;
  1157 +- 1366×768:按钮条 624→942(318×40),与左下角 `.om-zoom-hint`(221→333)不重叠;
  1158 +- 地图下的点位会被按钮条盖住(实测底部中间偶尔压住一个气泡),属于浮层控件的正常代价,
  1159 + 不做避让 —— 避让会把按钮推到地图外,又回到页脚方案。
  1160 +
  1161 +**2. 点按钮不跳页,用弹框装目标页**
  1162 +
  1163 +纯逻辑在 `src/views/gov/oneMap/nav-embed.ts`:
  1164 +
  1165 +- `mapNavItems(items, routePath)`:过滤当前页,路径判定沿用 `isNavActive`(全等 + 尾斜杠归一);
  1166 +- `resolveRouteView(router, path)`:从 `router.resolve(path).matched` **从后往前**取第一条
  1167 + `components.default`(前几条是 Layout / Parent 包装),懒加载函数 `await` 后取 `.default`;
  1168 + 解析失败、路由缺失、chunk 加载失败一律返回 `null`,页面显示「该页面没有注册到当前路由…
  1169 + 可以改用全屏打开」并保留「全屏打开」按钮,不白屏也不抛异常。
  1170 +
  1171 +弹框头显示目标页名 + 「页内打开 · 不离开一张图」+「全屏打开」+「关闭」;遮罩点击、Esc 关闭。
  1172 +连点两个入口时用 `embedToken` 递增丢弃先返回的那次解析结果,避免「后点的页面被前一次的
  1173 +异步结果覆盖」。`<component :is>` 渲染真实路由组件,不是截图/iframe。
  1174 +
  1175 +实测 1920×1080 四个页面在弹框里都铺满 `.om-embed-body`(969px),`scrollHeight == clientHeight`
  1176 +无内部滚动:专题分析 969、视频监控 944、综合监测 969、车辆追溯 969。样式上用
  1177 +`:deep()` 把 `.topic-analysis / .vm-page / .mon / .vt` 的高度钉成 `100% !important`,
  1178 +否则这些页面按整屏高度算会溢出弹框。
  1179 +
  1180 +**3. 车场气泡换成圆润泪滴**
  1181 +
  1182 +原来是一个圆 + 一个三角尾巴(`M20 30 L26 50 L32 30 Z`),放大后接缝处是折角。现在整块
  1183 +SVG 抽到 `src/views/gov/oneMap/marker-svg.ts`(纯函数,门禁可直接断言):
  1184 +
  1185 +| 部件 | 改前 | 改后 |
  1186 +|---|---|---|
  1187 +| 外轮廓 | 圆 + 三角形两条路径 | 一条贝塞尔泪滴 `M26 50 C 26 50 10 34 10 20 A 16 16 0 1 1 42 20 C 42 34 26 50 26 50 Z`(只有 M/C/A) |
  1188 +| 头部 | 圆 r=15,圆心 (26,19) | 圆 r=16,圆心 (26,20);路侧从 30×30 rx=9 改成 32×32 rx=12 |
  1189 +| 选中圈 / 重点虚线圈 | r=20 / r=18,(26,19) | r=20 / r=19,(26,20) |
  1190 +| 尖点 | y=50,marker 锚 `bottom-center` | 不变(画布 52×52,`LOT_ICON_SIZE`) |
  1191 +
  1192 +头部形状盖在泪滴上,露出来的只有平滑尾巴;描边 `stroke-linejoin="round"`,整体比三角尾巴
  1193 +宽一圈、没有直边。聚合气泡(`clusterBubbleSvg`)仍是同心圆,只做了搬家。
  1194 +
  1195 +**4. 顺带修 1366 顶栏重叠**
  1196 +
  1197 +`@media (max-width: 1500px)` 下右栏内容(搜索 118 + 区县 174 + 三个按钮 + 间距 ≈ 488px)
  1198 +超过 1fr 列宽(≈396px),向左溢出压住居中的标题。改成 `grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.25fr)`,
  1199 +搜索框 96px、区县下拉 `max-width: 128px`;实测右栏首个控件 x=935,标题右边界 899,不再相交。
  1200 +
  1201 +门禁:新增 `tests/verify-one-map-nav-embed.mjs`(导航过滤 + 路由组件解析/兜底)与
  1202 +`tests/verify-one-map-lot-bubble.mjs`(泪滴路径无直线命令、尖点仍压 y=50、圆形/路侧两种头、
  1203 +角标 999+、选中圈与虚线圈同心、聚合仍是圆)。前端仓库共 **23 个 `tests/verify-*.mjs` 全绿**,
  1204 +`eslint` / `prettier --check` 通过。
  1205 +
  1206 +### 7.25 喀喇沁旗纳入有数据区县 + 弹框去表头(2026-11-07)
  1207 +
  1208 +**1. 喀喇沁旗(第 4 个有数据的旗县区)**
  1209 +
  1210 +`demo.ts` 的 `COVERED_DISTRICTS` 从 3 个改成 4 个:`红山区 / 松山区 / 元宝山区 / 喀喇沁旗`。
  1211 +喀喇沁旗**保留自己的 6 条种子**(锦山镇中心 / 喀喇沁旗医院 / 王爷府景区 / 锦山大街路侧 /
  1212 +喀喇沁旗政府 / 美林谷景区),不再外迁;其余 43 条种子继续按原节奏迁进前三个区。
  1213 +
  1214 +| 项 | 改前 | 改后 |
  1215 +|---|---|---|
  1216 +| 有数据的区县 | 3(红山 / 松山 / 元宝山) | **4**(+ 喀喇沁旗) |
  1217 +| 车场分布 | 39 / 23 / 19 | **36 / 21 / 18 / 6**(合计 81 不变) |
  1218 +| 泊位总量 | 16,220 | **不变** |
  1219 +| 覆盖率 | 3/12 = 25% | **4/12 = 33.3%** |
  1220 +
  1221 +页面全部按数据推导,不需要改文案:KPI「覆盖旗县区 4/12」、右栏 4 张区县卡片、
  1222 +「其余 8 个旗县区暂无数据」+ 8 个灰色 chip、区县下拉里喀喇沁旗由 disabled 变为可选。
  1223 +地图取景按车场坐标算,自动把锦山镇一带纳入视野。
  1224 +
  1225 +门禁:`tests/verify-one-map-districts.mjs`(covered 4 个、分布 36/21/18/6、喀喇沁旗 bbox、
  1226 +「锦山」只允许出现在喀喇沁旗车场名里、覆盖率 4/12)、`tests/verify-topic-analysis.mjs`
  1227 +(红山区 36、喀喇沁旗 6)。
  1228 +
  1229 +**2. 弹框去掉自己的表头**
  1230 +
  1231 +目标页本来就有标题栏(监管中心 / 综合监测…),弹框再加一条 44px 表头,等于标题写两遍,
  1232 +还把页面标题压掉一截。现在去掉 `.om-embed-head`,关闭收成右下角浮层「全屏打开 / ✕」,
  1233 +Esc、点遮罩仍然可关。
  1234 +
  1235 +**3. 修弹框里页面标题被切的问题**
  1236 +
  1237 +专题分析 / 综合监测 / 车辆追溯 / 视频监控 这四页都用
  1238 +`margin: calc(-1 * var(--app-content-padding)) ... 0` 顶到 app 内容区边缘。放进弹框后
  1239 +整页向左上偏 20px,「综合监测」四个字的上半截被弹框上沿切掉。在 `.om-embed-body`
  1240 +里统一 `margin: 0 !important; width: 100% !important`,四页标题均完整显示,
  1241 +弹框 body 高度 1013px、`scrollHeight == clientHeight`。
  1242 +
  1243 +### 7.26 一张图全屏后把空白吃掉(2026-11-07)
  1244 +
  1245 +点「全屏」后顶栏消失,内容区比窗口模式高 136px(1920×1080:813 → 949),而三张左栏卡片
  1246 +和右栏列表都是固定高度,多出来的一截就空白地堆在栏底;区县卡片右半边除了进度条也是空的。
  1247 +四处一起改,全部用页面已有的数据补,不新造口径。
  1248 +
  1249 +**1. 左栏三张卡分摊剩余高度**
  1250 +
  1251 +`.om-left > .om-sec` 改 `flex: 1 0 auto`(只吃剩余高度、**绝不压缩**),卡内
  1252 +KPI 宫格 / 资源清单行 / 图层行改成 `flex: 1 1 auto` + `grid-auto-rows: 1fr` 均分。
  1253 +全屏下卡片从 150/427/175 变成 229/408/255,正好铺到页脚。
  1254 +
  1255 +> `flex-shrink` 必须是 0。第一版写的是 `flex: 1 1 auto` + `min-height: 0`,
  1256 +> 1366×768 下列高不够,三张卡被压到互相盖住,「覆盖旗县区」和「实时告警」两行直接压在
  1257 +> 下一张卡的标题上。改回 0 之后,矮屏沿用原有行为:卡片保持自然高度,左栏内滚动。
  1258 +
  1259 +**2. 右栏进出场:行均分 + 底部「进出场结构」**
  1260 +
  1261 +一次刷新只有 7~8 条抓拍,`slice(0, 8)` 之后右栏底部空一大截。列表行 `flex: 1 1 auto`
  1262 +(`min-height: 46px`,保证 42px 抓拍图压不扁),底部再加一块结构统计:
  1263 +进场 / 出场条数(占比条)、免费放行 / 收费条数、收费合计。
  1264 +
  1265 +数字来自同一份列表,口径在 `src/views/gov/oneMap/access.ts#summarizeAccess`:
  1266 +`¥8.00` → 8、`—` → 没有金额,出场且无金额算免费;进场还没结算,不参与费用统计。
  1267 +结构统计用**只按区县过滤**的那一份(`districtAccess`),页签切到「进场」时不会变成「出场 0」。
  1268 +
  1269 +**3. 区县卡片右侧补空余环形**
  1270 +
  1271 +卡片原来只有左半边有字,右半边空着。改成两栏:左边名称 / 占用率 / 「960 泊位 · 占用 758」,
  1272 +右边一个环形,弧长 = 空余占比、颜色沿用全站「空闲」三档色、环心是空余泊位数
  1273 +(喀喇沁旗 202、红山区 1,978、松山区 1,167、元宝山区 889)。
  1274 +拆分口径在 `districts.ts#berthSplit`:占用 = 总泊位 − 空余,空余大于总数时占用兜底为 0,
  1275 +总泊位为 0 时占比是 0 不是 NaN。
  1276 +
  1277 +**4. 门禁**
  1278 +
  1279 +`tests/verify-one-map-panel-fill.mjs`:`berthSplit` 的占用/空余/占比、0 泊位与空余超总数的
  1280 +边界;`summarizeAccess` 的进场/出场、免费/收费、`¥` 解析、只有进场、空数组、
  1281 +出场缺金额字段;再接一遍演示数据确认不出现 NaN。前端门禁 23 → **24 个**。
  1282 +
  1283 +实测:1920×1080 全屏下左栏 229/408/255 铺满、进出场 8 行各 86px + 结构块贴底
  1284 +(卡片底 1022 / 页脚 1032);1366×768 左栏卡片不再互相压住,溢出交给栏内滚动。
  1285 +
  1286 +### 7.27 一张图地图上加「基本指标」浮条(2026-11-07)
  1287 +
  1288 +地图上方原来只有图例和右下四个入口按钮,顶上正中一整条是空的。压一条 5 个指标的浮条:
  1289 +**车场 / 总泊位 / 剩余泊位 / 占用率 / 周转率**,左侧带当前范围(`全市` 或具体旗县)。
  1290 +
  1291 +口径在 `src/views/gov/oneMap/map-kpi.ts#buildMapKpis`,由
  1292 +`tests/verify-one-map-kpi.mjs` 守住:
  1293 +
  1294 +| 指标 | 算法 | 显示 |
  1295 +|---|---|---|
  1296 +| 车场 | `filteredLots.length` | 整数 +「个」 |
  1297 +| 总泊位 / 剩余泊位 | 车场逐个累加 | 整数 +「个」 |
  1298 +| 占用率 | (总泊位 − 剩余) / 总泊位,夹到 0~1 | 1 位小数 + `%` |
  1299 +| 周转率 | **按泊位加权**:Σ(周转率×泊位) / Σ泊位 | 2 位小数 +「次/日」 |
  1300 +
  1301 +- 周转率必须加权。车场之间泊位差两个量级(6 泊位 vs 2000 泊位),直接平均会让小场和大场
  1302 + 等权,全市数字没有意义;门禁里 100/300 两个车场的用例专门断言 2.25 而不是平均的 2.0。
  1303 + 0 泊位的车场不进分母,避免除零。
  1304 +- 单位和小数位与「专题分析」一致(`次/日`、两位小数),占用率只留 1 位——浮条是扫一眼的,
  1305 + 多一位反而看不清。
  1306 +- 数据源和左栏资源清单同一个 `filteredLots`,**跟着区县下拉一起变**,不另算一遍。
  1307 + 实测:全市 81 场 / 16,220 / 4,236 / 73.9% / 1.52 次/日;切到红山区 36 场 / 7,830 /
  1308 + 1,978 / 74.7% / 1.56 次/日,和红山区区县卡片的数字对得上。
  1309 +- 第一次排出来 534px 宽,1366×768 下几乎贴到左栏。`@media (max-width: 1500px)` 把每格
  1310 + `min-width` 96 → 74px、字号 20 → 17px,压到 415px(x=576,左栏右边界 511),不再打架。
  1311 +
  1312 +前端门禁 24 → **25 个**。全屏(1920×1080)下浮条同样居中压在顶边,和「运行总览」卡片、
  1313 +右侧进出场卡片都不重叠。
  1314 +
  1315 +### 7.28 一张图切到真实接口:109 个车场,台账和点位分开报(2026-11-09)
  1316 +
  1317 +一张图原来读 `oneMap/demo.ts` 夹具(81 场 / 16,220 泊位),页面上挂着「演示数据」标记。
  1318 +这一轮把数据源换成后端聚合接口,页面上所有数字都来自 `parking_cloud_yudao`。
  1319 +
  1320 +#### 7.28.1 接口
  1321 +
  1322 +| 项 | 值 |
  1323 +|---|---|
  1324 +| 路径 | `POST /park/resource-map/overview` |
  1325 +| 控制器 | `parking-module-park` · `ParkResourceMapController` |
  1326 +| 权限 | `park:park-lot-manage:query`(**不自动授权任何角色**,角色 2001 对 `park:dashboard:query` 的拒绝状态不动) |
  1327 +| 入参 | `{ region?: string }`,空 = 全市,否则按 `parking_lot.pl_country_name` 收窄 |
  1328 +| 出参 | `summary` / `districts` / `lots`(含坐标)/ `resources`(台账计数)/ `devices` / `access` |
  1329 +
  1330 +聚合 SQL 全在 `ParkResourceMapMapper.xml`:
  1331 +
  1332 +- `selectLots`:`data_status = 1 AND state = 1 AND pl_city_name = '赤峰市'`;
  1333 + 每个车场只取一条 `parking_lot_geo`(按 GCJ02 → 百度 → WGS84 优先级),
  1334 + 0/0 或缺失坐标的车场由前端丢弃。
  1335 +- `selectResourceCount`:路侧泊位(`tp_p_berths`)、地磁(`tp_p_eqp.eqp_type = 1`)、
  1336 + 充电桩(`tp_p_charging_pile`)六个计数子查询,**全部 `INNER JOIN parking_lot`
  1337 + 并跟随 `region`**,和车场、设备点位用同一套范围口径;不关联会报出别的旗县区的台账。
  1338 +- `selectRecentAccess`:只扫当天订单,`ORDER BY GREATEST(park_in_time, park_out_time) DESC`,
  1339 + Service 把每条订单拆成进场 / 出场两条事件。
  1340 +
  1341 +#### 7.28.2 数据实况(本地 2026-11-09)
  1342 +
  1343 +| 指标 | 数值 |
  1344 +|---|---|
  1345 +| 在用停车场 | **109**(12 个旗县区全有,红山区 40) |
  1346 +| 登记泊位 / 空余 | 16,210 / 4,655,占用率 **71.28%** |
  1347 +| 路侧泊位台账 | 2,575(占用 1,687),**`tp_p_berths` 没有坐标列**,一个都上不了图 |
  1348 +| 地磁设备 | 台账 2,097(在线 270 / 故障 119),**有坐标能上图的 673** |
  1349 +| 视频 / 充电桩 / 告警 | 0 / 0 / 0,接口如实返回 0,页面显示 0 与空态 |
  1350 +| 红山区今日进出 | 0:该区订单停在 2026-11-06,不是接口算错 |
  1351 +
  1352 +#### 7.28.3 台账 ≠ 点位,两套数字分开报
  1353 +
  1354 +`catalog.ts#buildCatalog` 新增可选 `ledger`(接口的 `resources`):
  1355 +
  1356 +- **有台账时一律按台账报数**:路侧泊位 2,575、感知设备 2,097;副信息用
  1357 + 「占用 / 空闲」「离线 / 故障」,离线 = 总数 − 在线 − 故障,异常数据兜底成 0;
  1358 +- 没有台账时退回数组计数(演示夹具、单测);
  1359 +- 地图只画有坐标的点(673 台地磁),**台账数和点位数永远不互相代替**;
  1360 +- 点「路侧泊位」行不切图层,改为 toast:
  1361 + 「路侧泊位台账 2,575 个,暂无路段坐标,不能在图上定位」;
  1362 +- 左栏「未定位资源」弹窗列出 2 条:路侧泊位 2,575 个缺路段坐标、
  1363 + 地磁 1,424 台缺坐标(2,097 − 673),提示「补录坐标后自动上图」。
  1364 +
  1365 +`tests/verify-one-map-catalog.mjs` 增加台账模式断言(2,575 / 1,687 / 888、2,097 / 1,708 / 119、
  1366 +视频充电告警为 0、离线不为负),并把标签改成「路侧泊位」。
  1367 +
  1368 +#### 7.28.4 前端改动
  1369 +
  1370 +- `mapper.ts#toPageModel`:DTO → 页面模型;坐标统一转 GCJ02;12 个旗县区固定补齐,
  1371 + 接口没返回的按「0 车场」处理,不是「占用率 0」;
  1372 +- `index.vue`:去掉「演示数据」标记、去掉每秒轮换的假进出场,改为
  1373 + `loadData()` + 60 秒静默刷新;失败时顶部显示「数据加载失败」,刷新按钮给 toast;
  1374 +- 区县下拉第一次用全市结果填充(`cityDistricts` 只在未选具体区县时覆盖),
  1375 + 选中一个区不会把另外 11 个标成暂无数据;
  1376 +- 「设备在线率」在台账为 0 时显示 `—`(松山区这类还没有地磁的区县不显示 0.0%);
  1377 +- 进出场记录没有抓拍图时显示「进 / 出」占位,不渲染裂图;
  1378 +- 区县态势 12 张卡片改为列表内滚动,「数据覆盖」块固定在底部;
  1379 + 未覆盖区县为 0 时不再输出「其余 0 个旗县区暂无数据」;
  1380 +- 车场聚合**去掉缩放下限**:全市视野的取级随容器尺寸在 6~8 级之间浮动,
  1381 + 写死 `>= 8` 会让车场整体消失;聚合本来就按 60px 屏幕格子分组,任何取级都不会叠压。
  1382 +
  1383 +#### 7.28.5 类型与门禁
  1384 +
  1385 +`vue-tsc` 在新 `oneMap/` 文件里报过的 6 个错误全部修掉(`PAGE_NAV` 未使用、
  1386 +`berthPoints` 早返回后的死比较、`berths: []` 空元组被推断成 `never[]`、
  1387 +`nav-embed` 的 `components` 未允许 null)。仓库其余页面原有类型错误不在本轮范围。
  1388 +
  1389 +前端门禁 **26 个全绿**(含 `verify-no-pending-wording`,扫描 3,230 个文件),
  1390 +eslint 对改动文件无告警。
  1391 +
  1392 +#### 7.28.6 实测
  1393 +
  1394 +- 全市:109 场 / 16,210 泊位 / 4,655 空余 / 71.3% / 1.04 次/日;
  1395 + 资源清单 109 / 16,210 / 2,575(占用 1,687 · 空闲 888)/ 2,097(离线 1,708 · 故障 119)
  1396 + / 0 / 0(桩)/ 0;
  1397 +- 红山区:资源台账与全市相同(泊位和地磁全在红山区),今日进出场 0 条;
  1398 +- 松山区:资源台账全 0,设备在线率显示 `—`;
  1399 +- 区县态势 12/12、覆盖 100.0%,卡片可滚动、底部覆盖块不重叠;
  1400 +- 重点车场 Top6、实时告警空态、未定位资源弹窗、1366×768、全屏 1920×1080 均正常。
  1401 +
  1402 +### 7.29 专题分析切到真实接口:7 个专题一次一个聚合(2026-11-09)
  1403 +
  1404 +`/park-map/analysis` 是最后一张仍读夹具的大屏(视频监控除外,等视频基础设施)。本期按设计方案 §8 的 P2 契约落地:一个专题一个只读聚合接口,前端 `mapper.ts` 把 DTO 转成页面快照,KPI / 结论 / 排行 / 分布 / 散点仍由 `assembleSnapshot` 统一生成。
  1405 +
  1406 +#### 7.29.1 接口
  1407 +
  1408 +| 方法 | 路径 | 说明 |
  1409 +|---|---|---|
  1410 +| POST | `/admin-api/park/topic-analysis/coverage` | 车场、泊位、设备在线结构;忽略日期 |
  1411 +| POST | `/admin-api/park/topic-analysis/duration` | 订单数、已结算数、实收、欠费、平均时长、周转 |
  1412 +| POST | `/admin-api/park/topic-analysis/flow` | 车场进出场 + 全天汇总 + 24 小时帧 + 日趋势 |
  1413 +| POST | `/admin-api/park/topic-analysis/heat` | 车场结构 + 24 小时帧 |
  1414 +| POST | `/admin-api/park/topic-analysis/hotspot` | 车场点位 + 区间车次,热点网格前端算 |
  1415 +| POST | `/admin-api/park/topic-analysis/price` | 首小时价、当前占用;忽略日期 |
  1416 +| POST | `/admin-api/park/topic-analysis/revenue` | 实收、订单、日趋势 |
  1417 +
  1418 +- 请求:`{"district":"红山区","beginDate":"2026-11-01","endDate":"2026-11-07"}`;`coverage` / `price` 前端不发日期,后端也忽略。
  1419 +- `district` 空串 = 全市;日期必须 `yyyy-MM-dd`,`beginDate <= endDate`,最长 366 天。参数错误由 Controller 自己翻成 `CommonResult.error(400, 中文提示)`,不交给 yudao 的 `GlobalExceptionHandler`(它会当未知异常返回 500,文案会丢)。
  1420 +- **没有实现设计稿里的 `/overview`**:每个专题接口已经带回该专题全部车场明细,KPI / 结论文案由前端从同一份 `TopicLotMetric[]` 生成;再开一个 overview 会把同样的聚合算两遍,且两处口径容易漂。`unavailable` 字段保留在响应里,当前恒为空数组。
  1421 +
  1422 +#### 7.29.2 数据源裁定:只读 `order_park`
  1423 +
  1424 +设计稿 §8.3 原计划读 `park_in_out_record`、`parking_running_statistic`、`duration_statistic_park_*`、`income_statistic_park_*`。实测本地副本库:
  1425 +
  1426 +- `parking_running_statistic` 只有最近 4 天;
  1427 +- `duration_statistic_*` / `income_statistic_*` 只有 2018 年演示数据;
  1428 +- `order_park` 有 667,559 行(2026-09-06 起),12 个区县里红山区 650,738 行是历史,其余区县主要是 2026-11-09 当天 16,821 行。
  1429 +
  1430 +**裁定:时间类专题统一读 `order_park`(`data_state = 1`)**。进出场分别按 `park_in_time` / `park_out_time` 落区间,营收、订单、时长按 `create_date` 落区间。代价是 366 天长区间的 `flow` / `heat` 会扫两遍时间列,上线前需按生产规模压测;本地 7 天区间实测 0.16–0.85 秒,`coverage` 89 毫秒。
  1431 +
  1432 +#### 7.29.3 口径
  1433 +
  1434 +- 在用停车场:`parking_lot.data_status = 1 AND state = 1 AND pl_city_name = '赤峰市'`,区县筛选走 `pl_country_name`。
  1435 +- 泊位总数:`COALESCE(temp_berth_num, temp_pl_berth_num, pl_berth_num)`,与一张图一致。
  1436 +- 实时空余:`parking_lot_berth_temp_info.free_berth`;实测 109 个在用场全部有行且非空。前端仍按「缺省 = 全部空闲」兜底,不把未知当满位。
  1437 +- 实收:只算已结算状态 **40 / 50 / 53 / 70**,金额 `order_act_fee - COALESCE(refund_act_fee, 0)`。
  1438 +- **`back_act_fee` 不计入**:本地库有明显超量样本(单笔 ¥1,448,同单应收 ¥15),加进来会污染营收。
  1439 +- 欠费率:`arrearage_fee / (order_total_fee + arrearage_fee)`;应收、欠费都不进营收。
  1440 +- 平均停车时长:`parking_duration > 0` 的订单加权,单位是**秒**。
  1441 +- 周转率:区间进场次数 ÷ 泊位数 ÷ 区间天数(次/日)。
  1442 +- 首小时价:每场取当前生效规则集(`data_state = 1`、`check_state = 3`、`effect_time <= NOW()`、未过期),小型车(`car_type = 2`)优先,汇总覆盖前 60 分钟的计时档位;没有规则返回 `null`,页面显示「未配置」。`t_billing_*` 三张表排序规则不一致,连接处显式 `COLLATE utf8mb4_bin`。
  1443 +- 当前价格实况:109 个在用场里 **38 个首小时 ¥3(中档)**,71 个没有首小时计时价;价格极差 ¥0.00 是真实结果,不是页面算错。
  1444 +- **71 个「未配置」的构成(2026-11-09 复核)**:69 个是 `gen-demo-district-data.py` 补的 11 个旗县区演示车场(全部不在红山区,本来就没有收费规则,不是生产缺口);2 个是红山区真实车场 —— `P1504022CK`(红山公园封闭车场,229 泊位 / 16,845 单)只有按次档位(`billing_config_type = 2`,¥3/次),按「首小时计时价」口径确实为空;`P15040410707`(西城市场封闭停车场,259 泊位 / 69,076 单)当前生效规则集里没有任何 `t_billing_rule` 行。**本地这个比例不能当成生产配置率**。若以后要把「按次」车场也纳入价格分布,需要单独定口径(首小时价与按次价混排会抬高/扭曲平均值),不是当前页面算错。
  1445 +
  1446 +#### 7.29.4 前端
  1447 +
  1448 +- `src/views/gov/topicAnalysis/demo.ts` 抽出 `assembleSnapshot(topic, query, metrics, hours, daily, hotspotCells, meta)`,演示夹具 `buildDemoSnapshot` 改为调用它;`tests/verify-topic-analysis.mjs` / `verify-topic-export.mjs` 的断言值不变。`DEMO_ENABLED` 仍导出但页面不再使用。
  1449 +- 新增 `mapper.ts`:DTO → `ParkingLot` / `TopicLotMetric`;坐标按 `coordType` 1 百度 / 2 WGS84 / 3 GCJ02 统一转 GCJ02;首小时价回写成 `首 60 分钟 X 元`,档位仍由 `parsePriceTier` 判。
  1450 +- 24 小时帧补成 **24 × 全部车场**:热度 = 占用率 ×(45% 底 + 55% 该小时活动占比),没有活动的车场仍有占用率底座,不会在地图上消失。后端只返回有活动的「车场 × 小时」。
  1451 +- 日趋势按自然日补齐:区间里没订单的那天是 0,不是「消失」;长区间不抽样。
  1452 +- 热点网格仍由 `aggregateHotspots` 算(约 1km 网格,取车次前 8)。
  1453 +- 新增 `request.ts` 放纯函数 `buildRequestBody`(不带 axios / Element Plus),`src/api/park/topicAnalysis/index.ts` 负责请求与 `toSnapshot`。
  1454 +- `useTopicAnalysis.load()` 改成真实请求,保留请求序号竞态保护;失败时清空快照并显示后端文案 + 「重试」。
  1455 +- `index.vue` 不再从演示夹具 `parkingLots` 判断「暂无车场」——库里 12 个旗县区都有在用场,下拉直接全开。
  1456 +
  1457 +#### 7.29.5 门禁与实测
  1458 +
  1459 +- 新增 `tests/verify-topic-analysis-mapper.mjs`(Vite SSR):坐标转换、缺 `berthFree` 不判满位、价格文案与档位、24 帧补齐、热度公式、日趋势补零、热点网格、请求体(结构类不带日期)。全部 28 个 `tests/verify-*.mjs` 通过。
  1460 +- `npx eslint src/views/gov/topicAnalysis src/api/park/topicAnalysis` 无告警;`npm run ts:check` 仍只有 §11 第 7 条那 28 个存量错误,本页 0 新增。
  1461 +- 浏览器实测(admin,48083):7 个专题全部 `200`,页头无「演示数据」角标。
  1462 + - coverage:109 场 / 16,210 泊位 / 平均 148.7 泊位 / 智能设备覆盖 13.8%;
  1463 + - flow(近 7 天):进场 51,703 / 出场 39,555,峰值 08:00 12,559,与独立 SQL 一致;24 小时与 7 天趋势均渲染;
  1464 + - duration:平均 58 分钟、中位 53、周转 0.88 次/日;时长分布 40 / 47 / 22 / 0 / 0;
  1465 + - heat:平均占用率 70.6%、高热车场 30 个、最热 08:00、当前空余 4,655;
  1466 + - hotspot:8 个网格、最热 963 车次、覆盖 10 场、周边空余 1,139;
  1467 + - price:38 场 ¥3、71 场未配置,平均 ¥3.00、极差 ¥0.00;
  1468 + - revenue:今日 ¥12,767 / 16,821 单;近 7 天 ¥50,127 / 51,703 单。
  1469 +- 区县筛选(红山区,近 7 天)¥37,360 / 34,882 单;空区间(2026-09-01~02)KPI 全 0、日趋势两点补零、不报错、不显示演示角标;1366×768 与 1920×1080 均无新增版式问题。
  1470 +- 权限:菜单 100501 已存在且只授予角色 2001,管理员走超管旁路;本次**没有新增权限点、没有授权任何其他角色**。
  1471 +
  1472 +#### 7.29.6 366 天区间性能(2026-11-09)
  1473 +
  1474 +初始实测(本地副本、一个 366 天区间、109 个车场):**flow 7.38 s / heat 3.37 s**、duration 1.88 s、hotspot 0.95 s、revenue 1.61 s —— flow、heat 超出设计方案 §13.6 的 3 秒预算。`EXPLAIN ANALYZE` 定位到两处:
  1475 +
  1476 +1. **连接顺序选错**:`selectHourlyFlow` / `selectDailyFlow` 的出场分支被优化器改成「按 109 个车场逐个回查 `order_park`」(`Nested loop … re-planned for each iteration`,每场约 45,542 行、29.6 ms),单条 3.1–3.3 s。这两条 SQL 改用 `STRAIGHT_JOIN`(新片段 `orderLotJoinStraight`),固定「先扫 `order_park` 再连 `parking_lot`」,回到 hash join 后降到 1.2 s。按车场汇总的 `selectLotAgg` / `selectLotFlow` / `selectDailyRevenue` **不要**加 —— 实测直连版本分别慢 0.0 / 0.2 / 0.0 秒。
  1477 +2. **重复扫描**:`flow` 原来同时查 `selectLotFlow`(按车场汇总)与 `selectHourlyFlow`(按车场 × 小时),后者逐小时求和与前者完全等价(对拍 0 差异)。服务层改为 `toFlowRows(hourRows)` 直接复用小时间行,少两次 `order_park` 扫描;`heat` 同样只查一次。
  1478 +
  1479 +优化后(同一 366 天区间,缓存预热):**flow 2.38 s、heat 1.27 s**、duration 1.89 s、hotspot 0.95 s、revenue 1.61 s。5 个专题响应与优化前逐字段一致(仅 `updatedAt` 不同);109 个车场的逐小时进/出求和 = 车场汇总 = `allDay`(667,559 进 / 653,235 出)全部对齐;7 天区间 flow 0.20 s、heat 0.13 s、hotspot 0.10 s。
  1480 +
  1481 +试过不采用:CTE 物化(MySQL 自动 merge,`NO_MERGE` 也未生效,三条聚合约 3.5 s,无收益)。可再优化项:`park_in_time` 目前只有单列索引,若加 `(park_in_time, pl_no, data_state)` 覆盖索引,进场分支可从 350 ms 降到约 110 ms —— 属于生产库 DDL,本次不动,按生产规模再评估。本地副本只有 2 个月 66.8 万行,这组数字是本地基准,生产需按 §13.6 复测。
  1482 +
  1483 +### 7.30 一张图加「重点车场」「实时流量」入口 + 去掉「未定位资源」+ 去掉弹框「全屏打开」(2026-11-09)
  1484 +
  1485 +**导航加两个入口**:`src/views/gov/oneMap/nav.ts` 的 `PAGE_NAV` 从 5 项变 7 项,新两项排在「专题分析」之后,
  1486 +和专题分析 / 视频监控 同一条按钮栏:
  1487 +
  1488 +| 入口 | 路径 | 权限码 | 打开方式 |
  1489 +|---|---|---|---|
  1490 +| 重点车场 | `/park/keyLotMonitor` | `park:dashboard:query` | 弹框内打开 |
  1491 +| 实时流量 | `/park/flowMonitor` | `park:flow-monitor:query` | 弹框内打开 |
  1492 +
  1493 +**七个入口一律在弹框里打开**(先做过一版 `PageNavItem.direct` 让这两项换页,用户要求改回弹框,`direct`
  1494 +字段与 `handleNav()` 已删除,纳维按钮回到 `@click="openNav(item)"`):目标页组件直接挂进 `.om-embed`,
  1495 +路由不变,地图和左右面板都还在。
  1496 +
  1497 +**权限取值**:重点车场页没有独立权限点(走存量车场接口),沿用监管类页面的 `park:dashboard:query`
  1498 +(菜单 100504 本身也没有 permission 串);实时流量复用已有的按钮权限 100506 `park:flow-monitor:query`。
  1499 +**没有新增权限点,也没有给任何角色补授权** —— 无权限时按钮不渲染,符合「有权限才显示」的老口径。
  1500 +
  1501 +**去掉「未定位资源」**:资源清单原来在 7 行之后挂了一个「未定位资源」按钮 + 弹框,点开列出没有坐标的
  1502 +点位。真实接口接进来后台账与点位已经分开报(§7.28:路侧泊位 2,575 只报数不上图),这一个入口既没有
  1503 +可信数据也没有明确用途,整块删掉:按钮、弹框、`unlocatedResources` 计算属性、`.om-unlocated*` 样式,
  1504 +以及为了它「没有标题、顶部留白压平」而加的 `.om-left .om-sec.is-last { padding-top: 12px }`(基础档与紧凑档各一条)。
  1505 +删掉后左栏最后一张卡是「图层管理」,它有自己的标题,用回正常的 30px / 紧凑档 24px 顶部留白。
  1506 +
  1507 +**去掉弹框右下角「全屏打开」**:`.om-embed-tools` 里只留关闭按钮(✕ / Esc / 点遮罩三种关法不变),
  1508 +`openEmbedFull()` 与 `.om-embed-full*` 样式一并删除;路由解析失败时的提示语也从「可以改用全屏打开」
  1509 +改成「请从左侧菜单打开」—— 弹框里已经没有换页出口了。目标页自带的「全屏」按钮(重点车场 / 实时流量页头)
  1510 +属于目标页本身,不受影响。
  1511 +
  1512 +门禁:`tests/verify-one-map-nav.mjs` 改成 7 项,并断言没有任何 `direct` 换页入口;
  1513 +`tests/verify-one-map-nav-embed.mjs` 的地图按钮列表(`mapNavItems`)同步为 6 项。
  1514 +实测(1920×1080,admin):按钮栏顺序 专题分析 / 重点车场 / 实时流量 / 视频监控 / 综合监测 / 车辆追溯,
  1515 +点「重点车场」「实时流量」都留在 `/park-map`、弹框 `aria-label` 对得上、页面正常渲染,弹框右下角只剩 ✕;
  1516 +资源清单 7 行、无「未定位资源」。`node --test tests/*.mjs` 65 通过 / `npm run ts:check` 28(存量)/ `npm run build:local` 退出 0。
  1517 +
  1518 +### 7.31 前端删掉上游「文档地址」广告条与登录页推广区(2026-11-09)
  1519 +
  1520 +上游 yudao 在几乎每个列表页顶上挂一条绿色横幅:`<doc-alert title="功能权限" url="https://doc.iocoder.cn/..." />`,
  1521 +渲染成「【功能权限】文档地址:https://doc.iocoder.cn/resource-permission」,点了跳上游官网。这些既不是本项目文档,
  1522 +也不该出现在交付给使用方的停车平台上,全部删除。
  1523 +
  1524 +**删除范围**(前端仓库 `parking-cloud-yudao-ui`):
  1525 +
  1526 +| 位置 | 内容 |
  1527 +|---|---|
  1528 +| 350 个 `.vue` / 389 处 | `<doc-alert … />` 标签整块删除(多行写法一并处理) |
  1529 +| `src/components/DocAlert/index.vue` | 组件本身删除(删完已无引用) |
  1530 +| `src/views/Login/components/LoginForm.vue` | 「萌新必读」分割线 + 开发指南 / 视频教程 / 面试手册 / 外包咨询 四个外链 |
  1531 +| `src/layout/components/UserInfo/src/UserInfo.vue` | 用户下拉菜单的「文档」项与 `toDocument()` |
  1532 +| `src/config/axios/service.ts` | 901 错误弹窗里的「参考 https://doc.iocoder.cn/ 教程」一行 |
  1533 +| 注释 7 处 | `infra/server/index.vue` 的排查提示、`mall/promotion/*.data.ts` 的 CrudSchema 文档链接改为不带 URL 的说法 |
  1534 +
  1535 +**没有用 `VITE_APP_DOCALERT_ENABLE=false` 关掉**:那只是把横幅藏起来,字符串和组件还在,换个环境变量又会冒出来。
  1536 +要的是删干净,所以连组件一起删了。`.env` 里那个开关保留(组件没了它也不再生效),不影响其它逻辑。
  1537 +
  1538 +**门禁**:新增 `tests/verify-no-doc-links.mjs`,扫描整个 `src/`(含注释),`<doc-alert` 与 `doc.iocoder.cn`
  1539 +命中任何一条即失败(首次运行 433 条 offender → 清零)。前端仓库共 **66 个 `tests/verify-*.mjs` 全绿**。
  1540 +
  1541 +**坑:`auto-components.d.ts` 是启动时按 glob 重新生成的**(gitignore 的 `auto-*.d.ts`)。删掉 `DocAlert` 组件后这个
  1542 +文件还留着两条 `typeof import('./../components/DocAlert/index.vue')`,`ts:check` 会报找不到模块;把文件删掉、
  1543 +重启 dev server 让它整份重建即可(重建后仍是全量 256 个组件,不是按需子集)。
  1544 +
  1545 +实测(admin):`/system/menu` 页面顶部两条绿条消失、搜索栏顶到最上;用户下拉只剩 个人中心 / 锁定屏幕 / 退出系统;
  1546 +未登录打开 `/login`,推广区与「萌新必读」分割线都不在了。`npm run ts:check` 28(存量)、`npm run build:local` 退出 0。
  1547 +
  1548 +### 7.32 组织管理只保留「赤峰市住建」+「赤峰中智」(2026-11-09)
  1549 +
  1550 +`system_org` 是从存量 `sys_org` 原样迁进来的 152 个有效组织(另有 90 个早已 `del_flag=1`),
  1551 +`/system/org` 页面把整棵树铺出来,演示/交付时只需要保留两级:**赤峰市住建(10001)** 和它下面的
  1552 +**赤峰中智(10003)**。清理脚本:`docs/sql/2026-11-09-组织管理只保留赤峰市住建与赤峰中智.sql`。
  1553 +
  1554 +**删除方式**:按 `OrgServiceImpl.deleteOrg` 的真实语义做**逻辑删除**,不是 `DELETE`——
  1555 +`OrgDO.delFlag` 上有 `@TableLogic(value="0", delval="1")`,页面点删除走的也是
  1556 +`UPDATE system_org SET del_flag=1`;`system_dept` 是 yudao 的镜像表,由
  1557 +`OrgDeptMirrorMapper.softDelete` 同步 `deleted = b'1'`。脚本照此办理,所以事后还能精确回滚。
  1558 +
  1559 +| 表 | 动作 | 行数 |
  1560 +|---|---|---|
  1561 +| `system_org` | 150 个存活组织 `del_flag` 0 → 1(保留 10001 / 10003) | 150 |
  1562 +| `system_dept` | 对应镜像行 `deleted` `b'0'` → `b'1'` | 150 |
  1563 +| `parking_lot` | `pl_id=12303` 西城市场封闭停车场:`org_id` 10228 → 10003 | 1 |
  1564 +
  1565 +**连带处理**:唯一挂在被删组织下的车场是 `12303 西城市场封闭停车场`(原属「赤峰市医院事业部」10228),
  1566 +把它改挂到赤峰中智,避免车场列表/一张图出现空组织;其余 108 个车场本来就在 10003。
  1567 +`system_users` 4 个用户(admin → 10001,ops001/cashier001/finance001 → 10003)全部落在保留节点,无孤儿用户。
  1568 +存量 `sys_op`(360 个老操作员)保留不动——它没有外键,本服务登录/菜单不读它的组织名。
  1569 +
  1570 +**执行时的插曲**:脚本跑的同时有人(浏览器已登录会话)调了一次
  1571 +`PUT /admin-api/system/org/update`,把 10003 的名字从「赤峰中智」改成「红山区」(后端日志 18:16:53)。
  1572 +按需求以 `sys_org.id=10003 = 赤峰中智` 为准,脚本第 5 步把它连同 `system_dept` 镜像拉回「赤峰中智」。
  1573 +以后在共享库里跑这类清理,跑完要看一眼后端日志(`/tmp/yudao-server.log` 的 `ApiAccessLogInterceptor`)有没有并发写。
  1574 +
  1575 +**校验**(脚本末尾自带):
  1576 +
  1577 +```sql
  1578 +SELECT id, name, parent_id FROM system_org WHERE del_flag = 0; -- 2 行:10001 赤峰市住建 / 10003 赤峰中智
  1579 +SELECT COUNT(*) FROM system_dept WHERE deleted = b'0'; -- 2
  1580 +SELECT COUNT(*) FROM parking_lot l WHERE NOT EXISTS (
  1581 + SELECT 1 FROM system_org o WHERE o.id = l.org_id AND o.del_flag = 0); -- 0
  1582 +```
  1583 +
  1584 +执行后跑了 `./scripts/warm-cache.sh`(车场缓存不会自动失效,见 §7.15 同款注意事项)。
  1585 +浏览器实测 `/system/org` 只剩两行,`赤峰中智` 行下有「新增下级」入口;回滚 SQL 在脚本第 7 节,
  1586 +按本次删除的 150 个 id 精确还原,不会把历史上早删的 90 个组织一起复活。
  1587 +
  1588 +### 7.33 车场列表支持「修改车场 + 车场照片」(2026-11-10)
  1589 +
  1590 +`/park/park-lot`(`views/park/parkLotList`)原来只有只读列表 + 详情抽屉,本次补上编辑与照片管理。
  1591 +后端**没有新增业务端点**——全部复用已迁好的 `ParkLotManageController`:
  1592 +
  1593 +| 页面动作 | 端点 | 权限 |
  1594 +|---|---|---|
  1595 +| 打开编辑(回显) | `POST /park/park-lot-manage/query-parking-lot-by-pk-no`(入参字段是存量拼写 `pklNo`) | `:query` |
  1596 +| 保存 | `POST /park/park-lot-manage/update-parking-lot-info` | `:update` |
  1597 +| 照片列表 | `POST /park/park-lot-manage/query-park-lot-pic-by-pl-no` | `:query` |
  1598 +| 上传照片 | `POST /park/park-lot-manage/upload-park-lot-pic`(base64 数组) | `:import` |
  1599 +| 删除照片 | `POST /park/park-lot-manage/delete-park-lot-pic`(软删 `data_state=0`) | `:delete` |
  1600 +
  1601 +新权限记录沿用菜单 310003-310007 上已有的 5 个 `park:park-lot-manage:*`,**没有新建、也没有自动授权给任何角色**
  1602 +(admin 是 `super_admin`,走 `*:*:*`)。
  1603 +
  1604 +**编辑表单的取舍**:
  1605 +
  1606 +- **不传经纬度**。`UpdateParkingLotInfo` 只在 `lonId&&latId` 非空时才走「删旧地理点 + 重建」,
  1607 + 表单里没有这两个字段 ⇒ 改任何属性都不会动地理数据。
  1608 +- **组织原样回传**。存量 `updateParkingLotInfo` 无条件用登录人组织覆盖 `orgId/orgName`
  1609 + (`sysCode` 还写死 `"1001"`)。本机 admin 的组织是 10001(赤峰市住建),而车场都在 10003(赤峰中智),
  1610 + 照搬会导致「编辑一次就被改挂到 10001」。在控制器加了一个条件:
  1611 + `@Value` 之外的唯一改动是 `request.getOrgId() == null` 才回填登录人组织(见该方法的
  1612 + `⚠️ 手改(非搬迁原样)` 注释),新增车场的行为不变。**这是本项目里第二处非照搬改造**
  1613 + (第一处是 `updateBlockForParkLot` 的 `modifyOp` 回填)。
  1614 +- 营业时间保留文本输入(存量数据同时存在 `08:00` 和 `00:00:00` 两种格式,用时间选择器会被强制改写);
  1615 + 收费标准保留 JSON 文本域(后端原样落库,前端只做展示解析)。
  1616 +- 回显映射抽在 `views/park/parkLotList/edit.ts`,并由 `tests/verify-park-lot-edit.mjs` 断言
  1617 + (存量接口会把 `pl_type`/`busi_circle` 返回成字符串,必须转数字,否则下拉框空白)。
  1618 +
  1619 +**本地文件存储(本机演示必需)**:本机 `FILE_OSS_CONFIG` 只有 `ENABLE_TYPE=ali`,
  1620 +FastDFS tracker 又是内网 `10.117.61.52:22122`,两条路都不可达,上传必失败。新增:
  1621 +
  1622 +- `com.clouds.common.oss.local.LocalOssClientWrapper`:落盘 `~/.parking-cloud-yudao/oss/{yyyyMMdd}/{uuid}.{ext}`,
  1623 + 返回 `http://localhost:48083/admin-api/park/local-file/...`;
  1624 +- `OssClientBaseWrapper.getOssClientService()` 新增 `parking.legacy.oss.mode=local` 分支(留空 = 存量行为);
  1625 +- `LocalOssFileController`(`GET /park/local-file/{*path}`,`@PermitAll` + `@Profile("local")`,路径归一化后校验在根目录内;生产 profile 不注册这个读端点);
  1626 +- `application-local.yaml` 的 `parking.legacy.oss.*`(`dir` / `url-prefix`,可用 `LEGACY_OSS_*` 覆盖)。
  1627 +
  1628 +生产/测试环境不配 `parking.legacy.oss.mode`,仍走阿里云 OSS / FastDFS,行为与存量一致;
  1629 +本机也没有读取或写入任何真实存储凭据。
  1630 +
  1631 +**实测(admin,2026-11-10)**:
  1632 +
  1633 +- `/park/park-lot` 行操作出现「编辑」;打开 `B1504020C7 静雅封闭车场`,17 个字段回显正确,所属组织只读为「赤峰中智」;
  1634 +- 改「备注」保存 → 后端 `modify_time` 更新,`org_id` 仍是 10003(未被改成 10001),其余字段(`pl_rate`、
  1635 + `pl_type_prop`、泊位数、营业时间)逐字段比对无变化;随后把备注改回空,数据复原;
  1636 +- 上传 1 张 264KB PNG → `td_c_file` 新增一行(`file_type=6`),页面缩略图 1920×1080 正常渲染,
  1637 + `/admin-api/park/local-file/...` 返回 200 `image/png`;删除 → `data_state=0` 软删,页面立即清空。
  1638 + 测试产生的文件与 `td_c_file` 行已清理(图片 8 张存量未动)。
  1639 +
  1640 +**验证**:`mvn -pl yudao-server -am -DskipTests package` 退出 0;`node --test tests/*.mjs` 67/67;
  1641 +`npm run ts:check` 28(存量基线,新增文件 0 条);`npm run build:local` 退出 0。
  1642 +
  1643 +### 7.34 车场列表支持「新增车场」(2026-11-10)
  1644 +
  1645 +`/park/park-lot` 增加「新增车场」按钮,与 §7.33 的编辑弹窗共用同一个 `el-dialog`
  1646 +(`dialogMode = 'create' | 'edit'` 区分标题、字段与提交端点)。后端同样没有新增业务端点:
  1647 +
  1648 +| 页面动作 | 端点 | 权限 |
  1649 +|---|---|---|
  1650 +| 名称查重 | `POST /park/park-lot-manage/check-unique-for-pl-name` | `:query` |
  1651 +| 保存 | `POST /park/park-lot-manage/save-parking-lot-info` | `:create` |
  1652 +
  1653 +权限沿用已存在的菜单 310003「停车场管理-新增」,**没有新建、也没有授权给任何角色**(admin 走 `*:*:*`)。
  1654 +
  1655 +**取的口径**:
  1656 +
  1657 +- **车场编码由后端生成**(`ParkingLotServiceImpl.generatorParkNo(state)`):`state=1` → `P1504…`(现场形如
  1658 + `P15040490013`),其他状态 → `TKL…`。表单里只读显示「保存后自动生成」。因此新增默认 `state=1`,
  1659 + 默认成演示车场会拿到 `TKL` 编码段。
  1660 +- **新增入参不能带 `plId`**:`parking_lot.pl_id` 是自增主键,`insertSelective` 是「非 null 才写」,
  1661 + 传 0 会把主键真的写成 0。`buildCreatePayload` 显式丢掉 `plId / plNo`
  1662 + (`orgId/orgName` 当时也丢;2026-11-10 后续改为「选了就带上」,见 §7.35)。
  1663 +- **名称查重语义与名字相反**:`check-unique-for-pl-name` 返回 **true = 已存在**(DAO 是 `count(1) > 0`)。
  1664 + 页面在 true 时提示换名、不发新增请求;查重接口自身失败**不拦**新增(后端新增本身不校验重名)。
  1665 +- **组织**:新增接口无条件用登录人组织覆盖 `orgId/orgName`(本机 admin → 10001 中兴智能交通)。
  1666 + 表单不提供组织选择,只显示「保存后归属当前登录人组织」。
  1667 + **(2026-11-10 后续已改为新增/修改都可选组织,见 §7.35。)**
  1668 +- **经纬度可选但必须成对**:后端 `saveParkGeo` 直接对 `lonId` 取 `doubleValue()`,且
  1669 + `parking_lot_geo.lon_id/lat_id` 是 `NOT NULL`,缺坐标会 500。为此在
  1670 + `ParkingLotServiceImpl.SaveParkingLotInfo` 加了与 `UpdateParkingLotInfo` 一致的判空——
  1671 + **两个坐标都有才落地理点**(`⚠️ 手改(非搬迁原样)`,这是本项目第三处非照搬改造)。
  1672 + 前端:两个都空允许(该车场不上一张图,表单里有说明),只填一个直接拦截。
  1673 +- **照片**:新增时隐藏照片区(照片必须绑定 `plNo`),提示「可在创建后点编辑上传」。
  1674 +- **创建后自动按名称筛选**:列表查询 `queryParkLotByCountryAndParkName` 没有 `ORDER BY`(InnoDB 按主键升序),
  1675 + 新车场会落到最后一页。创建成功后把搜索框置为新车场名称并回到第 1 页,用户立刻能看到刚建的车场
  1676 + (搜索框可见,不算隐式行为)。
  1677 +
  1678 +纯逻辑(`createEmptyCreateForm` / `buildCreatePayload` / `validateGeoPair`)放在
  1679 +`views/park/parkLotList/edit.ts`,由 `tests/verify-park-lot-edit.mjs` 断言(含「新增不能带 plId」
  1680 +「0 是有效泊位数」「只填经度不合法」)。
  1681 +
  1682 +**实测(admin,2026-11-10)**:
  1683 +
  1684 +- 新增「本地验证-新增车场A」(不带坐标)→ 成功,`pl_no=P15040490013`,`data_status=1`、`org_id=10001`,
  1685 + `parking_lot_geo` 无行(判空生效);
  1686 +- 新增「本地验证-新增车场B」(118.96 / 42.27)→ `pl_no=P15040490014`,`parking_lot_geo` 两条
  1687 + (百度 `lon_lat_type=1` + 火星 `lon_lat_type=3`)、`park_free_berth` 一条;
  1688 +- 重名再提交 → 页面提示「车场名称已存在」,无新增;只填经度 → 表单拦截「经度和纬度必须同时填写」;
  1689 +- 测试数据已清理:`parking_lot` / `parking_lot_geo` / `park_free_berth` 各删 2 行(按主键 + 编码双条件),
  1690 + Redis db1 的 `zteits-park:parking_lot:plNo_parkLotInfo`、`park_free_berth:plNo_freeBerth_map`
  1691 + 字段 HDEL、两个 geo GEO zset 成员 ZREM;车场总数回到 109。
  1692 +
  1693 +**验证**:`mvn -pl yudao-server -am -DskipTests package` 退出 0;`node tests/verify-park-lot-edit.mjs`
  1694 +全部通过;`npm run ts:check` 仍是 28 条存量基线。
  1695 +
252 ## 8. 视频接入(项目要求) 1696 ## 8. 视频接入(项目要求)
253 1697
  1698 +### 8.0 物联网模块:评估后关闭(2026-11-04)
  1699 +
  1700 +**结论:不启用。** 本项目只有视频设备接入,而视频取流不经过 IoT(模块内无任何
  1701 +GB28181/ONVIF/RTSP 代码),存量地磁/道闸又沿用 `tp_p_eqp`,启用 IoT 只会多出第三套设备台账。
  1702 +评估过程记录如下,重新开启时按本节回退。
  1703 +
  1704 +关闭动作(三处代码 + 一处数据):
  1705 +
  1706 +| 位置 | 改动 |
  1707 +|---|---|
  1708 +| 根 `pom.xml` | 注释 `<module>yudao-module-iot</module>` |
  1709 +| `yudao-server/pom.xml` | 注释 `yudao-module-iot-biz` 依赖(并删掉下方重复的旧注释块) |
  1710 +| `YudaoServerApplication` | 注释 `IotStorageInitRunner` 的 import 与 `excludeFilters` 过滤项 |
  1711 +| `application.yaml` | 注释 `yudao.iot.message-bus` / `yudao.iot.storage` |
  1712 +| `system_menu` | id=860 及其 73 个子菜单 `visible=b'0'`(父菜单隐藏整棵子树;`routerHelper.ts` 用 `hidden: !visible`,所以 0=隐藏) |
  1713 +
  1714 +验证:`mvn -pl yudao-server -am -DskipTests package` 通过,重新打包的 jar 内
  1715 +`yudao-module-iot` 条目数为 **0**;48083 重启后 `Started YudaoServerApplication`,
  1716 +登录、`/system/menu/list`、`/system/user/page`、
  1717 +`/park/area/query-area-and-block-tree` 均 `code=0`;侧边栏已无「IoT 物联网」;
  1718 +`/iot/**` 返回 501「已禁用」。启动日志只剩一条 banner 提示
  1719 +`[IoT 物联网 yudao-module-iot - 已禁用]`,无异常。
  1720 +
  1721 +数据库里的 17 张 `iot_*` 表**保留未删**:与停车表无外键关联,不影响任何现有功能;
  1722 +真要清理时按同名单表 `DROP` 即可。
  1723 +
  1724 +<details>
  1725 +<summary>评估过程:导入后的实测结果(供将来启用时参考)</summary>
  1726 +
  1727 +`parking_cloud_yudao` 原先没有 `iot_*` 表,前端点进「IoT 物联网」显示
  1728 +「[IoT 物联网 yudao-module-iot - 表结构未导入]」——这是 `GlobalExceptionHandler` 对
  1729 +`BadSqlGrammarException` 里含 `iot_` 的统一兜底文案,不是菜单或模块开关问题。
  1730 +
  1731 +已从 `docs/sql/iot-2026-11-02-传播违法.sql.zip` 导入 **17 张表**(`iot_device`、`iot_product`、
  1732 +`iot_product_category`、`iot_device_group`、`iot_thing_model`、`iot_device_shadow`、
  1733 +`iot_alert_config`、`iot_alert_record`、`iot_data_rule`、`iot_data_sink`、`iot_scene_rule`、
  1734 +`iot_ota_firmware`、`iot_ota_task`、`iot_ota_task_record`、`iot_device_modbus_config`、
  1735 +`iot_device_modbus_point`、`iot_rule_scene`)。导入前确认目标库**无同名表**,脚本只含
  1736 +`iot_*` 的 `CREATE/DROP`,不含 `USE`、不碰 `system_*` / `infra_*` / 停车业务表。
  1737 +
  1738 +导入后实测可用(`/admin-api/*`,admin + tenant-id 1):
  1739 +
  1740 +| 能力 | 端点 | 状态 |
  1741 +|---|---|---|
  1742 +| 产品分页 / 分类 / 物模型 | `/iot/product/page`、`/iot/product-category/*`、`/iot/thing-model/list` | ✅ |
  1743 +| 设备分页 / 精简列表 / 地图点位 / 影子 | `/iot/device/page`、`/simple-list`、`/location-list`、`/device/shadow/get` | ✅ |
  1744 +| 设备分组 | `/iot/device-group/page`、`/simple-list` | ✅ |
  1745 +| 告警配置 / 告警记录 | `/iot/alert-config/page`、`/iot/alert-record/*` | ✅ |
  1746 +| 规则引擎(场景联动 / 数据流转) | `/iot/scene-rule/*`、`/iot/data-rule/*` | ✅ |
  1747 +| 设备消息历史 / 属性历史 / 首页统计 | `/iot/device/message/page`、`/iot/statistics/get-summary` | ❌ 500 |
  1748 +
  1749 +失败的两类是同一根因:`yudao.iot.storage.type: tdengine`,时序数据(`device_message`、
  1750 +属性历史)走 TDengine/IoTDB,本机没有部署(启动类也按 D4 排除了 `IotStorageInitRunner`)。
  1751 +`device_message` 表不在 MySQL 里,所以查询报 `Table 'parking_cloud_yudao.device_message' doesn't exist`。
  1752 +`get-summary` 把 MySQL 计数和 TDengine 消息计数拼在一个接口里,一处失败整页显示 `--`。
  1753 +**影响范围仅限「消息历史/属性历史/IoT 首页统计」三处**,台账、产品、物模型、告警、规则全部可用。
  1754 +接真实设备(需要看上报报文)时再部署 TDengine,并把 `IotStorageInitRunner` 从 `excludeFilters` 移除。
  1755 +
  1756 +另有两点须知:
  1757 +
  1758 +- `yudao-module-iot-gateway` 是**独立部署**的协议服务(TCP 8091 / UDP 8093 / WebSocket 8094 /
  1759 + MQTT 1883 / Modbus 503),`yudao-server` 只依赖 `iot-biz`,**没有启动任何协议端口**。
  1760 + 因此本地导进来的 28 台设备不会有真实连接,在线状态停留在数据快照里。
  1761 +- IoT 设备管理页是卡片视图(不是表格),与停车侧 `el-table` 风格不同,属于上游原样。
  1762 +
  1763 +</details>
  1764 +
  1765 +#### 为什么不用 IoT 管存量的停车设备
  1766 +
  1767 +| 指标 | 数字 |
  1768 +|---|---|
  1769 +| `tp_p_eqp`(地磁,全部 `eqp_type=1`) | 500 台,带 `berth_no` / `is_online` / 心跳 / `gateway_no` / `rssi` / `mac` |
  1770 +| `parking_lot_equipment`(车场设备树) | 161 条:LED 显示屏 114、摄像头 46、道闸 1(类型见 `parking_lot_equipment_type`) |
  1771 +| `tp_p_eqp_log` / `tp_p_eqp_inout_record` | 按月分表,已建到 2027-03 |
  1772 +| Java 里含 `Eqp` 的类 | 469 个(park 268 / api 149 / order 28 / pay 9) |
  1773 +| 引用 `tp_p_eqp` 的 mapper XML | 23 |
  1774 +
  1775 +存量「设备管理」不是一张台账,是一条业务链:地磁上报 → `tp_p_eqp_log` → 泊位占用 →
  1776 +停车记录 → 计费 → 在线率考核/巡查/异常稽核,横跨 park、order、pay 三个模块。
  1777 +换成 IoT 物模型等于重做这条链,收益(产品/物模型/影子/OTA/规则)对已稳定运行的地磁没有增量价值。
  1778 +
  1779 +`parking_lot_equipment` 里虽有 46 条 `equipment_type=2`(摄像头)记录,但只有
  1780 +`equipment_name` / `channel_id` / `area_id`,没有码流地址、国标编码、云台能力,
  1781 +不能当视频台账用——视频设备的独立建模理由在这里,不在「停车设备管理不好用」。
  1782 +
254 上游 `yudao-module-iot` **没有** GB28181 / ONVIF / RTSP 视频接入能力;它只有设备/产品/物模型/OTA/规则/告警和 TCP/UDP/HTTP/MQTT/EMQX/CoAP/Modbus 协议。 1783 上游 `yudao-module-iot` **没有** GB28181 / ONVIF / RTSP 视频接入能力;它只有设备/产品/物模型/OTA/规则/告警和 TCP/UDP/HTTP/MQTT/EMQX/CoAP/Modbus 协议。
255 1784
256 建议方案(待选型): 1785 建议方案(待选型):
257 1786
258 -- 先挂载 `yudao-module-iot` 作为设备接入底座; 1787 +- ~~先挂载 `yudao-module-iot` 作为设备接入底座~~ —— 2026-11-04 评估后关闭,见 §8.0;
  1788 + 取流不经过 IoT,设备台账也不需要 IoT;
259 - 新增 `parking-module-video`,对接开源视频网关: 1789 - 新增 `parking-module-video`,对接开源视频网关:
260 - **WVP-PRO**(GB28181 信令 + 设备管理)+ **ZLMediaKit**(流媒体); 1790 - **WVP-PRO**(GB28181 信令 + 设备管理)+ **ZLMediaKit**(流媒体);
261 - 或直接接 RTSP/ONVIF 的轻量网关。 1791 - 或直接接 RTSP/ONVIF 的轻量网关。
262 - 该模块独立于 IoT,通过 `parking-module-api` 暴露视频分页/预览/回放接口。 1792 - 该模块独立于 IoT,通过 `parking-module-api` 暴露视频分页/预览/回放接口。
263 1793
  1794 +若将来确实需要 IoT(接 MQTT 类新传感器、统一告警中心、跨设备规则联动),
  1795 +按 §8.0 表格里的四处开关重新打开即可,数据表已经就位。
  1796 +
  1797 +### 8.1 WVP-PRO 合并为 Maven 模块(2026-11-09)
  1798 +
  1799 +方案见 `docs/superpowers/specs/2026-11-09-WVP-PRO视频能力合并方案.md`。
  1800 +
  1801 +**做法**:把 gitee `pan648540858/wvp-GB28181-pro` 3.0.0(commit `d8c95d0`,MIT)的
  1802 +`src/main/java`(806 个类,包名 `com.genersoft.iot.vmp.*`)整体搬进新模块
  1803 +`parking-module-video`,与 `parking-module-park` 平级,随 `yudao-server` 打进同一个 jar。
  1804 +
  1805 +**不改 `YudaoServerApplication`**:模块自带 `WvpAutoConfiguration`(`@AutoConfiguration` +
  1806 +`@ComponentScan` + `@MapperScan`),经 `AutoConfiguration.imports` 注册。视频子系统可用
  1807 +`parking.video.enabled=false` 整体关掉,关掉后 WVP 的 Controller/Mapper/定时任务一个都不进容器。
  1808 +
  1809 +| 关键点 | 结论 |
  1810 +|---|---|
  1811 +| 同名类几十个(`ChannelController`、`DeviceMapper`…) | 扫描与 Mapper 扫描都用**全限定类名**做 Bean 名(`FullyQualifiedAnnotationBeanNameGenerator`),与存量 `LegacyMapperBeanNameGenerator` 同一思路 |
  1812 +| `@DependsOn("sipLayer")` 按名字引用 | 唯一受影响的按名引用:把 `SipLayer` 排除出扫描,在自动配置里声明 `@Bean sipLayer()` |
  1813 +| `interface-authentication` | **必须保持 `true`**。置 `false` 时 `JwtAuthenticationFilter` 会塞一个"principal 为 null 但 authenticated=true"的令牌,等于把 `/api/**` 全开 |
  1814 +| 视频平台的账号 | **不建 `wvp_user` 种子账号**。`/api/user/login` 免鉴权,admin/admin 等于公开后门;身份统一来自 yudao(`VmpYudaoSsoFilter`) |
  1815 +| Redis | 单实例跑内存门面(`InMemoryStore` + `LocalMsgBus`);`RedisRpcConfig` 的模式判断改读 `parking.video.redis-mode`,否则它会误判成 Redis 模式并对 `null` 模板发消息 |
  1816 +| JDK 21 API | 虚拟线程、`SequencedCollection` 共 6 处 → 新增 `VmpConcurrency`(有界平台线程池)替代 |
  1817 +| SIP 生命周期 | 新增 `@PreDestroy` 停协议栈。JAIN-SIP 起非守护线程,原版上下文启动失败后 JVM 退不出去,5060 被自己占住 |
  1818 +| `System.exit(1)` | `SipLayer` 里两处改抛 `IllegalStateException`,让 Spring 走正常关闭(原写法跳过数据源/Redis 收尾) |
  1819 +| 建表 | `docs/sql/2026-11-09-WVP视频接入-wvp表初始化.sql`,22 张 `wvp_*` 表,幂等,无 DROP/INSERT,只对 `parking_cloud_yudao` 执行 |
  1820 +
  1821 +**坑:WVP 原版 `WebSecurityConfig` 上的 `@EnableGlobalMethodSecurity` 必须删掉**(2026-11-09)。
  1822 +它注册的是 Spring Security 5 时代的 `MethodSecurityInterceptor`,该拦截器**构造时就捕获了
  1823 +当时的 `SecurityContextHolder` 策略**;本项目 yudao 与 parking 两个 starter 各注册了一个
  1824 +`setStrategyName` 的 `MethodInvokingFactoryBean`,最终生效的策略与它捕获的不是同一个
  1825 +ThreadLocal。表现是 yudao 的 `@EnableMethodSecurity` 校验通过后,遗留拦截器从另一个 holder
  1826 +读到空认证,抛 `AuthenticationCredentialsNotFoundException`,**所有 `@PreAuthorize` 接口**
  1827 +(`/admin-api/park/**`、`system/user/page`、`system/role/page`、`infra/config/page`…)统统变成
  1828 +`code=500 msg=系统异常`,与具体业务无关,很容易误判成数据问题。WVP 自身没有任何
  1829 +`@PreAuthorize`(鉴权粒度只有"是否登录",见 `VmpYudaoSsoFilter`),删掉不影响视频平台,
  1830 +方法级权限由 yudao 的 `@EnableMethodSecurity` 统一处理——**不要再加回 `@EnableGlobalMethodSecurity`**。
  1831 +另注:`POST` 带非法请求体时返回 400 是 `@Valid` 参数解析先于方法级鉴权,这种 400 不能证明鉴权正常。
  1832 +
  1833 +**验收**:打包通过;`Started YudaoServerApplication in 18.187 seconds`;`POST /api/user/userInfo`
  1834 +带 yudao 令牌 → `code=0`,无令牌 → **401**;`GET /api/device/query/devices` → `code=0`;
  1835 +Ctrl-C 后 `[SIP SERVER] 协议栈已停止`,5060/48083 释放。
  1836 +
  1837 +**外部依赖**:ZLMediaKit 不在本仓库(C++ 独立进程),未部署时启动日志每 30 秒一条
  1838 +`[ZLM-尝试连接]失败`,属预期;设备注册与目录同步不受影响,只是点播不出图。
  1839 +
  1840 +**前端**:实时预览 / 通道树 / 回放 / 云台已移植到 `parking-cloud-yudao-ui`
  1841 +的 `/video/monitor`(`src/api/video/wvp.ts`、`videoMonitor/wvpSource.ts`、
  1842 +`components/WvpPlayer.vue`)。接口走 WVP 原生 `/api/**` + yudao 令牌,用独立 axios
  1843 +实例直连 `${VITE_BASE_URL}/api`,**不需要 Vite 代理**(后端 CORS 已放行)。播放器
  1844 +WebRTC(WHEP)优先、HLS(video.js)回退,不引入上游 jessibuca。
  1845 +
  1846 +行为要点:视频平台没有通道时页面自动退回演示夹具(角标「演示数据」),有通道时角标
  1847 +「国标视频平台」;点播/停播按宫格变化对账,不轮询;回放先查真实录像清单再起流,
  1848 +拖动走 `playback/seek`;页面被 keep-alive 缓存,切走用 `onDeactivated` 停流。
  1849 +
  1850 +抓图与片段下载:真实画面把 `<video>` 画进 canvas 再导出(WebRTC 不污染 canvas,
  1851 +跨域 HLS 会污染,给出「改用 WebRTC 播放」提示);片段下载走
  1852 +`gb_record/download/start|progress|stop`,轮询进度到 100% 后取 `downLoadFilePath`
  1853 +保存到本地,取消或离开页面即释放设备端会话(`useClipDownload.ts`)。
  1854 +注意 `download/progress` 是 WVP 里少数**不套 `{code,data}`** 的接口。
  1855 +设备台账、推流/拉流代理已接(见 §8.2);国标级联、云端录像仍未移植(P2)。
  1856 +
  1857 +### 8.2 视频管理菜单:WVP 管理端接入(批 1,2026-11-09)
  1858 +
  1859 +设计方案 `docs/superpowers/specs/2026-11-09-视频管理菜单接入设计方案.md`,
  1860 +实施计划 `docs/superpowers/plans/2026-11-09-视频管理菜单接入-批1实施计划.md`。
  1861 +上一节 `/video/monitor` 是「看视频」,本节补的是“管视频”——WVP 自己的 Vue2 管理端没有
  1862 +搬过来,管理能力按 Vue3 重写进本仓库前端。
  1863 +
  1864 +**菜单**:`docs/sql/2026-11-09-视频管理菜单.sql`(幂等,只对 `parking_cloud_yudao` 执行)。
  1865 +一级目录 **视频管理**(id `100525`,path `/video`)下 6 个菜单:
  1866 +
  1867 +| id | 名称 | path | 组件 |
  1868 +|---|---|---|---|
  1869 +| 100502 | 视频监控 | `monitor` | `gov/videoMonitor/index` |
  1870 +| 100503 | 视频设备 | `device` | `gov/videoDevice/index` |
  1871 +| 100521 | 国标通道 | `channel` | `gov/videoChannel/index` |
  1872 +| 100522 | 推流管理 | `stream-push` | `gov/videoStreamPush/index` |
  1873 +| 100523 | 拉流代理 | `stream-proxy` | `gov/videoStreamProxy/index` |
  1874 +| 100524 | JT1078 设备 | `jt-device` | `gov/videoJtDevice/index` |
  1875 +
  1876 +`100502` / `100503` 原是散落的顶菜单,脚本把它们挪进新目录;**子菜单 path 必须相对**
  1877 +(`device` 而不是 `/video/device`),否则路由拼成 `/video/video/device`。
  1878 +脚本**不向任何角色授权**,菜单靠 `system_role_menu` 手工分配,避免又出现「新功能默认全开」。
  1879 +
  1880 +**前端**:接口集中在 `src/api/video/wvpAdmin.ts`(复用 `wvp.ts` 的 axios 实例与 `unwrap`)。
  1881 +WVP 的 `/api/**` 响应被全局 advice 统一包成 `{code,msg,data}`,列表是 PageHelper 的
  1882 +`PageInfo`(`total`/`list`),所以全部走 `unwrap`;唯一例外仍是
  1883 +`gb_record/download/progress`(不包)。DTO→表格行的兜底抽到各页 `mapper.ts`,
  1884 +由 `tests/verify-video-admin-mapper.mjs`、`tests/verify-video-stream-mapper.mjs` 守住;
  1885 +国标通道的同步状态文案与空态引导由 `tests/verify-video-channel-sync.mjs` 守住。
  1886 +
  1887 +**页面行为**:
  1888 +
  1889 +- 视频设备:`/device/query/devices` 真实数据,10 条/页本地分页,10 秒轮询(隐藏页暂停);
  1890 + 行操作 同步(`devices/{id}/sync`,参数是**国标编号字符串**)/ 通道(跳 `?deviceId=`)/ 编辑 / 删除;
  1891 + 空态「视频平台尚未接入国标设备」。新增/编辑抽屉里的「厂商」是**下拉 + 可输入**
  1892 + (海康威视 / 大华 / 宇视 / 华为 / 天地伟业 / 科达 / 中星微,`allow-create` 允许列表外的值)——
  1893 + WVP 的 `manufacturer` 是自由文本,不是字典,不能做成封闭枚举;编辑存量设备时当前值由
  1894 + `mapper.ts#mergeManufacturerOptions` 并进选项,否则下拉没有匹配项会显示空白,看起来像把厂商清掉了
  1895 + (2026-11-10,`tests/verify-video-admin-mapper.mjs` 守住)。
  1896 +- 国标通道:顶部设备下拉 + `?deviceId=` 深链;`parental = 1` 的目录节点在 mapper 里剔除
  1897 + (它们点播必失败);预览用 `WvpPlayer`(WebRTC→HLS),关弹窗即 `play/stop`;编辑走
  1898 + `/common/channel/one` 取完整对象、改完整体回传 `/common/channel/update`。
  1899 + **通道本身不能手工新增**——它是设备响应 Catalog 查询后落下的记录,整个
  1900 + `/api/device/query` 没有通道新增端点;`/api/common/channel/add` 只服务推流/拉流代理
  1901 + (`dataType` 2/3),手工塞一条 `dataType=1` 只会让点播查不到源。
  1902 + 所以页面给的是唯一可操作的动作:工具栏「同步目录」(`GET /devices/{deviceId}/sync`),
  1903 + 设备需在线;同步是异步的,发完之后每 2 秒轮询 `/device/query/sync_status`,
  1904 + 按钮上显示「同步中 12/30」这类进度,60 秒未收工就停下并提示稍后刷新;
  1905 + 完成 / 设备没回通道 / 设备回错误三种结果分别 toast + 一条常驻提示,
  1906 + 失败时不再是一句「系统异常」。五种空态引导(读取失败、无设备、未选设备、设备离线、
  1907 + 在线但目录为空)抽到 `videoChannel/sync.ts`,由 `tests/verify-video-channel-sync.mjs`
  1908 + 的 13 条测试守住。
  1909 +- 推流 / 拉流代理:新增、编辑、启停、删除;「播放」拿到的 `StreamContent` 直接喂 `WvpPlayer`。
  1910 + 注意 `push/forceClose` 按 `app + stream` 停,而 `proxy/stop` 按 `id` 停。
  1911 +- JT1078:本项目默认 `jt1078.enable=false`(`wvp-video.yaml`,停车场景用不到车载终端),
  1912 + 该控制器 `@ConditionalOnProperty` 不注册,接口返回 404。页面识别后显示
  1913 + 「部标(JT/T 1078)接入未启用」,不当作故障;开启后自动加载终端/通道/直播/抓图。
  1914 +
  1915 +**实测**:`/api/device/query/devices` → `code=0, total=0`;4 个新页面浏览器打开无控制台报错,
  1916 +空态文案正确。`GET /api/device/query/devices/{任意编号}/channels` 对不存在的设备返回
  1917 +`code=500 系统异常`(`DeviceChannelServiceImpl` 抛「未找到设备」)——是预期,选设备下拉里的
  1918 +真实设备不会走到这条路。`node --test tests/*.mjs` 全绿,`npm run build:local` 通过。
  1919 +
  1920 +**未做(后续批次,见设计方案)**:云端录像 + 录制计划、组织结构 + 报警管理;
  1921 +接口级权限(WVP `/api/**` 只校验登录,菜单权限只挡入口)。国标级联 + 媒体节点见 §8.2.1。
  1922 +
  1923 +#### 8.2.1 批 2:国标级联 + 媒体节点(2026-11-09)
  1924 +
  1925 +设计方案 §5、实施计划 `docs/superpowers/plans/2026-11-09-视频管理菜单接入-批2实施计划.md`。
  1926 +菜单 SQL `docs/sql/2026-11-09-视频管理菜单-批2.sql`(幂等,只对 `parking_cloud_yudao` 执行):
  1927 +
  1928 +| id | 名称 | path | 组件 | 权限 |
  1929 +|---|---|---|---|---|
  1930 +| 100526 | 国标级联 | `platform` | `gov/videoPlatform/index` | `parking:video:platform:query` |
  1931 +| 100527 | 媒体节点 | `media-server` | `gov/videoMediaServer/index` | `parking:video:mediaserver:query` |
  1932 +
  1933 +同样只插 `system_menu`、不插 `system_role_menu`。
  1934 +
  1935 +**页面**:
  1936 +
  1937 +- 国标级联:列表 + 新增/编辑抽屉(三段分隔:基础信息 / 认证与信令 / 选项)、删除、推送目录、
  1938 + 通道共享抽屉(未共享 / 已共享两栏,各自关键字检索与 10 条分页,`共享选中` → `channel/add`,
  1939 + `取消共享` → `channel/remove`)。**两套 id 不能混用**:`/platform/info/{id}` 取的是上级平台
  1940 + 国标编号,`/platform/delete?id=` 与 `/platform/channel/push?id=` 取的是数据库自增 id。
  1941 + 新增时用 `/platform/server_config` 预填本平台编号 / IP / 端口。
  1942 +- 媒体节点:卡片列表 + 两步向导。第一步 `media_server/check`(GET + query 参数,20s 超时)
  1943 + 探测连通性并**拿回 mediaServerId 与全部端口**,成功才进第二步;第二步确认端口段、HOOK/流/SDP IP、
  1944 + 单/多端口、录像天数与路径后 `media_server/save`。默认节点禁止删除。
  1945 + 后端在探测失败时只抛「系统异常」,页面把它翻译成「连不上节点:请确认 IP / HTTP 端口可达,
  1946 + 且 SECRET 与节点配置里的 api.secret 一致」。
  1947 +- `secret` 读出即脱敏为 `******`(`ServerController.maskSecret`),编辑原样回传即可。
  1948 +
  1949 +**修掉的两个真问题**(批 1 的写操作其实也一直是坏的,只是被前端吞掉了):
  1950 +
  1951 +| 现象 | 根因 | 修法 |
  1952 +|---|---|---|
  1953 +| 所有 `POST /api/**` 带 `@RequestBody` 返回 `code=400 请求参数类型错误: request body 缺失`;`/admin-api/**` 正常 | WVP `JwtAuthenticationFilter` 用 `ContentCachingRequestWrapper` 包了请求,而它是**单次读取**的。yudao 的 `ApiAccessLogInterceptor` 注册在 `/**`,`preHandle` 里 `ServletUtils.getBody(request)` 先把 JSON body 读空,Controller 再读拿到同一个已耗尽的流 | 该过滤器只用 URI / header / parameter,从不需要重复读 body,直接去掉这层包装;JSON 的可重复读本来就由 yudao 的 `CacheRequestBodyFilter` 负责(`parking-module-video/.../conf/security/JwtAuthenticationFilter.java`) |
  1954 +| 写接口业务失败(`code != 0`)仍弹「保存成功」、列表静默刷新成 0 行 | `src/api/video/*.ts` 的写接口写成 `service.post(...).then(() => undefined)`,绕过了 `unwrap` | `wvp.ts` / `wvpAdmin.ts` / `wvpPlatform.ts` / `wvpMediaServer.ts` 里所有写接口改为 `unwrap(...)`;仍有两个例外保持裸读:`gb_record/download/progress`(后端不包 `{code,data}`)与 `ptzControl` / `queryPresets`(自定义判定) |
  1955 +
  1956 +**实测**:浏览器端完整跑通 新增 → 列表 → 编辑回显 → 删除(列表回到空态),
  1957 +新增抽屉预填本平台编号 `15040000000000000001` / `127.0.0.1` / `5060`;媒体节点页空态与
  1958 +两步向导第一步的失败提示均正确(本机没有 ZLMediaKit,`media_server/list` 为空)。
  1959 +`node --test tests/*.mjs` **50 通过 0 失败**(新增 `tests/verify-video-cascade-mapper.mjs` 12 条),
  1960 +`npm run ts:check` 28 条既有错误、无新增(`videoPlatform` / `videoMediaServer` / `api/video` 干净),
  1961 +`npm run build:local` 通过。
  1962 +
  1963 +**未做**:批 3 云端录像 + 录制计划(依赖本机先跑起 ZLMediaKit)、批 4 组织结构 + 报警管理。
  1964 +本机当前既无上级平台也无媒体节点,两页只能验到空态与表单层。
  1965 +
  1966 +#### 8.2.2 视频监控二级分组改读 WVP 业务分组 + 新增「通道分组」页(2026-11-09)
  1967 +
  1968 +**起因**:视频监控页左侧树的二级节点原来是演示夹具里的 `parkName`(`demo.ts` 的车场名),
  1969 +和真实通道没有关系。WVP 的「通用频道 / 业务分组」才是通道归属的正源,这里把它接上。
  1970 +
  1971 +**口径**:二级节点 = 通道的 `gbBusinessGroupId` 翻出来的分组名;分区县仍按通道的 `civilCode`
  1972 +(`shared/district-code.ts` 的 12 个旗县区 + 「未分区县」)。没挂分组的通道归到「未分组」。
  1973 +`/common/channel/group/add` 会把 `gb_business_group_id` 写成**顶层 215 节点**(挂到 216 下面也一样),
  1974 +所以监控页只拉一层 `/api/group/tree/list` 就够,不需要递归。
  1975 +
  1976 +**回放不受影响**:点播仍走「设备 → 通道」接口(`device.deviceId` + `channel.deviceId`),
  1977 +**不要**改成用 `gbParentId` —— 挂分组会把它覆盖成分组节点编号。
  1978 +
  1979 +**菜单**:`docs/sql/2026-11-09-视频管理菜单-通道分组.sql`
  1980 +
  1981 +| id | 名称 | path | 组件 | 权限 |
  1982 +|---|---|---|---|---|
  1983 +| 100532 | 通道分组 | `video-group` | `gov/videoGroup/index` | `parking:video:group:query` |
  1984 +
  1985 +只插 `system_menu`,不插 `system_role_menu`(新权限不自动授予任何角色)。100531 仍留给「组织结构」。
  1986 +
  1987 +**页面**(`src/views/gov/videoGroup/index.vue`):左边分组树(新建顶层 215 / 新建子节点 216 /
  1988 +重命名 / 删除),右边当前分组的通道列表(添加通道从通用通道池里选、移除所选)。
  1989 +20 位编号由 `shared/group.ts` 的 `nextGroupDeviceId` 按「中心(8)+行业(2)+类型(3)+网络(1)+序号(6)」
  1990 +生成,类型码 215/216 分开自增,序号满 999999 回绕到 1。
  1991 +
  1992 +**前端改动**:
  1993 +
  1994 +| 文件 | 作用 |
  1995 +|---|---|
  1996 +| `src/views/gov/shared/group.ts` | 新增。分组索引、名称/区划反查(216 往上找 215)、编号生成、`findNodeByDeviceId` |
  1997 +| `src/views/gov/shared/district-code.ts` | 新增。`civilCode` → 旗县区,12 个区县 + 未分区县 |
  1998 +| `src/views/gov/videoMonitor/mapping.ts` | 新增。设备/通道/分组 → 一路视频一行(纯函数,便于测) |
  1999 +| `src/views/gov/videoMonitor/wvpSource.ts` | 只保留取数,映射交给 `mapping.ts`;分组接口挂了就降级为空索引 |
  2000 +| `src/views/gov/videoMonitor/model.ts` | 二级节点兜底文案改成「未分组」 |
  2001 +| `src/api/video/wvpGroup.ts` | 新增。`/group/*`、`/common/channel/*` 封装 |
  2002 +| `src/api/video/wvp.ts` | `WvpChannel` 补 `businessGroupId` |
  2003 +
  2004 +**顺手修掉的两个真问题**:
  2005 +
  2006 +| 现象 | 根因 | 修法 |
  2007 +|---|---|---|
  2008 +| 左栏「重命名 / 删除」点不到,Playwright 报 `subtree intercepts pointer events` | 左栏只有 340px,「标题 + 共 N 条 + 四个按钮」挤在 `TableToolbar` 一行里放不下,按钮溢出到右栏坐标下被右栏工具栏盖住 | 四个按钮移出工具栏,单独一行 `.group-actions`(340px 内刚好放下);左栏改用 `.table-card` 骨架、树节点行高放开(`el-tree` 默认 26px 撑不住「名称 + 编号」两行)、两栏卡片等高、去掉树卡片上无意义的密度按钮 |
  2009 +| 改名后「当前分组」「目标分组」「删除确认」还显示老名字 | 改名后 `loadTree()` 重建了整棵树,`selected` 仍指着旧对象 | 记下 `deviceId`,重建后 `findNodeByDeviceId` 指回新节点,并 `setCurrentKey` 恢复高亮 |
  2010 +
  2011 +**门禁**:`node --test tests/*.mjs` **65 通过 0 失败**(新增 `tests/verify-video-group.mjs`、
  2012 +`tests/verify-video-monitor-group.mjs`);`npm run ts:check` 28 条既有错误、无新增;
  2013 +`npm run build:local` 通过。
  2014 +
  2015 +**实测**:浏览器端跑通 新建顶层 → 新建子节点 → 改名 → 删除(两个测试分组已删干净,树回到空态);
  2016 +后端契约另用 curl 验过一轮(215/216 / 改名 / `parent/list` / 删除后 `tree/list` 回 `[]`)。
  2017 +本地库当前没有 WVP 分组,监控页所有通道按「未分组」显示 —— 属预期。
  2018 +
  2019 +**未提交**:前端 `parking-cloud-yudao-ui` 与本文档均未 commit。
  2020 +
264 ## 8.1 启动实测与踩坑记录(2026-11-02) 2021 ## 8.1 启动实测与踩坑记录(2026-11-02)
265 2022
266 48083 首次完整跑通:`Started YudaoServerApplication in 18.156 seconds`,登录 2023 48083 首次完整跑通:`Started YudaoServerApplication in 18.156 seconds`,登录
@@ -278,24 +2035,27 @@ return `${parentPath}${childPath}`.replace(/\/+/g, &#39;/&#39;) // /park + /park/park- @@ -278,24 +2035,27 @@ return `${parentPath}${childPath}`.replace(/\/+/g, &#39;/&#39;) // /park + /park/park-
278 | 5 | `NoSuchFieldError: okhttp3...Companion` | okhttp 4.12 配 okio 1.11 | BOM 的 okio 改 3.6.0 | 2035 | 5 | `NoSuchFieldError: okhttp3...Companion` | okhttp 4.12 配 okio 1.11 | BOM 的 okio 改 3.6.0 |
279 | 6 | `WxCpProperties.getAppConfigs()` NPE | `WxCpConfiguration.initServices()` 直接 `.stream()` | `wechat.cp.app-configs: []` | 2036 | 6 | `WxCpProperties.getAppConfigs()` NPE | `WxCpConfiguration.initServices()` 直接 `.stream()` | `wechat.cp.app-configs: []` |
280 | 7 | `PlaceholderResolutionException: parking.refresh-cache.token` | 存量配置类强依赖 | `application.yaml` 加 `parking.refresh-cache.token: ${REFRESH_CACHE_TOKEN:}` | 2037 | 7 | `PlaceholderResolutionException: parking.refresh-cache.token` | 存量配置类强依赖 | `application.yaml` 加 `parking.refresh-cache.token: ${REFRESH_CACHE_TOKEN:}` |
281 -| 8 | `IotStorageInitRunner` 启动成功后又 `System.exit(1)` | 本机没有 TDengine/IoTDB | 启动类 `excludeFilters` 排除该 Runner(接视频/时序数据时部署 TDengine 再移除) | 2038 +| 8 | `IotStorageInitRunner` 启动成功后又 `System.exit(1)` | 本机没有 TDengine/IoTDB | 先是在启动类 `excludeFilters` 排除该 Runner;**2026-11-04 已整体关闭 IoT 模块**(§8.0),该 Runner 不再参与编译,排除项与 import 一并注释 |
282 | 9 | 存量接口全部 NPE(`UserInfo.getOrgId()` 空) | 合并工程里生效的是 **yudao 安全栈**,parking 的 starter 没注册(其 `OAuth2TokenCommonApi` 无实现),`SecurityContext` 里是 yudao 的 `LoginUser` | `LoginUserBridge` 增加 yudao `LoginUser` 兜底转换;`OAuth2TokenServiceImpl#buildUserInfo` 补 `username` | 2039 | 9 | 存量接口全部 NPE(`UserInfo.getOrgId()` 空) | 合并工程里生效的是 **yudao 安全栈**,parking 的 starter 没注册(其 `OAuth2TokenCommonApi` 无实现),`SecurityContext` 里是 yudao 的 `LoginUser` | `LoginUserBridge` 增加 yudao `LoginUser` 兜底转换;`OAuth2TokenServiceImpl#buildUserInfo` 补 `username` |
283 | 10 | 报表页报「请求参数类型错误:2026-11-01 00:00:00」 | 存量 DTO 的 `java.util.Date` 字段;yudao 只配了**序列化**(毫秒),没配反序列化格式 | `parking-legacy-common` 新增 `LegacyDateAutoConfiguration`(Jackson `Date` 反序列化器 + MVC `String→Date` 转换器),同时接受毫秒数字、`yyyy-MM-dd HH:mm:ss`、ISO-8601;走 `AutoConfiguration.imports` 注册 | 2040 | 10 | 报表页报「请求参数类型错误:2026-11-01 00:00:00」 | 存量 DTO 的 `java.util.Date` 字段;yudao 只配了**序列化**(毫秒),没配反序列化格式 | `parking-legacy-common` 新增 `LegacyDateAutoConfiguration`(Jackson `Date` 反序列化器 + MVC `String→Date` 转换器),同时接受毫秒数字、`yyyy-MM-dd HH:mm:ss`、ISO-8601;走 `AutoConfiguration.imports` 注册 |
284 | 11 | 日志刷 45 条 `ClassCastException: EmptyMap cannot be cast to List`(`RedissonStreamCommands.xReadGroup`) | yudao BOM 的 Redisson 4.8.0 + `redisson-spring-data-35` 有 RESP2 `xReadGroup` 适配 bug | `yudao-dependencies` 钉 Redisson **3.52.0**(自带 `redisson-spring-data-35`);重启后 0 条 | 2041 | 11 | 日志刷 45 条 `ClassCastException: EmptyMap cannot be cast to List`(`RedissonStreamCommands.xReadGroup`) | yudao BOM 的 Redisson 4.8.0 + `redisson-spring-data-35` 有 RESP2 `xReadGroup` 适配 bug | `yudao-dependencies` 钉 Redisson **3.52.0**(自带 `redisson-spring-data-35`);重启后 0 条 |
285 | 12 | 充电稽核/对账/会员卡等 5 个页面 HTTP 200 但 `code=500`:`MybatisPlusException: Failed to process ... ParseException: Encountered unexpected token: "AND"` | 上游 `DataPermissionInterceptor` 对**每条** mapper 语句先用 JSqlParser 解析;合并工程为 PageHelper 6.1.1 固定 JSqlParser **4.9**,存量 SQL(UNION ALL、动态 GROUP BY、count 包裹)大量解析失败 | 新增 `LegacySafeDataPermissionInterceptor`:非 `cn.iocoder.yudao.*` 的语句在解析前直接放行。配套 `LegacyDataPermissionAutoConfiguration` 重建上游 3 个 Bean,并在 **`application.yaml` 与 `application-local.yaml` 两处** exclude 上游 `YudaoDataPermissionAutoConfiguration`(profile 的 `autoconfigure.exclude` 会**替换**而非合并基座值,只改一处会 Bean 冲突) | 2042 | 12 | 充电稽核/对账/会员卡等 5 个页面 HTTP 200 但 `code=500`:`MybatisPlusException: Failed to process ... ParseException: Encountered unexpected token: "AND"` | 上游 `DataPermissionInterceptor` 对**每条** mapper 语句先用 JSqlParser 解析;合并工程为 PageHelper 6.1.1 固定 JSqlParser **4.9**,存量 SQL(UNION ALL、动态 GROUP BY、count 包裹)大量解析失败 | 新增 `LegacySafeDataPermissionInterceptor`:非 `cn.iocoder.yudao.*` 的语句在解析前直接放行。配套 `LegacyDataPermissionAutoConfiguration` 重建上游 3 个 Bean,并在 **`application.yaml` 与 `application-local.yaml` 两处** exclude 上游 `YudaoDataPermissionAutoConfiguration`(profile 的 `autoconfigure.exclude` 会**替换**而非合并基座值,只改一处会 Bean 冲突) |
286 | 13 | `/order/charger-audit` 报 `only_full_group_by`(`charger_income_statistic_day.charger_name`) | MySQL 8 默认开启 `ONLY_FULL_GROUP_BY`,存量 SQL 按 5.7 写 | JDBC URL 加 `sessionVariables=sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'`(**仅本应用连接**降级,不动 MySQL 全局,老 48082 不受影响) | 2043 | 13 | `/order/charger-audit` 报 `only_full_group_by`(`charger_income_statistic_day.charger_name`) | MySQL 8 默认开启 `ONLY_FULL_GROUP_BY`,存量 SQL 按 5.7 写 | JDBC URL 加 `sessionVariables=sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'`(**仅本应用连接**降级,不动 MySQL 全局,老 48082 不受影响) |
287 | 14 | `/finance/balance` 报 `near 'desc ) tmp_count'`;另有 `order_park`/`income_finance_bill`/`income_statistic_day`/`enclosed_parking_lot_config` 的同类 SQL | MySQL 8 已删除 `GROUP BY col DESC/ASC` 语法(5.7 起废弃),PageHelper 的 count 包裹后报语法错 | 逐条改为等价写法:`GROUP BY check_date ORDER BY check_date DESC`、`GROUP BY year,num ORDER BY year DESC,num DESC`、`GROUP BY staticDate ORDER BY staticDate DESC`、`GROUP BY l.sort ORDER BY l.sort asc`;子查询里无意义的 `desc` 直接去掉。已全仓 grep 排查,无残留 | 2044 | 14 | `/finance/balance` 报 `near 'desc ) tmp_count'`;另有 `order_park`/`income_finance_bill`/`income_statistic_day`/`enclosed_parking_lot_config` 的同类 SQL | MySQL 8 已删除 `GROUP BY col DESC/ASC` 语法(5.7 起废弃),PageHelper 的 count 包裹后报语法错 | 逐条改为等价写法:`GROUP BY check_date ORDER BY check_date DESC`、`GROUP BY year,num ORDER BY year DESC,num DESC`、`GROUP BY staticDate ORDER BY staticDate DESC`、`GROUP BY l.sort ORDER BY l.sort asc`;子查询里无意义的 `desc` 直接去掉。已全仓 grep 排查,无残留 |
288 -| 15 | `/finance/reconciliation` 报 `业务错误:appId为空!`,汇总报 `near 'and a.report_date >=...'` | 桥取 `LoginUser.deptId`(本地 admin=**103**,芋道演示部门树遗留)换算组织;`org-id-mapping` 只配了 `1:10003`,103 原样落到 `sys_org_app_id` 查不到 → `appIds` 为空 | 本地映射改为 `103:10003,1:10003`;对账入参变成 `orgIds=[10003]`,分页与汇总均 `BIZ_SUCCESS` | 2045 +| 15 | `/finance/reconciliation` 报 `业务错误:appId为空!`,汇总报 `near 'and a.report_date >=...'` | 桥取 `LoginUser.deptId`(本地 admin=**103**,芋道演示部门树遗留)换算组织;`org-id-mapping` 只配了 `1:10003`,103 原样落到 `sys_org_app_id` 查不到 → `appIds` 为空 | 本地映射改为 `103:10003,1:10003`;对账入参变成 `orgIds=[10003]`,分页与汇总均 `BIZ_SUCCESS`。**(2026-11-02 组织 id 统一后映射已删除,`system_users.dept_id` 直接就是 10003,本行仅作历史记录)** |
289 | 16 | 新服务把老服务的出场延迟消息抢走(潜在) | 新老服务默认都连 **Redis db 0**;存量 `EqpOutDelayQueueSchedule` 每 10s `poll()` 同一个 `RBlockingQueue`,缓存 key(`zteits-park:*`)也同名 | `application-local.yaml` 的 `spring.data.redis.database` 改 **1**(老服务仍是 0)。登录令牌/缓存/队列全部隔离,老环境不受影响 | 2046 | 16 | 新服务把老服务的出场延迟消息抢走(潜在) | 新老服务默认都连 **Redis db 0**;存量 `EqpOutDelayQueueSchedule` 每 10s `poll()` 同一个 `RBlockingQueue`,缓存 key(`zteits-park:*`)也同名 | `application-local.yaml` 的 `spring.data.redis.database` 改 **1**(老服务仍是 0)。登录令牌/缓存/队列全部隔离,老环境不受影响 |
290 | 17 | 切到 db1 后 `/order/park-record/query-park-order-for-finance` 500:`ParkingLotDTO.getOrgId()` NPE | 存量代码直接读 Redis hash(`ParkingLotCacheUtil`),新 db 里缓存为空 | 启动后调用 `/admin-api/system/refresh-cache/{park,sys,pay}-cache?type=-1` 与 `refresh-bill-cache?type=1`;已封装成 `scripts/warm-cache.sh`,首次启动或清 Redis 后执行 | 2047 | 17 | 切到 db1 后 `/order/park-record/query-park-order-for-finance` 500:`ParkingLotDTO.getOrgId()` NPE | 存量代码直接读 Redis hash(`ParkingLotCacheUtil`),新 db 里缓存为空 | 启动后调用 `/admin-api/system/refresh-cache/{park,sys,pay}-cache?type=-1` 与 `refresh-bill-cache?type=1`;已封装成 `scripts/warm-cache.sh`,首次启动或清 Redis 后执行 |
  2048 +| 18 | 系统接口正常,但 `park/*`、`park/index-*` 等接口一直挂起、响应 0 字节;后端日志出现随机 `ClassNotFoundException` / `NoClassDefFoundError: ch.qos.logback.classic.spi.ThrowableProxy`、`org.apache.ibatis.reflection.ExceptionUtil` | **JVM 运行期间覆盖了 `yudao-server/target/yudao-server.jar`**(IDE 重新 package)。Spring Boot 的 nested-jar 类加载器按启动时的 jar 索引从磁盘读类,文件被替换后索引失效 | 跑着服务时不要原地覆盖 jar。调试期用副本启动:`cp yudao-server/target/yudao-server.jar /tmp/yudao-server-cc.jar && java -jar /tmp/yudao-server-cc.jar --spring.profiles.active=local`;改完代码重新构建后**重启进程**再验证。日志里看到这类随机 CNFE 先怀疑 jar 被替换,不要先查业务代码 |
291 2049
292 已知未决(不阻塞当前联调): 2050 已知未决(不阻塞当前联调):
293 - **两套调度同时在跑**:老 48082 与新 48083 都会执行存量 `@Scheduled`/Quartz, 2051 - **两套调度同时在跑**:老 48082 与新 48083 都会执行存量 `@Scheduled`/Quartz,
294 联调期先只开一个;正式切换前把存量定时任务收口到新工程的 Quartz。 2052 联调期先只开一个;正式切换前把存量定时任务收口到新工程的 Quartz。
295 -- **组织 id 空间**:`LoginUserBridge` 用 `parking.legacy.org-id-mapping`(本地 `103:10003,1:10003`)换算;  
296 - 长期方案是把 `system_users.dept_id` 对齐到真实组织、老 `sys_org` 的 id 原样迁进 `system_org`,然后删掉映射。  
297 -- **菜单/权限收口**:672 个 `@PreAuthorize` 权限点中约 462 个在 `system_menu` 无记录,  
298 - 目前 admin 可通,非超管角色需补菜单(含导出权限)后才可用。 2053 +- ~~**组织 id 空间**~~ **(已完成,2026-11-02)**:`system_org` 沿用老 `sys_org.id`,
  2054 + `system_users.dept_id` 指向真实组织,`org-id-mapping` 已删除;见 §7.7。
  2055 + 剩余动作只在生产切换时执行一次 `scripts/migrate-org-ids.sql` 并清 token。
  2056 +- ~~**菜单/权限收口**~~ **(已完成,2026-11-02)**:672 个权限串全部有菜单,
  2057 + 3 个角色模板 + 联调账号已建;见 `scripts/README-permissions.md`。
  2058 + 生产要按真实角色重新勾选,别直接搬测试账号。
299 2059
300 ## 9. 常用命令 2060 ## 9. 常用命令
301 2061
@@ -331,10 +2091,25 @@ cd ../parking-cloud-yudao-ui &amp;&amp; pnpm install &amp;&amp; pnpm dev --port 5175 @@ -331,10 +2091,25 @@ cd ../parking-cloud-yudao-ui &amp;&amp; pnpm install &amp;&amp; pnpm dev --port 5175
331 ## 11. 已知坑 2091 ## 11. 已知坑
332 2092
333 1. 上游 `pay/member/report/...` 不能挂,路由和表都会撞。 2093 1. 上游 `pay/member/report/...` 不能挂,路由和表都会撞。
334 -2. 两个 `MetaObjectHandler` 不能同时注册。 2094 +2. 两个 `MetaObjectHandler` 不能同时注册。当前生效的只有 yudao 那个,它只认
  2095 + `cn.iocoder...BaseDO`;parking 的 `BaseDO` 子类(如 `OrgDO`)INSERT 时必须自己补
  2096 + `create_time/update_time`,否则 MyBatis-Plus 会显式写 NULL 触发非空约束。
  2097 + `OrgServiceImpl` 已显式处理,新增 parking 风格 MP 实体时照做。
335 3. `com.clouds.common.*` 扫描顺序错 → 启动 NPE。 2098 3. `com.clouds.common.*` 扫描顺序错 → 启动 NPE。
336 4. 上游全局 `logic-delete` 只配了值、没配字段,parking 无 `deleted` 的 DO 安全。 2099 4. 上游全局 `logic-delete` 只配了值、没配字段,parking 无 `deleted` 的 DO 安全。
337 5. 菜单 id 必须错开,否则和上游 seed 冲突。 2100 5. 菜单 id 必须错开,否则和上游 seed 冲突。
338 -6. 老库 `system_org` 与上游 `system_dept` 是两套,先保留 org,后续再谈合并。 2101 +6. `system_org` 与 `system_dept` 仍是两张表,但**已统一 id 空间**:`system_org`
  2102 + 是主表(沿用老 `sys_org.id`),`system_dept` 只是 yudao 用户模块的镜像,
  2103 + 由 `OrgServiceImpl` 写穿透维护。不要再直接改 `system_dept`,也不要恢复
  2104 + `parking.legacy.org-id-mapping`(见 §7.7 / `scripts/README-org.md`)。
339 7. `pnpm ts:check` 还有 **28 个存量页面的类型错误**(18 个 EP `el-tag`/`el-select` 可空 prop、2 个 `orgName`、8 个未使用变量), 2105 7. `pnpm ts:check` 还有 **28 个存量页面的类型错误**(18 个 EP `el-tag`/`el-select` 可空 prop、2 个 `orgName`、8 个未使用变量),
340 不影响 `pnpm dev` 与 `vite build`(构建不跑 vue-tsc);新代码提交前请保证自己改的文件不新增错误。 2106 不影响 `pnpm dev` 与 `vite build`(构建不跑 vue-tsc);新代码提交前请保证自己改的文件不新增错误。
  2107 +8. **存量 `@Scheduled` 默认不跑**(`parking.legacy.scheduler.enabled=false`)。新服务上
  2108 + “功能正常”不代表定时逻辑在跑;要验证调度相关需求,先显式打开开关并确认没有第二个实例
  2109 + 同时跑(见 §7.5)。同理,Redis db 隔离(db1)会让延迟队列在新服务上永远为空。
  2110 +9. **`get-permission-info` 会丢掉“父链没授权”的按钮**。`filterDisableMenus()` 只保留
  2111 + 父级存在且已授权的菜单,所以只给角色授权按钮、不给父页面/目录时,接口能过
  2112 + (`@PreAuthorize` 查的是 `menu_role_ids` 缓存),但前端 `v-hasPermi` 拿不到这个权限串,
  2113 + 按钮会消失。给角色新增按钮权限时,父链要一起给(例:运营有 `100426/100427`,
  2114 + 但其父页面 `100424` 挂在「系统管理」下未授权,因此这两个串不出现在权限列表里;
  2115 + 指挥中心页未用 `v-hasPermi` 所以不受影响)。
docs/sql/ai-2026-04-16-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/ai-2026-11-01-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/bpm-2026-04-18-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/crm2026-09-19-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/erp-2026-04-18-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/fms-2026-08-14-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/fms-2026-09-26-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/hrm-2026-08-04-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/im-2026-05-31-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/im-2026-06-20-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/iot-2026-10-31-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/iot-2026-11-02-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/mall-2026-04-18-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/member-2026-05-30-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/mes-2026-05-05-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/mes-2026-06-20-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/mes-206-09-26-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/mp-2026-04-18-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/oa-2026-09-18-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/oa-2026-10-21-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/pay-2026-04-18-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/pms2026-09-03-传播违法.sql.zip 0 → 100644
No preview for this file type
docs/sql/wms-2026-05-15-传播违法.sql.zip 0 → 100644
No preview for this file type
parking-dependencies/pom.xml
@@ -180,6 +180,11 @@ @@ -180,6 +180,11 @@
180 </dependency> 180 </dependency>
181 <dependency> 181 <dependency>
182 <groupId>com.zteits.boot</groupId> 182 <groupId>com.zteits.boot</groupId>
  183 + <artifactId>parking-module-video</artifactId>
  184 + <version>${revision}</version>
  185 + </dependency>
  186 + <dependency>
  187 + <groupId>com.zteits.boot</groupId>
183 <artifactId>parking-module-gov</artifactId> 188 <artifactId>parking-module-gov</artifactId>
184 <version>${revision}</version> 189 <version>${revision}</version>
185 </dependency> 190 </dependency>
parking-framework/parking-legacy-common/src/main/java/com/clouds/common/oss/base/OssClientBaseWrapper.java
@@ -3,6 +3,7 @@ package com.clouds.common.oss.base; @@ -3,6 +3,7 @@ package com.clouds.common.oss.base;
3 import com.clouds.common.oss.ali.AliOssClientConfig; 3 import com.clouds.common.oss.ali.AliOssClientConfig;
4 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.beans.factory.annotation.Autowired;
5 import org.springframework.beans.factory.annotation.Qualifier; 5 import org.springframework.beans.factory.annotation.Qualifier;
  6 +import org.springframework.beans.factory.annotation.Value;
6 import org.springframework.stereotype.Component; 7 import org.springframework.stereotype.Component;
7 8
8 /** 9 /**
@@ -19,12 +20,26 @@ public class OssClientBaseWrapper { @@ -19,12 +20,26 @@ public class OssClientBaseWrapper {
19 @Autowired 20 @Autowired
20 @Qualifier("com.clouds.common.oss.fastdfs.FastDFSOssClientWrapper") 21 @Qualifier("com.clouds.common.oss.fastdfs.FastDFSOssClientWrapper")
21 private OssClientService fastDFSOssClientWrapper; 22 private OssClientService fastDFSOssClientWrapper;
  23 + @Autowired
  24 + @Qualifier("com.clouds.common.oss.local.LocalOssClientWrapper")
  25 + private OssClientService localOssClientWrapper;
  26 +
  27 + /** 本地开发用磁盘存储;留空 = 走存量逻辑(阿里云 OSS / FastDFS) */
  28 + @Value("${parking.legacy.oss.mode:}")
  29 + private String ossMode;
22 30
23 /** 31 /**
24 * 根据数据库的配置自动选择是使用FastDFS 还是使用 阿里云OSS 32 * 根据数据库的配置自动选择是使用FastDFS 还是使用 阿里云OSS
25 * 字典 : FILE_OSS_CONFIG--ENABLE_TYPE 的配置 33 * 字典 : FILE_OSS_CONFIG--ENABLE_TYPE 的配置
  34 + *
  35 + * <p>⚠️ 手改(非搬迁原样):新增 `parking.legacy.oss.mode=local` 分支。本地既没有
  36 + * 可用的阿里云 OSS 配置,也没有可达的 FastDFS tracker,存量的两个实现都会失败;
  37 + * 该配置留空时行为与老代码完全一致。</p>
26 */ 38 */
27 public OssClientService getOssClientService() { 39 public OssClientService getOssClientService() {
  40 + if ("local".equalsIgnoreCase(ossMode)) {
  41 + return localOssClientWrapper;
  42 + }
28 if (AliOssClientConfig.ALI_OSS_ENABLE) { 43 if (AliOssClientConfig.ALI_OSS_ENABLE) {
29 return aliOssClientWrapper; 44 return aliOssClientWrapper;
30 } else { 45 } else {
parking-framework/parking-legacy-common/src/main/java/com/clouds/common/web/auth/LegacyOrgIdMappingConfig.java
@@ -12,12 +12,15 @@ import java.util.Map; @@ -12,12 +12,15 @@ import java.util.Map;
12 /** 12 /**
13 * 老新组织 id 换算表的加载器(P3 组织口径桥的第一步,2026-10-17 批 6 第四十七切片)。 13 * 老新组织 id 换算表的加载器(P3 组织口径桥的第一步,2026-10-17 批 6 第四十七切片)。
14 * 14 *
15 - * <h2>解决什么</h2> 15 + * <h2>解决什么(历史问题,2026-11-02 已从数据侧解决)</h2>
16 * 16 *
17 - * <p>新栈登录态里的 {@code deptId} 是 {@code system_org.id}(本地只有 1 行 {@code id=1}),  
18 - * 而存量业务表({@code tb_area_block.org_id}、{@code td_b_api_log.org_id} …)存的是  
19 - * <b>老 {@code sys_org.id}</b>(如 10003/10094)。不做换算时,存量接口 {@code code=0}  
20 - * 但结果为空——不报错、只少给数据(坑 22)。详见 {@link LoginUserBridge} 类注释。</p> 17 + * <p>改造早期,新栈登录态里的 {@code deptId} 是 {@code system_org.id}(当时只有 1 行
  18 + * {@code id=1}),而存量业务表({@code tb_area_block.org_id}、{@code td_b_api_log.org_id} …)
  19 + * 存的是<b>老 {@code sys_org.id}</b>(如 10003/10094),因此需要一张换算表。
  20 + * 现在 {@code sys_org} 的真实 id 已原样迁入 {@code system_org},
  21 + * {@code system_users.dept_id} 直接就是真实组织 id,默认空表(恒等映射)即正确行为。
  22 + * 换算表只作为过渡期兼容手段保留。详见 {@link LoginUserBridge} 类注释与
  23 + * {@code scripts/README-org.md}。</p>
21 * 24 *
22 * <h2>配置</h2> 25 * <h2>配置</h2>
23 * 26 *
@@ -26,7 +29,9 @@ import java.util.Map; @@ -26,7 +29,9 @@ import java.util.Map;
26 * 例:{@code 1:10003};</li> 29 * 例:{@code 1:10003};</li>
27 * <li>基座 {@code application.yaml} 声明为 {@code ${LEGACY_ORG_ID_MAPPING:}} ⇒ 30 * <li>基座 {@code application.yaml} 声明为 {@code ${LEGACY_ORG_ID_MAPPING:}} ⇒
28 * <b>默认空串 = 恒等映射</b>,任何环境不配都与改造前**完全一致**;</li> 31 * <b>默认空串 = 恒等映射</b>,任何环境不配都与改造前**完全一致**;</li>
29 - * <li>本地 {@code application-local.yaml} 给 {@code 1:10003}(明确标注本地测试值);</li> 32 + * <li>2026-11-02 组织 id 统一后,本地 {@code application-local.yaml} 已置空
  33 + * ({@code system_users.dept_id} 直接指向真实 {@code sys_org.id},见
  34 + * {@code scripts/README-org.md});只有过渡期数据未迁移时才需要配置;</li>
30 * <li>格式错误(少冒号、非数字、同一新 id 重复)**启动即失败**——这类错误静默跳过 35 * <li>格式错误(少冒号、非数字、同一新 id 重复)**启动即失败**——这类错误静默跳过
31 * 会变成"映射没生效但接口照样 200",正是坑 22 最难查的形态。</li> 36 * 会变成"映射没生效但接口照样 200",正是坑 22 最难查的形态。</li>
32 * </ul> 37 * </ul>
parking-framework/parking-legacy-common/src/main/java/com/clouds/common/web/auth/LoginUserBridge.java
@@ -112,9 +112,11 @@ import org.slf4j.LoggerFactory; @@ -112,9 +112,11 @@ import org.slf4j.LoggerFactory;
112 * {@code parking.legacy.org-id-mapping}(如 {@code 1:10003,2:10094}), 112 * {@code parking.legacy.org-id-mapping}(如 {@code 1:10003,2:10094}),
113 * 由 {@link LegacyOrgIdMappingConfig} 在启动时灌进 {@link #setOrgIdMapping(Map)}; 113 * 由 {@link LegacyOrgIdMappingConfig} 在启动时灌进 {@link #setOrgIdMapping(Map)};
114 * {@link #from(LoginUser)} 对 {@code orgId}/{@code orgIds} 查表换算。 114 * {@link #from(LoginUser)} 对 {@code orgId}/{@code orgIds} 查表换算。
115 - * <b>默认空表 = 恒等映射</b>(不配就与改造前完全一致,零行为变化);  
116 - * 本地 {@code application-local.yaml} 配 {@code 1:10003} 用于验证。  
117 - * 这是过渡手段,不是终态:上游数据迁移(①/②)落地后直接把表清空即可退役。</li> 115 + * <b>默认空表 = 恒等映射</b>(不配就与改造前完全一致,零行为变化)。
  116 + * <b>2026-11-02 起已按方案①完成迁移</b>:{@code sys_org} 的真实 id 原样迁入
  117 + * {@code system_org},{@code system_users.dept_id} 直接是真实组织 id,
  118 + * 本地配置已清空,换算表退役(保留代码只为过渡期兼容,见
  119 + * {@code scripts/README-org.md})。</li>
118 * </ol> 120 * </ol>
119 * 121 *
120 * <p>★ 换算只作用于 {@code orgId}/{@code orgIds}。{@code resIds} / {@code roleIds} / 122 * <p>★ 换算只作用于 {@code orgId}/{@code orgIds}。{@code resIds} / {@code roleIds} /
parking-framework/parking-legacy-common/src/main/java/com/zteits/parking/framework/legacy/jackson/LegacyDateAutoConfiguration.java 0 → 100644
  1 +package com.zteits.parking.framework.legacy.jackson;
  2 +
  3 +import org.springframework.boot.autoconfigure.AutoConfiguration;
  4 +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
  5 +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
  6 +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
  7 +import org.springframework.context.annotation.Bean;
  8 +import org.springframework.format.FormatterRegistry;
  9 +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  10 +
  11 +import java.util.Date;
  12 +
  13 +/**
  14 + * 让存量接口继续接受 {@code yyyy-MM-dd HH:mm:ss} 形式的日期。
  15 + *
  16 + * <p>背景见 {@link LegacyDateDeserializer}:yudao 只配了序列化格式,没配 {@code java.util.Date}
  17 + * 的反序列化格式,存量 DTO 接字符串日期会 400。</p>
  18 + *
  19 + * <p>这里提供两个入口,覆盖存量接口的两种入参方式:</p>
  20 + * <ul>
  21 + * <li>JSON body({@code @RequestBody} 里的 DTO 字段)→ Jackson 反序列化器;</li>
  22 + * <li>查询参数 / 表单({@code @RequestParam Date}、{@code @ModelAttribute})→ MVC 类型转换器。</li>
  23 + * </ul>
  24 + *
  25 + * <p>只加「字符串 → Date」,不覆盖序列化,因此不影响 yudao 新接口的毫秒时间戳输出。</p>
  26 + */
  27 +@AutoConfiguration(after = JacksonAutoConfiguration.class)
  28 +@ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper")
  29 +public class LegacyDateAutoConfiguration {
  30 +
  31 + @Bean
  32 + public Jackson2ObjectMapperBuilderCustomizer legacyDateDeserializerCustomizer() {
  33 + // 只替换 java.util.Date 的反序列化;序列化仍走 yudao 的 write-dates-as-timestamps
  34 + return builder -> builder.deserializerByType(Date.class, new LegacyDateDeserializer());
  35 + }
  36 +
  37 + /**
  38 + * 查询参数里的 {@code Date}:Spring 默认的 ConversionService 只认数字时间戳,
  39 + * 存量页面发的是 {@code yyyy-MM-dd HH:mm:ss},这里补一个转换器。
  40 + */
  41 + @Bean
  42 + public WebMvcConfigurer legacyDateWebMvcConfigurer() {
  43 + return new WebMvcConfigurer() {
  44 + @Override
  45 + public void addFormatters(FormatterRegistry registry) {
  46 + registry.addConverter(String.class, Date.class, LegacyDateDeserializer::parse);
  47 + }
  48 + };
  49 + }
  50 +}
parking-framework/parking-legacy-common/src/main/java/com/zteits/parking/framework/legacy/jackson/LegacyDateDeserializer.java 0 → 100644
  1 +package com.zteits.parking.framework.legacy.jackson;
  2 +
  3 +import com.fasterxml.jackson.core.JsonParser;
  4 +import com.fasterxml.jackson.core.JsonToken;
  5 +import com.fasterxml.jackson.databind.DeserializationContext;
  6 +import com.fasterxml.jackson.databind.JsonDeserializer;
  7 +
  8 +import java.io.IOException;
  9 +import java.time.Instant;
  10 +import java.time.LocalDate;
  11 +import java.time.LocalDateTime;
  12 +import java.time.OffsetDateTime;
  13 +import java.time.ZoneId;
  14 +import java.time.format.DateTimeFormatter;
  15 +import java.time.format.DateTimeParseException;
  16 +import java.util.Date;
  17 +
  18 +/**
  19 + * 存量 DTO 的 {@link Date} 反序列化器。
  20 + *
  21 + * <p>存量接口(1311 个 mapper XML 背后的那批 Controller/DTO)全部用 {@code java.util.Date} 接时间,
  22 + * 老门户的 Jackson 配置接受 {@code yyyy-MM-dd HH:mm:ss} 字符串;而 yudao 的
  23 + * {@code YudaoJacksonAutoConfiguration} 只定制了<b>序列化</b>({@code write-dates-as-timestamps=true}),
  24 + * 没有给反序列化配格式。迁移后新前端继续按老口径发字符串,于是 Spring 抛
  25 + * {@code InvalidFormatException},被 GlobalExceptionHandler 包成
  26 + * 「请求参数类型错误:2026-11-01 00:00:00」——2026-11-02 实测报表类页面全部受影响。</p>
  27 + *
  28 + * <p>这里同时接受三种输入,覆盖新前端的三种写法:</p>
  29 + * <ul>
  30 + * <li>数字(Epoch 毫秒)—— yudao 自家接口的序列化口径,前后端回传时用;</li>
  31 + * <li>{@code yyyy-MM-dd HH:mm:ss} / {@code yyyy-MM-dd HH:mm} / {@code yyyy-MM-dd}
  32 + * / {@code yyyy/MM/dd ...} —— 存量前端口径;</li>
  33 + * <li>ISO-8601({@code 2026-11-01T00:00:00}、带 {@code Z}/{@code +08:00} 的也认)。</li>
  34 + * </ul>
  35 + *
  36 + * <p>只影响 {@code java.util.Date} 的反序列化,不碰序列化(序列化仍由 yudao 输出毫秒时间戳,
  37 + * 前端 {@code formatDateTime} 已同时兼容数字与字符串)。空串按 {@code null} 处理 ——
  38 + * 老页面清空时间条件时发的就是空串。</p>
  39 + */
  40 +public class LegacyDateDeserializer extends JsonDeserializer<Date> {
  41 +
  42 + private static final DateTimeFormatter[] LOCAL_FORMATTERS = new DateTimeFormatter[]{
  43 + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
  44 + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"),
  45 + DateTimeFormatter.ofPattern("yyyy-MM-dd"),
  46 + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"),
  47 + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"),
  48 + DateTimeFormatter.ofPattern("yyyy/MM/dd"),
  49 + };
  50 +
  51 + @Override
  52 + public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
  53 + JsonToken token = p.currentToken();
  54 + if (token == JsonToken.VALUE_NULL) {
  55 + return null;
  56 + }
  57 + if (token == JsonToken.VALUE_NUMBER_INT) {
  58 + return new Date(p.getLongValue());
  59 + }
  60 + if (token == JsonToken.VALUE_NUMBER_FLOAT) {
  61 + return new Date((long) p.getDoubleValue());
  62 + }
  63 + if (token == JsonToken.VALUE_STRING) {
  64 + String text = p.getText().trim();
  65 + if (text.isEmpty()) {
  66 + return null;
  67 + }
  68 + try {
  69 + return parse(text);
  70 + } catch (DateTimeParseException e) {
  71 + throw ctxt.weirdStringException(text, Date.class, "无法解析为日期:" + e.getMessage());
  72 + }
  73 + }
  74 + return (Date) ctxt.handleUnexpectedToken(Date.class, p);
  75 + }
  76 +
  77 + /**
  78 + * 解析字符串日期,供 Jackson 反序列化与 Spring MVC 查询参数转换共用。
  79 + *
  80 + * @throws DateTimeParseException 所有格式都不匹配时抛出
  81 + */
  82 + public static Date parse(String text) {
  83 + String value = text.trim();
  84 + if (value.isEmpty()) {
  85 + return null;
  86 + }
  87 + // 纯数字字符串按 Epoch 毫秒处理(前端把时间戳 toString 后回传)
  88 + if (value.chars().allMatch(Character::isDigit)) {
  89 + return new Date(Long.parseLong(value));
  90 + }
  91 + // 带时区的 ISO-8601:2026-11-01T00:00:00+08:00 / ...Z
  92 + if (value.indexOf('T') > 0 && (value.endsWith("Z") || value.matches(".*[+-]\\d{2}:?\\d{2}$"))) {
  93 + return Date.from(OffsetDateTime.parse(value).toInstant());
  94 + }
  95 + // LocalDateTime 的 ISO 写法:2026-11-01T00:00:00[.SSS]
  96 + if (value.indexOf('T') > 0) {
  97 + return toDate(LocalDateTime.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
  98 + }
  99 + for (DateTimeFormatter formatter : LOCAL_FORMATTERS) {
  100 + try {
  101 + if (formatter == LOCAL_FORMATTERS[2] || formatter == LOCAL_FORMATTERS[5]) {
  102 + // 纯日期:按当天 00:00:00
  103 + return toDate(LocalDate.parse(value, formatter).atStartOfDay());
  104 + }
  105 + return toDate(LocalDateTime.parse(value, formatter));
  106 + } catch (DateTimeParseException ignored) {
  107 + // 换下一种格式
  108 + }
  109 + }
  110 + // 最后兜底 Instant(例如 "2026-11-01T00:00:00Z" 已被上面覆盖,这里防漏)
  111 + return Date.from(Instant.parse(value));
  112 + }
  113 +
  114 + private static Date toDate(LocalDateTime dateTime) {
  115 + return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
  116 + }
  117 +}
parking-framework/parking-legacy-common/src/main/java/com/zteits/parking/framework/legacy/mybatis/LegacyDataPermissionAutoConfiguration.java 0 → 100644
  1 +package com.zteits.parking.framework.legacy.mybatis;
  2 +
  3 +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor;
  4 +import cn.iocoder.yudao.framework.datapermission.core.db.DataPermissionRuleHandler;
  5 +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
  6 +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
  7 +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
  8 +import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
  9 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
  10 +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
  11 +import org.springframework.boot.autoconfigure.AutoConfiguration;
  12 +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
  13 +import org.springframework.context.annotation.Bean;
  14 +
  15 +import java.util.List;
  16 +
  17 +/**
  18 + * 用 {@link LegacySafeDataPermissionInterceptor} 替代上游的数据权限拦截器。
  19 + *
  20 + * <p>上游 {@code YudaoDataPermissionAutoConfiguration} 已由 {@code application.yaml} 的
  21 + * {@code spring.autoconfigure.exclude} 排除,这里按原样重建它注册的三个 Bean,
  22 + * 唯一的区别是拦截器换成了「只解析 yudao 自家 mapper」的子类。</p>
  23 + *
  24 + * <p>如果哪天不再需要兼容存量 SQL(或把 JSqlParser 升到 5.x),删掉本类、
  25 + * 同步删掉 yaml 里的 exclude 即可回到上游原样。</p>
  26 + */
  27 +@AutoConfiguration(after = YudaoMybatisAutoConfiguration.class)
  28 +@ConditionalOnBean(MybatisPlusInterceptor.class)
  29 +public class LegacyDataPermissionAutoConfiguration {
  30 +
  31 + @Bean
  32 + public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) {
  33 + return new DataPermissionRuleFactoryImpl(rules);
  34 + }
  35 +
  36 + @Bean
  37 + public DataPermissionRuleHandler dataPermissionRuleHandler(MybatisPlusInterceptor interceptor,
  38 + DataPermissionRuleFactory ruleFactory) {
  39 + DataPermissionRuleHandler handler = new DataPermissionRuleHandler(ruleFactory);
  40 + LegacySafeDataPermissionInterceptor inner = new LegacySafeDataPermissionInterceptor(handler);
  41 + // 与上游一致:加在首个,须在分页插件之前
  42 + MyBatisUtils.addInterceptor(interceptor, inner, 0);
  43 + return handler;
  44 + }
  45 +
  46 + @Bean
  47 + public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
  48 + return new DataPermissionAnnotationAdvisor();
  49 + }
  50 +}
parking-framework/parking-legacy-common/src/main/java/com/zteits/parking/framework/legacy/mybatis/LegacySafeDataPermissionInterceptor.java 0 → 100644
  1 +package com.zteits.parking.framework.legacy.mybatis;
  2 +
  3 +import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
  4 +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
  5 +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
  6 +import org.apache.ibatis.executor.Executor;
  7 +import org.apache.ibatis.executor.statement.StatementHandler;
  8 +import org.apache.ibatis.mapping.BoundSql;
  9 +import org.apache.ibatis.mapping.MappedStatement;
  10 +import org.apache.ibatis.session.ResultHandler;
  11 +import org.apache.ibatis.session.RowBounds;
  12 +
  13 +import java.sql.Connection;
  14 +import java.sql.SQLException;
  15 +
  16 +/**
  17 + * 只对 yudao 自己的 mapper 生效的数据权限拦截器。
  18 + *
  19 + * <h2>为什么必须收窄范围</h2>
  20 + *
  21 + * <p>上游的 {@link DataPermissionInterceptor#beforeQuery} 会给<b>每一条</b> mapper 语句
  22 + * 调一次 JSqlParser 解析(规则是否命中要到 {@code DataPermissionHandler} 里才判断,
  23 + * 解析发生在判断之前)。合并工程里 yudao 侧固定的是 JSqlParser 4.9(parking BOM
  24 + * 为了 PageHelper 6.1.1 钉的版本),而存量 1311 个 mapper XML 里大量 SQL 它解析不了:</p>
  25 + *
  26 + * <pre>
  27 + * 2026-11-02 实测 500:
  28 + * /park/charger-audit/query-eqp-and-pda-report-time-list (UNION ALL)
  29 + * /park/charger-audit/query-charger-income-statistic-list (GROUP BY + 动态条件)
  30 + * /bill/vip-time-card-cust-info/page (count 包裹)
  31 + * 报错:MybatisPlusException: Failed to process ... ParseException: Encountered
  32 + * unexpected token: "AND",was expecting: &lt;EOF&gt;
  33 + * </pre>
  34 + *
  35 + * <p>存量 SQL 与 yudao 的数据权限规则({@code system_users}/{@code system_dept} 等表)
  36 + * 本来就不相交——即使解析成功,handler 也只会返回 {@code null}。所以这里在解析之前
  37 + * 直接短路:只要 mapper 不是 {@code cn.iocoder.yudao.*},就<b>原样交给下一个插件</b>
  38 + * (PageHelper 照常工作)。</p>
  39 + *
  40 + * <p>停车自己的新风格 Mapper({@code com.zteits.*.dal.mysql})同样跳过:它们的数据权限
  41 + * 由存量 {@code orgId} 口径负责,不走 yudao 的部门数据权限。</p>
  42 + */
  43 +public class LegacySafeDataPermissionInterceptor extends DataPermissionInterceptor {
  44 +
  45 + /** yudao 自家 mapper 的语句 id 前缀(语句 id = mapper 全限定名 + "." + 方法名) */
  46 + private static final String YUDAO_MAPPER_PREFIX = "cn.iocoder.yudao.";
  47 +
  48 + /** 供断言/日志使用 */
  49 + static final String YUDAO_PREFIX = YUDAO_MAPPER_PREFIX;
  50 +
  51 + public LegacySafeDataPermissionInterceptor(DataPermissionHandler dataPermissionHandler) {
  52 + super(dataPermissionHandler);
  53 + }
  54 +
  55 + @Override
  56 + public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
  57 + ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  58 + if (!isYudaoStatement(ms)) {
  59 + return;
  60 + }
  61 + super.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  62 + }
  63 +
  64 + @Override
  65 + public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
  66 + MappedStatement ms = PluginUtils.mpStatementHandler(sh).mappedStatement();
  67 + if (!isYudaoStatement(ms)) {
  68 + return;
  69 + }
  70 + super.beforePrepare(sh, connection, transactionTimeout);
  71 + }
  72 +
  73 + private static boolean isYudaoStatement(MappedStatement ms) {
  74 + String id = ms == null ? null : ms.getId();
  75 + return id != null && id.startsWith(YUDAO_MAPPER_PREFIX);
  76 + }
  77 +}
parking-module-api/src/main/java/com/zteits/clouds/api/dto/park/dto/ParkingLotDTO.java
@@ -251,6 +251,15 @@ public class ParkingLotDTO implements Serializable { @@ -251,6 +251,15 @@ public class ParkingLotDTO implements Serializable {
251 /**是否支持高位上传至第三方,太极平台 1:是,0:否*/ 251 /**是否支持高位上传至第三方,太极平台 1:是,0:否*/
252 private Integer isUploadThird; 252 private Integer isUploadThird;
253 253
  254 + /**来源平台编码:SELF = 本平台录入;其余为下级上报平台编码(dsp_platform.platform_code)*/
  255 + private String sourcePlatformCode;
  256 +
  257 + /**来源平台名称:冗余保存,便于列表直接展示*/
  258 + private String sourcePlatformName;
  259 +
  260 + /**最近一次上报时间(仅下级上报车场有值)*/
  261 + private Date reportTime;
  262 +
254 263
255 264
256 /**是否给市平台推送流水 0 不推送 1 推送*/ 265 /**是否给市平台推送流水 0 不推送 1 推送*/
@@ -870,4 +879,28 @@ public class ParkingLotDTO implements Serializable { @@ -870,4 +879,28 @@ public class ParkingLotDTO implements Serializable {
870 public void setIsUploadThird(Integer isUploadThird) { 879 public void setIsUploadThird(Integer isUploadThird) {
871 this.isUploadThird = isUploadThird; 880 this.isUploadThird = isUploadThird;
872 } 881 }
  882 +
  883 + public String getSourcePlatformCode() {
  884 + return sourcePlatformCode;
  885 + }
  886 +
  887 + public void setSourcePlatformCode(String sourcePlatformCode) {
  888 + this.sourcePlatformCode = sourcePlatformCode;
  889 + }
  890 +
  891 + public String getSourcePlatformName() {
  892 + return sourcePlatformName;
  893 + }
  894 +
  895 + public void setSourcePlatformName(String sourcePlatformName) {
  896 + this.sourcePlatformName = sourcePlatformName;
  897 + }
  898 +
  899 + public Date getReportTime() {
  900 + return reportTime;
  901 + }
  902 +
  903 + public void setReportTime(Date reportTime) {
  904 + this.reportTime = reportTime;
  905 + }
873 } 906 }
parking-module-api/src/main/java/com/zteits/clouds/api/dto/park/param/QueryParkingLotByCountryRequest.java
@@ -39,6 +39,12 @@ public class QueryParkingLotByCountryRequest extends BaseRequest{ @@ -39,6 +39,12 @@ public class QueryParkingLotByCountryRequest extends BaseRequest{
39 /**停车场类型*/ 39 /**停车场类型*/
40 private List<Integer> plTypes; 40 private List<Integer> plTypes;
41 41
  42 + /**
  43 + * 来源平台编码(台账「来源平台」筛选)
  44 + * <p>SELF = 本平台录入;其余为下级平台上报。null / 空串 = 不按来源过滤。
  45 + */
  46 + private String sourcePlatformCode;
  47 +
42 public List<Integer> getPlTypes() { 48 public List<Integer> getPlTypes() {
43 return plTypes; 49 return plTypes;
44 } 50 }
@@ -100,7 +106,15 @@ public class QueryParkingLotByCountryRequest extends BaseRequest{ @@ -100,7 +106,15 @@ public class QueryParkingLotByCountryRequest extends BaseRequest{
100 public void setPlNos(List<String> plNos) { 106 public void setPlNos(List<String> plNos) {
101 this.plNos = plNos; 107 this.plNos = plNos;
102 } 108 }
103 - 109 +
  110 + public String getSourcePlatformCode() {
  111 + return sourcePlatformCode;
  112 + }
  113 +
  114 + public void setSourcePlatformCode(String sourcePlatformCode) {
  115 + this.sourcePlatformCode = sourcePlatformCode;
  116 + }
  117 +
104 118
105 119
106 120
parking-module-order/src/main/java/com/zteits/order/config/schedule/ArrearageSMSSendConfigTask.java
@@ -8,6 +8,7 @@ import org.apache.commons.lang.StringUtils; @@ -8,6 +8,7 @@ import org.apache.commons.lang.StringUtils;
8 import org.slf4j.Logger; 8 import org.slf4j.Logger;
9 import org.slf4j.LoggerFactory; 9 import org.slf4j.LoggerFactory;
10 import org.springframework.beans.factory.annotation.Autowired; 10 import org.springframework.beans.factory.annotation.Autowired;
  11 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
11 import org.springframework.scheduling.annotation.Scheduled; 12 import org.springframework.scheduling.annotation.Scheduled;
12 import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 13 import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
13 import org.springframework.scheduling.support.CronTrigger; 14 import org.springframework.scheduling.support.CronTrigger;
@@ -30,6 +31,9 @@ import java.util.concurrent.ScheduledFuture; @@ -30,6 +31,9 @@ import java.util.concurrent.ScheduledFuture;
30 * @ 2021/3/15 atao v1.0.0 创建 31 * @ 2021/3/15 atao v1.0.0 创建
31 */ 32 */
32 @Component 33 @Component
  34 +// 存量定时任务总开关:新老服务同时在线时默认关闭,避免同一份数据被处理两遍。
  35 +// 切换步骤见 MIGRATION.md「§7.5 存量定时任务去重」。
  36 +@ConditionalOnProperty(prefix = "parking.legacy.scheduler", name = "enabled", havingValue = "true")
33 public class ArrearageSMSSendConfigTask { 37 public class ArrearageSMSSendConfigTask {
34 Logger log = LoggerFactory.getLogger(ArrearageSMSSendConfigTask.class); 38 Logger log = LoggerFactory.getLogger(ArrearageSMSSendConfigTask.class);
35 39
parking-module-order/src/main/java/com/zteits/order/config/schedule/TaiJiUploadDeviceStatusTask.java
@@ -18,6 +18,7 @@ import com.zteits.order.util.HttpClientTutorial; @@ -18,6 +18,7 @@ import com.zteits.order.util.HttpClientTutorial;
18 import org.slf4j.Logger; 18 import org.slf4j.Logger;
19 import org.slf4j.LoggerFactory; 19 import org.slf4j.LoggerFactory;
20 import org.springframework.beans.factory.annotation.Autowired; 20 import org.springframework.beans.factory.annotation.Autowired;
  21 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
21 import org.springframework.scheduling.annotation.Scheduled; 22 import org.springframework.scheduling.annotation.Scheduled;
22 import org.springframework.stereotype.Component; 23 import org.springframework.stereotype.Component;
23 24
@@ -37,6 +38,9 @@ import java.util.stream.Collectors; @@ -37,6 +38,9 @@ import java.util.stream.Collectors;
37 * @ 2022/11/12 v1.0.0 创建 38 * @ 2022/11/12 v1.0.0 创建
38 */ 39 */
39 @Component 40 @Component
  41 +// 存量定时任务总开关:新老服务同时在线时默认关闭,避免同一份数据被处理两遍。
  42 +// 切换步骤见 MIGRATION.md「§7.5 存量定时任务去重」。
  43 +@ConditionalOnProperty(prefix = "parking.legacy.scheduler", name = "enabled", havingValue = "true")
40 public class TaiJiUploadDeviceStatusTask { 44 public class TaiJiUploadDeviceStatusTask {
41 Logger logger = LoggerFactory.getLogger(TaiJiUploadDeviceStatusTask.class); 45 Logger logger = LoggerFactory.getLogger(TaiJiUploadDeviceStatusTask.class);
42 @Autowired 46 @Autowired
parking-module-order/src/main/java/com/zteits/order/config/schedule/TaiJiUploadRefreshTokenTask.java
@@ -17,6 +17,7 @@ import org.slf4j.Logger; @@ -17,6 +17,7 @@ import org.slf4j.Logger;
17 import org.slf4j.LoggerFactory; 17 import org.slf4j.LoggerFactory;
18 import org.springframework.beans.factory.annotation.Autowired; 18 import org.springframework.beans.factory.annotation.Autowired;
19 import org.springframework.beans.factory.annotation.Value; 19 import org.springframework.beans.factory.annotation.Value;
  20 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
20 import org.springframework.scheduling.annotation.Scheduled; 21 import org.springframework.scheduling.annotation.Scheduled;
21 import org.springframework.stereotype.Component; 22 import org.springframework.stereotype.Component;
22 import java.util.*; 23 import java.util.*;
@@ -34,6 +35,9 @@ import java.util.*; @@ -34,6 +35,9 @@ import java.util.*;
34 * @ 2022/11/12 v1.0.0 创建 35 * @ 2022/11/12 v1.0.0 创建
35 */ 36 */
36 @Component 37 @Component
  38 +// 存量定时任务总开关:新老服务同时在线时默认关闭,避免同一份数据被处理两遍。
  39 +// 切换步骤见 MIGRATION.md「§7.5 存量定时任务去重」。
  40 +@ConditionalOnProperty(prefix = "parking.legacy.scheduler", name = "enabled", havingValue = "true")
37 public class TaiJiUploadRefreshTokenTask { 41 public class TaiJiUploadRefreshTokenTask {
38 42
39 Logger log = LoggerFactory.getLogger(TaiJiUploadRefreshTokenTask.class); 43 Logger log = LoggerFactory.getLogger(TaiJiUploadRefreshTokenTask.class);
parking-module-park/src/main/java/com/zteits/park/biz/ParkingLotQueryServiceImpl.java
@@ -188,7 +188,8 @@ public class ParkingLotQueryServiceImpl implements ParkingLotQueryService { @@ -188,7 +188,8 @@ public class ParkingLotQueryServiceImpl implements ParkingLotQueryService {
188 PageInfo<ParkingLot> pageInfo = parkingLotDao.queryParkLotByCountryAndParkName(request.getPlNos(), 188 PageInfo<ParkingLot> pageInfo = parkingLotDao.queryParkLotByCountryAndParkName(request.getPlNos(),
189 request.getCityId(), 189 request.getCityId(),
190 countryIds, request.getParkNameOrAddress(), 190 countryIds, request.getParkNameOrAddress(),
191 - request.getBaseRequest().getPageNum(), request.getBaseRequest().getPageSize(), null,request.getPlTypes()); 191 + request.getBaseRequest().getPageNum(), request.getBaseRequest().getPageSize(), null,request.getPlTypes(),
  192 + request.getSourcePlatformCode());
192 //获取经纬度 193 //获取经纬度
193 List<String> pklNos = this.getParkNos(pageInfo.getList()); 194 List<String> pklNos = this.getParkNos(pageInfo.getList());
194 Map<String, List<ParkingLotGeo>> parkingLotGeoList = new HashMap<>(); 195 Map<String, List<ParkingLotGeo>> parkingLotGeoList = new HashMap<>();
@@ -858,7 +859,8 @@ public class ParkingLotQueryServiceImpl implements ParkingLotQueryService { @@ -858,7 +859,8 @@ public class ParkingLotQueryServiceImpl implements ParkingLotQueryService {
858 public BizResult<PageBean<ParkSouceDeviceStaDTO>> queryParkSourceDeviceSta(QueryParkSourceRequest request) { 859 public BizResult<PageBean<ParkSouceDeviceStaDTO>> queryParkSourceDeviceSta(QueryParkSourceRequest request) {
859 // 先根据停车场名称模糊匹配,如果为空,则根据地址模糊匹配 860 // 先根据停车场名称模糊匹配,如果为空,则根据地址模糊匹配
860 PageInfo<ParkingLot> pageInfo = parkingLotDao.queryParkLotByCountryAndParkName(request.getPlNos(), 861 PageInfo<ParkingLot> pageInfo = parkingLotDao.queryParkLotByCountryAndParkName(request.getPlNos(),
861 - null, null, null, request.getBaseRequest().getPageNum(), request.getBaseRequest().getPageSize(), null,null); 862 + null, null, null, request.getBaseRequest().getPageNum(), request.getBaseRequest().getPageSize(), null,null,
  863 + null);
862 List<String> plNos = this.getParkNos(pageInfo.getList()); 864 List<String> plNos = this.getParkNos(pageInfo.getList());
863 /**根据停车场查询pda和地磁等的设备的数量*/ 865 /**根据停车场查询pda和地磁等的设备的数量*/
864 /**1.查询设备*/ 866 /**1.查询设备*/
parking-module-park/src/main/java/com/zteits/park/biz/ParkingLotServiceImpl.java
@@ -104,8 +104,17 @@ public class ParkingLotServiceImpl implements ParkingLotService { @@ -104,8 +104,17 @@ public class ParkingLotServiceImpl implements ParkingLotService {
104 ParkingLotCacheUtil.setParkLotByPlNo(parkCode, parkingLotDTO); 104 ParkingLotCacheUtil.setParkLotByPlNo(parkCode, parkingLotDTO);
105 } 105 }
106 } 106 }
107 - /**添加经纬度.*/  
108 - this.saveParkGeo(request, parkingLot.getPlId(), parkCode); 107 + /**
  108 + * 添加经纬度。
  109 + *
  110 + * ⚠️ 手改(非搬迁原样):老代码无条件调用 saveParkGeo,而它内部直接 lonId.doubleValue(),
  111 + * 新增表单没填坐标时 NPE(且 parking_lot_geo.lon_id/lat_id 是 NOT NULL,插空值同样失败)。
  112 + * 与 UpdateParkingLotInfo 的判空口径对齐:两个坐标都有才写地理点,否则新车场不落地理数据
  113 + * (不影响创建,只是地图上不显示该车场)。
  114 + */
  115 + if (request.getLonId() != null && request.getLatId() != null) {
  116 + this.saveParkGeo(request, parkingLot.getPlId(), parkCode);
  117 + }
109 BeanUtils.copyProperties(parkingLot, dto); 118 BeanUtils.copyProperties(parkingLot, dto);
110 if (flag < 1) { 119 if (flag < 1) {
111 throw new BizException(ErrorType.SAVE_PARK_ERROR, ErrorType.SAVE_PARK_ERROR.getMsg()); 120 throw new BizException(ErrorType.SAVE_PARK_ERROR, ErrorType.SAVE_PARK_ERROR.getMsg());
parking-module-park/src/main/java/com/zteits/park/configuration/schedule/DealGeoInOutParkDataSchedule.java
@@ -12,6 +12,7 @@ import org.apache.commons.lang3.StringUtils; @@ -12,6 +12,7 @@ import org.apache.commons.lang3.StringUtils;
12 import org.slf4j.Logger; 12 import org.slf4j.Logger;
13 import org.slf4j.LoggerFactory; 13 import org.slf4j.LoggerFactory;
14 import org.springframework.beans.factory.annotation.Autowired; 14 import org.springframework.beans.factory.annotation.Autowired;
  15 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
15 import org.springframework.scheduling.annotation.Scheduled; 16 import org.springframework.scheduling.annotation.Scheduled;
16 import org.springframework.stereotype.Component; 17 import org.springframework.stereotype.Component;
17 18
@@ -49,6 +50,9 @@ import com.zteits.park.service.eqpinoutrecord.oper.GeoInOutParkReportInnerServic @@ -49,6 +50,9 @@ import com.zteits.park.service.eqpinoutrecord.oper.GeoInOutParkReportInnerServic
49 */ 50 */
50 51
51 @Component 52 @Component
  53 +// 存量定时任务总开关:新老服务同时在线时默认关闭,避免同一份数据被处理两遍。
  54 +// 切换步骤见 MIGRATION.md「§7.5 存量定时任务去重」。
  55 +@ConditionalOnProperty(prefix = "parking.legacy.scheduler", name = "enabled", havingValue = "true")
52 public class DealGeoInOutParkDataSchedule{ 56 public class DealGeoInOutParkDataSchedule{
53 private static final Logger logger = LoggerFactory.getLogger(DealGeoInOutParkDataSchedule.class); 57 private static final Logger logger = LoggerFactory.getLogger(DealGeoInOutParkDataSchedule.class);
54 @Autowired 58 @Autowired
parking-module-park/src/main/java/com/zteits/park/configuration/schedule/EqpOutDelayQueueMsgSchedule.java
@@ -9,6 +9,7 @@ import com.zteits.park.service.queuemsg.park.EqpOutDelayQueueExecuteService; @@ -9,6 +9,7 @@ import com.zteits.park.service.queuemsg.park.EqpOutDelayQueueExecuteService;
9 import org.slf4j.Logger; 9 import org.slf4j.Logger;
10 import org.slf4j.LoggerFactory; 10 import org.slf4j.LoggerFactory;
11 import org.springframework.beans.factory.annotation.Autowired; 11 import org.springframework.beans.factory.annotation.Autowired;
  12 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
12 import org.springframework.scheduling.annotation.Scheduled; 13 import org.springframework.scheduling.annotation.Scheduled;
13 import org.springframework.stereotype.Component; 14 import org.springframework.stereotype.Component;
14 15
@@ -26,6 +27,9 @@ import org.springframework.stereotype.Component; @@ -26,6 +27,9 @@ import org.springframework.stereotype.Component;
26 * 2019/8/14 xiejianpeng v1.0.0 创建 27 * 2019/8/14 xiejianpeng v1.0.0 创建
27 */ 28 */
28 @Component 29 @Component
  30 +// 存量定时任务总开关:新老服务同时在线时默认关闭,避免同一份数据被处理两遍。
  31 +// 切换步骤见 MIGRATION.md「§7.5 存量定时任务去重」。
  32 +@ConditionalOnProperty(prefix = "parking.legacy.scheduler", name = "enabled", havingValue = "true")
29 public class EqpOutDelayQueueMsgSchedule { 33 public class EqpOutDelayQueueMsgSchedule {
30 34
31 Logger logger = LoggerFactory.getLogger(EqpOutDelayQueueMsgSchedule.class); 35 Logger logger = LoggerFactory.getLogger(EqpOutDelayQueueMsgSchedule.class);
parking-module-park/src/main/java/com/zteits/park/configuration/schedule/TaiJiEqpTokenTask.java
@@ -21,6 +21,7 @@ import com.zteits.park.domain.TpEEmployeeCharge; @@ -21,6 +21,7 @@ import com.zteits.park.domain.TpEEmployeeCharge;
21 import org.slf4j.Logger; 21 import org.slf4j.Logger;
22 import org.slf4j.LoggerFactory; 22 import org.slf4j.LoggerFactory;
23 import org.springframework.beans.factory.annotation.Autowired; 23 import org.springframework.beans.factory.annotation.Autowired;
  24 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
24 import org.springframework.scheduling.annotation.Scheduled; 25 import org.springframework.scheduling.annotation.Scheduled;
25 import org.springframework.stereotype.Component; 26 import org.springframework.stereotype.Component;
26 27
@@ -39,6 +40,9 @@ import java.util.List; @@ -39,6 +40,9 @@ import java.util.List;
39 * @ 2022/11/12 v1.0.0 创建 40 * @ 2022/11/12 v1.0.0 创建
40 */ 41 */
41 @Component 42 @Component
  43 +// 存量定时任务总开关:新老服务同时在线时默认关闭,避免同一份数据被处理两遍。
  44 +// 切换步骤见 MIGRATION.md「§7.5 存量定时任务去重」。
  45 +@ConditionalOnProperty(prefix = "parking.legacy.scheduler", name = "enabled", havingValue = "true")
42 public class TaiJiEqpTokenTask { 46 public class TaiJiEqpTokenTask {
43 47
44 Logger logger = LoggerFactory.getLogger(TaiJiEqpTokenTask.class); 48 Logger logger = LoggerFactory.getLogger(TaiJiEqpTokenTask.class);
parking-module-park/src/main/java/com/zteits/park/dao/ParkingLotDao.java
@@ -207,7 +207,8 @@ public interface ParkingLotDao { @@ -207,7 +207,8 @@ public interface ParkingLotDao {
207 * @return 2017年7月7日 xiejp 207 * @return 2017年7月7日 xiejp
208 */ 208 */
209 PageInfo<ParkingLot> queryParkLotByCountryAndParkName(List<String> plNos, Long cityId, List<Long> countryIds, 209 PageInfo<ParkingLot> queryParkLotByCountryAndParkName(List<String> plNos, Long cityId, List<Long> countryIds,
210 - String parkNameOrAddress, Integer pageNum, Integer pageSize, Integer state, List<Integer> plTypes); 210 + String parkNameOrAddress, Integer pageNum, Integer pageSize, Integer state, List<Integer> plTypes,
  211 + String sourcePlatformCode);
211 212
212 /** 213 /**
213 * 通过城市ID和区县ID获取停车场信息 - 分页 214 * 通过城市ID和区县ID获取停车场信息 - 分页
parking-module-park/src/main/java/com/zteits/park/dao/impl/ParkingLotDaoImpl.java
@@ -200,12 +200,12 @@ public class ParkingLotDaoImpl implements ParkingLotDao { @@ -200,12 +200,12 @@ public class ParkingLotDaoImpl implements ParkingLotDao {
200 @Override 200 @Override
201 public PageInfo<ParkingLot> queryParkLotByCountryAndParkName(List<String> plNos, Long cityId, List<Long> countryIds, 201 public PageInfo<ParkingLot> queryParkLotByCountryAndParkName(List<String> plNos, Long cityId, List<Long> countryIds,
202 String parkNameOrAddress, 202 String parkNameOrAddress,
203 - Integer pageNum, Integer pageSize, Integer state, List<Integer> plTypes) { 203 + Integer pageNum, Integer pageSize, Integer state, List<Integer> plTypes, String sourcePlatformCode) {
204 // 1.设置分页信息 204 // 1.设置分页信息
205 PageHelper.startPage(pageNum, pageSize); 205 PageHelper.startPage(pageNum, pageSize);
206 //2.查询数据 206 //2.查询数据
207 List<ParkingLot> list = parkingLotSMapper.queryParkLotByCountryAndParkName(plNos, cityId, countryIds, 207 List<ParkingLot> list = parkingLotSMapper.queryParkLotByCountryAndParkName(plNos, cityId, countryIds,
208 - parkNameOrAddress, state, plTypes); 208 + parkNameOrAddress, state, plTypes, sourcePlatformCode);
209 return new PageInfo<ParkingLot>(list); 209 return new PageInfo<ParkingLot>(list);
210 } 210 }
211 211
@@ -213,7 +213,7 @@ public class ParkingLotDaoImpl implements ParkingLotDao { @@ -213,7 +213,7 @@ public class ParkingLotDaoImpl implements ParkingLotDao {
213 public List<ParkingLot> queryParkLotByCountryAndParkNameOfNoPage(Long cityId, List<Long> countryIds, 213 public List<ParkingLot> queryParkLotByCountryAndParkNameOfNoPage(Long cityId, List<Long> countryIds,
214 String parkNameOrAddress, Integer state) { 214 String parkNameOrAddress, Integer state) {
215 List<ParkingLot> list = parkingLotSMapper.queryParkLotByCountryAndParkName(null, cityId, countryIds, 215 List<ParkingLot> list = parkingLotSMapper.queryParkLotByCountryAndParkName(null, cityId, countryIds,
216 - parkNameOrAddress, state, null); 216 + parkNameOrAddress, state, null, null);
217 return list; 217 return list;
218 } 218 }
219 219
parking-module-park/src/main/java/com/zteits/park/domain/ParkingLot.java
@@ -144,6 +144,15 @@ public class ParkingLot { @@ -144,6 +144,15 @@ public class ParkingLot {
144 /**是否支持高位上传至第三方,太极平台 1:是,0:否*/ 144 /**是否支持高位上传至第三方,太极平台 1:是,0:否*/
145 private Integer isUploadThird; 145 private Integer isUploadThird;
146 146
  147 + /**来源平台编码:SELF = 本平台录入;其余为下级上报平台编码(dsp_platform.platform_code)*/
  148 + private String sourcePlatformCode;
  149 +
  150 + /**来源平台名称:冗余保存,便于列表直接展示*/
  151 + private String sourcePlatformName;
  152 +
  153 + /**最近一次上报时间(仅下级上报车场有值)*/
  154 + private Date reportTime;
  155 +
147 /**获取停车场主键*/ 156 /**获取停车场主键*/
148 public Long getPlId() { 157 public Long getPlId() {
149 return plId; 158 return plId;
@@ -613,4 +622,34 @@ public class ParkingLot { @@ -613,4 +622,34 @@ public class ParkingLot {
613 public void setIsUploadThird(Integer isUploadThird) { 622 public void setIsUploadThird(Integer isUploadThird) {
614 this.isUploadThird = isUploadThird; 623 this.isUploadThird = isUploadThird;
615 } 624 }
  625 +
  626 + /**获取来源平台编码*/
  627 + public String getSourcePlatformCode() {
  628 + return sourcePlatformCode;
  629 + }
  630 +
  631 + /**设置来源平台编码*/
  632 + public void setSourcePlatformCode(String sourcePlatformCode) {
  633 + this.sourcePlatformCode = sourcePlatformCode;
  634 + }
  635 +
  636 + /**获取来源平台名称*/
  637 + public String getSourcePlatformName() {
  638 + return sourcePlatformName;
  639 + }
  640 +
  641 + /**设置来源平台名称*/
  642 + public void setSourcePlatformName(String sourcePlatformName) {
  643 + this.sourcePlatformName = sourcePlatformName;
  644 + }
  645 +
  646 + /**获取最近上报时间*/
  647 + public Date getReportTime() {
  648 + return reportTime;
  649 + }
  650 +
  651 + /**设置最近上报时间*/
  652 + public void setReportTime(Date reportTime) {
  653 + this.reportTime = reportTime;
  654 + }
616 } 655 }
617 \ No newline at end of file 656 \ No newline at end of file
parking-module-park/src/main/java/com/zteits/park/job/datahub/doc/停车场基本信息.md 0 → 100644
  1 +```
  2 +create table hub_parking_info
  3 +(
  4 + id bigint auto_increment comment '主键'
  5 + primary key,
  6 + park_code varchar(128) not null comment '车场编码(车场唯一标识)',
  7 + organization_no varchar(32) not null comment '运营企业组织机构代码(一般和统一社会信用代码一致)',
  8 + organization_name varchar(64) not null comment '运营企业公司名称',
  9 + legal_representative varchar(64) not null comment '运营企业法人',
  10 + social_credit_code varchar(255) not null comment '统一社会信用代码',
  11 + company_address varchar(255) not null comment '运营企业地址',
  12 + park_nature varchar(10) not null comment '停车场信息分类 备案:1 ,登记 2, 道路:3',
  13 + park_region_id1 varchar(10) default '9' not null comment '车场所属区,参照字典,选取id列填入 9 房山',
  14 + park_region_id2 varchar(10) not null comment '车场所属街道,参照字典,选取id列填入',
  15 + park_type varchar(4) not null comment '停车场类型:公共停车场(G)、专用停车场(Z)、P+R停车场(R)、道路停车位(D)',
  16 + park_class varchar(4) not null comment '停车场分类:路外公共停车场(10)、公共建筑配建停车场(11)、立交桥下停车场(12)、单位大院停车场(14)、居住小区停车场(15)、P+R停车场(13)、道路白实线车位(30)、道路白虚线车位(31)',
  17 + park_classification varchar(10) not null comment '如果停车场分类选择的是公共建筑配建停车场,则此字段必填:医院(01)、大专院校(02)、交通枢纽(03)、商场(04)、写字楼(05)、机关企事业单位(06)、其他公共建筑停车场(07)',
  18 + space_loc varchar(4) not null comment '停车场所属环路位置:二环(含二环)内(1)、二三环间(含三环)(2)、三四环间(含四环)(3)、四五环间(含五环)(4)、五六环间(含六环)(5)、六环外(6)',
  19 + is_shared varchar(4) default '2' not null comment '是否共享 共享:1 非共享:2',
  20 + shared_time varchar(64) null comment '共享停放时段(不共享则不用传递) (HH:mm:ss-HH:mm:ss)',
  21 + shared_park_place_num int default 0 not null comment '共享车位泊位数(不共享则不用传递)',
  22 + park_stage varchar(4) default '01' not null comment '停车场阶段 当前运行:01 车场注销:11',
  23 + free_num int default 0 not null comment '无障碍泊位数 若无该类型设施,该数据项应为0',
  24 + large_vehicle_num int default 0 not null comment '大型车位泊位数 若无该类型设施,该数据项应为0',
  25 + light_vehicle_num int default 0 not null comment '小型车位泊位数 若无该类型设施,该数据项应为0',
  26 + park_place_num01 int default 0 not null comment '地上平面车位数 若无该类型设施,该数据项应为0',
  27 + park_place_num02 int default 0 not null comment '地下平面车位数 若无该类型设施,该数据项应为0',
  28 + park_place_num03 int default 0 not null comment '地上机械式停车设施车位数 若无该类型设施,该数据项应为0',
  29 + park_place_num04 int default 0 not null comment '地下机械式停车设施车位数 若无该类型设施,该数据项应为0',
  30 + park_place_num05 int default 0 not null comment '自走式立体停车设施车位数 若无该类型设施,该数据项应为0',
  31 + park_place_num06 int default 0 not null comment '人防车位数 若无该类型设施,该数据项应为0',
  32 + property_type varchar(4) not null comment '产权性质 自有:01 租赁:02',
  33 + property_owner varchar(4) not null comment '产权方名称 产权性质为(租赁)则必填项',
  34 + property_date datetime null comment '停车场租赁有效期(yyyy-MM-dd HH:mm:ss) 产权性质为(租赁)则必填项',
  35 + gross_area varchar(20) null comment '停车场建筑面积',
  36 + park_location_class varchar(4) null comment '停放设施用地 临时用地:01 永久用地:02',
  37 + worker_num int default 0 not null comment '从业人员数量',
  38 + park_charger varchar(10) not null comment '停车场负责人',
  39 + mobile_phone varchar(32) not null comment '停车场负责人手机号',
  40 + tel_phone varchar(32) not null comment '停车场电话',
  41 + day_time_start time not null comment '计费白天开始时间(HH:mm:ss)',
  42 + day_time_end time not null comment '计费白天结束时间(HH:mm:ss)',
  43 + night_time_start time not null comment '计费夜晚开始时间(HH:mm:ss)',
  44 + night_time_end time not null comment '计费夜晚结束时间(HH:mm:ss)',
  45 + charging_standard_temp_small_car_day_first_hour float not null comment '白天首小时小型车收费金额(元)',
  46 + day_small_car_chronon_mins_first_hour int not null comment '白天首小时小型车计费周期(分钟)',
  47 + charging_standard_temp_small_car_day float not null comment '小型车首小时后白天收费金额(元)',
  48 + day_small_car_chronon_mins int not null comment '小型车首小时后白天金额计费周期(分钟)',
  49 + charging_standard_temp_small_car_night float not null comment '小型车夜间收费金额(元)',
  50 + night_small_car_chronon_mins int not null comment '小型车夜间计费周期(分钟)',
  51 + charging_standard_temp_big_car_day_first_hour float not null comment '白天首小时大型车收费金额(元)',
  52 + day_big_car_chronon_mins_first_hour int not null comment ' 白天首小时大型车的计费周期(分钟)',
  53 + charging_standard_temp_big_car_day float not null comment ' 大型车首小时后白天金额(元)',
  54 + day_big_car_chronon_mins int not null comment ' 大型车首小时后白天计费周期(分钟)',
  55 + charging_standard_temp_big_car_night float not null comment ' 大型车夜间金额(元)',
  56 + night_big_car_chronon_mins int not null comment ' 大型车夜间计费周期(分钟)',
  57 + charging_standard_lease_small_car int null comment ' 每月长租小型车金额(元),若无此项收费,可不传 ',
  58 + charging_standard_lease_big_car int null comment ' 每月长租大型车金额(元),若无此项收费,可不传 ',
  59 + tsaa_fee float null comment ' 计次小型车费用,若无此项收费,可不传 ',
  60 + tbaa_fee float null comment ' 计次大型车费用,若无此项收费,可不传 ',
  61 + park_use_date date null comment ' 车场投入使用时间(yyyy-MM-dd)',
  62 + licence varchar(32) not null comment ' 备案号,新备案车场可先传临时值后期更新 ',
  63 + record_effect_date date not null comment ' 备案有效期开始时间(yyyy-MM-dd),新备案车场可先传临时值后期更新 ',
  64 + record_invalid_date date not null comment ' 备案有效期结束时间(yyyy-MM-dd),新备案车场可先传临时值后期更新 ',
  65 + sent_date_time datetime not null comment ' 数据发送时间(yyyy-MM-dd HH:mm:ss)',
  66 + charge_fa_picture varchar(255) not null comment ' 收费标价牌正面图片链接地址 ',
  67 + charge_op_picture varchar(255) null comment ' 收费标价牌反面图片链接地址 ',
  68 + open_time varchar(64) not null comment ' 停车场开放时段,例如:24 小时或者 6:30-21:30',
  69 + data_status int default 1 null comment '数据状态:0-失效,1-生效',
  70 + create_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '创建时间',
  71 + create_op varchar(20) null comment '创建人',
  72 + modify_time datetime null comment '更新时间',
  73 + modify_op varchar(32) null comment '更新人',
  74 + remark varchar(128) null comment '备注'
  75 +)
  76 + comment '数据归集-停车场基本信息';
  77 +
  78 +-- 随机生成的两条数据,float类型字段包含小数
  79 +INSERT INTO hub_parking_info (id, park_code, organization_no, organization_name, legal_representative, social_credit_code, company_address, park_nature, park_region_id1, park_region_id2, park_type, park_class, park_classification, space_loc, is_shared, shared_time, shared_park_place_num, park_stage, free_num, large_vehicle_num, light_vehicle_num, park_place_num01, park_place_num02, park_place_num03, park_place_num04, park_place_num05, park_place_num06, property_type, property_owner, property_date, gross_area, park_location_class, worker_num, park_charger, mobile_phone, tel_phone, day_time_start, day_time_end, night_time_start, night_time_end, charging_standard_temp_small_car_day_first_hour, day_small_car_chronon_mins_first_hour, charging_standard_temp_small_car_day, day_small_car_chronon_mins, charging_standard_temp_small_car_night, night_small_car_chronon_mins, charging_standard_temp_big_car_day_first_hour, day_big_car_chronon_mins_first_hour, charging_standard_temp_big_car_day, day_big_car_chronon_mins, charging_standard_temp_big_car_night, night_big_car_chronon_mins, charging_standard_lease_small_car, charging_standard_lease_big_car, tsaa_fee, tbaa_fee, park_use_date, licence, record_effect_date, record_invalid_date, sent_date_time, charge_fa_picture, charge_op_picture, open_time, data_status, create_time, create_op, modify_time, modify_op, remark) VALUES (1, 'PC2023001', '911101001234567890', '北京智慧停车有限公司', '张三', '911101001234567890', '北京市朝阳区建国路88号', '1', '9', '12', 'G', '10', '01', '3', '2', NULL, 0, '01', 5, 20, 100, 60, 40, 0, 0, 0, 0, '01', '自有产权', '2030-12-31 00:00:00', '5000平方米', '02', 15, '李四', '13800138000', '010-12345678', '07:00:00', '19:00:00', '19:00:00', '07:00:00', 10.50, 60, 5.25, 30, 3.75, 30, 15.80, 60, 8.30, 30, 5.60, 30, 300, 500, 20.40, 30.90, '2020-01-01', 'LC2023001', '2023-01-01', '2030-01-01', '2023-06-01 10:00:00', 'http://example.com/charge1.jpg', 'http://example.com/charge2.jpg', '24小时', 1, '2023-06-01 10:00:00', '系统管理员', NULL, NULL, '测试数据1');
  80 +
  81 +INSERT INTO hub_parking_info (id, park_code, organization_no, organization_name, legal_representative, social_credit_code, company_address, park_nature, park_region_id1, park_region_id2, park_type, park_class, park_classification, space_loc, is_shared, shared_time, shared_park_place_num, park_stage, free_num, large_vehicle_num, light_vehicle_num, park_place_num01, park_place_num02, park_place_num03, park_place_num04, park_place_num05, park_place_num06, property_type, property_owner, property_date, gross_area, park_location_class, worker_num, park_charger, mobile_phone, tel_phone, day_time_start, day_time_end, night_time_start, night_time_end, charging_standard_temp_small_car_day_first_hour, day_small_car_chronon_mins_first_hour, charging_standard_temp_small_car_day, day_small_car_chronon_mins, charging_standard_temp_small_car_night, night_small_car_chronon_mins, charging_standard_temp_big_car_day_first_hour, day_big_car_chronon_mins_first_hour, charging_standard_temp_big_car_day, day_big_car_chronon_mins, charging_standard_temp_big_car_night, night_big_car_chronon_mins, charging_standard_lease_small_car, charging_standard_lease_big_car, tsaa_fee, tbaa_fee, park_use_date, licence, record_effect_date, record_invalid_date, sent_date_time, charge_fa_picture, charge_op_picture, open_time, data_status, create_time, create_op, modify_time, modify_op, remark) VALUES (2, 'PC2023002', '911101000987654321', '北京安心停车管理有限公司', '王五', '911101000987654321', '北京市海淀区中关村大街1号', '2', '9', '15', 'Z', '11', '04', '4', '1', '18:00:00-22:00:00', 30, '01', 2, 10, 80, 50, 30, 0, 0, 0, 0, '02', '北京市国有资产经营管理有限公司', '2025-12-31 00:00:00', '3000平方米', '02', 8, '赵六', '13900139000', '010-87654321', '08:00:00', '20:00:00', '20:00:00', '08:00:00', 8.75, 60, 4.50, 30, 2.25, 30, 12.60, 60, 6.80, 30, 3.90, 30, 200, 400, 15.30, 25.70, '2021-05-01', 'LC2023002', '2023-05-01', '2025-05-01', '2023-06-01 11:00:00', 'http://example.com/charge3.jpg', 'http://example.com/charge4.jpg', '6:30-21:30', 1, '2023-06-01 11:00:00', '系统管理员', NULL, NULL, '测试数据2');
  82 +
parking-module-park/src/main/java/com/zteits/park/mapper/smapper/ParkingLotSMapper.java
@@ -122,7 +122,8 @@ public interface ParkingLotSMapper { @@ -122,7 +122,8 @@ public interface ParkingLotSMapper {
122 */ 122 */
123 List<ParkingLot> queryParkLotByCountryAndParkName(@Param("plNos") List<String> plNos, @Param("cityId") Long cityId, 123 List<ParkingLot> queryParkLotByCountryAndParkName(@Param("plNos") List<String> plNos, @Param("cityId") Long cityId,
124 @Param("countryIds") List<Long> countryIds, @Param("parkNameOrAddress") String parkNameOrAddress, 124 @Param("countryIds") List<Long> countryIds, @Param("parkNameOrAddress") String parkNameOrAddress,
125 - @Param("state") Integer state,@Param("plTypes")List<Integer> plTypes); 125 + @Param("state") Integer state,@Param("plTypes")List<Integer> plTypes,
  126 + @Param("sourcePlatformCode") String sourcePlatformCode);
126 127
127 /** 128 /**
128 * 查询停车场及停车场经纬度信息.<br/> 129 * 查询停车场及停车场经纬度信息.<br/>
parking-module-park/src/main/java/com/zteits/parking/module/park/controller/admin/indexrealtime/IndexRealTimeController.java
@@ -20,6 +20,7 @@ import com.zteits.clouds.api.service.park.ParkingLotQueryService; @@ -20,6 +20,7 @@ import com.zteits.clouds.api.service.park.ParkingLotQueryService;
20 import com.zteits.clouds.api.service.park.parkrealtime.ParkRealTimeQueryService; 20 import com.zteits.clouds.api.service.park.parkrealtime.ParkRealTimeQueryService;
21 import com.zteits.parking.framework.common.pojo.CommonResult; 21 import com.zteits.parking.framework.common.pojo.CommonResult;
22 import com.zteits.parking.framework.common.pojo.PageResult; 22 import com.zteits.parking.framework.common.pojo.PageResult;
  23 +import com.zteits.parking.module.park.service.dashboard.LegacyRealtimeBerthFiller;
23 import io.swagger.v3.oas.annotations.Operation; 24 import io.swagger.v3.oas.annotations.Operation;
24 import io.swagger.v3.oas.annotations.tags.Tag; 25 import io.swagger.v3.oas.annotations.tags.Tag;
25 import lombok.extern.slf4j.Slf4j; 26 import lombok.extern.slf4j.Slf4j;
@@ -111,6 +112,9 @@ public class IndexRealTimeController { @@ -111,6 +112,9 @@ public class IndexRealTimeController {
111 @Resource 112 @Resource
112 private SessionCommUtil sessionCommUtil; 113 private SessionCommUtil sessionCommUtil;
113 114
  115 + @Resource
  116 + private LegacyRealtimeBerthFiller legacyRealtimeBerthFiller;
  117 +
114 /** 118 /**
115 * 根据停车场编号查询收费员人数。 119 * 根据停车场编号查询收费员人数。
116 * 120 *
@@ -201,6 +205,10 @@ public class IndexRealTimeController { @@ -201,6 +205,10 @@ public class IndexRealTimeController {
201 total = 0; 205 total = 0;
202 } else { 206 } else {
203 total = request.getPlNos().size(); 207 total = request.getPlNos().size();
  208 + // 新栈不跑存量实时快照任务(PLRealTimeStatusCacheJob),Redis 无值时:
  209 + // 泊位取 parking_lot_berth_temp_info、今日进出取 order_park,
  210 + // 口径与指挥中心 / 综合监测一致
  211 + legacyRealtimeBerthFiller.fillMissingRealtime(data);
204 } 212 }
205 213
206 log.info("end查询车场概况返回={}", result); 214 log.info("end查询车场概况返回={}", result);
@@ -243,6 +251,8 @@ public class IndexRealTimeController { @@ -243,6 +251,8 @@ public class IndexRealTimeController {
243 List<Object[]> contentList = new ArrayList<>(); 251 List<Object[]> contentList = new ArrayList<>();
244 if (result != null && !CollectionUtils.isEmpty(result.getData())) { 252 if (result != null && !CollectionUtils.isEmpty(result.getData())) {
245 List<ParkinglotBasicFactsDTO> list = result.getData(); 253 List<ParkinglotBasicFactsDTO> list = result.getData();
  254 + // 与查询端点同一兜底,导出的总泊位/占用率才和页面一致
  255 + legacyRealtimeBerthFiller.fillMissingRealtime(list);
246 for (ParkinglotBasicFactsDTO e : list) { 256 for (ParkinglotBasicFactsDTO e : list) {
247 if (e != null) { 257 if (e != null) {
248 Object[] obj = new Object[title.length]; 258 Object[] obj = new Object[title.length];
parking-module-park/src/main/java/com/zteits/parking/module/park/controller/admin/parklotmanage/ParkLotManageController.java
@@ -43,11 +43,13 @@ import com.zteits.clouds.api.dto.pay.param.SysFileGetBySourceIdAndTypeRequest; @@ -43,11 +43,13 @@ import com.zteits.clouds.api.dto.pay.param.SysFileGetBySourceIdAndTypeRequest;
43 import com.zteits.clouds.api.dto.pay.param.SysFileSaveBySourceIdAndTypeRequest; 43 import com.zteits.clouds.api.dto.pay.param.SysFileSaveBySourceIdAndTypeRequest;
44 import com.zteits.clouds.api.dto.sys.AreaTreeDTO; 44 import com.zteits.clouds.api.dto.sys.AreaTreeDTO;
45 import com.zteits.clouds.api.dto.sys.SysOrgDTO; 45 import com.zteits.clouds.api.dto.sys.SysOrgDTO;
  46 +import com.zteits.clouds.api.dto.sys.TbAreaDTO;
46 import com.zteits.clouds.api.dto.sys.param.QueryAreaBlockByOpIdAndOrgIdRequest; 47 import com.zteits.clouds.api.dto.sys.param.QueryAreaBlockByOpIdAndOrgIdRequest;
47 import com.zteits.clouds.api.dto.sys.param.ParkLotPicDeleteReq; 48 import com.zteits.clouds.api.dto.sys.param.ParkLotPicDeleteReq;
48 import com.zteits.clouds.api.dto.sys.param.ParkLotPicQueryByPlNoReq; 49 import com.zteits.clouds.api.dto.sys.param.ParkLotPicQueryByPlNoReq;
49 import com.zteits.clouds.api.dto.sys.param.ParkLotPicUploadReq; 50 import com.zteits.clouds.api.dto.sys.param.ParkLotPicUploadReq;
50 import com.zteits.clouds.api.dto.sys.param.SysOrgByIdQueryRequest; 51 import com.zteits.clouds.api.dto.sys.param.SysOrgByIdQueryRequest;
  52 +import com.zteits.clouds.api.dto.sys.param.TbAreaQueryRequest;
51 import com.zteits.clouds.api.service.park.ParkingLotQueryService; 53 import com.zteits.clouds.api.service.park.ParkingLotQueryService;
52 import com.zteits.clouds.api.service.park.ParkingLotService; 54 import com.zteits.clouds.api.service.park.ParkingLotService;
53 import com.zteits.clouds.api.service.park.berths.oper.ParkFreeBerthsOperService; 55 import com.zteits.clouds.api.service.park.berths.oper.ParkFreeBerthsOperService;
@@ -241,6 +243,26 @@ public class ParkLotManageController { @@ -241,6 +243,26 @@ public class ParkLotManageController {
241 return success(result); 243 return success(result);
242 } 244 }
243 245
  246 + /**
  247 + * 按父级 id 查询区域下一级(车场表单的省 / 市 / 区县三级联动)。
  248 + *
  249 + * <p>数据源是存量 `tb_area`(只取 `data_state = 1`):`id = 0` 返回省份(31 条),
  250 + * 其余按 `parent_id` 下沉一级。车场 `pl_province_id / pl_city_id / pl_country_id`
  251 + * 存的就是这里的 `id`(不是行政区划码)。权限沿用本模块的 `:query`,
  252 + * **不新增权限记录、不授予任何角色**。</p>
  253 + */
  254 + @PostMapping("/query-lower-area")
  255 + @Operation(summary = "查询区域下一级(车场所属区域联动)")
  256 + @PreAuthorize("@ss.hasPermission('park:park-lot-manage:query')")
  257 + public CommonResult<List<TbAreaDTO>> queryLowerArea(
  258 + @RequestBody(required = false) TbAreaQueryRequest request) {
  259 + TbAreaQueryRequest areaRequest = new TbAreaQueryRequest();
  260 + // 根节点 0(全国)的下一级就是省份;不传按根处理
  261 + areaRequest.setId(request == null || request.getId() == null ? 0L : request.getId());
  262 + BizResult<List<TbAreaDTO>> result = tbAreaService.queryNextLowerLeverById(areaRequest);
  263 + return success(BizResultAdapter.getDataOrThrow(result));
  264 + }
  265 +
244 private List<String> getParkLotNosByCurrUser(String sysCode) { 266 private List<String> getParkLotNosByCurrUser(String sysCode) {
245 log.info("根据登录人权限获取停车场列表"); 267 log.info("根据登录人权限获取停车场列表");
246 UserInfo userInfo = sessionCommUtil.getUserInfo(); 268 UserInfo userInfo = sessionCommUtil.getUserInfo();
@@ -324,7 +346,10 @@ public class ParkLotManageController { @@ -324,7 +346,10 @@ public class ParkLotManageController {
324 throws Exception { 346 throws Exception {
325 log.info("调用添加停车场信息dubbo服务入参:request={}", request); 347 log.info("调用添加停车场信息dubbo服务入参:request={}", request);
326 UserInfo userInfo = sessionCommUtil.getUserInfo(); 348 UserInfo userInfo = sessionCommUtil.getUserInfo();
327 - if (userInfo != null && null != userInfo.getOrgId()) { 349 + // ⚠️ 手改(非搬迁原样):老代码无条件用登录人组织覆盖 orgId/orgName,新增表单没得选。
  350 + // 现在新增/修改都支持选组织:请求带了 orgId 就保留(orgName 由前端同步提交),
  351 + // 只有没带时才回填登录人组织 —— 与 updateParkingLotInfo 的判空口径一致。
  352 + if (userInfo != null && null != userInfo.getOrgId() && null == request.getOrgId()) {
328 request.setOrgId(userInfo.getOrgId()); 353 request.setOrgId(userInfo.getOrgId());
329 SysOrgByIdQueryRequest orgRequest = new SysOrgByIdQueryRequest(); 354 SysOrgByIdQueryRequest orgRequest = new SysOrgByIdQueryRequest();
330 orgRequest.setId(userInfo.getOrgId()); 355 orgRequest.setId(userInfo.getOrgId());
@@ -348,7 +373,10 @@ public class ParkLotManageController { @@ -348,7 +373,10 @@ public class ParkLotManageController {
348 throws Exception { 373 throws Exception {
349 log.info("调用修改停车场信息dubbo服务入参:request={}", request); 374 log.info("调用修改停车场信息dubbo服务入参:request={}", request);
350 UserInfo userInfo = sessionCommUtil.getUserInfo(); 375 UserInfo userInfo = sessionCommUtil.getUserInfo();
351 - if (userInfo != null && null != userInfo.getOrgId()) { 376 + // ⚠️ 手改(非搬迁原样):老代码无条件用登录人组织覆盖 orgId/orgName。本机 admin 的组织是
  377 + // 10001(赤峰市住建),而车场都挂在 10003(赤峰中智),照搬会导致「每编辑一次车场就被改挂到
  378 + // 10001」。改为:编辑场景前端会带上车场当前 orgId,此时保留;只有新增(orgId 为空)才回填。
  379 + if (userInfo != null && null != userInfo.getOrgId() && null == request.getOrgId()) {
352 request.setOrgId(userInfo.getOrgId()); 380 request.setOrgId(userInfo.getOrgId());
353 SysOrgByIdQueryRequest orgRequest = new SysOrgByIdQueryRequest(); 381 SysOrgByIdQueryRequest orgRequest = new SysOrgByIdQueryRequest();
354 orgRequest.setId(userInfo.getOrgId()); 382 orgRequest.setId(userInfo.getOrgId());
parking-module-park/src/main/resources/mybatis/smapper/ParkingLotSMapper.xml
@@ -247,7 +247,9 @@ @@ -247,7 +247,9 @@
247 pl_out_no as plOutNo, 247 pl_out_no as plOutNo,
248 pl_rate as plRate, service_tel as serviceTel, pl_picture_path as plPicturePath, 248 pl_rate as plRate, service_tel as serviceTel, pl_picture_path as plPicturePath,
249 busi_circle as busiCircle,org_id as orgId,org_name as orgName,area_type as areaType, 249 busi_circle as busiCircle,org_id as orgId,org_name as orgName,area_type as areaType,
250 - area_card_limit_num as areaCardLimitNum 250 + area_card_limit_num as areaCardLimitNum,
  251 + source_platform_code as sourcePlatformCode, source_platform_name as sourcePlatformName,
  252 + report_time as reportTime
251 FROM parking_lot 253 FROM parking_lot
252 WHERE 254 WHERE
253 data_status = 1 255 data_status = 1
@@ -280,6 +282,9 @@ @@ -280,6 +282,9 @@
280 AND (pl_name like concat('%',#{parkNameOrAddress},'%') OR pl_address like 282 AND (pl_name like concat('%',#{parkNameOrAddress},'%') OR pl_address like
281 concat('%',#{parkNameOrAddress},'%')) 283 concat('%',#{parkNameOrAddress},'%'))
282 </if> 284 </if>
  285 + <if test="sourcePlatformCode != null and sourcePlatformCode != ''">
  286 + AND source_platform_code = #{sourcePlatformCode}
  287 + </if>
283 GROUP BY pl_id 288 GROUP BY pl_id
284 </select> 289 </select>
285 290
parking-module-pay/src/main/resources/汉仪书宋一简.ttf 0 → 100644
No preview for this file type
parking-module-pay/src/main/resources/长城新艺体.ttf 0 → 100755
No preview for this file type
parking-module-system-legacy/src/main/java/com/zteits/parking/module/system/dal/dataobject/user/UserDO.java
1 package com.zteits.parking.module.system.dal.dataobject.user; 1 package com.zteits.parking.module.system.dal.dataobject.user;
2 2
  3 +import com.baomidou.mybatisplus.annotation.TableField;
3 import com.baomidou.mybatisplus.annotation.TableId; 4 import com.baomidou.mybatisplus.annotation.TableId;
4 import com.baomidou.mybatisplus.annotation.TableLogic; 5 import com.baomidou.mybatisplus.annotation.TableLogic;
5 import com.baomidou.mybatisplus.annotation.TableName; 6 import com.baomidou.mybatisplus.annotation.TableName;
@@ -52,7 +53,12 @@ public class UserDO extends BaseDO { @@ -52,7 +53,12 @@ public class UserDO extends BaseDO {
52 53
53 /** 54 /**
54 * 所属组织编号 55 * 所属组织编号
  56 + *
  57 + * ⚠️ 列名是 {@code dept_id}:表沿用的是 yudao 的 {@code system_users},
  58 + * 组织 id 统一后这列存的就是 {@code system_org.id}。属性名叫 {@code orgId}
  59 + * 是因为存量业务和前端契约都用 org,别被列名带偏。
55 */ 60 */
  61 + @TableField("dept_id")
56 private Long orgId; 62 private Long orgId;
57 63
58 /** 64 /**
@@ -92,7 +98,11 @@ public class UserDO extends BaseDO { @@ -92,7 +98,11 @@ public class UserDO extends BaseDO {
92 98
93 /** 99 /**
94 * 删除标记:0 未删除,1 已删除 100 * 删除标记:0 未删除,1 已删除
  101 + *
  102 + * ⚠️ {@code system_users} 的列名是 {@code deleted}(bit(1)),不是存量表的
  103 + * {@code del_flag};不显式指定列名会让每次查询都拼出 {@code del_flag=0} 而报错。
95 */ 104 */
  105 + @TableField("deleted")
96 @TableLogic(value = "0", delval = "1") 106 @TableLogic(value = "0", delval = "1")
97 private Integer delFlag; 107 private Integer delFlag;
98 108
parking-module-system-legacy/src/main/java/com/zteits/parking/module/system/service/org/OrgServiceImpl.java
@@ -4,16 +4,20 @@ import cn.hutool.core.collection.CollUtil; @@ -4,16 +4,20 @@ import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.util.ObjUtil; 4 import cn.hutool.core.util.ObjUtil;
5 import com.zteits.parking.framework.common.util.object.BeanUtils; 5 import com.zteits.parking.framework.common.util.object.BeanUtils;
6 import com.zteits.parking.framework.mybatis.core.query.LambdaQueryWrapperX; 6 import com.zteits.parking.framework.mybatis.core.query.LambdaQueryWrapperX;
  7 +import com.zteits.parking.framework.security.core.util.SecurityFrameworkUtils;
7 import com.zteits.parking.module.system.controller.admin.org.vo.OrgListReqVO; 8 import com.zteits.parking.module.system.controller.admin.org.vo.OrgListReqVO;
8 import com.zteits.parking.module.system.controller.admin.org.vo.OrgSaveReqVO; 9 import com.zteits.parking.module.system.controller.admin.org.vo.OrgSaveReqVO;
9 import com.zteits.parking.module.system.dal.dataobject.org.OrgDO; 10 import com.zteits.parking.module.system.dal.dataobject.org.OrgDO;
10 import com.zteits.parking.module.system.dal.dataobject.user.UserDO; 11 import com.zteits.parking.module.system.dal.dataobject.user.UserDO;
  12 +import com.zteits.parking.module.system.dal.mysql.org.OrgDeptMirrorMapper;
11 import com.zteits.parking.module.system.dal.mysql.org.OrgMapper; 13 import com.zteits.parking.module.system.dal.mysql.org.OrgMapper;
12 import com.zteits.parking.module.system.dal.mysql.user.UserMapper; 14 import com.zteits.parking.module.system.dal.mysql.user.UserMapper;
13 import lombok.extern.slf4j.Slf4j; 15 import lombok.extern.slf4j.Slf4j;
14 import org.springframework.stereotype.Service; 16 import org.springframework.stereotype.Service;
  17 +import org.springframework.transaction.annotation.Transactional;
15 18
16 import jakarta.annotation.Resource; 19 import jakarta.annotation.Resource;
  20 +import java.time.LocalDateTime;
17 import java.util.Collection; 21 import java.util.Collection;
18 import java.util.Collections; 22 import java.util.Collections;
19 import java.util.List; 23 import java.util.List;
@@ -44,27 +48,41 @@ public class OrgServiceImpl implements OrgService { @@ -44,27 +48,41 @@ public class OrgServiceImpl implements OrgService {
44 @Resource 48 @Resource
45 private OrgMapper orgMapper; 49 private OrgMapper orgMapper;
46 @Resource 50 @Resource
  51 + private OrgDeptMirrorMapper orgDeptMirrorMapper;
  52 + @Resource
47 private UserMapper userMapper; 53 private UserMapper userMapper;
48 54
49 @Override 55 @Override
  56 + @Transactional(rollbackFor = Exception.class)
50 public Long createOrg(OrgSaveReqVO reqVO) { 57 public Long createOrg(OrgSaveReqVO reqVO) {
51 validateParentOrg(reqVO.getParentId(), null); 58 validateParentOrg(reqVO.getParentId(), null);
52 validateOrgNameUnique(reqVO.getParentId(), reqVO.getName(), null); 59 validateOrgNameUnique(reqVO.getParentId(), reqVO.getName(), null);
53 OrgDO org = BeanUtils.toBean(reqVO, OrgDO.class); 60 OrgDO org = BeanUtils.toBean(reqVO, OrgDO.class);
  61 + // parking 的 BaseDO 没有注册 MetaObjectHandler(全局只有 yudao 那个,
  62 + // 它只认 cn.iocoder 的 BaseDO),INSERT 会把 create_time 显式写成 NULL
  63 + // 触发 system_org 的非空约束。这里显式补审计字段。
  64 + fillInsertAudit(org);
54 orgMapper.insert(org); 65 orgMapper.insert(org);
  66 + // system_dept 是 yudao 用户模块的兼容镜像,必须同事务维护
  67 + syncDeptMirror(org);
55 return org.getId(); 68 return org.getId();
56 } 69 }
57 70
58 @Override 71 @Override
  72 + @Transactional(rollbackFor = Exception.class)
59 public void updateOrg(OrgSaveReqVO reqVO) { 73 public void updateOrg(OrgSaveReqVO reqVO) {
60 validateOrgExists(reqVO.getId()); 74 validateOrgExists(reqVO.getId());
61 // 不能把自己设成父组织,否则组织树里会出现环 75 // 不能把自己设成父组织,否则组织树里会出现环
62 validateParentOrg(reqVO.getParentId(), reqVO.getId()); 76 validateParentOrg(reqVO.getParentId(), reqVO.getId());
63 validateOrgNameUnique(reqVO.getParentId(), reqVO.getName(), reqVO.getId()); 77 validateOrgNameUnique(reqVO.getParentId(), reqVO.getName(), reqVO.getId());
64 - orgMapper.updateById(BeanUtils.toBean(reqVO, OrgDO.class)); 78 + OrgDO org = BeanUtils.toBean(reqVO, OrgDO.class);
  79 + fillUpdateAudit(org);
  80 + orgMapper.updateById(org);
  81 + syncDeptMirror(org);
65 } 82 }
66 83
67 @Override 84 @Override
  85 + @Transactional(rollbackFor = Exception.class)
68 public void deleteOrg(Long id) { 86 public void deleteOrg(Long id) {
69 validateOrgExists(id); 87 validateOrgExists(id);
70 if (orgMapper.selectCount(OrgDO::getParentId, id) > 0) { 88 if (orgMapper.selectCount(OrgDO::getParentId, id) > 0) {
@@ -74,9 +92,11 @@ public class OrgServiceImpl implements OrgService { @@ -74,9 +92,11 @@ public class OrgServiceImpl implements OrgService {
74 throw exception(ORG_EXISTS_USER); 92 throw exception(ORG_EXISTS_USER);
75 } 93 }
76 orgMapper.deleteById(id); 94 orgMapper.deleteById(id);
  95 + orgDeptMirrorMapper.softDelete(id);
77 } 96 }
78 97
79 @Override 98 @Override
  99 + @Transactional(rollbackFor = Exception.class)
80 public void deleteOrgList(Collection<Long> ids) { 100 public void deleteOrgList(Collection<Long> ids) {
81 if (CollUtil.isEmpty(ids)) { 101 if (CollUtil.isEmpty(ids)) {
82 return; 102 return;
@@ -114,6 +134,58 @@ public class OrgServiceImpl implements OrgService { @@ -114,6 +134,58 @@ public class OrgServiceImpl implements OrgService {
114 return org; 134 return org;
115 } 135 }
116 136
  137 + // ========== 审计字段 ==========
  138 +
  139 + /**
  140 + * parking 的 {@code BaseDO} 目前没有生效的 {@code MetaObjectHandler}:
  141 + * 容器里唯一的填充器是 yudao 的,只处理 {@code cn.iocoder...BaseDO}。
  142 + * 不显式填时间,MyBatis-Plus 会把 {@code create_time} 以 NULL 写库。
  143 + */
  144 + private void fillInsertAudit(OrgDO org) {
  145 + LocalDateTime now = LocalDateTime.now();
  146 + org.setCreateTime(now);
  147 + org.setUpdateTime(now);
  148 + org.setCreateBy(currentOperator());
  149 + org.setUpdateBy(currentOperator());
  150 + }
  151 +
  152 + private void fillUpdateAudit(OrgDO org) {
  153 + org.setUpdateTime(LocalDateTime.now());
  154 + org.setUpdateBy(currentOperator());
  155 + }
  156 +
  157 + private String currentOperator() {
  158 + Long userId = SecurityFrameworkUtils.getLoginUserId();
  159 + return userId == null ? "system" : userId.toString();
  160 + }
  161 +
  162 + // ========== system_dept 镜像 ==========
  163 +
  164 + /**
  165 + * 把组织写穿透到 {@code system_dept}。
  166 + *
  167 + * <p>yudao 的用户模块只认 {@code system_dept}:用户表单的组织下拉、列表按组织过滤、
  168 + * 保存用户时的部门校验都走它。两边 id 必须一致,否则新组织下挂不了用户。</p>
  169 + *
  170 + * <p>{@code leader} 是姓名字符串,{@code system_dept} 存的是 {@code leader_user_id},
  171 + * 没有稳定映射,镜像里留空;{@code phone} 列只有 11 位,超长就置空(不截断,避免假号码)。</p>
  172 + */
  173 + private void syncDeptMirror(OrgDO org) {
  174 + if (org == null || org.getId() == null) {
  175 + return;
  176 + }
  177 + String mobile = org.getMobile();
  178 + String phone = mobile != null && mobile.length() <= 11 ? mobile : null;
  179 + orgDeptMirrorMapper.upsert(
  180 + org.getId(),
  181 + org.getName(),
  182 + org.getParentId() == null ? ROOT_PARENT_ID : org.getParentId(),
  183 + org.getSort() == null ? 0 : org.getSort(),
  184 + phone,
  185 + org.getEmail(),
  186 + org.getStatus());
  187 + }
  188 +
117 // ========== 校验 ========== 189 // ========== 校验 ==========
118 190
119 private void validateParentOrg(Long parentId, Long selfId) { 191 private void validateParentOrg(Long parentId, Long selfId) {
@@ -23,7 +23,10 @@ @@ -23,7 +23,10 @@
23 <!-- <module>yudao-module-mall</module>--> 23 <!-- <module>yudao-module-mall</module>-->
24 <!-- <module>yudao-module-crm</module>--> 24 <!-- <module>yudao-module-crm</module>-->
25 <!-- <module>yudao-module-erp</module>--> 25 <!-- <module>yudao-module-erp</module>-->
26 - <module>yudao-module-iot</module> 26 + <!-- IoT 物联网:本项目只用视频接入,取流走 WVP-PRO + ZLMediaKit,不经 IoT;
  27 + 存量地磁/道闸沿用 tp_p_eqp,不迁 IoT。如需重新开启,取消本行与
  28 + yudao-server/pom.xml 中 yudao-module-iot-biz 依赖的注释。 -->
  29 +<!-- <module>yudao-module-iot</module>-->
27 <!-- <module>yudao-module-mes</module>--> 30 <!-- <module>yudao-module-mes</module>-->
28 <!-- <module>yudao-module-wms</module>--> 31 <!-- <module>yudao-module-wms</module>-->
29 <!-- <module>yudao-module-hrm</module>--> 32 <!-- <module>yudao-module-hrm</module>-->
@@ -52,6 +55,7 @@ @@ -52,6 +55,7 @@
52 <module>parking-module-business</module> 55 <module>parking-module-business</module>
53 <module>parking-module-datacollect</module> 56 <module>parking-module-datacollect</module>
54 <module>parking-module-system-legacy</module> 57 <module>parking-module-system-legacy</module>
  58 + <module>parking-module-video</module>
55 </modules> 59 </modules>
56 60
57 <name>${project.artifactId}</name> 61 <name>${project.artifactId}</name>
yudao-dependencies/pom.xml
@@ -29,7 +29,11 @@ @@ -29,7 +29,11 @@
29 <mybatis-plus-join.version>1.5.9</mybatis-plus-join.version> 29 <mybatis-plus-join.version>1.5.9</mybatis-plus-join.version>
30 <dynamic-datasource.version>4.6.0</dynamic-datasource.version> 30 <dynamic-datasource.version>4.6.0</dynamic-datasource.version>
31 <easy-trans.version>3.1.8</easy-trans.version> 31 <easy-trans.version>3.1.8</easy-trans.version>
32 - <redisson.version>4.8.0</redisson.version> 32 + <!-- parking-cloud-yudao:4.8.0 的 redisson-spring-data-35 在 RESP2 下
  33 + xReadGroup 返回 Map,适配层强转 List 抛 ClassCastException
  34 + (IoT Redis Stream 消息总线每秒报错)。回退到 3.52.0:
  35 + 与 parking 老工程一致、starter 同样带 redisson-spring-data-35。 -->
  36 + <redisson.version>3.52.0</redisson.version>
33 <dm8.jdbc.version>8.1.3.140</dm8.jdbc.version> 37 <dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
34 <kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version> 38 <kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version>
35 <opengauss.jdbc.version>7.0.0-og</opengauss.jdbc.version> 39 <opengauss.jdbc.version>7.0.0-og</opengauss.jdbc.version>
yudao-server/pom.xml
@@ -32,11 +32,13 @@ @@ -32,11 +32,13 @@
32 <artifactId>yudao-module-infra</artifactId> 32 <artifactId>yudao-module-infra</artifactId>
33 <version>${revision}</version> 33 <version>${revision}</version>
34 </dependency> 34 </dependency>
35 - <dependency>  
36 - <groupId>cn.iocoder.boot</groupId>  
37 - <artifactId>yudao-module-iot-biz</artifactId>  
38 - <version>${revision}</version>  
39 - </dependency> 35 + <!-- IoT 物联网:已关闭。视频取流走 WVP-PRO + ZLMediaKit,不经 IoT;
  36 + 存量地磁/道闸沿用 tp_p_eqp。重新开启时同时放开根 pom 的 yudao-module-iot 模块 -->
  37 +<!-- <dependency>-->
  38 +<!-- <groupId>cn.iocoder.boot</groupId>-->
  39 +<!-- <artifactId>yudao-module-iot-biz</artifactId>-->
  40 +<!-- <version>${revision}</version>-->
  41 +<!-- </dependency>-->
40 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-api</artifactId></dependency> 42 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-api</artifactId></dependency>
41 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-system-legacy</artifactId></dependency> 43 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-system-legacy</artifactId></dependency>
42 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-park</artifactId></dependency> 44 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-park</artifactId></dependency>
@@ -46,6 +48,7 @@ @@ -46,6 +48,7 @@
46 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-charge</artifactId></dependency> 48 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-charge</artifactId></dependency>
47 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-business</artifactId></dependency> 49 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-business</artifactId></dependency>
48 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-datacollect</artifactId></dependency> 50 <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-datacollect</artifactId></dependency>
  51 + <dependency><groupId>com.zteits.boot</groupId><artifactId>parking-module-video</artifactId></dependency>
49 52
50 <!-- 会员中心。默认注释,保证编译速度 --> 53 <!-- 会员中心。默认注释,保证编译速度 -->
51 <!-- <dependency>--> 54 <!-- <dependency>-->
@@ -130,13 +133,6 @@ @@ -130,13 +133,6 @@
130 <!-- <version>${revision}</version>--> 133 <!-- <version>${revision}</version>-->
131 <!-- </dependency>--> 134 <!-- </dependency>-->
132 135
133 - <!-- IoT 物联网相关模块。默认注释,保证编译速度 -->  
134 -<!-- <dependency>-->  
135 -<!-- <groupId>cn.iocoder.boot</groupId>-->  
136 -<!-- <artifactId>yudao-module-iot-biz</artifactId>-->  
137 -<!-- <version>${revision}</version>-->  
138 -<!-- </dependency>-->  
139 -  
140 <!-- MES 制造执行系统相关模块。默认注释,保证编译速度 --> 136 <!-- MES 制造执行系统相关模块。默认注释,保证编译速度 -->
141 <!-- <dependency>--> 137 <!-- <dependency>-->
142 <!-- <groupId>cn.iocoder.boot</groupId>--> 138 <!-- <groupId>cn.iocoder.boot</groupId>-->
yudao-server/src/main/java/cn/iocoder/yudao/server/YudaoServerApplication.java
1 package cn.iocoder.yudao.server; 1 package cn.iocoder.yudao.server;
2 2
3 -import cn.iocoder.yudao.module.iot.framework.storage.config.IotStorageInitRunner; 3 +// IoT 物联网模块已关闭(见根 pom 与 yudao-server/pom.xml),该 Runner 也随之不参与编译。
  4 +// 重新开启 IoT 时需要恢复本 import 与下方 excludeFilters 中的过滤项。
  5 +// import cn.iocoder.yudao.module.iot.framework.storage.config.IotStorageInitRunner;
4 import com.clouds.common.mybatis.LegacyMapperBeanNameGenerator; 6 import com.clouds.common.mybatis.LegacyMapperBeanNameGenerator;
5 import com.zteits.charge.config.ChargeConfig; 7 import com.zteits.charge.config.ChargeConfig;
6 import org.mybatis.spring.annotation.MapperScan; 8 import org.mybatis.spring.annotation.MapperScan;
@@ -65,10 +67,9 @@ import org.springframework.scheduling.annotation.EnableScheduling; @@ -65,10 +67,9 @@ import org.springframework.scheduling.annotation.EnableScheduling;
65 }, 67 },
66 excludeFilters = { 68 excludeFilters = {
67 // 存量充电 starter 里的重复扫描配置 69 // 存量充电 starter 里的重复扫描配置
68 - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = ChargeConfig.class),  
69 - // 本地没有 TDengine/IoTDB:该 Runner 校验失败会 System.exit(1)。  
70 - // 本机联调不需要 IoT 时序存储;接视频/设备数据时再部署 TDengine 并移除本排除。  
71 - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = IotStorageInitRunner.class) 70 + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = ChargeConfig.class)
  71 + // IoT 已关闭,原先排除 IotStorageInitRunner 的过滤器随之去掉:
  72 + // @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = IotStorageInitRunner.class)
72 }) 73 })
73 @MapperScan(basePackages = { 74 @MapperScan(basePackages = {
74 "com.zteits.park.mapper", 75 "com.zteits.park.mapper",
@@ -78,7 +79,10 @@ import org.springframework.scheduling.annotation.EnableScheduling; @@ -78,7 +79,10 @@ import org.springframework.scheduling.annotation.EnableScheduling;
78 "com.zteits.charge.mapper", 79 "com.zteits.charge.mapper",
79 "com.zteits.business.mapper", 80 "com.zteits.business.mapper",
80 "com.zteits.sys.mapper", 81 "com.zteits.sys.mapper",
81 - "com.zteits.parking.module.system.dal.mysql" 82 + "com.zteits.parking.module.system.dal.mysql",
  83 + "com.zteits.parking.module.park.dal.mysql",
  84 + // 下级平台数据接入(DSP):yudao 风格 Mapper,XML 在 mybatis/mapper/dsp/
  85 + "com.zteits.parking.module.park.dsp.dal.mysql"
82 }, nameGenerator = LegacyMapperBeanNameGenerator.class) 86 }, nameGenerator = LegacyMapperBeanNameGenerator.class)
83 public class YudaoServerApplication { 87 public class YudaoServerApplication {
84 88
yudao-server/src/main/resources/application-local.yaml
@@ -255,10 +255,37 @@ wx: @@ -255,10 +255,37 @@ wx:
255 # 停车存量兼容配置(与老工程 application-local.yaml 同值) 255 # 停车存量兼容配置(与老工程 application-local.yaml 同值)
256 parking: 256 parking:
257 legacy: 257 legacy:
258 - # 本地 admin 的 system_users.dept_id=103(芋道演示部门树遗留),故同时把 103 指向老组织 10003;  
259 - # 1:10003 对应 system_org『总公司』,两处都是本地测试值,生产按真实组织配置覆盖。  
260 - org-id-mapping: ${LEGACY_ORG_ID_MAPPING:103:10003,1:10003} # 新 orgId:老 orgId 258 + # 组织 id 空间已统一:system_org 直接沿用老 sys_org 的 id,system_users.dept_id
  259 + # 也指向真实组织(scripts/migrate-org-ids.sql),因此不再需要换算表。
  260 + # 留空 = 恒等映射;只有过渡期数据未迁移时才通过环境变量临时配置。
  261 + org-id-mapping: ${LEGACY_ORG_ID_MAPPING:} # 新 orgId:老 orgId
261 op-id-mapping: ${LEGACY_OP_ID_MAPPING:1:10001} # 新 操作员id:老 操作员id 262 op-id-mapping: ${LEGACY_OP_ID_MAPPING:1:10001} # 新 操作员id:老 操作员id
  263 + # 文件存储:本机既没有阿里云 OSS 配置(FILE_OSS_CONFIG/ENABLE_TYPE 未配置),
  264 + # 也没有可达的 FastDFS tracker(fdfs_client.conf 是内网 10.117.61.52),存量上传必失败。
  265 + # local = 落本机磁盘,由 /admin-api/park/local-file/** 读回;留空 = 保持存量行为。
  266 + oss:
  267 + mode: ${LEGACY_OSS_MODE:local}
  268 + local:
  269 + dir: ${LEGACY_OSS_LOCAL_DIR:${user.home}/.parking-cloud-yudao/oss}
  270 + url-prefix: ${LEGACY_OSS_LOCAL_URL_PREFIX:http://localhost:48083/admin-api/park/local-file}
  271 +
  272 +# 视频接入(WVP-PRO / GB-T 28181)本地覆盖:
  273 +# 本机没有摄像机和 ZLMediaKit,只保证服务能起来、接口能调通
  274 +sip:
  275 + # 本地没有可被设备访问的网卡,展示地址用回环
  276 + show-ip: ${WVP_SIP_SHOW_IP:127.0.0.1}
  277 + # 与老服务(48082)、新服务(48083)都不冲突;5060 是国标默认端口
  278 + port: ${WVP_SIP_PORT:5060}
  279 + # 本地不接收公共密码注册,必须手工添加设备
  280 + password: ${WVP_SIP_PASSWORD:}
  281 +
  282 +media:
  283 + # 本机未部署 ZLMediaKit,先指向回环;装上之后改 ZLM_IP / ZLM_SECRET 即可
  284 + ip: ${ZLM_IP:127.0.0.1}
  285 + http-port: ${ZLM_HTTP_PORT:8080}
  286 + secret: ${ZLM_SECRET:}
  287 + # 没有 zlm 可对接时不要让它去改 zlm 的 config.ini
  288 + auto-config: ${ZLM_AUTO_CONFIG:false}
262 289
263 # 芋道配置项,设置当前项目所有自定义的配置 290 # 芋道配置项,设置当前项目所有自定义的配置
264 yudao: 291 yudao:
yudao-server/src/main/resources/application.yaml
@@ -27,8 +27,10 @@ spring: @@ -27,8 +27,10 @@ spring:
27 27
28 # 存量业务参数(37 个 @Value 占位符,来自老工程各模块的 application*.properties) 28 # 存量业务参数(37 个 @Value 占位符,来自老工程各模块的 application*.properties)
29 # 不引入会启动失败 "Could not resolve placeholder" 29 # 不引入会启动失败 "Could not resolve placeholder"
  30 + # wvp-video.yaml:视频接入(WVP-PRO)的 sip.* / media.* / user-settings.* 配置。
  31 + # 这些 key 名是 WVP 上游代码里写死的,只能单独放一个文件,import 不进来就启动失败。
30 config: 32 config:
31 - import: classpath:legacy-params.properties 33 + import: classpath:legacy-params.properties,classpath:wvp-video.yaml
32 34
33 # ========== 邮件(存量 park/pay 的 MailService,发票/异常告警用)========== 35 # ========== 邮件(存量 park/pay 的 MailService,发票/异常告警用)==========
34 # 老工程把 spring.mail.* 写在各模块的 application.properties 里(未随迁)。 36 # 老工程把 spring.mail.* 写在各模块的 application.properties 里(未随迁)。
@@ -386,6 +388,7 @@ yudao: @@ -386,6 +388,7 @@ yudao:
386 enable: true 388 enable: true
387 ignore-urls: 389 ignore-urls:
388 - /jmreport/* # 积木报表,无法携带租户编号 390 - /jmreport/* # 积木报表,无法携带租户编号
  391 + - /open-api/** # 下级平台数据接入,签名鉴权,不带租户编号
389 ignore-visit-urls: 392 ignore-visit-urls:
390 - /admin-api/system/user/profile/** 393 - /admin-api/system/user/profile/**
391 - /admin-api/system/auth/** 394 - /admin-api/system/auth/**
@@ -421,11 +424,13 @@ yudao: @@ -421,11 +424,13 @@ yudao:
421 kd100: 424 kd100:
422 key: pLXUGAwK5305 425 key: pLXUGAwK5305
423 customer: E77DF18BE109F454A5CD319E44BF5177 426 customer: E77DF18BE109F454A5CD319E44BF5177
424 - iot:  
425 - message-bus:  
426 - type: redis # 消息总线的类型  
427 - storage:  
428 - type: tdengine # 时序存储的类型:tdengine、iotdb,需要配置同名的 spring.datasource.dynamic.datasource 数据源 427 + # IoT 物联网模块已关闭(见根 pom 与 yudao-server/pom.xml),以下配置暂不生效。
  428 + # 重新开启 IoT 时取消注释,并部署 TDengine / IoTDB 供时序存储使用。
  429 +# iot:
  430 +# message-bus:
  431 +# type: redis # 消息总线的类型
  432 +# storage:
  433 +# type: tdengine # 时序存储的类型:tdengine、iotdb,需要配置同名的 spring.datasource.dynamic.datasource 数据源
429 434
430 debug: false 435 debug: false
431 436
@@ -435,3 +440,19 @@ parking: @@ -435,3 +440,19 @@ parking:
435 # 默认空 ⇒ fail-closed(一律回 token 不正确),本地/生产用环境变量配。 440 # 默认空 ⇒ fail-closed(一律回 token 不正确),本地/生产用环境变量配。
436 refresh-cache: 441 refresh-cache:
437 token: ${REFRESH_CACHE_TOKEN:} 442 token: ${REFRESH_CACHE_TOKEN:}
  443 + # 存量 @Scheduled 定时任务总开关(11 个类,见 MIGRATION.md §7.5)。
  444 + # 默认 false:新老服务同时在线时,只有老服务跑定时任务,避免同一份数据被处理两遍
  445 + # (地磁进出场、出场延迟队列、太极/西城上报、欠费催缴短信)。
  446 + # 正式切换(停老服务、只保留新服务)时置 true。
  447 + legacy:
  448 + scheduler:
  449 + enabled: ${PARKING_LEGACY_SCHEDULER_ENABLED:false}
  450 + # 视频接入(WVP-PRO / GB-T 28181)总开关。false 时整个 parking-module-video 不装配:
  451 + # 不占 SIP 端口、不扫 wvp_* 表、不注册 /api/** 安全链。具体参数见 wvp-video.yaml
  452 + video:
  453 + enabled: ${PARKING_VIDEO_ENABLED:true}
  454 + # 下级平台数据接入(/open-api/dsp/v1/**)
  455 + dsp:
  456 + # 只有前面确实有自己控制的反向代理时才置 true,否则 X-Forwarded-For 可被伪造,
  457 + # 会让 IP 白名单与按 IP 封禁失效
  458 + trust-forwarded-for: ${PARKING_DSP_TRUST_FORWARDED_FOR:false}