diff --git a/MIGRATION.md b/MIGRATION.md index 5addd26..b690c8c 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -16,7 +16,7 @@ - [x] 新建前端仓库(官方最新) - [x] 设计稿落盘 - [x] 数据库拷贝 + 上游表准备(`parking_cloud_yudao`,重名旧表改 `pcbak_system_*`) -- [x] 上游模块裁剪(只保留 system / infra / iot) +- [x] 上游模块裁剪(只保留 system / infra / iot;2026-11-04 进一步关闭 iot,见 §8.0) - [x] parking 业务模块搬迁(10 个模块编译通过) - [x] 系统兼容桥(`LoginUserBridge` 打通 yudao 登录态 → 存量 `UserInfo`) - [x] 前端业务页面搬迁(park/order/finance/report/ops/... 已进 `src/views`,构建通过) @@ -25,10 +25,38 @@ - [x] **与老环境隔离:新服务 Redis 用 db1(老服务 db0)+ `scripts/warm-cache.sh` 缓存预热**(2026-11-02) - [x] **菜单 path 归一化:子页改相对路径,侧边栏不再出现 `/park/park/park-lot` 404**(2026-11-02,见 §7.4) - [x] **业务页全局样式回流:令牌/版式类映射到芋道主题(`src/styles/parking-pages.scss`)**(2026-11-02,见 §7.3 #5) -- [ ] 菜单/权限收口(462 个 `@PreAuthorize` 权限串尚无菜单记录;admin 不受影响) -- [ ] 调度去重(48082 与 48083 同时跑存量 `@Scheduled`) -- [ ] 组织 id 空间打通(`system_org` ↔ 老 `sys_org`) -- [ ] 视频接入(WVP-PRO + ZLMediaKit)与 IoT 时序存储(TDengine) +- [x] **存量定时任务去重:`parking.legacy.scheduler.enabled` 总开关,默认 false,6 个活跃任务全部停跑**(2026-11-02,见 §7.5) +- [x] **指挥中心:按「赤峰市智慧停车数据中心」大屏重做(通栏 7 KPI;左:供给结构/联网接入/重点车场/时长/收入排行;中:全市 12 旗县区地图(有实时数据的区按占用率着色 + 四角标注卡引线)+ 24h 车流;右:紧凑进出场列表/用户与支付(图标 + 环形图)/运行预警(图标);1280–4K 分辨率自适应),旧版大屏保留入口**(2026-11-02 版式,见 §7.6) +- [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) +- [x] **菜单/权限收口:672 个权限串全覆盖,3 个角色模板 + 3 个联调账号,接口矩阵验证通过**(2026-11-02,见 `scripts/README-permissions.md`) +- [x] **组织 id 空间统一:`sys_org` 真实 id 迁入 `system_org`,用户指向真实组织,去掉 `org-id-mapping` 换算**(2026-11-02,见 §7.7 与 `scripts/README-org.md`) +- [x] **首页监管总览:`POST /park/dashboard/overview` 单接口聚合 8 个区块,前端按 `01-dashboard.html` 重做首页**(2026-11-02,见 §7.8) +- [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`) +- [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) +- [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。 +- [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) +- [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) +- [x] **大屏配色统一**:7 张在役大屏全部改用 `src/styles/bigscreen.scss` 的 `--bs-*` 色板与同一套「深空蓝 + 蓝色辉光 + 点阵」底纹,区县中心四区颜色对齐色板(2026-11-05,见设计方案 §0.1.1) +- [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) +- [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) +- [x] **指挥中心「各区联网接入进度」12 个旗县区一屏看全**:原来「区名 / 已接入数」两行一列,1080p 下只能露 5~6 行;改成单行 + 自动分栏(列宽 ≥120px 就两栏),并给同心环补一块中心圆盘垫住数字,1728 以下单栏滚动时底部渐隐;同页还修了 1366×768 整页多出的 50px 滚动(矮屏定高漏减 `--app-footer-height`,把页脚顶出视口)(2026-11-06,见 §7.13) +- [x] **综合监测「区县占用率」13 行一屏**:两行排压成「场数并入首行 + 进度条单独一行」,行高 47 → 29px,1080p 左栏 403=403 全部可见(原来只能看到 8 个区县,红山区等 5 个要滚动);≤960 高度加底部渐隐(2026-11-06,见 §7.17) +- [x] **一张图 / 专题分析的演示夹具补齐 11 个旗县区**:夹具原来只有 3 个区 14 个车场,一张图 9 个区县标「未接入」、专题分析下拉 11 个区县「(暂无车场)」,与读库的指挥中心 / 综合监测对不上;按演示库原样补 `districtSeeds` 67 条(名称 / 坐标 / 泊位 / 空余 / 今日进出)→ 全市 81 车场,12 个区县全部有数;视频监控设备树顺带补齐为 12 个区县 / 112 台(2026-11-06,见 §7.18) +- [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) +- [x] **三张高德地图换蓝色底图**:一张图 / 专题分析 / 视频监控原来用 `amap://styles/darkblue`,实测地块取色 **0,0,1(纯黑)**,跟深空蓝大屏对不上;改成高德内置的 `amap://styles/blue`(同一套矢量样式里唯一的蓝底,路网 / 水系仍可见),三处同时改,页面高度不变(2026-11-06,见 §7.20) +- [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) +- [x] **实时停车流量监控区分「停更」与「归零」**:库内最后一条比现在旧 5 分钟以上时,「近 5 分钟入场/出场」显示「—」+「数据停更」芯片,不再拿 0 冒充「确实没车」;纯函数 `staleMinutesOf` 走 `tests/verify-flow-monitor.mjs` TDD 门禁(2026-11-06,见 §7.16) +- [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) +- [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) +- [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) +- [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) +- [x] **视频接入(WVP-PRO)**:WVP-PRO 3.0.0 已合并为 `parking-module-video`,随 `yudao-server` 启动,yudao 登录态即视频平台登录态(见 §8.1);前端实时预览 / 通道树 / 回放 / 云台 / 抓图 / 片段下载已移植到 `/video/monitor`;ZLMediaKit 仍需单独部署 +- [x] **视频监控二级分组改读 WVP 业务分组 + 新增「通道分组」页(100532)**:左侧树二级节点由演示夹具的车场名改成通道 `gbBusinessGroupId` 翻出的分组名(区县仍按 `civilCode`,没挂分组的归「未分组」),并补上分组维护页(新建 215 / 子节点 216 / 改名 / 删除 / 挂通道);点播仍走设备→通道接口,不受分组影响(2026-11-09,见 §8.2.2) +- [x] **一张图加「重点车场」「实时流量」入口 + 去掉「未定位资源」「全屏打开」**:`PAGE_NAV` 5 → 7 项,新两项和专题分析 / 视频监控 并列、同样在弹框里打开(权限沿用 `park:dashboard:query` / `park:flow-monitor:query`,**不新增权限、不授权任何角色**);资源清单里的「未定位资源」按钮 + 弹框 + 样式整块删除;弹框右下角的「全屏打开」按钮删除,只留关闭(2026-11-09,见 §7.30) +- [x] **前端删掉上游「文档地址」广告条**:350 个 `.vue` 里 389 处 ``(「【功能权限】文档地址:https://doc.iocoder.cn/…」)连同组件一起删除,登录页推广区、用户菜单「文档」项、901 错误弹窗的教程链接同步去掉;新增 `tests/verify-no-doc-links.mjs` 守着,`src/` 里再出现 `` | 迁过来的页面统一 `` | -| 4 | `value.trim is not a function` | 存量接口时间字段是**毫秒数字**,`formatDateTime` 只处理字符串 | `format.ts` 用 dayjs 格式化数字,0/负数/null 显示 `-` | +| 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 显示 `-`;停车记录、异常记录、高位异常、违法记录、白名单、语音告警等时间列都改走它 | | 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-*` 的整套覆盖(那会把整站改成政务蓝) | | 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+ 个页面用法不变,无需逐页改 | | 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, '/') // /park + /park/park- 验证:重新登录后展开「停车设施」点「车场列表」,`location.pathname` = `/park/park-lot`,标题「芋道管理系统 - 车场列表」,0 条 console error。 +### 7.5 存量定时任务去重(`@Scheduled` 总开关) + +老工程把不少业务逻辑挂在 Spring `@Scheduled` 上,搬进新工程后**默认会跟着启动**。 +新旧服务同时在线时,同一份数据会被处理两遍(最危险的是往太极/西城等外部平台重复上报)。 + +代码里共 11 个 `@Scheduled`,实际会跑的只有 **6 个**(其余 `@Component` 或 `@Scheduled` +已被注释,等于停用): + +| 模块 | 类 | 频率 | 作用 | 状态 | +|---|---|---|---|---| +| park | `DealGeoInOutParkDataSchedule` | 1s | 处理地磁进出场接口日志(`TP_P_INTERFACE_LOG`) | 活跃 → 已加开关 | +| park | `EqpOutDelayQueueMsgSchedule` | 10s | 地磁出场延迟队列(Redisson `RBlockingQueue`) | 活跃 → 已加开关 | +| park | `TaiJiEqpTokenTask` | 30min | 西城太极设备 token 刷新 | 活跃 → 已加开关 | +| park | `GetExceptionMsgSchedule` | — | 异常消息拉取 | 整类注释,未注册 | +| order | `ArrearageSMSSendConfigTask` | 60s + 动态 cron | 刷新欠费催缴短信配置并按 cron 发短信 | 活跃 → 已加开关 | +| order | `TaiJiUploadDeviceStatusTask` | 20min | 设备状态上报太极 | 活跃 → 已加开关 | +| order | `TaiJiUploadRefreshTokenTask` | 30min | 太极 token 刷新 | 活跃 → 已加开关 | +| order | `TaiJiUploadInOutParkRecordTask` | 5min | 进出场记录上报 | `@Component` 已注释 | +| order | `TaiJiUploadXJCParkFlowTask` | 14min | 巡检车流水上报 | `@Component` 已注释(改用 xxl-job) | +| order | `UserPayOutDelayQueueMsgSchedule` | — | 用户出场延迟队列 | 类在,`@Scheduled` 已注释 | +| order | `OrderDelayQueueMsgOpServiceImpl` | — | 订单延迟队列 | 类在,`@Scheduled` 已注释 | + +**开关**(6 个活跃类统一加 `@ConditionalOnProperty`): + +```java +@ConditionalOnProperty(prefix = "parking.legacy.scheduler", name = "enabled", havingValue = "true") +``` + +```yaml +parking: + legacy: + scheduler: + enabled: ${PARKING_LEGACY_SCHEDULER_ENABLED:false} # 默认关 +``` + +**默认 false 的理由**:本地/并行期 48082 仍在跑这些任务,新服务只做读写接口; +再加上新服务连的是 `parking_cloud_yudao` 副本库、Redis 用 db1,跑起来既无收益又可能 +把副本库的数据当生产数据往外部平台推。注意有些任务自身还有业务开关 +(`BJXC_TAIJI_UPLOAD` 为空、`DISTRIBUTED_TASK_PROP` 未配置时会提前 return), +但那是**数据层兜底**,不能替代结构性开关——副本库配置一变就会“复活”。 + +**验证(2026-11-02)**:重启前日志里 `延迟队列开始取数据` 每 10s 一条(累计 828 条)、 +`欠费催缴短信…刷新` 每 60s 一条;加开关并重启(16:26:57 启动)后 70s 内上述关键字 +**0 条**,其余 6 个任务也无任何调度线程日志,接口功能正常。 + +**正式切换步骤**(新服务独占生产时): + +1. 停老服务(48082),确认没有其它实例还在连原库; +2. 把新服务数据源指到生产库,并把 Redis 切回生产者用的同一个 db + (当前 local 用 db1 是为了并行期隔离;**只改 db 而不开开关,延迟队列仍然消费不到消息**); +3. 设 `PARKING_LEGACY_SCHEDULER_ENABLED=true` 并重启; +4. 按上表逐个确认日志:地磁延迟队列、太极 token/设备状态、欠费催缴刷新; +5. 多实例部署时**只在一个实例开**(这些任务没有统一分布式锁; + `DealGeoInOutParkDataSchedule` 自带 `DISTRIBUTED_TASK_PROP` IP 取模,其余没有), + 后续可整体迁到 yudao Quartz / xxl-job 再按任务开关。 + +### 7.6 指挥中心(真实数据聚合) + +原 iframe 壳已替换为自研页面,**菜单、路由、component 均未变**: +`100498` / `/command-center` / `gov/commandCenter/index`。右上角「旧版大屏」仍指向 +存量综合监测大屏(`VITE_APP_COMMAND_CENTER_URL`,缺省 +`http://139.224.115.129/.../systemFirstPage.html`),过渡期可对照。 + +2026-11-02 按用户提供的「赤峰市智慧停车数据中心」大屏重做版式,随后按「站在政府角度、 +重排整页比例」的要求调整为**通栏 KPI + 左(供给与规划)/ 中(城市态势)/ 右(运行与服务)** +三段式。**2026-11-06 起接真实数据**:`demo.ts` 已删除,页面只读后端聚合接口 +`POST /park/command-center/overview`(一次返回整屏),60 秒轮询 + 手动「刷新」。 +左上角标识由「演示数据」改为绿色「实时数据」,接口失败时变粉色「数据加载失败」并保留上次数据。 + +| 项 | 内容 | +|---|---| +| 页面 | `src/views/gov/commandCenter/index.vue`(视图) + `mapper.ts`(响应 → 页面模型) | +| 接口 | `POST /admin-api/park/command-center/overview`;`ParkCommandCenterController`,权限沿用 `park:dashboard:query`(**不新增权限点、不授权任何角色**) | +| 聚合 | `ParkCommandCenterServiceImpl`;SQL 在 `ParkCommandCenterMapper` + `mybatis/mapper/commandcenter/ParkCommandCenterMapper.xml`,车场/实时/订单汇总复用 `ParkDashboardMapper` | +| 前端数据源 | `src/api/park/commandCenter/index.ts` → `queryCommandCenterOverview()`;TDD 门禁 `tests/verify-command-center-mapper.mjs` | +| 路由 | `/command-center`(顶级菜单,type=2,parent_id=0,sort=5,`keep_alive=0`) | +| 菜单 id / 种子 | `100498` / `scripts/add-command-center-menu.sql`(幂等) | +| 角色授权 | `scripts/seed-role-templates.sql` 给运营(2001)显式加了 `100498`;超级管理员天然全量 | +| 地图数据 | `src/assets/map/json/chifeng.json`(DataV 赤峰市 `150400_full`,12 个旗县区,随包发布) | + +**口径与诚实原则**(改这里之前先读): + +- 在用停车场 = `parking_lot.data_status = 1 AND state = 1`;订单 = `order_park.data_state = 1`, + 取消(`-50`)不计入收入/欠费;金额字段是**分**,后端统一转元。 +- 实时泊位取 `parking_lot_berth_temp_info`。**原始导入**状态下该表为空 → 剩余泊位 / 占用率 / + 地图标注卡 / 重点车场 TOP10 一律 `null`,页面显「—」或「暂无数据」,**不写 0、不造演示值**; + 同理无用户表 → 用户三项 `null`。后端用 `unavailableModules` 把这些模块名回传给页脚。 + (2026-11-06 起本地跑过演示数据脚本,该表有值,页面不再是空态,见 §7.11。) +- 支付构成取**在线支付表 `pay_order`**(`pay_order_state = 40` 成功、`order_big_type = 100` + 停车订单、`create_date` 当日)。**不要改用 `td_b_pay_order`**:那是 2017 年的历史表 + (`pay_order_state = 1`),且没有 `pl_no`,按车场关联会直接 SQL 报错。 + `pay_order_success` 是归档表,同样不用。`pay_type` 33 本地未知,落到「其它」。 + +版式:**顶部通栏 7 个 KPI**(竖分隔线)+ 下方三栏城市态势。标题签仍为顶部居中的异形 +(两端斜切)标签,面板为深空蓝半透明底 + 内描线 + 四角折线,页面底为星点纹理 + 顶部斜纹。 + +| 位置 | 面板 | 真实口径 | +|---|---|---| +| 通栏 | KPI ×7 | 停车场总数 / 停车泊位总数 / 今日进场车次 / 当前剩余泊位 / 泊位占用率 / 平均周转率 / 今日收入(≥1 万元切「万元」并保留 1 位小数,否则「元」取整) | +| 左 | 泊位供给结构 | 总泊位数 / 路侧泊位 / 封闭泊位 + 「在用泊位合计」;副标题给 `dataThrough`(订单数据截止时间) | +| 左 | 各区联网接入进度 | 按区县的**同心环**(外圈向内递减)+ 中心「已接入 40」+ 右侧「已接入/计划」列表与百分比;副标题「今日有订单 40 / 40(100%)」。分母是各区在用停车场,分子是当天有订单的车场 | +| 左 | 重点区域占用率 | **TOP 10**(按占用率降序),每页 5 条、5 秒轮播,悬停暂停;无实时泊位数据时整块显示「暂无实时泊位数据」 | +| 左 | 各区停车时长分布 | ≤30 分钟 / 30–60 / 1–2 小时 / >2 小时,按区县堆叠;时长优先 `order_park.parking_duration`,缺省用出场 − 入场现算 | +| 左 | 各区今日收入排行 | 按区县今日收入降序(来源同上,含无实时数据的区县);副标题合计(万元,按千元取整) | +| 中 | 全市停车占用率分布 | **12 旗县区完整轮廓**;只有拿到实时占用率的区县才按分档着色并出四角标注卡(车场数 / 泊位数 / 占用率 / 周转率 + 引线),其余深色底、弱化标签;无任何实时数据时整块「暂无实时占用率数据」 | +| 中 | 24 小时进出场车流 | 进场(青)/ 出场(绿)柱 + 停车指数(黄)线(右轴 0–100%,无泊位数据时为空),右上「今日累计进场 … 次」 | +| 右 | 实时进出场记录 | 最近 8 条(车牌 / 车场 / 时间 / 进·出标签),取 `order_park` 按 `COALESCE(park_out_time, park_in_time)` 倒序;60 秒随整屏刷新,不再本地模拟推送 | +| 右 | 用户与支付 | 累计用户 / 今日活跃 / 昨日新增(本库无用户表 → 三项均「—」)+ 支付方式**环形图**,图例按当日笔数占比降序(本地:微信公众号 / 支付宝 / 微信 / 个人账户) | +| 右 | 运行预警 | 设备离线 / 满位车场 / 数据延迟区县 / 欠费逃逸订单 四格:常规高度 2×2,视口高 ≥1100px 改 4 行通栏;图标按后端稳定键 `device/full/stale/arrears` 取,色调固定不随数据顺序跳动 | + +三栏按 `1.05fr / 1.85fr / 1.05fr` 配比——**左右两栏等宽,地图栏才落在页面正中**; +左栏下限 300px、右栏下限 350px,保证记录列表四列不挤。左列 5 个面板按 +`0.66 / 1.18 / 1.02 / 0.95 / 0.95` 分配高度,中列地图 `1.62` / 车流 `0.88`, +右列记录 `1.38` / 用户与支付 `1.15` / 预警 `0.87`。 + +**视觉规格(2026-11-02 定稿)**: + +- 标题 `SCREEN_TITLE = 赤峰市智慧停车数据中心`(换城市改这一处);顶栏左侧「实时监测 + + 实时数据」、右侧日期/星期/时钟与「刷新 / 全屏 / 旧版大屏」。 +- 配色:底 `#050C1C`、正文 `#DCEBF9`、弱化 `#8AA9C9`、描线 `#2D5C86`、主青 `#35D6FF`、 + 蓝 `#4D8DFF`、绿 `#5BE3B3`、黄 `#FFD166`、橙 `#FF9F43`、粉 `#FF8FB1`、紫 `#B79CFF`。 +- 地图为**星空深空底**(`#05101e` + SVG 星点平铺 + 两团星云径向光晕,**不放横竖网格线**), + 注册完整 12 区(`chifeng-full`,不动全局 `chifeng` 注册)。有数据的 4 区按占用率分档: + `<50%` 绿 `#2BD9A4` / `50–70%` 青 `#35D6FF` / `70–85%` 黄 `#FFC11E` / `>85%` 红 `#FF5A5A`, + 白色描边 + 同色外发光;其余 8 区 `#0C253C` 深底 + 弱化标签。红山 / 元宝山两块小区相邻, + 标签按 `FOCUS_LABEL_OFFSET` 外移(红山 `[0, 8]`、元宝山 `[30, 12]`),避免两块相邻区县的 + 标签互相压字(此前红山偏移到了松山区内,地图上会看到两个「红山区」字样)。 +- **地图悬停联动**:鼠标滑过任一有数据的区县 → 区域颜色提亮 + 白色描边加粗(2.6px)+ + 同色外发光,区名标签放大到 13.5px/700;同时对应四角标注卡高亮(边框换成分档色、 + 上浮 2px)并与引线同步加粗发光(`is-active`)。反向从标注卡悬停 → `dispatchAction` + 高亮地图区块;移出后统一 `downplay` 复原。无数据区县悬停仅轻微提亮、不弹卡。 +- 四角标注卡为青边圆角卡(区名分隔线 + 2×2 指标:车场数 / 泊位数 / 占用率 / 周转率), + 引线颜色跟随占用率分档;落点不是简单连形心——按「离卡片最近的外环顶点 → 形心」取中点, + 避免红山 / 喀喇沁两条引线横穿相邻区县交叉。引线在 `ResizeObserver` / 窗口变化后 + 用 `convertToPixel` 重算,全屏与 1366 下都不会错位。 +- 右列「用户与支付」用 ECharts 环形图(`#08182C` 分隔线、中心「N 种方式」由实际支付方式数决定) + 替代原单条堆叠条,图例竖排;「运行预警」图标 + 数值按面板高度放大,名称在左、数值/单位在右 + 撑满整格;面板高度比从 `1.45/1.02/0.78` 调为 `1.38/1.15/0.87`,与左侧面板节奏对齐。 +- **分辨率自适应**:卡片文字/图标统一用 + `clamp(最小值, min(vw, vh), 最大值)`(宽或高任一方向变小时先收敛,1366 用最小值、 + 4K 用最大值兜底);面板内边距、进度条、圆环直径等按 `vh` 缩放;ECharts 不继承 CSS, + 用 `chartPx()` 按 `min(宽比, 高比)` 同步缩放图例/轴标签/地图标签,窗口尺寸变化后 + 180ms 防抖重绘。`max-height: 820px` 保留 1366×768 紧凑规则;`max-height: 760px` + 再收紧页头/列表。高屏(`min-height: 1100px`)预警改 4 行通栏,避免 2×2 大卡留白。 + 已验证 1280×720 / 1366×768 / 1600×900 / 1920×1080 / 1920×1200 / 2000×1253 / + 2560×1440 / 3440×1440 / 3840×2160 均无页面滚动、无面板内容溢出。 +- **保留的动效**:① 重点区域占用率取 TOP 10 分两页(5 条/页),`translateY` 每 5 秒翻页、 + 悬停暂停,右下角圆点指示当前页;② 面板进入时的数字/条形过渡;③ 60 秒整屏轮询后重绘, + 不做本地模拟推送、不做 KPI 抖动——`demo.ts` 时期的模拟记录流与 KPI 波动已删除。 + +**接入结论(2026-11-06)**: + +1. `demo.ts` 已删除,页面模型改由 `mapper.ts` 从接口响应生成;旧 `useCommandCenter.ts` + 保留但本页不再引用。 +2. 记录列表不展示抓拍图;如需巡检再按 `getOrderImages(orderId)`(键 `4` 进场 / + `5` 出场)补图,车牌按存量规则打码。 +3. 本地缺数据的模块(实时泊位/占用率、重点车场、用户指标)不补数,后端在 + `unavailableModules` 里列明、页脚展示。 + +**接入时的数据陷阱(提前记录)**:存量实时接口对**没有实时缓存的车场**返回 +「泊位数 + 其余全 0」,直接按 `(泊位-空闲)/泊位` 会算成 100%。新聚合接口只把 +`parking_lot_berth_temp_info` 有行的车场计入占用率,无行即 `null`;改 SQL 时 +不要用 `COALESCE(..., 0)` 兜底。 + +**两处已知缺口(沿用旧版结论)**: + +1. 存量「本月各区车场评分排名」在本栈没有对应数据源。`ParkDashboardController` + (`/park/dashboard/overview`)能算,但权限点 `park:dashboard:query` 在 + `system_menu` 里没有记录,新增菜单并授权运营属于权限变更,未经确认不做。 + **新版式已把「评分排行」从页面移除**(换成「各区今日收入排行」),该权限缺口不再阻塞本页。 +2. 存量「当日 APP 服务次数」没有等价字段,右侧改用「今日活跃」(`todayActiveNum`, + 按 `modfiy_date` 统计),并补「昨日新增 / 昨日活跃」。 + +**本地数据现状**:原始导入只有红山区 40 个在用车场、3,720 个泊位、 +`parking_lot_berth_temp_info` 为空 → 实时泊位/占用率/重点车场全部空;当日进场 4,889 次、 +出场 2,899 次、收入 2,464 元;支付构成 4 种(微信公众号 670 笔占 89.5%);欠费逃逸订单 546 单; +无用户表。这是本地数据覆盖范围,不是页面缺陷。跑过 §7.11 的演示数据脚本后变成 +109 个在用车场 / 16,210 个泊位 / 12 个旗县区,红山区的车场与订单仍是真实导入数据。 + +**验证(2026-11-06)**:`POST /admin-api/park/command-center/overview` 返回 `code 0`, +字段与页面一致(40 / 3,720 / 4,889 / 2,464 元 / 红山区单区 / 支付 4 项 / 预警 0·0·0·546); +灌入 §7.11 演示数据后同一接口返回 109 / 16,210 / 21,710 / 4,655 / 70.0% / 1.34 / 24,643 元, +12 个旗县区齐全,预警 0·3·1·1,927,支付 5 种,1920×1080 与 1366×768 截面无串版 +(**2026-11-06 复核后数字变为 71.3% / 28,349 元**:占用率口径与精度已修,见 §7.15; +收入随演示数据重灌而变化); +`tests/verify-command-center-mapper.mjs` 通过,前端 16 个 verify 脚本全绿, +`vue-tsc` 在 `commandCenter/*` 0 error,ESLint 干净;浏览器登录 admin 后实测 +1920×1080 与 1366×768 均无页面滚动、控制台 0 error(仅 4 条既有 Vue Router 弃用告警), +空态显示「暂无实时泊位数据 / 暂无实时占用率数据 / —」。 + +**验证(2026-11-02,版式阶段)**:admin 与 ops001 登录后页面 6 个接口全部 HTTP 200, +控制台 0 error;8 种分辨率下截图核对,面板、地图标签、车流轴均无遮挡,页面无纵向滚动; +`pnpm ts:check` 在 `commandCenter/index.vue` 0 error(仓库其他历史页面仍有既有类型告警), +`pnpm build:local` 通过。 + +**权限注意**:运营角色直接拥有 `100426/100427`,接口 `@PreAuthorize` 走 +`menu_role_ids` 缓存因此通过;但这两个按钮的父页面 `100424` 挂在「系统管理」下且 +未授权给运营,`get-permission-info` 的权限串里不会出现 flow 项。本页未使用 +`v-hasPermi`,所以不影响。以后若给页面按钮加 `v-hasPermi`,需同时授权父链。 + +### 7.7 组织 id 空间统一(2026-11-02) + +**问题**:登录态 `deptId` 来自 `system_users.dept_id`(本地 103,芋道演示部门), +而存量业务表存的是老 `sys_org.id`(10003 等)。之前靠 +`parking.legacy.org-id-mapping=103:10003` 在 `LoginUserBridge` 里硬换算, +切生产必须重配,配错会**静默查错组织的数据**(不报错、只是结果不对)。 + +**改造**(`scripts/migrate-org-ids.sql`,幂等): + +1. `sys_org` → `system_org`,真实 id 原样保留(242 行,152 个有效;单个根 10001); +2. `system_users.dept_id` 的 `103` → `10003`(admin + 3 个联调账号); +3. `system_org` 写穿透到 `system_dept`(镜像)——yudao 用户模块仍读它; +4. 软删芋道演示部门 `100~125`,隐藏「部门管理」菜单(组织统一从「组织管理」维护); +5. `application-local.yaml` 的 `org-id-mapping` 置空 ⇒ 恒等映射,启动日志确认。 + +**为什么保留 `system_dept` 镜像**:`AdminUserServiceImpl` 仍通过 `DeptService` +读 `system_dept` 做用户表单组织下拉、用户列表子树过滤、保存用户时部门校验。 +替换整条链路风险大;镜像 + 写穿透是这里最小、可回退的方案。镜像只在 +`OrgServiceImpl` 的 create/update/delete 里同事务维护,读路径仍以 `system_org` 为准。 + +**顺带修掉的两个存量 bug**(改组织 CRUD 时暴露): + +- parking 的 `BaseDO` **没有生效的 `MetaObjectHandler`**:容器里只有 yudao 的填充器, + 只认 `cn.iocoder` 的 BaseDO,导致 `system_org` 的 INSERT 显式写 + `create_time=NULL` 触发非空约束 —— 「新增组织」原来是 500。现在 + `OrgServiceImpl` 显式补审计字段。 +- parking `UserDO` 的两个列名写错:`orgId` 实际列是 `dept_id`、`delFlag` 实际列是 + `deleted`(`system_users` 沿用的是 yudao 表)。不修的话「删除组织」检查 + 「组织下是否有用户」时 SQL 直接报错。已用 `@TableField` 纠正。 + +**验证**:组织树 1 根 / 10003 下 11 个子组织;`dept/simple-list` 152 条; +四个用户 `deptId=10003`;组织新增/改名/删除时 `system_dept` 同 id 同步出现/消失; +`ops001` 登录后 `get-permission-info` 的 `deptId` 为 10003,存量接口仍 `code=0`。 +迁移后需清 `oauth2_access_token:*`(旧 token 里还是 103)。 + +### 7.8 首页监管总览(2026-11-02) + +**页面**:`parking-cloud-yudao-ui/src/views/Home/Index.vue`(静态路由 `/index`,名称 `Index` 不变), +按老库原型 `parking-cloud-ui/docs/design/prototype/01-dashboard.html` 复刻 8 个区块。 +样式全部收在 `.gov-dashboard` 下(`src/views/Home/dashboard/dashboard.scss`), +浅色用政务蓝 `#14539a`,暗色用 `--el-*` 暗色令牌 + 校验过的图表色,不影响站内其它芋道页面。 + +**后端**:`parking-module-park` 新增 +`POST /admin-api/park/dashboard/overview`(`ParkDashboardController`,权限 `park:dashboard:query`), +一次返回六张指标卡、24 小时/逐日车流、三级预警、区县排名、考核指标、重点车场、设备在线率、数据时效。 +实现类 `ParkDashboardServiceImpl`,SQL 在 `mybatis/mapper/dashboard/ParkDashboardMapper.xml`。 + +**口径**(页面底部同步展示): + +| 指标 | 来源 | +|---|---| +| 在用停车场 / 泊位 | `parking_lot`,`data_status=1 AND state=1` | +| 订单 / 欠费追缴率 | `order_park`(排除 `-50`;欠费 52、已补缴 53) | +| 工单闭环率 / 逾期 | `order_work`(`solve_state=3`;逾期 = 超 24h 未办结)。**JOIN `parking_lot` 后只统计在用车场**,与页面「接入车场」口径一致,避免把已停用车场的历史工单算进预警 | +| 设备在线率 | `parking_lot_equipment`(`equipment_status=1`) | +| 实时占用率 | `parking_lot_berth_temp_info`,无实时行返回 null(页面显示「—」) | +| 车流 / 占用率曲线 | `parking_running_statistic`,`data_source=10`;今日 `time_cycle=40` 按小时,本周/本月 `50` 按天 | + +**三级预警是实时推导,不是预警表**:红 = 车场设备离线率 ≥30%(设备 ≥5 台)或占用率 ≥95%; +橙 = 占用率 ≥85% 或工单超 24h;黄 = 设备离线率 ≥10% 或区县欠费率 ≥15%。 +阈值集中在 `ParkDashboardServiceImpl` 常量区,后续接预警规则表时替换 `buildAlarms` 即可。 +页面显式标注「实时推导 · 非工单流转」,不伪造处置状态与响应时长。 + +**暂无数据源的模块**:数据上报及时率 / 上报记录(无对应表)。响应里用 +`unavailableModules` 声明,页面显示「—」并在底部说明,**不使用模拟值**。 + +**权限**:`scripts/add-dashboard-permission.sql` 幂等插入隐藏目录 + 按钮 +(`park:dashboard:query`,id 310900/310901),角色管理可勾选;超管天然可见。 + +**本地验证注意**:本地 `parking_cloud_yudao` 是稀疏样本(4 个在用车场、订单集中在 2017–2018), +因此「今日」指标多为 0、车流图为空态,属预期;页面必须能正常展示空态与「—」。 +实测 `{}` / `{"region":"红山区","period":"month"}` / `{"period":"week"}` 三种入参均 `code=0`, +且切区县后 `regions` 仍返回全量区县(否则下拉只剩当前区县,回不到全市)。 + +已修的两个运行期问题: +1. `Map.entry(lot, null)` 抛 NPE(`Map.entry` 不接受 null 值),改用 `record LotRate` 先过滤再排序; +2. 逾期工单原本只按 `pl_country_name` 过滤,会把不在用车场(如丹阳市)的 2018 年工单算进来, + 现统一 JOIN `parking_lot` + `activeLotCondition`,本地逾期数因此为 0(4 个在用车场无工单)。 + +### 7.9 地图区县配色(2026-11-04) + +「停车资源一张图」「专题分析」「指挥中心」三张地图的区县底色不再写死中心四区,改为**全市 12 个旗县区各一色**, +并且颜色与开关由后端参数控制,前端只做兜底。 + +| 项 | 内容 | +|---|---| +| 参数键 | `parking.map.district-theme`(基础设施 → 参数管理,`visible=1`,登录用户可读) | +| 种子脚本 | `scripts/add-district-theme-config.sql`(幂等) | +| 参数结构 | `enabled` 总开关(默认 `true`);`off` 关闭高亮的区县数组;`colors` 单区县颜色覆盖 | +| 前端 | `parking-cloud-yudao-ui/src/views/gov/shared/district-theme.ts`(纯逻辑 + 默认色表)、`district-theme-loader.ts`(读参数、单次缓存) | +| 接入页面 | 一张图 `oneMap/useOneMap.ts`、专题分析 `topicAnalysis/useTopicMap.ts`(高德 `Polygon`)、指挥中心 `commandCenter/index.vue`(ECharts `map`,用 `toRgba()` 输出带透明度 `areaColor`) | +| 兜底 | 参数缺失 / JSON 损坏 / 接口失败 → 全部高亮 + 默认配色,地图不会失色 | + +默认色(中心四区沿用智慧中心):红山区 `#FF5A5A`、松山区 `#FFC11E`、元宝山区 `#35D6FF`、 +喀喇沁旗 `#2BD9A4`、阿鲁科尔沁旗 `#6C8DFF`、巴林左旗 `#B79CFF`、巴林右旗 `#FF9F43`、 +林西县 `#FF7AC6`、克什克腾旗 `#A3E635`、翁牛特旗 `#2DD4BF`、宁城县 `#E879F9`、敖汉旗 `#9AA7C7`。 + +填充透明度:中心四区 0.74、其余区县 0.44(街区尺度分别淡出到 0.12 / 0.08), +保证 12 块颜色区分明显又不遮挡车场点位。指挥中心有实时数据的 4 个区仍用白描边、白色区名, +其余 8 个区填充更淡、标签弱化;关闭高亮后回到指挥中心原样式(有数据的区按占用率分档、其余深色底)。 + +参数改动在**下一次页面加载**生效(前端按页面生命周期缓存一次请求,避免两张地图重复取参数)。 + +### 7.10 视频监控前端(演示数据版,2026-11-04) + +方案见 `docs/superpowers/specs/2026-11-04-视频监控模块设计方案.md`,计划见 +`docs/superpowers/plans/2026-11-04-视频监控前端实施计划.md`。**当前只做前端**,后端 `parking-module-video` +(GB28181 / WVP-PRO + ZLMediaKit)待页面确认后再落地;接口契约、错误码、数据表已写进方案 §6,前端按契约类型消费演示数据。 + +| 页面 | 路由 | 组件 | 内容 | +|---|---|---|---| +| 视频监控 | `/video/monitor` | `gov/videoMonitor/index` | 1/4/9 宫格视频墙、三级设备树(12 区县 → 车场/路段 → 112 台设备)、收藏、双击最大化、宫格右键菜单、历史回放时间轴、地图点位 + 设备卡、云台控制、视频轮巡 / 巡逻方案 | +| 视频设备 | `/video/device` | `gov/videoDevice/index` | 设备台账:区县 / 类型 / 在线状态筛选、分页、预览跳转、新增 / 编辑 / 删除 / 批量启停(内存态)、编辑抽屉 | + +- 前端目录 `parking-cloud-yudao-ui/src/views/gov/videoMonitor/`:`types.ts`(契约)、`model.ts`(纯函数: + `deviceTree` 三级树、`favoriteDevices` 收藏、`buildPatrolOrder` 巡逻分组)、`demo.ts`(确定性演示数据,112 台)、 + `useVideoMonitor.ts`(页面状态)、`components/`(设备树、视频墙、播放器、回放条、地图、云台、巡逻)、 + `video-monitor.scss`(非 scoped,统一 `vm-*` 类)。 +- 设备树为三级:区县 → 车场/路段(车场在前)→ 设备(在线优先、中文名排序);收藏存 + `localStorage['video-monitor:favorites']`,只存设备 id,后端上线后迁到表。 +- 「轮巡」与「巡逻方案」是两个功能:轮巡按当前筛选结果 + 宫格大小自动翻页(工具栏,15/30/60 秒), + 巡逻方案是右栏预置设备集;两者互斥,手动播放 / 回放 / 切地图都会停止轮巡。 +- 播放画面为 canvas 合成的示意监控画面(车场 / 路侧两种场景),不依赖网络与二进制样片;配置 + `VITE_DEMO_VIDEO_URL` 可改用循环样片。协议无关边界已留好:`PlaySource.protocol`(`webrtc` / `flv` / `hls` / + `demo`),后续只换 `VideoPlayer` 实现。 +- 菜单种子 `scripts/add-video-menu.sql`:`system_menu` 100502 `/video/monitor`、100503 `/video/device`, + `keep_alive=0`(页面带定时器与地图实例),同时授给运营角色 2001;幂等,已在 `parking_cloud_yudao` 执行。 +- 验收:`parking-cloud-yudao-ui/tests/verify-video-monitor.mjs` 全绿;1920 / 1366 / 1200 宽无横向滚动; + 宫格切换、最大化 + Esc、回放暂停与空录像日、地图模式 + 设备卡实时/回放、巡逻启动、轮巡推进与停止、 + 三级树折叠、收藏 + 只看收藏、宫格右键菜单、枪机云台置灰、设备台账新增/编辑/删除/批量启停均实测通过。 +- 演示数据每周第 7 天(如 2026-11-05)无录像,用于验证「当日无录像」空态;设备坐标在区县锚点 ±0.15° 内展开, + 便于在地图上看出点位分布。 + +### 7.11 本地演示数据:11 个旗县区(2026-11-06) + +本地副本库只导入了红山区,监管中心 / 指挥中心 / 综合监测里其余 11 个旗县区全是空白。 +`scripts/gen-demo-district-data.py` 给**本地** `parking_cloud_yudao` 补一批可识别、可回滚的 +演示数据,让 12 个旗县区的版式、图表、地图着色都能看到真实量级的效果。 + +```bash +python3 scripts/gen-demo-district-data.py --dry-run # 只打印计划(默认) +python3 scripts/gen-demo-district-data.py --apply # 先清旧演示数据,再重建 +python3 scripts/gen-demo-district-data.py --clean # 只删演示数据 +``` + +| 项 | 内容 | +|---|---| +| 规模 | 11 个旗县区共 **69 个演示车场 / 12,490 个泊位**;含红山区后全库 109 个在用车场、16,210 个泊位 | +| 车场 | `parking_lot`:`pl_no` 以 `P1504` 开头、`remark = 本地演示数据(脚本生成)`;约 6 成路侧(`pl_type=1`)/ 4 成封闭(`pl_type=2`),占用率 0.35–0.94,每 19 个车场有 1 个 ≥95%(让「满位车场」预警非 0) | +| 坐标 | `parking_lot_geo`,每个车场一条,取旗县区政府驻地 ±0.05° | +| 实时泊位 | `parking_lot_berth_temp_info`,**覆盖全部在用车场**(红山区按 `pl_no` 稳定哈希给 0.62/0.74 基准占用率),页面不再落空态 | +| 订单 | `order_park`,只写入演示车场、只写「今天 00:00–现在」,按 24 小时进车权重分布;状态分布 46% 完成(50) / 34% 在场(10) / 8% 待支付(20) / 10% 逃逸(52) / 2% 已补缴(53);每个车场额外补 1 条最近 25 分钟内的在场单,避免「数据延迟区县」误报 | +| 支付 | `pay_order`,已完成且有费用的订单生成支付单(`order_big_type=100`、`pay_order_state=40`),5 种 `pay_type` | +| 运行统计 | `parking_running_statistic`(`data_source=10`):`time_cycle=40` 按小时 1,526 行 + `time_cycle=50` 日汇总,由当天真实订单聚合 | + +**口径(与真实数据保持一致,改脚本前先看)**: + +- 金额单位是**分**,`order_park.parking_duration` 单位是**秒**(早期写成分钟会把时长全部落进 + 「≤30 分钟」一档)。 +- 收入只算 `order_state = 50/53` 的已完成订单,取消(`-50`)不计;首 30 分钟免费, + 之后按小时向上取整计费(路侧 200 分/时、封闭 300 分/时)。 +- 演示车场 `state = 1`、`data_status = 1`,与真实车场同口径参与统计; + 它们的 `remark` 与 `pl_no` 前缀是唯一识别标记。 + +**安全边界**: + +- 脚本只从 `application-local.yaml` 进程内解析本地库连接,不打印凭据,不连 `parking_cloud`; + 共享库 / 生产库禁止执行 `--apply`。 +- `--apply` 先按演示标记删旧数据(`order_park` / `pay_order` / `parking_lot_berth_temp_info` / + `parking_lot_geo` / `parking_running_statistic` / `parking_lot`),再重建,可重复执行。 +- 红山区只补 `parking_lot_berth_temp_info` 与 `parking_running_statistic` 两张 + 缓存 / 统计表,**不动它的 `parking_lot` 与 `order_park`**——那里是真实导入数据。 + +**已知副作用(诚实记录)**: + +1. 红山区的实时泊位、运行统计是脚本合成的(真实车场没有实时上报),所以它的占用率 + 是估算值;订单、支付、车场清单仍是真实的。演示答辩时若被追问,按这个口径回答。 +2. 演示订单只覆盖「今天」,**次日看板会回到接近空态**;演示前重跑一次 `--apply`。 +3. 红山区真实订单流停在上次导入时刻,所以「数据延迟区县」恒为 1(红山区),这是真实缺口, + 不是脚本 bug。 +4. `scripts/warm-cache.sh` 与此无关(演示数据只进 MySQL),但改完真实车场数据后仍需预热缓存。 + +### 7.12 重点车场占用率监测:实时数据兜底(2026-11-06) + +`/park/keyLotMonitor` 之前是「47 监测车场 / 0 行 / 暂无可计算的占用率」。原因不在前端: +`ParkingLotQueryServiceImpl#queryParkinglotBasicFactsByPlNos` 的 `berthNum / berthFreeNum` +是从 Redis 实时快照(`PLRealTimeStatusCacheUtil`)整段 copy 的,而写这份快照的 XXL-Job +`PLRealTimeStatusCacheJob` 在新栈不跑(§7.5),Redis 里没有 key。 + +处理方式:新增 `LegacyRealtimeBerthFiller` +(`com.zteits.parking.module.park.service.dashboard`),在 `IndexRealTimeController` 的 +查询与导出两个端点里兜底—— + +- **只在存量快照没值(`berthNum == null`)时补**;Redis 有值(生产环境)整段跳过,不改变存量行为; +- 取 `parking_lot_berth_temp_info`(`ParkDashboardMapper#selectRealtime`),与指挥中心、综合监测同源, + 同一时刻三张页面的占用率一致; +- `freeBerthsStateDesc` 逐行照搬存量公式(`1 - round2(空余/总数)`,0 停满 / <0.20 紧张 / 其余充足)。 + 这个公式对全空车场会算出「停满」,但存量就是这口径,且前端只在 `caryardOverview` 的 TS 类型里 + 声明过该字段、从未渲染 —— 不顺手改口径; +- 今日进出(`inCarNum / outCarNum`)同样在 Redis 快照缺失时兜底:用 `order_park` + 现算「今日 00:00 至今」进场 / 出场台次(`ParkDashboardMapper#selectTodayFlowByLot`), + 与指挥中心「今日进出场」同一张表、同一口径,**不是补零**。存量任务统计的是 + `park_in_out_record`,两者在停车业务里一一对应;上线后 Redis 快照有值时不走这条兜底。 + +页面上另有两块指标「泊位周转(次/日)」「平均占用率」,接口取的是 `parking_running_statistic` +里**最近 10 分钟**(`time_cycle = 10`)的聚合行,本地没有这类行 → 恒为「—」。 +前端 `occupancy.ts` 新增 `summarizeDaily`,在接口返回 null 时用同一次返回的今日小时统计兜底: +平均占用率 = 各小时 `1 - freeRatio` 的均值(与存量 SQL `AVG(1 - free_ratio)` 同口径), +周转 = 最后一条小时的 `turnoverRatio`(当日累计,取平均会低估)。接口有值时优先用接口值。 +TDD 门禁:`tests/verify-key-lot-occupancy.mjs`(先失败后通过)。 + +验证(2026-11-06):用页面实际取到的 47 个车场请求 `query-parkinglot-basic-facts-by-pl-nos`, +47/47 都有 `berthNum / berthFreeNum`,今日进出与 `order_park` 逐车场核对一致 +(如林西镇中心 317 / 166);页面变成 47 监测车场 / 11 超阈值 / 平均 74.4%, +Top 20 排名、24 小时占用曲线、两块指标全部有值。1366×768 下右侧曲线原本被面板裁成一条底边 +(`.klm-body` 只剩 155px,被 `.klm-table-wrap` 的 `max-height: 38vh` 挤压), +加 `@media (max-height: 900px) { .klm-table-wrap { max-height: 26vh; } }` 把高度让给曲线, +1366×768 与 1920×1080 均已实测。 + +注意:**演示数据 `--apply` 之后仍要跑一次 `scripts/warm-cache.sh`**。本页车场清单与 +`plNo` 回填都走存量车场缓存(`ParkLotCacheUtil`),新加的车场不在缓存里时 DTO 的 `plNo` +为 null,接口会按存量口径返回空列表 —— 这次就是先被这个坑挡了一次,跑完预热才出数。 + +### 7.13 指挥中心「各区联网接入进度」:12 个旗县区一屏看全(2026-11-06) + +补完 11 个旗县区数据后,这块面板的缺陷才显出来:`dc-access` 是「同心环 + 明细列表」两栏, +列表每行把「区名」和「已接入数」上下两行堆叠,12 行共约 317px,而 1080p 下面板内容区只有 +138px —— 一屏只能看到 5~6 个区,其余要在面板内滚动;大屏没人会去滚,等于丢了 6 个区。 + +改法(只动 `src/views/gov/commandCenter/index.vue` 的样式,接口与模板结构不变): + +- **行改单行**:`.dc-access-info` 由纵向 flex 改横向 baseline,「区名 3/5 60%」一行读完,行高 21px → 12px; +- **列表自动分栏**:`grid-template-columns: repeat(auto-fill, minmax(120px, 1fr))`, + 列宽够就两栏(1920×1080 起),不够自动退回一栏,不用写死断点; + 行间距用 `align-content: space-between` 铺满面板高度,与同心环上下对齐; +- **环那栏按环的实际直径取值**:`--dc-ring-size: clamp(96px, 11.7vh, 180px)`(与 `ringStyle()` + 同一套 clamp),原来固定 1.05fr 分掉一半宽度、环两侧各空 40px,现在把宽度让给列表; +- **同心环补中心圆盘**:`.dc-rings-center::before` 用同一套尺寸算出内径,盖住第 4~6 圈, + 「109 / 已接入车场」不再被 20~48px 的小环压在下面(原来字和环糊在一起); +- **1728 以下单栏滚动时底部渐隐**(`@media (max-width: 1710px)` 的 `.dc-access::after`): + 一栏装不下 12 行,硬切会像渲染坏了,渐隐提示下面还有内容。 + +为什么不在窄屏也两栏:1366×768 左列只有 308px,扣掉环的 100px 后列表 190px, +两栏各 85px,「阿鲁科尔沁旗」只能显示成省略号 —— 宁可滚动也不截区名。大屏的标准尺寸是 +1920×1080(及以上),那一档必须一屏看全。 + +**同页补修:1366×768 整页多出 50px 滚动**(2026-11-06,量 `v-layout-content-scrollbar` +的 `scrollHeight - clientHeight` 发现)。`@media (max-height: 820px)` 里给 +`.command-center` 写的是 `height: calc(100vh - --top-tool-height - --tags-view-height)`, +**漏减了 `--app-footer-height`**:页脚在内容之后是普通流,等于把 50px 的页脚顶出视口, +页面反而能滚 50px。补上 `- var(--app-footer-height)` 后与基础规则一致,页脚 718..768 完整可见、 +`scrollHeight == clientHeight`;代价是矮屏面板内容区少 50px,「各区联网接入进度」由 +138px 变 92px(12 行里完整可见 5~6 行 + 底部渐隐,本来就是这一档的降级形态)。 +1920×1080 不受影响(列表仍 138px / 12 行全见)。 + +**同页补修:「各区今日收入排行」同样只露 5 个区**(2026-11-06)。12 行单列要 244px, +面板内容区 111px,第 6~12 名(宁城县、喀喇沁旗、巴林左旗、林西县、阿鲁科尔沁旗、巴林右旗、 +克什克腾旗)要面板内滚动才能看到,而表头写的是「合计 2.8 万元」、屏上只能加出 1.7 万。 +改法与联网接入进度一致:`.dc-revenues` 从 flex 列改成 `grid-template-columns: +repeat(auto-fill, minmax(150px, 1fr))`(1920 两栏 6 行、2560 三栏 4 行、1366 自动退回一栏), +`row-gap` 由 `0.6vh` 收到 `0.4vh` 才塞得下 6 行;1460 以下退回一栏时用 +`mask-image` 压底部渐隐 —— **不能用 `.dc-panel--revenue::after`**(已被面板四角装饰占用), +**也不能用 `.dc-revenues::after`**(滚动容器里的伪元素会跟着内容滚走)。 +实测:1920×1080 2 栏 / 111=111、1600×900 2 栏 / 87=87、2560×1440 3 栏 / 159=159, +三档页面滚动均为 0;1366×768 单栏 74/216 + 底部渐隐,页面滚动 0。 + +实测(`playwright-cli`,登录态):1920×1080 列表 290px / 两栏各 134px / 12 行全部在内 +(`scrollHeight == clientHeight`);2560×1440 两栏各 189px、行高 16px;1366×768 单栏、 +可见 7.6 行 + 底部渐隐。同期复测其余 4 个左栏面板(供给结构、重点车场、时长分布、收入排行) +高度与内容均未变 —— 本次只改面板内部布局,没动 `.dc-panel--*` 的 flex 比例。 + +### 7.14 监管中心:三行磁贴的 1920×1080 / 1366×768 一屏(2026-11-06) + +§7.6 里写「监管中心也已一屏适配」,**这条是错的**。复核时发现该页两档标准尺寸都在溢出: + +| 尺寸 | 内容高 | 可视高 | 溢出 | 表现 | +|---|---|---|---|---| +| 1920×1080 | 1077 | 994 | 83 | 第三行「进入模块」压到视口边缘,数据口径脚注和页脚都看不见 | +| 1600×900 | 843 | 764 | 79 | 同上,第三行与页脚被切 | +| 1366×768 | 864 | 682 | 182 | 第三行整行 + 脚注、页脚要滚动才能看到 | + +原因是 8 张磁贴按 `span 3` / `span 6` 排成三行(01·02 宽卡一行,03·04·05、06·07·08 各一行), +每行高度由内容撑开(1080p 下 251px、768p 下 200px),三行加上页头、六格通栏指标就超了。 + +改法全部是样式(模板、数据、接口未动,文件 `src/views/gov/supervisionCenter/index.vue`): + +- **960 档(`@media (max-height: 960px)`,1600×900 这一档)只去掉 `.sc-tile-tags` 标签行** + (模块说明已由 brief 承担),三行各少一行 chips 正好补上 79px;再收 1px 通栏指标留白。 + 这一档**不能**并进下面的 820 紧凑块 —— 那块带 `.supervision-center { min-height: 0 }`, + 用在 900 高的窗口上内容只剩 576px,底部会空一大片; +- **820 档(`@media (max-height: 820px)`)压内部而非压字号**:同样去掉标签行、 + `.sc-tile-icon` 钉到 26px(卡头 36 → 26)、`--sc-tile-pad-y` 收到 8px、 + 卡内与入口条 padding、`.sc-main` / `.sc-tile-group` / `.sc-kpis` 间距各收一档; +- **1080p 也是三行,所以还要动全局值**:图标 clamp 上限 52 → 44(`2.6vw` → `2.15vw`,1366 下仍是 min 30,不受影响)、 + `--sc-tile-pad-y` 上限 24 → 21、`.sc-tile-brief` 行高 1.65 → 1.55、`.sc-tile-desc` 行高 1.75 → 1.6、 + 磁贴网格 gap `0.8vw` → `0.6vw`、卡内 gap `0.7vh` → `0.55vh`、入口条 padding 上限 11 → 9。 + 这些值改完卡高 251 → 226,信息密度更高但不再靠省略号裁字(brief 仍是 3 行 clamp)。 + +实测(`playwright-cli`,登录态,量 `scrollHeight` 与 `clientHeight`): +1920×1080 994 / 994(卡高 226,脚注底 1017);1600×900 764 / 764(卡高 179,标签行隐藏); +1366×768 682 / 682(卡高 141,脚注底 698);2560×1440 与 1280×800 同样一屏。 + +**复核大屏一屏适配时不要量 `document.documentElement.scrollHeight`**:应用内容在 +`.el-scrollbar__wrap` 里滚,`documentElement.scrollHeight` 恒等于视口高度,永远量不出溢出 —— +§7.6 的误判就是这么来的。正确写法: + +```js +const root = document.querySelector('.supervision-center') +const wrap = root.closest('.el-scrollbar__wrap') // 注意是「内容页」所属的那个 wrap, + // 不是侧边栏的(侧边栏 wrap 能 scrollTop 但页面不动) +wrap.scrollHeight - wrap.clientHeight // > 0 才是真的溢出 +``` + +### 7.15 演示数据重灌 + 指挥中心占用率口径/精度对齐(2026-11-06) + +**演示数据是「当日快照」,隔天必空**。§7.11 的订单只写「今天 00:00–现在」,第二天打开 +指挥中心 / 实时流量监控就是一片「—」。演示前固定跑两步,缺一不可: + +```bash +python3 scripts/gen-demo-district-data.py --apply # 重建当天订单 / 支付 / 实时泊位 / 运行统计 +./scripts/warm-cache.sh # 预热存量车场缓存(§7.12 末尾已踩过这个坑) +``` + +本次现场症状:实时流量监控 KPI 全是「—」,`dataThrough` 停在 13:37:58(上一次导入的时刻), +其余图表还在画上一批数据 —— 数据截止时间与页面刷新时间差了几个小时。重灌 + 预热后变成 +在停 11,641 / 近 5 分钟入场 101 / 出场 44 / 平均停放 1 小时 33 分。 + +**脚本修正:当日订单的分钟分布**(`scripts/gen-demo-district-data.py`)。原来按小时权重取到 +「当前这一小时」后,再在整小时里随机取分钟,`now - randrange(2, 30)` 的兜底把落在未来的记录 +压回最后 30 分钟,于是「近 60 分钟进出场」图上是一段 4 倍高的假峰接断崖。现在: + +- 当前小时的权重按已过分钟数打折(`weights[-1] *= (now.minute + 1) / 60`),整小时的量不会 + 全挤进已过去的那几分钟; +- 分钟只在 `[0, now.minute]` 里取,不再生成未来时间再往回压。 + +合成验证(导入模块造假车场):近 60 分钟 59 个桶都有值,峰值 22、中位数 14,曲线单调自然。 + +**指挥中心「泊位占用率」70.0% vs 综合监测 71.3%**:两处偏差,都在 +`ParkCommandCenterServiceImpl`。 + +1. **口径**:实时空闲泊位的取值要求「同一行既有 `free_berth` 又有 `berth_num`」,缺 `berth_num` + 的车场被整场跳过 —— 分子分母同时少算,分母因此比综合监测小(15,517 vs 16,210)。 + 现在 `freeBerth()` 只要求 `free_berth` 有值;分母 `occupancyBerths()` 优先用实时行的 + `berth_num`,缺省回落到车场登记的 `pl_berth_num`,与综合监测(`ParkMonitorServiceImpl#toLot` + 的 `firstNonNull`)完全同口径。KPI、各区分布、重点车场、满位车场预警四处全部改用它。 +2. **精度**:占用率是 0~1 的比例,前端再乘 100 显示。原来复用金额的 `round1`,等于 **10% 一档**, + 65%~74% 全显示成 `70.0%`;综合监测保留 3 位小数(71.3%),两页怎么都对不上。 + 新增 `round3()`(`BigDecimal.setScale(3, HALF_UP)`),只用于比例字段。 + +改完 `POST /admin-api/park/command-center/overview` 返回(数值随每次 `--apply` 的随机种子浮动, +车场 / 泊位 / 进场车次是确定的,收入有几百元级差异) +`kpis { lotTotal 109, berthTotal 16,210, todayInCount 21,710, freeBerths 4,655, occupancy 0.713, +turnover 1.34, todayIncome 28,349 }`,各区 松山区 0.721 / 敖汉旗 0.705 / 元宝山区 0.813 / +翁牛特旗 0.681,重点车场 0.986 / 0.979 / 0.973 / 0.944 / 0.939 / 0.919;与 +`POST /park/monitor/realtime-overview` 的 `summary.occupyRate = 0.7128` 及逐车场值一致。 +页面 DOM 校验 7 个 KPI 依次为 109 个 / 16,210 个 / 21,710 次 / 4,655 个 / **71.3%** / 1.3 次/日 / +2.8 万元。 + +> 同类问题排查入口:凡是「同一指标两个页面数字不同」,先比**分母口径**(哪张表给的容量、 +> 缺字段的行是否被跳过),再比**舍入位数**。这次两个都中了。 + +### 7.17 综合监测「区县占用率」13 行一屏(2026-11-06) + +和 §7.13、§7.15 同一类:左栏 `mon-rail-body` 装「全市 + 12 个旗县区」13 行, +原来是两行排(区名/占用率 一行 + 进度条/场数 一行),每行 47px、合计 650px, +1080p 内容区 403px —— 一屏只看到 8 个,红山区、阿鲁科尔沁旗、翁牛特旗、巴林左旗、 +宁城县这 5 个要在栏内滚动才看得到,而地图上 12 个区县是全部着色的。 + +改法(`src/views/park/monitor/index.vue`,样式 + 行内结构,数据与接口不动): +把「场数」并到首行,进度条单独占第二行并铺满整栏;行高 47 → 29px,13 行 377px。 +实测 1920×1080 `mon-rail-body` 403 = 403,13 行全部在内、页面滚动 0。 +1366×768 栏高只有 212px,仍要滚动,这档加 `.mon-rail-body` 底部 `mask-image` 渐隐; +1920 不受影响(渐隐在 `max-height: 960px` 内)。 + +> 顺带核对:右侧「车场实时状态」页脚「共 109 个车场」与 KPI「车场数 109」一致, +> 接口 `lots` 也有 109 条 —— 之前的「100」是截图分辨率看错,不是缺陷。 + +### 7.16 实时停车流量监控:区分「数据停更」和「流量归零」(2026-11-06) + +演示数据只写到脚本运行的那一刻。跑完 5 分钟以后再打开 `/park/flowMonitor`, +「近 5 分钟入场 / 出场」两张卡是 **0**,60 分钟曲线末端是一段贴着 0 的断崖 —— +看上去像流量真的没了。原页面只在**整个窗口都没有记录**时才提示「数据停更」, +窗口里还有历史、只是尾部停了的情况没有任何提示。 + +改法: + +- `flow.ts` 新增纯函数 `staleMinutesOf(dataThrough, now)`:返回库内最后一条距今多少分钟, + 解析不了返回 `null`,时钟误差(now 早于记录)夹到 0; +- 页面 `staleMinutes >= 5`(正好是「近 5 分钟」这个窗口)时:两张卡显示「—」而不是 0, + 备注改「数据停更 · 近 5 分钟无上报」,页头「数据停更 · 最后一条 …」芯片也一并出现 + (原来只在 `!hasData` 时出现)。这样「0」只表示「确实没车」,「—」才表示「没数据」; +- 10 秒轮询每次刷新 `data` 时重算,不需要额外定时器。 + +TDD 门禁:`tests/verify-flow-monitor.mjs` 先加断言(`staleMinutesOf is not a function` 失败)再实现。 +实测:16:06 停更时两张卡为「—」+ 停更芯片;重灌演示数据(16:26)后变成 106 / 49,芯片消失。 + +**演示纪律**:`--apply` 之后 5 分钟内这两个指标才非 0,演示超过 10 分钟它们会重新变成「—」 +(这是真实的数据停更状态,页面现在会明说)。要连续演示就中途再跑一次 `--apply`。 + +### 7.18 一张图 / 专题分析的演示夹具补齐 11 个旗县区(2026-11-06) + +§7.11 已把 69 个车场灌进本地演示库,指挥中心、综合监测、监管中心都是读库、12 个区县都有数。 +但「停车资源一张图」「专题分析」读的是前端夹具 `src/views/gov/oneMap/demo.ts`(不是接口), +里面只有 14 个手工车场、覆盖红山区 / 松山区 / 元宝山区 —— 于是同一场演示里两个页面对不上: + +- 一张图的地图上 9 个区县标「未接入」; +- 专题分析的区县下拉里 11 个区县写「(暂无车场)」且不可选。 + +改法(只动夹具,接口与视图层不改):在 `curatedLotSeeds` 之后加一份 `districtSeeds` +元组表(`[名称, 区县, 经度, 纬度, 泊位, 空余, 今日进场, 今日出场, pl_type]`,67 条, +名称 / 坐标 / 泊位 / 实时空余 / 今日进出原样取自本地演示库,与综合监测同源), +再用 `districtLotSeeds` 展开成完整 `ParkingLot`: + +- id 从 101 起编、`code = PL0101…`,与手工夹具的 1–14 不撞号; +- `pl_type` 1 → `type: 'roadside'` + `plTypeProp: '1101'`,2 → `'enclosed'` + `'2102'`; +- 收费口径、营业时间、服务电话、分区、出入口按类型给合理默认值,路侧车场不给分区/出入口 + (与手工夹具里路侧车场的写法一致); +- 占用率 ≥95% 记 `special`,与指挥中心「满位车场」同口径; +- 与手工夹具重名的两个(松山万达广场停车场、松山区政务服务中心停车场)不重复补。 + +最后 `const lotSeeds = [...curatedLotSeeds, ...districtLotSeeds]`,原有 +`parkingLots = lotSeeds.map(...)` 的建号 / 造设备逻辑不动。 + +实测(1920×1080):一张图资源总览 **81 车场 / 16,220 泊位 / 4,236 空余 / 73.9% / 24,527 今日进场**, +12 个区县全部着色并带占用率;专题分析下拉 12 个区县全部可选、无「(暂无车场)」, +选阿鲁科尔沁旗得 4 车场 / 630 泊位且覆盖·流量·收费·营收四个专题都有图。 + +TDD 门禁 `tests/verify-topic-analysis.mjs` 同步更新:全市 14 → **81** 条;原来的空态断言 +用阿鲁科尔沁旗,该区补齐后恒不为空,改为不存在的区县名(空态分支仍在,接口返回空时也会走到), +另加一条「阿鲁科尔沁旗 > 0」守住补齐结果。16 个 `tests/verify-*.mjs` 全绿,`eslint` 无告警。 + +留了两个**已知差异**,都不是缺陷,别当成 bug 去追: + +1. **一张图 / 专题分析 81 个车场,综合监测 / 指挥中心 109 个**。前者是前端演示夹具,后者读 + `parking_cloud_yudao`,本来就是两套数据源(一张图页面顶部固定展示「演示数据」标识)。 + 红山区库里那 40 个真实车场没有镜像进夹具:其中 2 个坐标在省外(116.36/39.92、119.55/32.00), + 放进一张图会把视野拉到北京 / 镇江,而夹具里已有 10 个精修的红山区车场。是否改成读真实接口 + 由后续决定。 +2. **「智能设备覆盖 100%」**。夹具给每个车场都造了设备(`buildLotDevices`),覆盖必然是 100%, + 是指标口径,不是算错。 + +**附带修好的**:视频监控页的 `demo.ts` 直接 `import { parkingLots }`,夹具补齐后它的设备树 +从原来只有红山区 / 元宝山区两个区县变成 **12 个区县 / 112 台设备(在线 101)**, +和页面注释里写的「12 个旗县区、约 130 台摄像头」终于对上了,不用再单独补数据; +树变长后在 `.vm-tree-list` 里内部滚动(4182px / 758px),页面本身仍一屏。 + +### 7.19 三个列表页改本地分页(2026-11-06) + +「反馈管理」「公告管理」「视频设备」和别的停车列表页不一样:接口一次拉全量回来,筛选在页面上本地做, +表格就把全部行铺出来。数据一多,页面高度直接失控 —— 实测(1920×1080,量的是 +`.v-layout-content-scrollbar .el-scrollbar__wrap` 的 `scrollHeight - clientHeight`): + +| 页面 | 行数 | 修复前溢出 | 修复后 | +|---|---|---|---| +| 反馈管理 `/park/feedback` | 501(本地库全部待处理) | **21,483 px** | 0(994 = 994) | +| 视频设备 `/video/device` | 112 | 428 px | 0(994 = 994) | +| 公告管理 `/park/notice` | 19 | 174 px | 0(994 = 994) | + +改法:新增共享 `src/utils/page.ts#pagedSlice(items, page, pageSize)`,把「页码夹到合法区间」 +这件事收在一处 —— 筛选后条数变少、页码还停在旧值时会切出空表,用户会以为数据没了: + +- 页码 `< 1` 夹到第 1 页;大于总页数夹到最后一页;`pageSize` 为 0 按 1 算(避免除零); +- 三个页面统一 `pageSize = 10`(与其它停车列表页一致,20 条在 1920 下就会溢出 370 px 左右); +- `handleSearch()` 结尾 `page.value = 1`,换筛选条件回第一页; +- `watch(rows, …)` 在条数变少时把页码夹回最后一页 —— 答复 / 发布 / 删除都会改状态、进而改排序; +- 序号列用 `:index="rowIndexOf"`,第 2 页从 11 开始,不是每页都从 1 数; +- 分页条统一 `.table-pager` + `layout="total, sizes, prev, pager, next, jumper"`。 + +TDD:`tests/verify-feedback-manage.mjs` 增加 8 条 `pagedSlice` 断言(先 RED:`pagedSlice is not a function`, +再 GREEN),覆盖首/末/越界/`page<1`/`pageSize=0`/空列表。16 个 `tests/verify-*.mjs` 全绿。 + +**1366×768 的取舍**:三张列表页在这个尺寸下仍分别溢出 294 / 193 / 363 px。这是**有意保留**的 —— +它们是有搜索区 + 工具条 + 表头的普通后台列表,把 10 行再压到 768px 内只能砍到 4~5 行/页,翻页次数翻倍; +其余 8 张演示页(监管中心、指挥中心、一张图、重点车场占用率、专题分析、实时流量、视频监控、综合监测) +在该尺寸下仍是 `scrollHeight == clientHeight`,一屏要求针对的是大屏与监测页。 + +页面数据与真实库一致,未造数据:反馈 501 条全部 `is_answer = 0`(库里就没有一条被答复过), +所以演示时「待处理 501 / 已回复 0 / 超时 501」是真实状态,不是算错,也没有为了好看伪造答复 +(答复会触发真实极光推送)。 + +### 7.20 三张高德地图的底图从近黑换成蓝色(2026-11-06) + +一张图中间的底图看上去是黑的,和周围深空蓝的面板脱节。实测原因是高德内置样式的问题, +不是页面配色写错: + +- `features: ['bg','road','building']` 下地块颜色由样式决定; +- `amap://styles/darkblue` 的地块取色 **rgb(0,0,1)**,等于纯黑,全屏裁图后几乎看不出边界; +- 试过 `normal`(浅灰)、`grey`(灰)、`dark`(#313131 灰黑),都不是大屏要的蓝。 + +改成 `amap://styles/blue`:同为矢量样式,地块变成 rgb(1,69,79) 的深蓝绿,路网 / 水系仍然可见, +区县着色、气泡、标注在上面都还看得清。三处一起改(都写死同一个字符串): + +- `src/views/gov/oneMap/useOneMap.ts`(一张图) +- `src/views/gov/topicAnalysis/useTopicMap.ts`(专题分析) +- `src/views/gov/videoMonitor/components/DeviceMap.vue`(视频监控「地图」页签) + +只改 `mapStyle` 一个取值,`features`、缩放、交互都没动;三页在 1920×1080 实测仍是一屏 +(`scrollHeight == clientHeight`),一张图 12 个区县标注全部带占用率、无 `NaN / undefined`。 +**别再改回 `darkblue`** —— 它是黑底,不是深蓝底。 + +### 7.21 一张图加入场动效(2026-11-06) + +参考浅色门户模板的 GIF(`大屏素材/01_大屏创意设计/0314_…/05.jpg`,实为 75 帧动图), +**只借动态节奏、不借配色**:页面仍是深空蓝(`--bs-*` 一套色板),没有改成浅色模板。 + +**整页 3 秒加载完成**(`PAGE_ENTER_MS = 3000`),顺序按产品要求:地图先从中间 +「开花」铺开,左右面板的卡片再从上到下一张张滑入,最后底部卡片浮起 —— + +| 顺序 | 区块 | 动作 | 延迟 / 时长 | +|---|---|---|---| +| 0 | 顶栏 | 淡入落下 | 0 / 500ms | +| 1 | **地图** | 从圆心**开花**:`circle(4%) → circle(150%)` + `scale(0.55) → 1` + 淡入 | 0 / 1200ms | +| 2 | **左栏第 1 张** | **从左往右**滑入(`translateX(-40px) → 0`) | 900 / 600ms | +| 3 | 左栏第 2~6 张 | 同上,每张再延后 140ms(`PANEL_STAGGER_MS`) | 1040…1600 / 600ms | +| 4 | **右栏第 1 张** | **从右往左**滑入(`translateX(40px) → 0`) | 1050 / 600ms | +| 5 | 右栏第 2~3 块 | 每块再延后 140ms | 1190 / 1330 / 600ms | +| 6 | **底部** | **从下往上**浮起(`translateY(24px) → 0`) | 1600 / 800ms | +| — | 九宫格数字 | 0 → 真值滚动(跟左栏第 2 张「资源总览」一起出来) | 1040 / 1960ms | + +**地图改成整块背景,不再套框**:`.om-map-wrap` 从网格单元改成 `position: absolute; inset: 0`, +铺满 `.om-body`,左右面板(`z-index: 2`)浮在地图上;同时删掉原来的 +`border: 1px solid rgba(84,140,200,.34)` 与 `background: #050c1c`。 + +**坑(记一笔)**:地图绝对定位后就不占网格单元了,`grid` 自动排布会把右栏塞进中间那一列、 +直接盖住地图(右栏一度变成 1020px 宽)。所以 `.om-left` 必须显式 `grid-column: 1`、 +`.om-right` 必须 `grid-column: 3`。 + +纯逻辑抽在 `src/views/gov/shared/enter-motion.ts`(`entranceDelay` / `entranceFade` / +`panelItemDelay` / `entranceEnd` / `countUpTiming` / `easeOutCubic` / `planCountUp` / +`formatCount`),`tests/verify-one-map-motion.mjs` 是门禁(TDD,先 RED +`formatCount is not a function`):门禁钉住**顺序**(map ≤ left < right < foot)、 +**逐张递增**(左栏 6 张 = 900/1040/…/1600)、**预算**(各区块落位 ≤ 3000ms, +数字延迟 + 时长恰好 = 3000ms、数字跟随「资源总览」那张卡)。 + +几个刻意的取舍: + +- **数字只滚一次**:切区县、点刷新、30 秒轮询都会重算汇总,每次从 0 重滚整页永远在抖; + 首屏滚完 `played` 置真,之后直接给值。 +- **格式口径不变**:`formatCount` 复刻原页面的 `fmtInt` / `fmtPct` —— 整数千分位四舍五入, + 百分比默认一位小数。第一版把百分比按 0 位取整,占用率 `73.9%` 变成 `74%`,被门禁抓了。 +- **节奏值只有一份**:`entranceVars` 把 `entranceDelay()` / `entranceFade()` / + `panelItemDelay()` 注入成 `--om-delay-*` / `--om-fade-*` / `--om-item-left-N`, + CSS 只取值,不在样式里另写一份毫秒数。 +- **地图「开花」用 clip-path 不用滤镜**:`circle(4% → 150%)` 配合 `scale(0.55 → 1)`, + 终态必须写成 `circle(150%)` 而不是 `none`,否则两端不可插值、会直接跳变。 +- **不碰地图多边形**:区县色块「生长」动画高德没有 API,硬做会和重绘 / 缩放打架,没做。 + +实测(`performance.now()`,从导航开始算):476ms 开始开花(scale 0.55 / clip 4% / 透明度 0), +1676ms 地图完全铺开,此后左栏卡片 900+140n 逐张进场,**2992ms 数字收尾** —— 刷新到全部完成约 3 秒。 +1920×1080 与 1366×768 仍一屏(`scrollHeight == clientHeight`,944 / 632), +17 个 `tests/verify-*.mjs` 全绿。 + +已知遗留(本次不动):1366×768 下顶栏中间的标题与右侧搜索框有横向重叠,是该页顶栏 +`grid-template-columns: 1fr auto 1fr` 在窄屏的老问题,与动效无关。 + +### 7.22 一张图左右资源清单重排 + 底部大屏导航(2026-11-06) + +方案见 `docs/superpowers/specs/2026-11-06-停车资源一张图左右资源清单与底部导航设计方案.md`(方案 A)。 +一句话:左栏从「按功能凑的 6 张卡」收敛成 **资产 3 张卡**(运行总览 / 资源清单 / 图层管理), +右栏只留 **1 张卡 4 个页签**(实时进出场 / 区县态势 / 重点车场 / 实时告警,选中对象整卡换详情), +页脚 40px 一条:左边 5 个大屏导航按钮,右边压缩图例(**导航已于 §7.24 移到地图上,页脚只留图例**)。 + +**资源清单(新增,7 行 + 1 行未定位)**,口径全在 `src/views/gov/oneMap/catalog.ts` 的 `buildCatalog()`: +(2026-11-09 那 1 行「未定位资源」已整块删除,见 §7.30) + +| 行 | 数值 | 副信息 | 点击 | +|---|---|---|---| +| 停车场 | 81 个 | 占用率 73.9% | 只显示车场,右栏切「重点车场」 | +| 泊位 | 16,220 个 | 空余 4,236 | 同车场 | +| 路侧泊位点位 | 984 个 | 占用 620 / 空闲 282 | 只显示点位,缩放锁 16–17 | +| 感知设备 | 961 台 | 离线 92 · 故障 17 | 只显示设备点,缩放锁 11–15 | +| 视频设备 | 242 路 | 视频桩 121 · 摄像机 121 | 只显示视频点 | +| 充电设施 | 4 站(后缀 36 桩) | 在线 3/4 | 只显示充电站 | +| 实时告警 | 5 起 | 红 1 / 橙 2 / 黄 2 | 右栏切「实时告警」 | + +点已选中的行 = 取消筛选、恢复全部图层并 `fitPoints` 回全市。 + +**口径坑(门禁抓到过两次)**: + +1. **两个「泊位」不是一回事**:16,220 是车场登记泊位数(地图不逐位画),984 是已上图的路侧泊位点位。 + 合成一个数就是对外发布错的资源量,所以清单必须两行分开。 +2. **视频设备 = 242,不是 121**:`devices.kind` 里 `video`(视频桩)与 `camera`(摄像机)各 121 台, + 只数 `video` 一类会少一半。副信息分开写成「视频桩 121 · 摄像机 121」。 +3. 告警级别以 `demo.ts` 实际数据为准(红 1 / 橙 2 / 黄 2),方案稿里估的 2/2/1 是错的。 + +**底部导航**:`src/views/gov/oneMap/nav.ts` 的 `PAGE_NAV` 5 项(一张图 `/park-map`、专题分析 +`/park-map/analysis`、视频监控 `/video/monitor`、综合监测 `/park/monitor`、车辆追溯 +`/park/vehicleTrace`),按权限码过滤后才渲染,**不新增权限、不调整任何角色授权**。 + +坑:当前页高亮必须用**路径全等**(`isNavActive`),不能用 `startsWith` —— `/park-map` 是 +`/park-map/analysis` 的字符串前缀,前缀匹配会让「一张图」在专题分析页也亮着。 + +**入场动效跟着改**(`enter-motion.ts`):左栏卡片数 6 → 3(新增 `panelItemCount()`,门禁同步断言), +数字滚动改为跟**第 1 张卡「运行总览」**同拍(原为第 2 张),仍恰好第 3 秒收尾。 +§7.21 里那张「左栏 6 张 900 / 1040 / … / 1600」的节奏表已被本节取代:现在是 900 / 1040 / 1180。 + +**实现取舍**:不拆 `ResourceCatalog.vue` / `PageNav.vue`。`index.vue` 的样式是 `scoped`,清单与导航大量 +复用 `.om-sec` / `.om-cat*` / `.om-legend` 这些局部类,子组件拿不到这些样式,拆了只能复制一遍面板样式或改成全局。 +纯逻辑(算数、路径判定、权限过滤)仍然抽成 `catalog.ts` / `nav.ts` 并被门禁覆盖。 + +**一屏(≤1500px 紧凑档)**:1366×768 下左栏原本需要 763px、只有 514px。加 +`@media (max-width: 1500px)` 后把清单行压成一行(副信息右对齐、隐藏迷你条)、缩小面板 padding/gap、 +KPI、图层行、导航按钮与右栏抓拍图,字号最低 9–11px。实测 1920:整页 944/944、左 813/813、右 813/813、图例 1238/1238; +1366:整页 632/632、左 514/514、右 514/514(4 个页签都 514/514)、图例 772/772。**左栏靠 `scrollHeight == +clientHeight` 验收,不靠滚动条兜底。** + +小坑:`.om-legend-note { display: none }` 在断点里写不生效 —— 基础规则 `.om-legend > span`(0,1,1) +优先级高于 `.om-legend-note`(0,1,0),必须写成 `.om-legend > .om-legend-note`。 + +门禁:新增 `tests/verify-one-map-catalog.mjs`(7 行顺序 / 口径 / 颜色 / 空数据 / 无站名兜底)、 +`tests/verify-one-map-nav.mjs`(5 入口 / 路径全等 / 权限过滤),改 `tests/verify-one-map-motion.mjs`。 +前端仓库共 **19 个 `tests/verify-*.mjs` 全绿**。 + +### 7.23 一张图按「只有 3 个区有数据」重排:空区留白、有数据区铺满(2026-11-06) + +> 2026-11-07 追加喀喇沁旗为第 4 个有数据的旗县区,见 §7.25;本节其余口径不变。 + +背景:全市 12 个旗县区里,**只有红山区 / 松山区 / 元宝山区**真正接入数据,其余 9 个 +后期也不会有数据接入。§7.18 当时把 11 个旗县区都补了演示车场,等于在大屏上对外宣称 +全域已联网;页面又按「12 个区都有」排版,一旦按真实口径只剩 3 个区,右栏会空掉一大半。 + +**口径改成 3 个区,总量不变**: + +| 项 | 改前 | 改后 | +|---|---|---| +| 有数据的区县 | 12 | **3**(`COVERED_DISTRICTS`,`isCoveredDistrict()`) | +| 车场 / 泊位总量 | 81 场 / 16,220 位 | **不变** | +| 49 条补充种子 | 挂在 9 个空区 | 全部改挂到 3 个有数据的区(`relocationPlan`) | + +搬迁不是改数字,是**重生成名称与坐标**:名字取自三个区的地名池(不能出现「经棚」「锦山」 +这类别的旗县镇名),坐标按黄金角螺旋落在各区城区中心附近(半径 `0.01° × √(n+1)`, +最外圈约 5 km),保证点真的落在本区行政边界内。`tests/verify-one-map-districts.mjs` +断言 81 个车场全部落在所属区县 bbox 内(39 / 23 / 19 分布)。 + +**空区怎么呈现**:地图上只画灰蓝底 + 虚线边界,标签只有区名,**不写任何状态或排期字样**。 +全平台新增门禁 `tests/verify-no-pending-wording.mjs`:扫 `src/` 下全部 `.vue/.ts/.mjs/.json/.scss` +(含注释)里出现「待接入」即失败 —— 把这件事挂在页面上等于向使用方承诺一个不存在的排期。 +空态统一用「暂无数据 / 暂未开放 / 暂未迁入 / 非实时数据」。 + +**页面怎么在只有 3 个区时不显空**(这是本节的主要工作量): + +1. **KPI 4 → 6**:补「今日订单」和「覆盖旗县区 3/12」,一屏 6 个数字把「全市」讲完整。 +2. **右栏区县态势**:从「12 行」改成「3 张有数据卡片 + 数据覆盖块」。卡片按 `flex: 1 1 0` 均分 + 右栏剩余高度(`justify-content: space-between` + 显式 `align-items: stretch`,否则 button 的 + UA 样式会把三行缩成一小坨),内容是大号占用率 + 泊位 / 空余 + 进度条;底部「数据覆盖 3/12」 + 进度条 + 「其余 9 个旗县区暂无数据」+ 9 个灰色 chip。 +3. **重点车场 / 实时告警**同样只有 6 / 5 条,行均分剩余高度;告警底部再加一块「告警分级」 + (红 / 橙 / 黄条数,从同一份 `alerts` 算),把版式收住。 +4. **地图取景改成数据驱动**:`fitAllDistricts()` 只按有数据的车场坐标取景(原来按区县多边形取景, + 会把 9 个空区一起框进来,导致有数据的三区缩成一小块)。缩放取 `Math.floor` 整档 —— + 高德按整数级渲染,四舍五入会多放大半级,最外侧的点被挤出屏幕。 +5. **区县填充透明度下调**:`CORE_DISTRICT_FILL_OPACITY` 0.74 → 0.46、`OTHER` 0.44 → 0.34 + (`gov/shared/district-theme.ts`),让底图和车场气泡透出来,否则三块色块把地图糊住。 +6. **聚合改像素网格**:`renderCluster()` 原来按经纬度距离单链聚合,车场挪密之后会串成一个大球 + (实测 39 / 60 / 62 三坨)。改成 `map.lngLatToContainer()` 换算成屏幕像素后按 60px 方格分组, + 15 个气泡互不重叠。`render()` 的散点阈值也随之下调到 10 级。 + +**实测一屏**:1920×1080 左 813/813、右 813/813、整页 1080/1080,右栏 4 个页签底部余量 13px; +1366×768 左 514/514、右 514/514,区县态势 / 重点车场 / 实时告警余量 9px(实时进出场 8 条列表 +允许滚动,超出 94px)。地图 15 个聚合气泡,三区全部在左右面板之间的可视区内。 + +门禁:新增 `tests/verify-no-pending-wording.mjs`;改 `tests/verify-one-map-districts.mjs` +(`covered` / `uncovered` 分组、3 区 / 12 个、bbox 包含、39/23/19 分布)与 +`tests/verify-map-theme.mjs`(填充透明度新值)。前端仓库共 **21 个 `tests/verify-*.mjs` 全绿**。 + +### 7.24 一张图:导航压到地图上、点开走弹框、车场气泡改圆润泪滴(2026-11-07) + +三件事,都在一张图页内完成,不新增路由、不改权限数据。 + +**1. 5 个导航中的 4 个移到地图下沿** + +`一张图` 是当前页,不再出现在按钮里(`mapNavItems()` 按 `isNavActive` 过滤)。剩下 +专题分析 / 视频监控 / 综合监测 / 车辆追溯 从 40px 页脚挪进 `.om-map-wrap`, +`.om-map-nav` 绝对定位 `bottom: 12px; left: 50%`,深色底条 + 描边。页脚只剩图例一条: + +- 1920×1080:按钮条 869→1251(382×42),`.om-foot` 只剩图例; +- 1366×768:按钮条 624→942(318×40),与左下角 `.om-zoom-hint`(221→333)不重叠; +- 地图下的点位会被按钮条盖住(实测底部中间偶尔压住一个气泡),属于浮层控件的正常代价, + 不做避让 —— 避让会把按钮推到地图外,又回到页脚方案。 + +**2. 点按钮不跳页,用弹框装目标页** + +纯逻辑在 `src/views/gov/oneMap/nav-embed.ts`: + +- `mapNavItems(items, routePath)`:过滤当前页,路径判定沿用 `isNavActive`(全等 + 尾斜杠归一); +- `resolveRouteView(router, path)`:从 `router.resolve(path).matched` **从后往前**取第一条 + `components.default`(前几条是 Layout / Parent 包装),懒加载函数 `await` 后取 `.default`; + 解析失败、路由缺失、chunk 加载失败一律返回 `null`,页面显示「该页面没有注册到当前路由… + 可以改用全屏打开」并保留「全屏打开」按钮,不白屏也不抛异常。 + +弹框头显示目标页名 + 「页内打开 · 不离开一张图」+「全屏打开」+「关闭」;遮罩点击、Esc 关闭。 +连点两个入口时用 `embedToken` 递增丢弃先返回的那次解析结果,避免「后点的页面被前一次的 +异步结果覆盖」。`` 渲染真实路由组件,不是截图/iframe。 + +实测 1920×1080 四个页面在弹框里都铺满 `.om-embed-body`(969px),`scrollHeight == clientHeight` +无内部滚动:专题分析 969、视频监控 944、综合监测 969、车辆追溯 969。样式上用 +`:deep()` 把 `.topic-analysis / .vm-page / .mon / .vt` 的高度钉成 `100% !important`, +否则这些页面按整屏高度算会溢出弹框。 + +**3. 车场气泡换成圆润泪滴** + +原来是一个圆 + 一个三角尾巴(`M20 30 L26 50 L32 30 Z`),放大后接缝处是折角。现在整块 +SVG 抽到 `src/views/gov/oneMap/marker-svg.ts`(纯函数,门禁可直接断言): + +| 部件 | 改前 | 改后 | +|---|---|---| +| 外轮廓 | 圆 + 三角形两条路径 | 一条贝塞尔泪滴 `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) | +| 头部 | 圆 r=15,圆心 (26,19) | 圆 r=16,圆心 (26,20);路侧从 30×30 rx=9 改成 32×32 rx=12 | +| 选中圈 / 重点虚线圈 | r=20 / r=18,(26,19) | r=20 / r=19,(26,20) | +| 尖点 | y=50,marker 锚 `bottom-center` | 不变(画布 52×52,`LOT_ICON_SIZE`) | + +头部形状盖在泪滴上,露出来的只有平滑尾巴;描边 `stroke-linejoin="round"`,整体比三角尾巴 +宽一圈、没有直边。聚合气泡(`clusterBubbleSvg`)仍是同心圆,只做了搬家。 + +**4. 顺带修 1366 顶栏重叠** + +`@media (max-width: 1500px)` 下右栏内容(搜索 118 + 区县 174 + 三个按钮 + 间距 ≈ 488px) +超过 1fr 列宽(≈396px),向左溢出压住居中的标题。改成 `grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.25fr)`, +搜索框 96px、区县下拉 `max-width: 128px`;实测右栏首个控件 x=935,标题右边界 899,不再相交。 + +门禁:新增 `tests/verify-one-map-nav-embed.mjs`(导航过滤 + 路由组件解析/兜底)与 +`tests/verify-one-map-lot-bubble.mjs`(泪滴路径无直线命令、尖点仍压 y=50、圆形/路侧两种头、 +角标 999+、选中圈与虚线圈同心、聚合仍是圆)。前端仓库共 **23 个 `tests/verify-*.mjs` 全绿**, +`eslint` / `prettier --check` 通过。 + +### 7.25 喀喇沁旗纳入有数据区县 + 弹框去表头(2026-11-07) + +**1. 喀喇沁旗(第 4 个有数据的旗县区)** + +`demo.ts` 的 `COVERED_DISTRICTS` 从 3 个改成 4 个:`红山区 / 松山区 / 元宝山区 / 喀喇沁旗`。 +喀喇沁旗**保留自己的 6 条种子**(锦山镇中心 / 喀喇沁旗医院 / 王爷府景区 / 锦山大街路侧 / +喀喇沁旗政府 / 美林谷景区),不再外迁;其余 43 条种子继续按原节奏迁进前三个区。 + +| 项 | 改前 | 改后 | +|---|---|---| +| 有数据的区县 | 3(红山 / 松山 / 元宝山) | **4**(+ 喀喇沁旗) | +| 车场分布 | 39 / 23 / 19 | **36 / 21 / 18 / 6**(合计 81 不变) | +| 泊位总量 | 16,220 | **不变** | +| 覆盖率 | 3/12 = 25% | **4/12 = 33.3%** | + +页面全部按数据推导,不需要改文案:KPI「覆盖旗县区 4/12」、右栏 4 张区县卡片、 +「其余 8 个旗县区暂无数据」+ 8 个灰色 chip、区县下拉里喀喇沁旗由 disabled 变为可选。 +地图取景按车场坐标算,自动把锦山镇一带纳入视野。 + +门禁:`tests/verify-one-map-districts.mjs`(covered 4 个、分布 36/21/18/6、喀喇沁旗 bbox、 +「锦山」只允许出现在喀喇沁旗车场名里、覆盖率 4/12)、`tests/verify-topic-analysis.mjs` +(红山区 36、喀喇沁旗 6)。 + +**2. 弹框去掉自己的表头** + +目标页本来就有标题栏(监管中心 / 综合监测…),弹框再加一条 44px 表头,等于标题写两遍, +还把页面标题压掉一截。现在去掉 `.om-embed-head`,关闭收成右下角浮层「全屏打开 / ✕」, +Esc、点遮罩仍然可关。 + +**3. 修弹框里页面标题被切的问题** + +专题分析 / 综合监测 / 车辆追溯 / 视频监控 这四页都用 +`margin: calc(-1 * var(--app-content-padding)) ... 0` 顶到 app 内容区边缘。放进弹框后 +整页向左上偏 20px,「综合监测」四个字的上半截被弹框上沿切掉。在 `.om-embed-body` +里统一 `margin: 0 !important; width: 100% !important`,四页标题均完整显示, +弹框 body 高度 1013px、`scrollHeight == clientHeight`。 + +### 7.26 一张图全屏后把空白吃掉(2026-11-07) + +点「全屏」后顶栏消失,内容区比窗口模式高 136px(1920×1080:813 → 949),而三张左栏卡片 +和右栏列表都是固定高度,多出来的一截就空白地堆在栏底;区县卡片右半边除了进度条也是空的。 +四处一起改,全部用页面已有的数据补,不新造口径。 + +**1. 左栏三张卡分摊剩余高度** + +`.om-left > .om-sec` 改 `flex: 1 0 auto`(只吃剩余高度、**绝不压缩**),卡内 +KPI 宫格 / 资源清单行 / 图层行改成 `flex: 1 1 auto` + `grid-auto-rows: 1fr` 均分。 +全屏下卡片从 150/427/175 变成 229/408/255,正好铺到页脚。 + +> `flex-shrink` 必须是 0。第一版写的是 `flex: 1 1 auto` + `min-height: 0`, +> 1366×768 下列高不够,三张卡被压到互相盖住,「覆盖旗县区」和「实时告警」两行直接压在 +> 下一张卡的标题上。改回 0 之后,矮屏沿用原有行为:卡片保持自然高度,左栏内滚动。 + +**2. 右栏进出场:行均分 + 底部「进出场结构」** + +一次刷新只有 7~8 条抓拍,`slice(0, 8)` 之后右栏底部空一大截。列表行 `flex: 1 1 auto` +(`min-height: 46px`,保证 42px 抓拍图压不扁),底部再加一块结构统计: +进场 / 出场条数(占比条)、免费放行 / 收费条数、收费合计。 + +数字来自同一份列表,口径在 `src/views/gov/oneMap/access.ts#summarizeAccess`: +`¥8.00` → 8、`—` → 没有金额,出场且无金额算免费;进场还没结算,不参与费用统计。 +结构统计用**只按区县过滤**的那一份(`districtAccess`),页签切到「进场」时不会变成「出场 0」。 + +**3. 区县卡片右侧补空余环形** + +卡片原来只有左半边有字,右半边空着。改成两栏:左边名称 / 占用率 / 「960 泊位 · 占用 758」, +右边一个环形,弧长 = 空余占比、颜色沿用全站「空闲」三档色、环心是空余泊位数 +(喀喇沁旗 202、红山区 1,978、松山区 1,167、元宝山区 889)。 +拆分口径在 `districts.ts#berthSplit`:占用 = 总泊位 − 空余,空余大于总数时占用兜底为 0, +总泊位为 0 时占比是 0 不是 NaN。 + +**4. 门禁** + +`tests/verify-one-map-panel-fill.mjs`:`berthSplit` 的占用/空余/占比、0 泊位与空余超总数的 +边界;`summarizeAccess` 的进场/出场、免费/收费、`¥` 解析、只有进场、空数组、 +出场缺金额字段;再接一遍演示数据确认不出现 NaN。前端门禁 23 → **24 个**。 + +实测:1920×1080 全屏下左栏 229/408/255 铺满、进出场 8 行各 86px + 结构块贴底 +(卡片底 1022 / 页脚 1032);1366×768 左栏卡片不再互相压住,溢出交给栏内滚动。 + +### 7.27 一张图地图上加「基本指标」浮条(2026-11-07) + +地图上方原来只有图例和右下四个入口按钮,顶上正中一整条是空的。压一条 5 个指标的浮条: +**车场 / 总泊位 / 剩余泊位 / 占用率 / 周转率**,左侧带当前范围(`全市` 或具体旗县)。 + +口径在 `src/views/gov/oneMap/map-kpi.ts#buildMapKpis`,由 +`tests/verify-one-map-kpi.mjs` 守住: + +| 指标 | 算法 | 显示 | +|---|---|---| +| 车场 | `filteredLots.length` | 整数 +「个」 | +| 总泊位 / 剩余泊位 | 车场逐个累加 | 整数 +「个」 | +| 占用率 | (总泊位 − 剩余) / 总泊位,夹到 0~1 | 1 位小数 + `%` | +| 周转率 | **按泊位加权**:Σ(周转率×泊位) / Σ泊位 | 2 位小数 +「次/日」 | + +- 周转率必须加权。车场之间泊位差两个量级(6 泊位 vs 2000 泊位),直接平均会让小场和大场 + 等权,全市数字没有意义;门禁里 100/300 两个车场的用例专门断言 2.25 而不是平均的 2.0。 + 0 泊位的车场不进分母,避免除零。 +- 单位和小数位与「专题分析」一致(`次/日`、两位小数),占用率只留 1 位——浮条是扫一眼的, + 多一位反而看不清。 +- 数据源和左栏资源清单同一个 `filteredLots`,**跟着区县下拉一起变**,不另算一遍。 + 实测:全市 81 场 / 16,220 / 4,236 / 73.9% / 1.52 次/日;切到红山区 36 场 / 7,830 / + 1,978 / 74.7% / 1.56 次/日,和红山区区县卡片的数字对得上。 +- 第一次排出来 534px 宽,1366×768 下几乎贴到左栏。`@media (max-width: 1500px)` 把每格 + `min-width` 96 → 74px、字号 20 → 17px,压到 415px(x=576,左栏右边界 511),不再打架。 + +前端门禁 24 → **25 个**。全屏(1920×1080)下浮条同样居中压在顶边,和「运行总览」卡片、 +右侧进出场卡片都不重叠。 + +### 7.28 一张图切到真实接口:109 个车场,台账和点位分开报(2026-11-09) + +一张图原来读 `oneMap/demo.ts` 夹具(81 场 / 16,220 泊位),页面上挂着「演示数据」标记。 +这一轮把数据源换成后端聚合接口,页面上所有数字都来自 `parking_cloud_yudao`。 + +#### 7.28.1 接口 + +| 项 | 值 | +|---|---| +| 路径 | `POST /park/resource-map/overview` | +| 控制器 | `parking-module-park` · `ParkResourceMapController` | +| 权限 | `park:park-lot-manage:query`(**不自动授权任何角色**,角色 2001 对 `park:dashboard:query` 的拒绝状态不动) | +| 入参 | `{ region?: string }`,空 = 全市,否则按 `parking_lot.pl_country_name` 收窄 | +| 出参 | `summary` / `districts` / `lots`(含坐标)/ `resources`(台账计数)/ `devices` / `access` | + +聚合 SQL 全在 `ParkResourceMapMapper.xml`: + +- `selectLots`:`data_status = 1 AND state = 1 AND pl_city_name = '赤峰市'`; + 每个车场只取一条 `parking_lot_geo`(按 GCJ02 → 百度 → WGS84 优先级), + 0/0 或缺失坐标的车场由前端丢弃。 +- `selectResourceCount`:路侧泊位(`tp_p_berths`)、地磁(`tp_p_eqp.eqp_type = 1`)、 + 充电桩(`tp_p_charging_pile`)六个计数子查询,**全部 `INNER JOIN parking_lot` + 并跟随 `region`**,和车场、设备点位用同一套范围口径;不关联会报出别的旗县区的台账。 +- `selectRecentAccess`:只扫当天订单,`ORDER BY GREATEST(park_in_time, park_out_time) DESC`, + Service 把每条订单拆成进场 / 出场两条事件。 + +#### 7.28.2 数据实况(本地 2026-11-09) + +| 指标 | 数值 | +|---|---| +| 在用停车场 | **109**(12 个旗县区全有,红山区 40) | +| 登记泊位 / 空余 | 16,210 / 4,655,占用率 **71.28%** | +| 路侧泊位台账 | 2,575(占用 1,687),**`tp_p_berths` 没有坐标列**,一个都上不了图 | +| 地磁设备 | 台账 2,097(在线 270 / 故障 119),**有坐标能上图的 673** | +| 视频 / 充电桩 / 告警 | 0 / 0 / 0,接口如实返回 0,页面显示 0 与空态 | +| 红山区今日进出 | 0:该区订单停在 2026-11-06,不是接口算错 | + +#### 7.28.3 台账 ≠ 点位,两套数字分开报 + +`catalog.ts#buildCatalog` 新增可选 `ledger`(接口的 `resources`): + +- **有台账时一律按台账报数**:路侧泊位 2,575、感知设备 2,097;副信息用 + 「占用 / 空闲」「离线 / 故障」,离线 = 总数 − 在线 − 故障,异常数据兜底成 0; +- 没有台账时退回数组计数(演示夹具、单测); +- 地图只画有坐标的点(673 台地磁),**台账数和点位数永远不互相代替**; +- 点「路侧泊位」行不切图层,改为 toast: + 「路侧泊位台账 2,575 个,暂无路段坐标,不能在图上定位」; +- 左栏「未定位资源」弹窗列出 2 条:路侧泊位 2,575 个缺路段坐标、 + 地磁 1,424 台缺坐标(2,097 − 673),提示「补录坐标后自动上图」。 + +`tests/verify-one-map-catalog.mjs` 增加台账模式断言(2,575 / 1,687 / 888、2,097 / 1,708 / 119、 +视频充电告警为 0、离线不为负),并把标签改成「路侧泊位」。 + +#### 7.28.4 前端改动 + +- `mapper.ts#toPageModel`:DTO → 页面模型;坐标统一转 GCJ02;12 个旗县区固定补齐, + 接口没返回的按「0 车场」处理,不是「占用率 0」; +- `index.vue`:去掉「演示数据」标记、去掉每秒轮换的假进出场,改为 + `loadData()` + 60 秒静默刷新;失败时顶部显示「数据加载失败」,刷新按钮给 toast; +- 区县下拉第一次用全市结果填充(`cityDistricts` 只在未选具体区县时覆盖), + 选中一个区不会把另外 11 个标成暂无数据; +- 「设备在线率」在台账为 0 时显示 `—`(松山区这类还没有地磁的区县不显示 0.0%); +- 进出场记录没有抓拍图时显示「进 / 出」占位,不渲染裂图; +- 区县态势 12 张卡片改为列表内滚动,「数据覆盖」块固定在底部; + 未覆盖区县为 0 时不再输出「其余 0 个旗县区暂无数据」; +- 车场聚合**去掉缩放下限**:全市视野的取级随容器尺寸在 6~8 级之间浮动, + 写死 `>= 8` 会让车场整体消失;聚合本来就按 60px 屏幕格子分组,任何取级都不会叠压。 + +#### 7.28.5 类型与门禁 + +`vue-tsc` 在新 `oneMap/` 文件里报过的 6 个错误全部修掉(`PAGE_NAV` 未使用、 +`berthPoints` 早返回后的死比较、`berths: []` 空元组被推断成 `never[]`、 +`nav-embed` 的 `components` 未允许 null)。仓库其余页面原有类型错误不在本轮范围。 + +前端门禁 **26 个全绿**(含 `verify-no-pending-wording`,扫描 3,230 个文件), +eslint 对改动文件无告警。 + +#### 7.28.6 实测 + +- 全市:109 场 / 16,210 泊位 / 4,655 空余 / 71.3% / 1.04 次/日; + 资源清单 109 / 16,210 / 2,575(占用 1,687 · 空闲 888)/ 2,097(离线 1,708 · 故障 119) + / 0 / 0(桩)/ 0; +- 红山区:资源台账与全市相同(泊位和地磁全在红山区),今日进出场 0 条; +- 松山区:资源台账全 0,设备在线率显示 `—`; +- 区县态势 12/12、覆盖 100.0%,卡片可滚动、底部覆盖块不重叠; +- 重点车场 Top6、实时告警空态、未定位资源弹窗、1366×768、全屏 1920×1080 均正常。 + +### 7.29 专题分析切到真实接口:7 个专题一次一个聚合(2026-11-09) + +`/park-map/analysis` 是最后一张仍读夹具的大屏(视频监控除外,等视频基础设施)。本期按设计方案 §8 的 P2 契约落地:一个专题一个只读聚合接口,前端 `mapper.ts` 把 DTO 转成页面快照,KPI / 结论 / 排行 / 分布 / 散点仍由 `assembleSnapshot` 统一生成。 + +#### 7.29.1 接口 + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/admin-api/park/topic-analysis/coverage` | 车场、泊位、设备在线结构;忽略日期 | +| POST | `/admin-api/park/topic-analysis/duration` | 订单数、已结算数、实收、欠费、平均时长、周转 | +| POST | `/admin-api/park/topic-analysis/flow` | 车场进出场 + 全天汇总 + 24 小时帧 + 日趋势 | +| POST | `/admin-api/park/topic-analysis/heat` | 车场结构 + 24 小时帧 | +| POST | `/admin-api/park/topic-analysis/hotspot` | 车场点位 + 区间车次,热点网格前端算 | +| POST | `/admin-api/park/topic-analysis/price` | 首小时价、当前占用;忽略日期 | +| POST | `/admin-api/park/topic-analysis/revenue` | 实收、订单、日趋势 | + +- 请求:`{"district":"红山区","beginDate":"2026-11-01","endDate":"2026-11-07"}`;`coverage` / `price` 前端不发日期,后端也忽略。 +- `district` 空串 = 全市;日期必须 `yyyy-MM-dd`,`beginDate <= endDate`,最长 366 天。参数错误由 Controller 自己翻成 `CommonResult.error(400, 中文提示)`,不交给 yudao 的 `GlobalExceptionHandler`(它会当未知异常返回 500,文案会丢)。 +- **没有实现设计稿里的 `/overview`**:每个专题接口已经带回该专题全部车场明细,KPI / 结论文案由前端从同一份 `TopicLotMetric[]` 生成;再开一个 overview 会把同样的聚合算两遍,且两处口径容易漂。`unavailable` 字段保留在响应里,当前恒为空数组。 + +#### 7.29.2 数据源裁定:只读 `order_park` + +设计稿 §8.3 原计划读 `park_in_out_record`、`parking_running_statistic`、`duration_statistic_park_*`、`income_statistic_park_*`。实测本地副本库: + +- `parking_running_statistic` 只有最近 4 天; +- `duration_statistic_*` / `income_statistic_*` 只有 2018 年演示数据; +- `order_park` 有 667,559 行(2026-09-06 起),12 个区县里红山区 650,738 行是历史,其余区县主要是 2026-11-09 当天 16,821 行。 + +**裁定:时间类专题统一读 `order_park`(`data_state = 1`)**。进出场分别按 `park_in_time` / `park_out_time` 落区间,营收、订单、时长按 `create_date` 落区间。代价是 366 天长区间的 `flow` / `heat` 会扫两遍时间列,上线前需按生产规模压测;本地 7 天区间实测 0.16–0.85 秒,`coverage` 89 毫秒。 + +#### 7.29.3 口径 + +- 在用停车场:`parking_lot.data_status = 1 AND state = 1 AND pl_city_name = '赤峰市'`,区县筛选走 `pl_country_name`。 +- 泊位总数:`COALESCE(temp_berth_num, temp_pl_berth_num, pl_berth_num)`,与一张图一致。 +- 实时空余:`parking_lot_berth_temp_info.free_berth`;实测 109 个在用场全部有行且非空。前端仍按「缺省 = 全部空闲」兜底,不把未知当满位。 +- 实收:只算已结算状态 **40 / 50 / 53 / 70**,金额 `order_act_fee - COALESCE(refund_act_fee, 0)`。 +- **`back_act_fee` 不计入**:本地库有明显超量样本(单笔 ¥1,448,同单应收 ¥15),加进来会污染营收。 +- 欠费率:`arrearage_fee / (order_total_fee + arrearage_fee)`;应收、欠费都不进营收。 +- 平均停车时长:`parking_duration > 0` 的订单加权,单位是**秒**。 +- 周转率:区间进场次数 ÷ 泊位数 ÷ 区间天数(次/日)。 +- 首小时价:每场取当前生效规则集(`data_state = 1`、`check_state = 3`、`effect_time <= NOW()`、未过期),小型车(`car_type = 2`)优先,汇总覆盖前 60 分钟的计时档位;没有规则返回 `null`,页面显示「未配置」。`t_billing_*` 三张表排序规则不一致,连接处显式 `COLLATE utf8mb4_bin`。 +- 当前价格实况:109 个在用场里 **38 个首小时 ¥3(中档)**,71 个没有首小时计时价;价格极差 ¥0.00 是真实结果,不是页面算错。 +- **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` 行。**本地这个比例不能当成生产配置率**。若以后要把「按次」车场也纳入价格分布,需要单独定口径(首小时价与按次价混排会抬高/扭曲平均值),不是当前页面算错。 + +#### 7.29.4 前端 + +- `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` 仍导出但页面不再使用。 +- 新增 `mapper.ts`:DTO → `ParkingLot` / `TopicLotMetric`;坐标按 `coordType` 1 百度 / 2 WGS84 / 3 GCJ02 统一转 GCJ02;首小时价回写成 `首 60 分钟 X 元`,档位仍由 `parsePriceTier` 判。 +- 24 小时帧补成 **24 × 全部车场**:热度 = 占用率 ×(45% 底 + 55% 该小时活动占比),没有活动的车场仍有占用率底座,不会在地图上消失。后端只返回有活动的「车场 × 小时」。 +- 日趋势按自然日补齐:区间里没订单的那天是 0,不是「消失」;长区间不抽样。 +- 热点网格仍由 `aggregateHotspots` 算(约 1km 网格,取车次前 8)。 +- 新增 `request.ts` 放纯函数 `buildRequestBody`(不带 axios / Element Plus),`src/api/park/topicAnalysis/index.ts` 负责请求与 `toSnapshot`。 +- `useTopicAnalysis.load()` 改成真实请求,保留请求序号竞态保护;失败时清空快照并显示后端文案 + 「重试」。 +- `index.vue` 不再从演示夹具 `parkingLots` 判断「暂无车场」——库里 12 个旗县区都有在用场,下拉直接全开。 + +#### 7.29.5 门禁与实测 + +- 新增 `tests/verify-topic-analysis-mapper.mjs`(Vite SSR):坐标转换、缺 `berthFree` 不判满位、价格文案与档位、24 帧补齐、热度公式、日趋势补零、热点网格、请求体(结构类不带日期)。全部 28 个 `tests/verify-*.mjs` 通过。 +- `npx eslint src/views/gov/topicAnalysis src/api/park/topicAnalysis` 无告警;`npm run ts:check` 仍只有 §11 第 7 条那 28 个存量错误,本页 0 新增。 +- 浏览器实测(admin,48083):7 个专题全部 `200`,页头无「演示数据」角标。 + - coverage:109 场 / 16,210 泊位 / 平均 148.7 泊位 / 智能设备覆盖 13.8%; + - flow(近 7 天):进场 51,703 / 出场 39,555,峰值 08:00 12,559,与独立 SQL 一致;24 小时与 7 天趋势均渲染; + - duration:平均 58 分钟、中位 53、周转 0.88 次/日;时长分布 40 / 47 / 22 / 0 / 0; + - heat:平均占用率 70.6%、高热车场 30 个、最热 08:00、当前空余 4,655; + - hotspot:8 个网格、最热 963 车次、覆盖 10 场、周边空余 1,139; + - price:38 场 ¥3、71 场未配置,平均 ¥3.00、极差 ¥0.00; + - revenue:今日 ¥12,767 / 16,821 单;近 7 天 ¥50,127 / 51,703 单。 +- 区县筛选(红山区,近 7 天)¥37,360 / 34,882 单;空区间(2026-09-01~02)KPI 全 0、日趋势两点补零、不报错、不显示演示角标;1366×768 与 1920×1080 均无新增版式问题。 +- 权限:菜单 100501 已存在且只授予角色 2001,管理员走超管旁路;本次**没有新增权限点、没有授权任何其他角色**。 + +#### 7.29.6 366 天区间性能(2026-11-09) + +初始实测(本地副本、一个 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` 定位到两处: + +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 秒。 +2. **重复扫描**:`flow` 原来同时查 `selectLotFlow`(按车场汇总)与 `selectHourlyFlow`(按车场 × 小时),后者逐小时求和与前者完全等价(对拍 0 差异)。服务层改为 `toFlowRows(hourRows)` 直接复用小时间行,少两次 `order_park` 扫描;`heat` 同样只查一次。 + +优化后(同一 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。 + +试过不采用: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 复测。 + +### 7.30 一张图加「重点车场」「实时流量」入口 + 去掉「未定位资源」+ 去掉弹框「全屏打开」(2026-11-09) + +**导航加两个入口**:`src/views/gov/oneMap/nav.ts` 的 `PAGE_NAV` 从 5 项变 7 项,新两项排在「专题分析」之后, +和专题分析 / 视频监控 同一条按钮栏: + +| 入口 | 路径 | 权限码 | 打开方式 | +|---|---|---|---| +| 重点车场 | `/park/keyLotMonitor` | `park:dashboard:query` | 弹框内打开 | +| 实时流量 | `/park/flowMonitor` | `park:flow-monitor:query` | 弹框内打开 | + +**七个入口一律在弹框里打开**(先做过一版 `PageNavItem.direct` 让这两项换页,用户要求改回弹框,`direct` +字段与 `handleNav()` 已删除,纳维按钮回到 `@click="openNav(item)"`):目标页组件直接挂进 `.om-embed`, +路由不变,地图和左右面板都还在。 + +**权限取值**:重点车场页没有独立权限点(走存量车场接口),沿用监管类页面的 `park:dashboard:query` +(菜单 100504 本身也没有 permission 串);实时流量复用已有的按钮权限 100506 `park:flow-monitor:query`。 +**没有新增权限点,也没有给任何角色补授权** —— 无权限时按钮不渲染,符合「有权限才显示」的老口径。 + +**去掉「未定位资源」**:资源清单原来在 7 行之后挂了一个「未定位资源」按钮 + 弹框,点开列出没有坐标的 +点位。真实接口接进来后台账与点位已经分开报(§7.28:路侧泊位 2,575 只报数不上图),这一个入口既没有 +可信数据也没有明确用途,整块删掉:按钮、弹框、`unlocatedResources` 计算属性、`.om-unlocated*` 样式, +以及为了它「没有标题、顶部留白压平」而加的 `.om-left .om-sec.is-last { padding-top: 12px }`(基础档与紧凑档各一条)。 +删掉后左栏最后一张卡是「图层管理」,它有自己的标题,用回正常的 30px / 紧凑档 24px 顶部留白。 + +**去掉弹框右下角「全屏打开」**:`.om-embed-tools` 里只留关闭按钮(✕ / Esc / 点遮罩三种关法不变), +`openEmbedFull()` 与 `.om-embed-full*` 样式一并删除;路由解析失败时的提示语也从「可以改用全屏打开」 +改成「请从左侧菜单打开」—— 弹框里已经没有换页出口了。目标页自带的「全屏」按钮(重点车场 / 实时流量页头) +属于目标页本身,不受影响。 + +门禁:`tests/verify-one-map-nav.mjs` 改成 7 项,并断言没有任何 `direct` 换页入口; +`tests/verify-one-map-nav-embed.mjs` 的地图按钮列表(`mapNavItems`)同步为 6 项。 +实测(1920×1080,admin):按钮栏顺序 专题分析 / 重点车场 / 实时流量 / 视频监控 / 综合监测 / 车辆追溯, +点「重点车场」「实时流量」都留在 `/park-map`、弹框 `aria-label` 对得上、页面正常渲染,弹框右下角只剩 ✕; +资源清单 7 行、无「未定位资源」。`node --test tests/*.mjs` 65 通过 / `npm run ts:check` 28(存量)/ `npm run build:local` 退出 0。 + +### 7.31 前端删掉上游「文档地址」广告条与登录页推广区(2026-11-09) + +上游 yudao 在几乎每个列表页顶上挂一条绿色横幅:``, +渲染成「【功能权限】文档地址:https://doc.iocoder.cn/resource-permission」,点了跳上游官网。这些既不是本项目文档, +也不该出现在交付给使用方的停车平台上,全部删除。 + +**删除范围**(前端仓库 `parking-cloud-yudao-ui`): + +| 位置 | 内容 | +|---|---| +| 350 个 `.vue` / 389 处 | `` 标签整块删除(多行写法一并处理) | +| `src/components/DocAlert/index.vue` | 组件本身删除(删完已无引用) | +| `src/views/Login/components/LoginForm.vue` | 「萌新必读」分割线 + 开发指南 / 视频教程 / 面试手册 / 外包咨询 四个外链 | +| `src/layout/components/UserInfo/src/UserInfo.vue` | 用户下拉菜单的「文档」项与 `toDocument()` | +| `src/config/axios/service.ts` | 901 错误弹窗里的「参考 https://doc.iocoder.cn/ 教程」一行 | +| 注释 7 处 | `infra/server/index.vue` 的排查提示、`mall/promotion/*.data.ts` 的 CrudSchema 文档链接改为不带 URL 的说法 | + +**没有用 `VITE_APP_DOCALERT_ENABLE=false` 关掉**:那只是把横幅藏起来,字符串和组件还在,换个环境变量又会冒出来。 +要的是删干净,所以连组件一起删了。`.env` 里那个开关保留(组件没了它也不再生效),不影响其它逻辑。 + +**门禁**:新增 `tests/verify-no-doc-links.mjs`,扫描整个 `src/`(含注释),` 0`)。 + 页面在 true 时提示换名、不发新增请求;查重接口自身失败**不拦**新增(后端新增本身不校验重名)。 +- **组织**:新增接口无条件用登录人组织覆盖 `orgId/orgName`(本机 admin → 10001 中兴智能交通)。 + 表单不提供组织选择,只显示「保存后归属当前登录人组织」。 + **(2026-11-10 后续已改为新增/修改都可选组织,见 §7.35。)** +- **经纬度可选但必须成对**:后端 `saveParkGeo` 直接对 `lonId` 取 `doubleValue()`,且 + `parking_lot_geo.lon_id/lat_id` 是 `NOT NULL`,缺坐标会 500。为此在 + `ParkingLotServiceImpl.SaveParkingLotInfo` 加了与 `UpdateParkingLotInfo` 一致的判空—— + **两个坐标都有才落地理点**(`⚠️ 手改(非搬迁原样)`,这是本项目第三处非照搬改造)。 + 前端:两个都空允许(该车场不上一张图,表单里有说明),只填一个直接拦截。 +- **照片**:新增时隐藏照片区(照片必须绑定 `plNo`),提示「可在创建后点编辑上传」。 +- **创建后自动按名称筛选**:列表查询 `queryParkLotByCountryAndParkName` 没有 `ORDER BY`(InnoDB 按主键升序), + 新车场会落到最后一页。创建成功后把搜索框置为新车场名称并回到第 1 页,用户立刻能看到刚建的车场 + (搜索框可见,不算隐式行为)。 + +纯逻辑(`createEmptyCreateForm` / `buildCreatePayload` / `validateGeoPair`)放在 +`views/park/parkLotList/edit.ts`,由 `tests/verify-park-lot-edit.mjs` 断言(含「新增不能带 plId」 +「0 是有效泊位数」「只填经度不合法」)。 + +**实测(admin,2026-11-10)**: + +- 新增「本地验证-新增车场A」(不带坐标)→ 成功,`pl_no=P15040490013`,`data_status=1`、`org_id=10001`, + `parking_lot_geo` 无行(判空生效); +- 新增「本地验证-新增车场B」(118.96 / 42.27)→ `pl_no=P15040490014`,`parking_lot_geo` 两条 + (百度 `lon_lat_type=1` + 火星 `lon_lat_type=3`)、`park_free_berth` 一条; +- 重名再提交 → 页面提示「车场名称已存在」,无新增;只填经度 → 表单拦截「经度和纬度必须同时填写」; +- 测试数据已清理:`parking_lot` / `parking_lot_geo` / `park_free_berth` 各删 2 行(按主键 + 编码双条件), + Redis db1 的 `zteits-park:parking_lot:plNo_parkLotInfo`、`park_free_berth:plNo_freeBerth_map` + 字段 HDEL、两个 geo GEO zset 成员 ZREM;车场总数回到 109。 + +**验证**:`mvn -pl yudao-server -am -DskipTests package` 退出 0;`node tests/verify-park-lot-edit.mjs` +全部通过;`npm run ts:check` 仍是 28 条存量基线。 + ## 8. 视频接入(项目要求) +### 8.0 物联网模块:评估后关闭(2026-11-04) + +**结论:不启用。** 本项目只有视频设备接入,而视频取流不经过 IoT(模块内无任何 +GB28181/ONVIF/RTSP 代码),存量地磁/道闸又沿用 `tp_p_eqp`,启用 IoT 只会多出第三套设备台账。 +评估过程记录如下,重新开启时按本节回退。 + +关闭动作(三处代码 + 一处数据): + +| 位置 | 改动 | +|---|---| +| 根 `pom.xml` | 注释 `yudao-module-iot` | +| `yudao-server/pom.xml` | 注释 `yudao-module-iot-biz` 依赖(并删掉下方重复的旧注释块) | +| `YudaoServerApplication` | 注释 `IotStorageInitRunner` 的 import 与 `excludeFilters` 过滤项 | +| `application.yaml` | 注释 `yudao.iot.message-bus` / `yudao.iot.storage` | +| `system_menu` | id=860 及其 73 个子菜单 `visible=b'0'`(父菜单隐藏整棵子树;`routerHelper.ts` 用 `hidden: !visible`,所以 0=隐藏) | + +验证:`mvn -pl yudao-server -am -DskipTests package` 通过,重新打包的 jar 内 +`yudao-module-iot` 条目数为 **0**;48083 重启后 `Started YudaoServerApplication`, +登录、`/system/menu/list`、`/system/user/page`、 +`/park/area/query-area-and-block-tree` 均 `code=0`;侧边栏已无「IoT 物联网」; +`/iot/**` 返回 501「已禁用」。启动日志只剩一条 banner 提示 +`[IoT 物联网 yudao-module-iot - 已禁用]`,无异常。 + +数据库里的 17 张 `iot_*` 表**保留未删**:与停车表无外键关联,不影响任何现有功能; +真要清理时按同名单表 `DROP` 即可。 + +
+评估过程:导入后的实测结果(供将来启用时参考) + +`parking_cloud_yudao` 原先没有 `iot_*` 表,前端点进「IoT 物联网」显示 +「[IoT 物联网 yudao-module-iot - 表结构未导入]」——这是 `GlobalExceptionHandler` 对 +`BadSqlGrammarException` 里含 `iot_` 的统一兜底文案,不是菜单或模块开关问题。 + +已从 `docs/sql/iot-2026-11-02-传播违法.sql.zip` 导入 **17 张表**(`iot_device`、`iot_product`、 +`iot_product_category`、`iot_device_group`、`iot_thing_model`、`iot_device_shadow`、 +`iot_alert_config`、`iot_alert_record`、`iot_data_rule`、`iot_data_sink`、`iot_scene_rule`、 +`iot_ota_firmware`、`iot_ota_task`、`iot_ota_task_record`、`iot_device_modbus_config`、 +`iot_device_modbus_point`、`iot_rule_scene`)。导入前确认目标库**无同名表**,脚本只含 +`iot_*` 的 `CREATE/DROP`,不含 `USE`、不碰 `system_*` / `infra_*` / 停车业务表。 + +导入后实测可用(`/admin-api/*`,admin + tenant-id 1): + +| 能力 | 端点 | 状态 | +|---|---|---| +| 产品分页 / 分类 / 物模型 | `/iot/product/page`、`/iot/product-category/*`、`/iot/thing-model/list` | ✅ | +| 设备分页 / 精简列表 / 地图点位 / 影子 | `/iot/device/page`、`/simple-list`、`/location-list`、`/device/shadow/get` | ✅ | +| 设备分组 | `/iot/device-group/page`、`/simple-list` | ✅ | +| 告警配置 / 告警记录 | `/iot/alert-config/page`、`/iot/alert-record/*` | ✅ | +| 规则引擎(场景联动 / 数据流转) | `/iot/scene-rule/*`、`/iot/data-rule/*` | ✅ | +| 设备消息历史 / 属性历史 / 首页统计 | `/iot/device/message/page`、`/iot/statistics/get-summary` | ❌ 500 | + +失败的两类是同一根因:`yudao.iot.storage.type: tdengine`,时序数据(`device_message`、 +属性历史)走 TDengine/IoTDB,本机没有部署(启动类也按 D4 排除了 `IotStorageInitRunner`)。 +`device_message` 表不在 MySQL 里,所以查询报 `Table 'parking_cloud_yudao.device_message' doesn't exist`。 +`get-summary` 把 MySQL 计数和 TDengine 消息计数拼在一个接口里,一处失败整页显示 `--`。 +**影响范围仅限「消息历史/属性历史/IoT 首页统计」三处**,台账、产品、物模型、告警、规则全部可用。 +接真实设备(需要看上报报文)时再部署 TDengine,并把 `IotStorageInitRunner` 从 `excludeFilters` 移除。 + +另有两点须知: + +- `yudao-module-iot-gateway` 是**独立部署**的协议服务(TCP 8091 / UDP 8093 / WebSocket 8094 / + MQTT 1883 / Modbus 503),`yudao-server` 只依赖 `iot-biz`,**没有启动任何协议端口**。 + 因此本地导进来的 28 台设备不会有真实连接,在线状态停留在数据快照里。 +- IoT 设备管理页是卡片视图(不是表格),与停车侧 `el-table` 风格不同,属于上游原样。 + +
+ +#### 为什么不用 IoT 管存量的停车设备 + +| 指标 | 数字 | +|---|---| +| `tp_p_eqp`(地磁,全部 `eqp_type=1`) | 500 台,带 `berth_no` / `is_online` / 心跳 / `gateway_no` / `rssi` / `mac` | +| `parking_lot_equipment`(车场设备树) | 161 条:LED 显示屏 114、摄像头 46、道闸 1(类型见 `parking_lot_equipment_type`) | +| `tp_p_eqp_log` / `tp_p_eqp_inout_record` | 按月分表,已建到 2027-03 | +| Java 里含 `Eqp` 的类 | 469 个(park 268 / api 149 / order 28 / pay 9) | +| 引用 `tp_p_eqp` 的 mapper XML | 23 | + +存量「设备管理」不是一张台账,是一条业务链:地磁上报 → `tp_p_eqp_log` → 泊位占用 → +停车记录 → 计费 → 在线率考核/巡查/异常稽核,横跨 park、order、pay 三个模块。 +换成 IoT 物模型等于重做这条链,收益(产品/物模型/影子/OTA/规则)对已稳定运行的地磁没有增量价值。 + +`parking_lot_equipment` 里虽有 46 条 `equipment_type=2`(摄像头)记录,但只有 +`equipment_name` / `channel_id` / `area_id`,没有码流地址、国标编码、云台能力, +不能当视频台账用——视频设备的独立建模理由在这里,不在「停车设备管理不好用」。 + 上游 `yudao-module-iot` **没有** GB28181 / ONVIF / RTSP 视频接入能力;它只有设备/产品/物模型/OTA/规则/告警和 TCP/UDP/HTTP/MQTT/EMQX/CoAP/Modbus 协议。 建议方案(待选型): -- 先挂载 `yudao-module-iot` 作为设备接入底座; +- ~~先挂载 `yudao-module-iot` 作为设备接入底座~~ —— 2026-11-04 评估后关闭,见 §8.0; + 取流不经过 IoT,设备台账也不需要 IoT; - 新增 `parking-module-video`,对接开源视频网关: - **WVP-PRO**(GB28181 信令 + 设备管理)+ **ZLMediaKit**(流媒体); - 或直接接 RTSP/ONVIF 的轻量网关。 - 该模块独立于 IoT,通过 `parking-module-api` 暴露视频分页/预览/回放接口。 +若将来确实需要 IoT(接 MQTT 类新传感器、统一告警中心、跨设备规则联动), +按 §8.0 表格里的四处开关重新打开即可,数据表已经就位。 + +### 8.1 WVP-PRO 合并为 Maven 模块(2026-11-09) + +方案见 `docs/superpowers/specs/2026-11-09-WVP-PRO视频能力合并方案.md`。 + +**做法**:把 gitee `pan648540858/wvp-GB28181-pro` 3.0.0(commit `d8c95d0`,MIT)的 +`src/main/java`(806 个类,包名 `com.genersoft.iot.vmp.*`)整体搬进新模块 +`parking-module-video`,与 `parking-module-park` 平级,随 `yudao-server` 打进同一个 jar。 + +**不改 `YudaoServerApplication`**:模块自带 `WvpAutoConfiguration`(`@AutoConfiguration` + +`@ComponentScan` + `@MapperScan`),经 `AutoConfiguration.imports` 注册。视频子系统可用 +`parking.video.enabled=false` 整体关掉,关掉后 WVP 的 Controller/Mapper/定时任务一个都不进容器。 + +| 关键点 | 结论 | +|---|---| +| 同名类几十个(`ChannelController`、`DeviceMapper`…) | 扫描与 Mapper 扫描都用**全限定类名**做 Bean 名(`FullyQualifiedAnnotationBeanNameGenerator`),与存量 `LegacyMapperBeanNameGenerator` 同一思路 | +| `@DependsOn("sipLayer")` 按名字引用 | 唯一受影响的按名引用:把 `SipLayer` 排除出扫描,在自动配置里声明 `@Bean sipLayer()` | +| `interface-authentication` | **必须保持 `true`**。置 `false` 时 `JwtAuthenticationFilter` 会塞一个"principal 为 null 但 authenticated=true"的令牌,等于把 `/api/**` 全开 | +| 视频平台的账号 | **不建 `wvp_user` 种子账号**。`/api/user/login` 免鉴权,admin/admin 等于公开后门;身份统一来自 yudao(`VmpYudaoSsoFilter`) | +| Redis | 单实例跑内存门面(`InMemoryStore` + `LocalMsgBus`);`RedisRpcConfig` 的模式判断改读 `parking.video.redis-mode`,否则它会误判成 Redis 模式并对 `null` 模板发消息 | +| JDK 21 API | 虚拟线程、`SequencedCollection` 共 6 处 → 新增 `VmpConcurrency`(有界平台线程池)替代 | +| SIP 生命周期 | 新增 `@PreDestroy` 停协议栈。JAIN-SIP 起非守护线程,原版上下文启动失败后 JVM 退不出去,5060 被自己占住 | +| `System.exit(1)` | `SipLayer` 里两处改抛 `IllegalStateException`,让 Spring 走正常关闭(原写法跳过数据源/Redis 收尾) | +| 建表 | `docs/sql/2026-11-09-WVP视频接入-wvp表初始化.sql`,22 张 `wvp_*` 表,幂等,无 DROP/INSERT,只对 `parking_cloud_yudao` 执行 | + +**坑:WVP 原版 `WebSecurityConfig` 上的 `@EnableGlobalMethodSecurity` 必须删掉**(2026-11-09)。 +它注册的是 Spring Security 5 时代的 `MethodSecurityInterceptor`,该拦截器**构造时就捕获了 +当时的 `SecurityContextHolder` 策略**;本项目 yudao 与 parking 两个 starter 各注册了一个 +`setStrategyName` 的 `MethodInvokingFactoryBean`,最终生效的策略与它捕获的不是同一个 +ThreadLocal。表现是 yudao 的 `@EnableMethodSecurity` 校验通过后,遗留拦截器从另一个 holder +读到空认证,抛 `AuthenticationCredentialsNotFoundException`,**所有 `@PreAuthorize` 接口** +(`/admin-api/park/**`、`system/user/page`、`system/role/page`、`infra/config/page`…)统统变成 +`code=500 msg=系统异常`,与具体业务无关,很容易误判成数据问题。WVP 自身没有任何 +`@PreAuthorize`(鉴权粒度只有"是否登录",见 `VmpYudaoSsoFilter`),删掉不影响视频平台, +方法级权限由 yudao 的 `@EnableMethodSecurity` 统一处理——**不要再加回 `@EnableGlobalMethodSecurity`**。 +另注:`POST` 带非法请求体时返回 400 是 `@Valid` 参数解析先于方法级鉴权,这种 400 不能证明鉴权正常。 + +**验收**:打包通过;`Started YudaoServerApplication in 18.187 seconds`;`POST /api/user/userInfo` +带 yudao 令牌 → `code=0`,无令牌 → **401**;`GET /api/device/query/devices` → `code=0`; +Ctrl-C 后 `[SIP SERVER] 协议栈已停止`,5060/48083 释放。 + +**外部依赖**:ZLMediaKit 不在本仓库(C++ 独立进程),未部署时启动日志每 30 秒一条 +`[ZLM-尝试连接]失败`,属预期;设备注册与目录同步不受影响,只是点播不出图。 + +**前端**:实时预览 / 通道树 / 回放 / 云台已移植到 `parking-cloud-yudao-ui` +的 `/video/monitor`(`src/api/video/wvp.ts`、`videoMonitor/wvpSource.ts`、 +`components/WvpPlayer.vue`)。接口走 WVP 原生 `/api/**` + yudao 令牌,用独立 axios +实例直连 `${VITE_BASE_URL}/api`,**不需要 Vite 代理**(后端 CORS 已放行)。播放器 +WebRTC(WHEP)优先、HLS(video.js)回退,不引入上游 jessibuca。 + +行为要点:视频平台没有通道时页面自动退回演示夹具(角标「演示数据」),有通道时角标 +「国标视频平台」;点播/停播按宫格变化对账,不轮询;回放先查真实录像清单再起流, +拖动走 `playback/seek`;页面被 keep-alive 缓存,切走用 `onDeactivated` 停流。 + +抓图与片段下载:真实画面把 `