MIGRATION.md 181 KB

parking-cloud-yudao 迁移手册

新工程根目录。老工程 /Users/wangbiao/IdeaProjects/parking/parking-cloud 与老服务 48082 保持原样,本工程的任何操作都不改它。 所有新文档都放在本目录下;设计稿见 docs/superpowers/specs/2026-11-02-yudao-replatform-design.md。

0. 两个新仓库

仓库 路径 来源 版本
后端 parking-cloud-yudao chifeng/ruoyi-vue-pro 的 master-jdk17 commit 0b1889a8e8,Boot 3.5.15 / JDK17 / MP 3.5.17
前端 parking-cloud-yudao-ui yudao-ui-admin-vue3 最新 2026.10-snapshot,Vue 3.5.34 / EP 2.13.7

1. 当前进度

  • [x] 新建后端仓库(master-jdk17)
  • [x] 新建前端仓库(官方最新)
  • [x] 设计稿落盘
  • [x] 数据库拷贝 + 上游表准备(parking_cloud_yudao,重名旧表改 pcbak_system_*)
  • [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,构建通过)
  • [x] 后端单 Jar 在 48083 启动成功,登录 + 存量接口冒烟通过(2026-11-02)
  • [x] 全页面运行巡检:58 个停车页 + 12 个系统页,code != 0 调用 0 个(2026-11-02)
  • [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)
  • [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 处 <doc-alert>(「【功能权限】文档地址:https://doc.iocoder.cn/…」)连同组件一起删除,登录页推广区、用户菜单「文档」项、901 错误弹窗的教程链接同步去掉;新增 tests/verify-no-doc-links.mjs 守着,src/ 里再出现 <doc-alert 或 doc.iocoder.cn 即失败(2026-11-09,见 §7.31)
  • [x] 组织管理只保留「赤峰市住建」+「赤峰中智」:system_org 152 个有效组织里软删其余 150 个(del_flag=1,与 OrgServiceImpl.deleteOrg 的 @TableLogic 行为一致),system_dept 镜像同步软删;唯一挂在被删组织(赤峰市医院事业部 10228)下的车场 12303 改挂赤峰中智;4 个用户都在 10001/10003,不受影响(2026-11-09,见 §7.32)
  • [x] 车场列表支持「修改车场 + 车场照片」:/park/park-lot 行操作新增「编辑」弹窗(17 个字段,不传经纬度、组织原样回传),照片支持上传/删除;编辑与照片端点全部复用已迁好的 ParkLotManageController,不新增权限;本机新增磁盘存储实现(parking.legacy.oss.mode=local + @PermitAll 读取端点),生产不配则仍走阿里云 OSS / FastDFS(2026-11-10,见 §7.33)
  • [x] 车场列表支持「新增车场」:/park/park-lot 工具栏新增按钮,复用 save-parking-lot-info(:create,权限记录 310003 早已存在),编码由后端 generatorParkNo(state) 生成、组织按登录人回填;经纬度选填但必须成对,后端补了与修改接口一致的判空,避免不填坐标就 500(2026-11-10,见 §7.34)

2. 关键决策(已定)

编号 决策
D1 独立新仓库,不原地改老工程
D2 业务代码保留 com.zteits.* 包名与 parking-module-* artifactId
D3 框架层用 yudao-framework;只保留 parking-common + parking-legacy-common 作兼容层
D4 上游 yudao-module-system 接管登录/权限/菜单/字典/日志;parking 系统模块按「丢弃 / 保留 / 桥接」三类拆分
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
D5 新工程连 parking_cloud_yudao(parking_cloud 的拷贝);原库不动
D6 业务菜单 id 整体 +100000 导入,避免与上游菜单 id 冲突
D7 前端只搬业务 views / api / 自研组件,框架壳用上游
D8 业务 Controller 继续返回 com.zteits.parking.framework.common.pojo.CommonResult;JSON 与上游一致
D9 保留 pagehelper 6.1.1 + jsqlparser 4.9,约 700 处 PageHelper.startPage 不动
D10 启动类扫描顺序:com.clouds.common.redis/threadpool/web/oss → cn.iocoder.yudao.server/.module → com.zteits.*
D11 组织 id 直接沿用老 sys_org.id:system_org 为主表,system_users.dept_id 指向真实组织;system_dept 只作 yudao 用户模块的镜像(OrgServiceImpl 写穿透),不再维护两套组织数据,也不再需要 parking.legacy.org-id-mapping

3. 数据库方案(安全优先)

新库 = parking_cloud_yudao,是 parking_cloud 的完整拷贝。

  1. mysqldump parking_cloud → 还原到 parking_cloud_yudao。
  2. 在拷贝里把 12 张与上游重名的旧表改名为 pcbak_system_*(system_org 是停车专有表,保留原名)。
  3. 从上游模板库 yudao_template 导入 system_* / infra_* / iot_* / QRTZ_*。
  4. 旧管理员密码写入新 system_users id=1,登录密码不变。
  5. 旧业务菜单(id>=153 的 345 条 + parking 专有系统页)以 id+100000 导入,parent_id 按规则重映射。
  6. 旧字典(5 类 / 17 条)以新 id 导入新 system_dict_*。
  7. 原库 parking_cloud 完全不动,老服务 48082 继续可用。

脚本:scripts/migrate-db.sh(拷贝 + 建表)、scripts/migrate-menus.sql(菜单/字典/管理员)。

4. 模块范围

4.1 保留的上游模块

yudao-dependencies
yudao-framework            # 全部 starter
yudao-module-system
yudao-module-infra
yudao-module-iot           # core / gateway / biz
yudao-server

4.2 搬迁的 parking 模块

parking-framework/parking-common          # 67 个,兼容 POJO
parking-framework/parking-legacy-common   # 567 个,存量工具
parking-module-api                        # 2581 个,存量契约
parking-module-park                       # 1150
parking-module-order                      # 1018
parking-module-pay                        # 977
parking-module-bill                       # 413
parking-module-charge                     # 67
parking-module-business                   # 100
parking-module-datacollect                # 1(空实现)
parking-module-system-legacy              # 从 parking-module-system 剥出的停车专有系统代码

4.3 系统模块三分法

  • 丢弃(上游已有):auth / user / role / menu / permission / dict / notice / operate-log / oauth2。
  • 保留(停车专有):org / refresh-cache / tb-work-order / tb-work-order-process / aliyuncs-ecs / message-center / app-news。
  • 桥接(存量引用):UserDO / MenuDO / RoleDO / OrgDO / OperateLogDO / PermissionService 等。

已核对冲突:parking 有 8 条 /system/* 路由与上游重名,全部属于「丢弃」类;保留类路径与上游不冲突。

5. 兼容层清单(业务模块只引用 36 个唯一类)

类别 处理
common.*(CommonResult / PageResult / PageParam / ServiceException / 枚举 / 校验 / 工具) 保留 parking-common 原类
BaseDO / BaseMapperX / LambdaQueryWrapperX / QueryWrapperX 从 parking-spring-boot-starter-mybatis 摘出 4 个类,去掉自动配置,放进 parking-common 或 parking-framework-compat
SecurityFrameworkUtils / LoginUser 薄适配到上游 SecurityContext
OperateLog / OperateTypeEnum 先保留注解(不生效也不影响编译),后续映射到上游操作日志
ExcelUtils 复制工具类
common.biz.system.oauth2/permission DTO 薄适配到上游 system API

注意:不要保留 parking 的 ParkingMybatisAutoConfiguration / ParkingSecurityAutoConfiguration / ParkingWebAutoConfiguration,它们会和上游自动配置冲突。

6. 启动类改造要点

新启动类 cn.iocoder.yudao.server.YudaoServerApplication:

@SpringBootApplication(scanBasePackages = {
    "com.clouds.common.redis", "com.clouds.common.threadpool",
    "com.clouds.common.web", "com.clouds.common.oss",
    "${yudao.info.base-package}.server", "${yudao.info.base-package}.module",
    "com.zteits.park", "com.zteits.order", "com.zteits.pay",
    "com.zteits.bill", "com.zteits.charge", "com.zteits.business",
    "com.zteits.sys", "com.zteits.ipay", "com.zteits.configuration",
    "com.zteits.wxcp", "com.zteits.glockcollect"
})
@MapperScan(basePackages = {
    "com.zteits.park.mapper", "com.zteits.order.mapper", "com.zteits.pay.mapper",
    "com.zteits.bill.mapper", "com.zteits.charge.mapper",
    "com.zteits.business.mapper", "com.zteits.sys.mapper"
}, nameGenerator = LegacyMapperBeanNameGenerator.class)
  • com.clouds.common.* 必须排最前,否则存量 CacheUtil 静态字段被钉成 null,启动 NPE。
  • LegacyMapperBeanNameGenerator 需要从老工程搬过来。
  • 上游 yudao.info.base-package=cn.iocoder.yudao 保持不变。
  • 租户关闭:yudao.tenant.enable: false。
  • 审计字段:合并一个 MetaObjectHandler,同时填 createTime/updateTime/createBy/updateBy 与 creator/updater。

7. 前端搬迁

从老 parking-cloud-ui 搬:

src/api/{park,order,pay,report,ops,system,index,login,ext}
src/views/{park,order,finance,report,ops,member,assess,ext}
src/components/ 下自研组件
src/locales/ 中停车文案

保留上游:layout / components 基础组件 / utils / store / router / permission / config / styles。

  • 版本漂移:Vue 3.3→3.5、Element Plus 2.4→2.13、Vite 4→最新、UnoCSS 0.57→最新。
  • 先搬高频 10 页跑通,再批量修剩余。

7.1 兼容层 src/utils/request.ts

老 API 文件用 axios 原生风格 request.post(url, data),URL 里已经带了 /admin-api (ADMIN_API_PREFIX);而上游 axios 实例的 baseURL 是 VITE_BASE_URL + VITE_API_URL,同样以 /admin-api 结尾。兼容层必须 剥离 URL 里的前缀,否则实际请求会变成 /admin-api/admin-api/... → 404。 (2026-11-02 实测踩到,已在 stripApiPrefix 里修掉。)

7.2 本地联调配置(.env.local)

变量 值 说明
VITE_BASE_URL http://localhost:48083 新后端
VITE_APP_TENANT_ENABLE false 后端 yudao.tenant.enable=false
VITE_APP_CAPTCHA_ENABLE false 后端 yudao.captcha.enable=false

⚠️ 端口:VITE_PORT=80 在 macOS 非 root 下绑不了;联调时用 pnpm dev --port 5175(5173/5174 可能被其他工程占用)。

7.3 浏览器实测(2026-11-02)

用 playwright-cli 在 http://localhost:5175 实测:

  • 登录页 → admin/admin123 → 进入 /index,无 401;
  • /park/park-lot:车场列表返回真实数据(中国传感网国际创新园停车场等 2700 泊位);
  • /order/parking-record、/park/caryard-overview、/park/berth 均有数据、无报错。

全量页面巡检(同日):用 page.goto 逐页跑完 system_menu 里 55 个停车页面(订单 12 / 财务 11 / 运维 9 / 会员 4 / 报表 8 / 停车设施 6 / 考核·外部 2)+ 12 个系统页面,逐页收集 console.error 与 HTTP ≥400 的后端请求。结果:0 个页面报错、0 个失败请求;有数据的页面 (停车记录、车场、泊位、工单、巡检、会员卡、报表等)均渲染出真实行, 无数据的页面落「暂无数据」空态而非报错。

第二轮巡检(同日,修正版):第一轮只看 console.error 与 HTTP ≥400,漏掉了 「HTTP 200 但响应体 code=500」的静默失败。第二轮改为捕获所有 /admin-api JSON 响应的 code != 0,58 个停车页面 + 12 个系统页面全部复跑, 修完后结果 [](0 个失败调用)。详见 §8.1 的 #12–#15。

侧边栏点击巡检(同日):不止按 URL 巡检,还从侧边栏逐项点进去。范围 = 迁移的 60 个停车页面(8 个停车目录 55 个 + 挂在芋道「系统管理」下的组织管理/收费员账号/ 消息中心/规则设置/运行概况 5 个),记录落地 path、是否出现「您访问的页面不存在」、 以及响应体 code != 0。结果:60/60 正确落地,0 个 404、0 个失败调用。 (脚本坑:路由切换后 el-menu 会自动收起子菜单,点击前必须重新展开;另 2 个报表项 因滚动/动画导致脚本点击超时,人工复核可直接进入,不是页面问题。)

巡检同时修掉 4 个「老页面写法 vs 上游组件契约」的前端兼容问题(改的都是共享层, 不是逐页改):

# 现象 根因 修法
1 请求变成 /admin-api/admin-api/... 404 老 API 文件 URL 自带前缀,上游 axios baseURL 也带 request.ts 加 stripApiPrefix
2 hasPermi 指令抛 permission.hasPermission 老页面写字符串(198 处),上游指令只收数组 hasPermi.ts 同时接受 string / array
3 Missing required prop: "type" 老页面用 <DictTag dict-type=...> 迁过来的页面统一 <DictTag type="..." :value="...">
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 换算

7.4 菜单 path 归一化(侧边栏 404 的根因)

现象:点侧边栏「停车设施 → 车场列表」,地址变成 /park/park/park-lot, 页面显示「抱歉,您访问的页面不存在」。

根因:老 system_menu 的子页 path 写的是绝对路径(/park/park-lot), 而 yudao 约定是「目录绝对路径 + 子页相对路径」(上游自己就是 /system + user)。 src/utils/routerHelper.ts 的 pathResolve(parentPath, path) 会无条件拼接:

const childPath = path.startsWith('/') ? path : `/${path}`
return `${parentPath}${childPath}`.replace(/\/+/g, '/')   // /park + /park/park-lot

所以只要子页 path 以 / 开头且目录已经有同名首段,就必然重复一段。

修法(两处,缺一不可):

  1. 数据:system_menu 里 id>=100000 的 60 条子页记录,path 去掉目录首段 (/park/park-lot → park-lot、/order/parking-record → parking-record、 /system/org → org、/finance/bill → bill …)。
  2. 脚本:scripts/migrate-menus.sql 的 INSERT 加 CASE 归一化, 重建库时不会再写回绝对路径:
   CASE
     WHEN type = 2 AND path LIKE '/%' AND LOCATE('/', path, 2) > 0
       THEN SUBSTRING(path, LOCATE('/', path, 2) + 1)
     ELSE path
   END

(type=2 才是菜单页;type=1 目录保持绝对路径,type=3 按钮无 path。)

验证:重新登录后展开「停车设施」点「车场列表」,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):

@ConditionalOnProperty(prefix = "parking.legacy.scheduler", name = "enabled", havingValue = "true")
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 个旗县区的版式、图表、地图着色都能看到真实量级的效果。

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 停满 / 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 的误判就是这么来的。正确写法:

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–现在」,第二天打开 指挥中心 / 实时流量监控就是一片「—」。演示前固定跑两步,缺一不可:

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 逐张递增(左栏 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 递增丢弃先返回的那次解析结果,避免「后点的页面被前一次的 异步结果覆盖」。<component :is> 渲染真实路由组件,不是截图/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 在几乎每个列表页顶上挂一条绿色横幅:<doc-alert title="功能权限" url="https://doc.iocoder.cn/..." />, 渲染成「【功能权限】文档地址:https://doc.iocoder.cn/resource-permission」,点了跳上游官网。这些既不是本项目文档, 也不该出现在交付给使用方的停车平台上,全部删除。

删除范围(前端仓库 parking-cloud-yudao-ui):

位置 内容
350 个 .vue / 389 处 <doc-alert … /> 标签整块删除(多行写法一并处理)
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/(含注释),<doc-alert 与 doc.iocoder.cn 命中任何一条即失败(首次运行 433 条 offender → 清零)。前端仓库共 66 个 tests/verify-*.mjs 全绿。

坑:auto-components.d.ts 是启动时按 glob 重新生成的(gitignore 的 auto-*.d.ts)。删掉 DocAlert 组件后这个 文件还留着两条 typeof import('./../components/DocAlert/index.vue'),ts:check 会报找不到模块;把文件删掉、 重启 dev server 让它整份重建即可(重建后仍是全量 256 个组件,不是按需子集)。

实测(admin):/system/menu 页面顶部两条绿条消失、搜索栏顶到最上;用户下拉只剩 个人中心 / 锁定屏幕 / 退出系统; 未登录打开 /login,推广区与「萌新必读」分割线都不在了。npm run ts:check 28(存量)、npm run build:local 退出 0。

7.32 组织管理只保留「赤峰市住建」+「赤峰中智」(2026-11-09)

system_org 是从存量 sys_org 原样迁进来的 152 个有效组织(另有 90 个早已 del_flag=1), /system/org 页面把整棵树铺出来,演示/交付时只需要保留两级:赤峰市住建(10001) 和它下面的 赤峰中智(10003)。清理脚本:docs/sql/2026-11-09-组织管理只保留赤峰市住建与赤峰中智.sql。

删除方式:按 OrgServiceImpl.deleteOrg 的真实语义做逻辑删除,不是 DELETE—— OrgDO.delFlag 上有 @TableLogic(value="0", delval="1"),页面点删除走的也是 UPDATE system_org SET del_flag=1;system_dept 是 yudao 的镜像表,由 OrgDeptMirrorMapper.softDelete 同步 deleted = b'1'。脚本照此办理,所以事后还能精确回滚。

表 动作 行数
system_org 150 个存活组织 del_flag 0 → 1(保留 10001 / 10003) 150
system_dept 对应镜像行 deleted b'0' → b'1' 150
parking_lot pl_id=12303 西城市场封闭停车场:org_id 10228 → 10003 1

连带处理:唯一挂在被删组织下的车场是 12303 西城市场封闭停车场(原属「赤峰市医院事业部」10228), 把它改挂到赤峰中智,避免车场列表/一张图出现空组织;其余 108 个车场本来就在 10003。 system_users 4 个用户(admin → 10001,ops001/cashier001/finance001 → 10003)全部落在保留节点,无孤儿用户。 存量 sys_op(360 个老操作员)保留不动——它没有外键,本服务登录/菜单不读它的组织名。

执行时的插曲:脚本跑的同时有人(浏览器已登录会话)调了一次 PUT /admin-api/system/org/update,把 10003 的名字从「赤峰中智」改成「红山区」(后端日志 18:16:53)。 按需求以 sys_org.id=10003 = 赤峰中智 为准,脚本第 5 步把它连同 system_dept 镜像拉回「赤峰中智」。 以后在共享库里跑这类清理,跑完要看一眼后端日志(/tmp/yudao-server.log 的 ApiAccessLogInterceptor)有没有并发写。

校验(脚本末尾自带):

SELECT id, name, parent_id FROM system_org WHERE del_flag = 0;   -- 2 行:10001 赤峰市住建 / 10003 赤峰中智
SELECT COUNT(*) FROM system_dept WHERE deleted = b'0';           -- 2
SELECT COUNT(*) FROM parking_lot l WHERE NOT EXISTS (
  SELECT 1 FROM system_org o WHERE o.id = l.org_id AND o.del_flag = 0);  -- 0

执行后跑了 ./scripts/warm-cache.sh(车场缓存不会自动失效,见 §7.15 同款注意事项)。 浏览器实测 /system/org 只剩两行,赤峰中智 行下有「新增下级」入口;回滚 SQL 在脚本第 7 节, 按本次删除的 150 个 id 精确还原,不会把历史上早删的 90 个组织一起复活。

7.33 车场列表支持「修改车场 + 车场照片」(2026-11-10)

/park/park-lot(views/park/parkLotList)原来只有只读列表 + 详情抽屉,本次补上编辑与照片管理。 后端没有新增业务端点——全部复用已迁好的 ParkLotManageController:

页面动作 端点 权限
打开编辑(回显) POST /park/park-lot-manage/query-parking-lot-by-pk-no(入参字段是存量拼写 pklNo) :query
保存 POST /park/park-lot-manage/update-parking-lot-info :update
照片列表 POST /park/park-lot-manage/query-park-lot-pic-by-pl-no :query
上传照片 POST /park/park-lot-manage/upload-park-lot-pic(base64 数组) :import
删除照片 POST /park/park-lot-manage/delete-park-lot-pic(软删 data_state=0) :delete

新权限记录沿用菜单 310003-310007 上已有的 5 个 park:park-lot-manage:*,没有新建、也没有自动授权给任何角色 (admin 是 super_admin,走 *:*:*)。

编辑表单的取舍:

  • 不传经纬度。UpdateParkingLotInfo 只在 lonId&&latId 非空时才走「删旧地理点 + 重建」, 表单里没有这两个字段 ⇒ 改任何属性都不会动地理数据。
  • 组织原样回传。存量 updateParkingLotInfo 无条件用登录人组织覆盖 orgId/orgName (sysCode 还写死 "1001")。本机 admin 的组织是 10001(赤峰市住建),而车场都在 10003(赤峰中智), 照搬会导致「编辑一次就被改挂到 10001」。在控制器加了一个条件: @Value 之外的唯一改动是 request.getOrgId() == null 才回填登录人组织(见该方法的 ⚠️ 手改(非搬迁原样) 注释),新增车场的行为不变。这是本项目里第二处非照搬改造 (第一处是 updateBlockForParkLot 的 modifyOp 回填)。
  • 营业时间保留文本输入(存量数据同时存在 08:00 和 00:00:00 两种格式,用时间选择器会被强制改写); 收费标准保留 JSON 文本域(后端原样落库,前端只做展示解析)。
  • 回显映射抽在 views/park/parkLotList/edit.ts,并由 tests/verify-park-lot-edit.mjs 断言 (存量接口会把 pl_type/busi_circle 返回成字符串,必须转数字,否则下拉框空白)。

本地文件存储(本机演示必需):本机 FILE_OSS_CONFIG 只有 ENABLE_TYPE=ali, FastDFS tracker 又是内网 10.117.61.52:22122,两条路都不可达,上传必失败。新增:

  • com.clouds.common.oss.local.LocalOssClientWrapper:落盘 ~/.parking-cloud-yudao/oss/{yyyyMMdd}/{uuid}.{ext}, 返回 http://localhost:48083/admin-api/park/local-file/...;
  • OssClientBaseWrapper.getOssClientService() 新增 parking.legacy.oss.mode=local 分支(留空 = 存量行为);
  • LocalOssFileController(GET /park/local-file/{*path},@PermitAll + @Profile("local"),路径归一化后校验在根目录内;生产 profile 不注册这个读端点);
  • application-local.yaml 的 parking.legacy.oss.*(dir / url-prefix,可用 LEGACY_OSS_* 覆盖)。

生产/测试环境不配 parking.legacy.oss.mode,仍走阿里云 OSS / FastDFS,行为与存量一致; 本机也没有读取或写入任何真实存储凭据。

实测(admin,2026-11-10):

  • /park/park-lot 行操作出现「编辑」;打开 B1504020C7 静雅封闭车场,17 个字段回显正确,所属组织只读为「赤峰中智」;
  • 改「备注」保存 → 后端 modify_time 更新,org_id 仍是 10003(未被改成 10001),其余字段(pl_rate、 pl_type_prop、泊位数、营业时间)逐字段比对无变化;随后把备注改回空,数据复原;
  • 上传 1 张 264KB PNG → td_c_file 新增一行(file_type=6),页面缩略图 1920×1080 正常渲染, /admin-api/park/local-file/... 返回 200 image/png;删除 → data_state=0 软删,页面立即清空。 测试产生的文件与 td_c_file 行已清理(图片 8 张存量未动)。

验证:mvn -pl yudao-server -am -DskipTests package 退出 0;node --test tests/*.mjs 67/67; npm run ts:check 28(存量基线,新增文件 0 条);npm run build:local 退出 0。

7.34 车场列表支持「新增车场」(2026-11-10)

/park/park-lot 增加「新增车场」按钮,与 §7.33 的编辑弹窗共用同一个 el-dialog (dialogMode = 'create' | 'edit' 区分标题、字段与提交端点)。后端同样没有新增业务端点:

页面动作 端点 权限
名称查重 POST /park/park-lot-manage/check-unique-for-pl-name :query
保存 POST /park/park-lot-manage/save-parking-lot-info :create

权限沿用已存在的菜单 310003「停车场管理-新增」,没有新建、也没有授权给任何角色(admin 走 *:*:*)。

取的口径:

  • 车场编码由后端生成(ParkingLotServiceImpl.generatorParkNo(state)):state=1 → P1504…(现场形如 P15040490013),其他状态 → TKL…。表单里只读显示「保存后自动生成」。因此新增默认 state=1, 默认成演示车场会拿到 TKL 编码段。
  • 新增入参不能带 plId:parking_lot.pl_id 是自增主键,insertSelective 是「非 null 才写」, 传 0 会把主键真的写成 0。buildCreatePayload 显式丢掉 plId / plNo (orgId/orgName 当时也丢;2026-11-10 后续改为「选了就带上」,见 §7.35)。
  • 名称查重语义与名字相反:check-unique-for-pl-name 返回 true = 已存在(DAO 是 count(1) > 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 注释 <module>yudao-module-iot</module>
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 作为设备接入底座 —— 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 停流。

抓图与片段下载:真实画面把 <video> 画进 canvas 再导出(WebRTC 不污染 canvas, 跨域 HLS 会污染,给出「改用 WebRTC 播放」提示);片段下载走 gb_record/download/start|progress|stop,轮询进度到 100% 后取 downLoadFilePath 保存到本地,取消或离开页面即释放设备端会话(useClipDownload.ts)。 注意 download/progress 是 WVP 里少数不套 {code,data} 的接口。 设备台账、推流/拉流代理已接(见 §8.2);国标级联、云端录像仍未移植(P2)。

8.2 视频管理菜单:WVP 管理端接入(批 1,2026-11-09)

设计方案 docs/superpowers/specs/2026-11-09-视频管理菜单接入设计方案.md, 实施计划 docs/superpowers/plans/2026-11-09-视频管理菜单接入-批1实施计划.md。 上一节 /video/monitor 是「看视频」,本节补的是“管视频”——WVP 自己的 Vue2 管理端没有 搬过来,管理能力按 Vue3 重写进本仓库前端。

菜单:docs/sql/2026-11-09-视频管理菜单.sql(幂等,只对 parking_cloud_yudao 执行)。 一级目录 视频管理(id 100525,path /video)下 6 个菜单:

id 名称 path 组件
100502 视频监控 monitor gov/videoMonitor/index
100503 视频设备 device gov/videoDevice/index
100521 国标通道 channel gov/videoChannel/index
100522 推流管理 stream-push gov/videoStreamPush/index
100523 拉流代理 stream-proxy gov/videoStreamProxy/index
100524 JT1078 设备 jt-device gov/videoJtDevice/index

100502 / 100503 原是散落的顶菜单,脚本把它们挪进新目录;子菜单 path 必须相对 (device 而不是 /video/device),否则路由拼成 /video/video/device。 脚本不向任何角色授权,菜单靠 system_role_menu 手工分配,避免又出现「新功能默认全开」。

前端:接口集中在 src/api/video/wvpAdmin.ts(复用 wvp.ts 的 axios 实例与 unwrap)。 WVP 的 /api/** 响应被全局 advice 统一包成 {code,msg,data},列表是 PageHelper 的 PageInfo(total/list),所以全部走 unwrap;唯一例外仍是 gb_record/download/progress(不包)。DTO→表格行的兜底抽到各页 mapper.ts, 由 tests/verify-video-admin-mapper.mjs、tests/verify-video-stream-mapper.mjs 守住; 国标通道的同步状态文案与空态引导由 tests/verify-video-channel-sync.mjs 守住。

页面行为:

  • 视频设备:/device/query/devices 真实数据,10 条/页本地分页,10 秒轮询(隐藏页暂停); 行操作 同步(devices/{id}/sync,参数是国标编号字符串)/ 通道(跳 ?deviceId=)/ 编辑 / 删除; 空态「视频平台尚未接入国标设备」。新增/编辑抽屉里的「厂商」是下拉 + 可输入 (海康威视 / 大华 / 宇视 / 华为 / 天地伟业 / 科达 / 中星微,allow-create 允许列表外的值)—— WVP 的 manufacturer 是自由文本,不是字典,不能做成封闭枚举;编辑存量设备时当前值由 mapper.ts#mergeManufacturerOptions 并进选项,否则下拉没有匹配项会显示空白,看起来像把厂商清掉了 (2026-11-10,tests/verify-video-admin-mapper.mjs 守住)。
  • 国标通道:顶部设备下拉 + ?deviceId= 深链;parental = 1 的目录节点在 mapper 里剔除 (它们点播必失败);预览用 WvpPlayer(WebRTC→HLS),关弹窗即 play/stop;编辑走 /common/channel/one 取完整对象、改完整体回传 /common/channel/update。 通道本身不能手工新增——它是设备响应 Catalog 查询后落下的记录,整个 /api/device/query 没有通道新增端点;/api/common/channel/add 只服务推流/拉流代理 (dataType 2/3),手工塞一条 dataType=1 只会让点播查不到源。 所以页面给的是唯一可操作的动作:工具栏「同步目录」(GET /devices/{deviceId}/sync), 设备需在线;同步是异步的,发完之后每 2 秒轮询 /device/query/sync_status, 按钮上显示「同步中 12/30」这类进度,60 秒未收工就停下并提示稍后刷新; 完成 / 设备没回通道 / 设备回错误三种结果分别 toast + 一条常驻提示, 失败时不再是一句「系统异常」。五种空态引导(读取失败、无设备、未选设备、设备离线、 在线但目录为空)抽到 videoChannel/sync.ts,由 tests/verify-video-channel-sync.mjs 的 13 条测试守住。
  • 推流 / 拉流代理:新增、编辑、启停、删除;「播放」拿到的 StreamContent 直接喂 WvpPlayer。 注意 push/forceClose 按 app + stream 停,而 proxy/stop 按 id 停。
  • JT1078:本项目默认 jt1078.enable=false(wvp-video.yaml,停车场景用不到车载终端), 该控制器 @ConditionalOnProperty 不注册,接口返回 404。页面识别后显示 「部标(JT/T 1078)接入未启用」,不当作故障;开启后自动加载终端/通道/直播/抓图。

实测:/api/device/query/devices → code=0, total=0;4 个新页面浏览器打开无控制台报错, 空态文案正确。GET /api/device/query/devices/{任意编号}/channels 对不存在的设备返回 code=500 系统异常(DeviceChannelServiceImpl 抛「未找到设备」)——是预期,选设备下拉里的 真实设备不会走到这条路。node --test tests/*.mjs 全绿,npm run build:local 通过。

未做(后续批次,见设计方案):云端录像 + 录制计划、组织结构 + 报警管理; 接口级权限(WVP /api/** 只校验登录,菜单权限只挡入口)。国标级联 + 媒体节点见 §8.2.1。

8.2.1 批 2:国标级联 + 媒体节点(2026-11-09)

设计方案 §5、实施计划 docs/superpowers/plans/2026-11-09-视频管理菜单接入-批2实施计划.md。 菜单 SQL docs/sql/2026-11-09-视频管理菜单-批2.sql(幂等,只对 parking_cloud_yudao 执行):

id 名称 path 组件 权限
100526 国标级联 platform gov/videoPlatform/index parking:video:platform:query
100527 媒体节点 media-server gov/videoMediaServer/index parking:video:mediaserver:query

同样只插 system_menu、不插 system_role_menu。

页面:

  • 国标级联:列表 + 新增/编辑抽屉(三段分隔:基础信息 / 认证与信令 / 选项)、删除、推送目录、 通道共享抽屉(未共享 / 已共享两栏,各自关键字检索与 10 条分页,共享选中 → channel/add, 取消共享 → channel/remove)。两套 id 不能混用:/platform/info/{id} 取的是上级平台 国标编号,/platform/delete?id= 与 /platform/channel/push?id= 取的是数据库自增 id。 新增时用 /platform/server_config 预填本平台编号 / IP / 端口。
  • 媒体节点:卡片列表 + 两步向导。第一步 media_server/check(GET + query 参数,20s 超时) 探测连通性并拿回 mediaServerId 与全部端口,成功才进第二步;第二步确认端口段、HOOK/流/SDP IP、 单/多端口、录像天数与路径后 media_server/save。默认节点禁止删除。 后端在探测失败时只抛「系统异常」,页面把它翻译成「连不上节点:请确认 IP / HTTP 端口可达, 且 SECRET 与节点配置里的 api.secret 一致」。
  • secret 读出即脱敏为 ******(ServerController.maskSecret),编辑原样回传即可。

修掉的两个真问题(批 1 的写操作其实也一直是坏的,只是被前端吞掉了):

现象 根因 修法
所有 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)
写接口业务失败(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(自定义判定)

实测:浏览器端完整跑通 新增 → 列表 → 编辑回显 → 删除(列表回到空态), 新增抽屉预填本平台编号 15040000000000000001 / 127.0.0.1 / 5060;媒体节点页空态与 两步向导第一步的失败提示均正确(本机没有 ZLMediaKit,media_server/list 为空)。 node --test tests/*.mjs 50 通过 0 失败(新增 tests/verify-video-cascade-mapper.mjs 12 条), npm run ts:check 28 条既有错误、无新增(videoPlatform / videoMediaServer / api/video 干净), npm run build:local 通过。

未做:批 3 云端录像 + 录制计划(依赖本机先跑起 ZLMediaKit)、批 4 组织结构 + 报警管理。 本机当前既无上级平台也无媒体节点,两页只能验到空态与表单层。

8.2.2 视频监控二级分组改读 WVP 业务分组 + 新增「通道分组」页(2026-11-09)

起因:视频监控页左侧树的二级节点原来是演示夹具里的 parkName(demo.ts 的车场名), 和真实通道没有关系。WVP 的「通用频道 / 业务分组」才是通道归属的正源,这里把它接上。

口径:二级节点 = 通道的 gbBusinessGroupId 翻出来的分组名;分区县仍按通道的 civilCode (shared/district-code.ts 的 12 个旗县区 + 「未分区县」)。没挂分组的通道归到「未分组」。 /common/channel/group/add 会把 gb_business_group_id 写成顶层 215 节点(挂到 216 下面也一样), 所以监控页只拉一层 /api/group/tree/list 就够,不需要递归。

回放不受影响:点播仍走「设备 → 通道」接口(device.deviceId + channel.deviceId), 不要改成用 gbParentId —— 挂分组会把它覆盖成分组节点编号。

菜单:docs/sql/2026-11-09-视频管理菜单-通道分组.sql

id 名称 path 组件 权限
100532 通道分组 video-group gov/videoGroup/index parking:video:group:query

只插 system_menu,不插 system_role_menu(新权限不自动授予任何角色)。100531 仍留给「组织结构」。

页面(src/views/gov/videoGroup/index.vue):左边分组树(新建顶层 215 / 新建子节点 216 / 重命名 / 删除),右边当前分组的通道列表(添加通道从通用通道池里选、移除所选)。 20 位编号由 shared/group.ts 的 nextGroupDeviceId 按「中心(8)+行业(2)+类型(3)+网络(1)+序号(6)」 生成,类型码 215/216 分开自增,序号满 999999 回绕到 1。

前端改动:

文件 作用
src/views/gov/shared/group.ts 新增。分组索引、名称/区划反查(216 往上找 215)、编号生成、findNodeByDeviceId
src/views/gov/shared/district-code.ts 新增。civilCode → 旗县区,12 个区县 + 未分区县
src/views/gov/videoMonitor/mapping.ts 新增。设备/通道/分组 → 一路视频一行(纯函数,便于测)
src/views/gov/videoMonitor/wvpSource.ts 只保留取数,映射交给 mapping.ts;分组接口挂了就降级为空索引
src/views/gov/videoMonitor/model.ts 二级节点兜底文案改成「未分组」
src/api/video/wvpGroup.ts 新增。/group/*、/common/channel/* 封装
src/api/video/wvp.ts WvpChannel 补 businessGroupId

顺手修掉的两个真问题:

现象 根因 修法
左栏「重命名 / 删除」点不到,Playwright 报 subtree intercepts pointer events 左栏只有 340px,「标题 + 共 N 条 + 四个按钮」挤在 TableToolbar 一行里放不下,按钮溢出到右栏坐标下被右栏工具栏盖住 四个按钮移出工具栏,单独一行 .group-actions(340px 内刚好放下);左栏改用 .table-card 骨架、树节点行高放开(el-tree 默认 26px 撑不住「名称 + 编号」两行)、两栏卡片等高、去掉树卡片上无意义的密度按钮
改名后「当前分组」「目标分组」「删除确认」还显示老名字 改名后 loadTree() 重建了整棵树,selected 仍指着旧对象 记下 deviceId,重建后 findNodeByDeviceId 指回新节点,并 setCurrentKey 恢复高亮

门禁:node --test tests/*.mjs 65 通过 0 失败(新增 tests/verify-video-group.mjs、 tests/verify-video-monitor-group.mjs);npm run ts:check 28 条既有错误、无新增; npm run build:local 通过。

实测:浏览器端跑通 新建顶层 → 新建子节点 → 改名 → 删除(两个测试分组已删干净,树回到空态); 后端契约另用 curl 验过一轮(215/216 / 改名 / parent/list / 删除后 tree/list 回 [])。 本地库当前没有 WVP 分组,监控页所有通道按「未分组」显示 —— 属预期。

未提交:前端 parking-cloud-yudao-ui 与本文档均未 commit。

8.1 启动实测与踩坑记录(2026-11-02)

48083 首次完整跑通:Started YudaoServerApplication in 18.156 seconds,登录 POST /admin-api/system/auth/login(admin/admin123,tenant-id 头可带可不带)返回 code=0, 存量接口 POST /admin-api/park/area/query-area-and-block-tree 返回省市区数据。

启动路上逐个踩掉的坑(都已修在代码/配置里,不是临时命令行参数):

# 现象 根因 修法
1 NoClassDefFoundError: WxRedisOps 老微信 cp starter 3.3.0 拖来 weixin-java-common 3.3.0,没有该类 parking-dependencies 钉 weixin-java-common 4.8.6-*
2 BeanDefinitionOverrideException: springContextUtil easy-trans TransServiceConfig 注册了同名 @Bean 存量 SpringContextUtil 改 Bean 名 legacySpringContextUtil(全仓只用静态方法)
3 No qualifying bean RocketMQTemplate Boot 3 不再读老 starter 的 spring.factories parking-legacy-common 补 NoopRocketMQTemplate + ParkingLegacyRocketMQAutoConfiguration,走 AutoConfiguration.imports
4 MailService 缺 JavaMailSender 没配 spring.mail application.yaml 新增 spring.mail.*(全部环境变量)
5 NoSuchFieldError: okhttp3...Companion okhttp 4.12 配 okio 1.11 BOM 的 okio 改 3.6.0
6 WxCpProperties.getAppConfigs() NPE WxCpConfiguration.initServices() 直接 .stream() wechat.cp.app-configs: []
7 PlaceholderResolutionException: parking.refresh-cache.token 存量配置类强依赖 application.yaml 加 parking.refresh-cache.token: ${REFRESH_CACHE_TOKEN:}
8 IotStorageInitRunner 启动成功后又 System.exit(1) 本机没有 TDengine/IoTDB 先是在启动类 excludeFilters 排除该 Runner;2026-11-04 已整体关闭 IoT 模块(§8.0),该 Runner 不再参与编译,排除项与 import 一并注释
9 存量接口全部 NPE(UserInfo.getOrgId() 空) 合并工程里生效的是 yudao 安全栈,parking 的 starter 没注册(其 OAuth2TokenCommonApi 无实现),SecurityContext 里是 yudao 的 LoginUser LoginUserBridge 增加 yudao LoginUser 兜底转换;OAuth2TokenServiceImpl#buildUserInfo 补 username
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 注册
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 条
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 冲突)
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 不受影响)
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 排查,无残留
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,本行仅作历史记录)
16 新服务把老服务的出场延迟消息抢走(潜在) 新老服务默认都连 Redis db 0;存量 EqpOutDelayQueueSchedule 每 10s poll() 同一个 RBlockingQueue,缓存 key(zteits-park:*)也同名 application-local.yaml 的 spring.data.redis.database 改 1(老服务仍是 0)。登录令牌/缓存/队列全部隔离,老环境不受影响
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 后执行
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 被替换,不要先查业务代码

已知未决(不阻塞当前联调):

  • 两套调度同时在跑:老 48082 与新 48083 都会执行存量 @Scheduled/Quartz, 联调期先只开一个;正式切换前把存量定时任务收口到新工程的 Quartz。
  • 组织 id 空间 (已完成,2026-11-02):system_org 沿用老 sys_org.id, system_users.dept_id 指向真实组织,org-id-mapping 已删除;见 §7.7。 剩余动作只在生产切换时执行一次 scripts/migrate-org-ids.sql 并清 token。
  • 菜单/权限收口 (已完成,2026-11-02):672 个权限串全部有菜单, 3 个角色模板 + 联调账号已建;见 scripts/README-permissions.md。 生产要按真实角色重新勾选,别直接搬测试账号。

9. 常用命令

export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home
export PATH=$JAVA_HOME/bin:$PATH

# 后端构建(裁模块后)
mvn -T 1C -DskipTests clean package
mvn -pl yudao-server -am -DskipTests package

# 启动(新服务 48083,避免与老服务 48082 冲突;配置全在 application-local.yaml)
java -jar yudao-server/target/yudao-server.jar --spring.profiles.active=local

# 预热存量缓存(首次启动 / 清过 Redis 后必须执行,否则依赖 Redis 的存量接口会 NPE)
# 注意:同一账号再登录会把旧 token 踢下线;脚本内部会登录,跑完浏览器需重新登录
./scripts/warm-cache.sh

# 冒烟
curl -s -X POST http://127.0.0.1:48083/admin-api/system/auth/login \
  -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}'

# 前端(VITE_PORT=80 在 macOS 非 root 下绑不了,用 5175)
cd ../parking-cloud-yudao-ui && pnpm install && pnpm dev --port 5175

10. 续跑口令

在新目录开 Claude 窗口后,直接说:

读 MIGRATION.md 和 docs/superpowers/specs/2026-11-02-yudao-replatform-design.md,从「当前进度」的下一项继续迁移。先确认 mvn -pl yudao-server -am -DskipTests package 能过,再按模块推进。数据库用 parking_cloud_yudao,不要动 parking_cloud。

11. 已知坑

  1. 上游 pay/member/report/... 不能挂,路由和表都会撞。
  2. 两个 MetaObjectHandler 不能同时注册。当前生效的只有 yudao 那个,它只认 cn.iocoder...BaseDO;parking 的 BaseDO 子类(如 OrgDO)INSERT 时必须自己补 create_time/update_time,否则 MyBatis-Plus 会显式写 NULL 触发非空约束。 OrgServiceImpl 已显式处理,新增 parking 风格 MP 实体时照做。
  3. com.clouds.common.* 扫描顺序错 → 启动 NPE。
  4. 上游全局 logic-delete 只配了值、没配字段,parking 无 deleted 的 DO 安全。
  5. 菜单 id 必须错开,否则和上游 seed 冲突。
  6. system_org 与 system_dept 仍是两张表,但已统一 id 空间:system_org 是主表(沿用老 sys_org.id),system_dept 只是 yudao 用户模块的镜像, 由 OrgServiceImpl 写穿透维护。不要再直接改 system_dept,也不要恢复 parking.legacy.org-id-mapping(见 §7.7 / scripts/README-org.md)。
  7. pnpm ts:check 还有 28 个存量页面的类型错误(18 个 EP el-tag/el-select 可空 prop、2 个 orgName、8 个未使用变量), 不影响 pnpm dev 与 vite build(构建不跑 vue-tsc);新代码提交前请保证自己改的文件不新增错误。
  8. 存量 @Scheduled 默认不跑(parking.legacy.scheduler.enabled=false)。新服务上 “功能正常”不代表定时逻辑在跑;要验证调度相关需求,先显式打开开关并确认没有第二个实例 同时跑(见 §7.5)。同理,Redis db 隔离(db1)会让延迟队列在新服务上永远为空。
  9. get-permission-info 会丢掉“父链没授权”的按钮。filterDisableMenus() 只保留 父级存在且已授权的菜单,所以只给角色授权按钮、不给父页面/目录时,接口能过 (@PreAuthorize 查的是 menu_role_ids 缓存),但前端 v-hasPermi 拿不到这个权限串, 按钮会消失。给角色新增按钮权限时,父链要一起给(例:运营有 100426/100427, 但其父页面 100424 挂在「系统管理」下未授权,因此这两个串不出现在权限列表里; 指挥中心页未用 v-hasPermi 所以不受影响)。