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-lot404(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_answer0 待处理 / 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 个区县「(暂无车场)」,与读库的指挥中心 / 综合监测对不上;按演示库原样补
districtSeeds67 条(名称 / 坐标 / 泊位 / 空余 / 今日进出)→ 全市 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.mjsTDD 门禁(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_NAV5 → 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_org152 个有效组织里软删其余 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 的完整拷贝。
mysqldump parking_cloud→ 还原到parking_cloud_yudao。- 在拷贝里把 12 张与上游重名的旧表改名为
pcbak_system_*(system_org是停车专有表,保留原名)。 - 从上游模板库
yudao_template导入system_* / infra_* / iot_* / QRTZ_*。 - 旧管理员密码写入新
system_usersid=1,登录密码不变。 - 旧业务菜单(id>=153 的 345 条 + parking 专有系统页)以
id+100000导入,parent_id按规则重映射。 - 旧字典(5 类 / 17 条)以新 id 导入新
system_dict_*。 - 原库
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 以 / 开头且目录已经有同名首段,就必然重复一段。
修法(两处,缺一不可):
- 数据:
system_menu里id>=100000的 60 条子页记录,path 去掉目录首段 (/park/park-lot→park-lot、/order/parking-record→parking-record、/system/org→org、/finance/bill→bill…)。 - 脚本:
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 个任务也无任何调度线程日志,接口功能正常。
正式切换步骤(新服务独占生产时):
- 停老服务(48082),确认没有其它实例还在连原库;
- 把新服务数据源指到生产库,并把 Redis 切回生产者用的同一个 db (当前 local 用 db1 是为了并行期隔离;只改 db 而不开开关,延迟队列仍然消费不到消息);
- 设
PARKING_LEGACY_SCHEDULER_ENABLED=true并重启; - 按上表逐个确认日志:地磁延迟队列、太极 token/设备状态、欠费催缴刷新;
- 多实例部署时只在一个实例开(这些任务没有统一分布式锁;
DealGeoInOutParkDataSchedule自带DISTRIBUTED_TASK_PROPIP 取模,其余没有), 后续可整体迁到 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_type33 本地未知,落到「其它」。
版式:顶部通栏 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):
demo.ts已删除,页面模型改由mapper.ts从接口响应生成;旧useCommandCenter.ts保留但本页不再引用。- 记录列表不展示抓拍图;如需巡检再按
getOrderImages(orderId)(键4进场 /5出场)补图,车牌按存量规则打码。 - 本地缺数据的模块(实时泊位/占用率、重点车场、用户指标)不补数,后端在
unavailableModules里列明、页脚展示。
接入时的数据陷阱(提前记录):存量实时接口对没有实时缓存的车场返回
「泊位数 + 其余全 0」,直接按 (泊位-空闲)/泊位 会算成 100%。新聚合接口只把
parking_lot_berth_temp_info 有行的车场计入占用率,无行即 null;改 SQL 时
不要用 COALESCE(..., 0) 兜底。
两处已知缺口(沿用旧版结论):
- 存量「本月各区车场评分排名」在本栈没有对应数据源。
ParkDashboardController(/park/dashboard/overview)能算,但权限点park:dashboard:query在system_menu里没有记录,新增菜单并授权运营属于权限变更,未经确认不做。 新版式已把「评分排行」从页面移除(换成「各区今日收入排行」),该权限缺口不再阻塞本页。 - 存量「当日 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,幂等):
sys_org→system_org,真实 id 原样保留(242 行,152 个有效;单个根 10001);system_users.dept_id的103→10003(admin + 3 个联调账号);system_org写穿透到system_dept(镜像)——yudao 用户模块仍读它;- 软删芋道演示部门
100~125,隐藏「部门管理」菜单(组织统一从「组织管理」维护); 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 仍返回全量区县(否则下拉只剩当前区县,回不到全市)。
已修的两个运行期问题:
Map.entry(lot, null)抛 NPE(Map.entry不接受 null 值),改用record LotRate先过滤再排序;- 逾期工单原本只按
pl_country_name过滤,会把不在用车场(如丹阳市)的 2018 年工单算进来, 现统一 JOINparking_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_menu100502/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——那里是真实导入数据。
已知副作用(诚实记录):
- 红山区的实时泊位、运行统计是脚本合成的(真实车场没有实时上报),所以它的占用率 是估算值;订单、支付、车场清单仍是真实的。演示答辩时若被追问,按这个口径回答。
- 演示订单只覆盖「今天」,次日看板会回到接近空态;演示前重跑一次
--apply。 - 红山区真实订单流停在上次导入时刻,所以「数据延迟区县」恒为 1(红山区),这是真实缺口, 不是脚本 bug。
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、 磁贴网格 gap0.8vw→0.6vw、卡内 gap0.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。
- 口径:实时空闲泊位的取值要求「同一行既有
free_berth又有berth_num」,缺berth_num的车场被整场跳过 —— 分子分母同时少算,分母因此比综合监测小(15,517 vs 16,210)。 现在freeBerth()只要求free_berth有值;分母occupancyBerths()优先用实时行的berth_num,缺省回落到车场登记的pl_berth_num,与综合监测(ParkMonitorServiceImpl#toLot的firstNonNull)完全同口径。KPI、各区分布、重点车场、满位车场预警四处全部改用它。 - 精度:占用率是 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_type1 →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 去追:
- 一张图 / 专题分析 81 个车场,综合监测 / 指挥中心 109 个。前者是前端演示夹具,后者读
parking_cloud_yudao,本来就是两套数据源(一张图页面顶部固定展示「演示数据」标识)。 红山区库里那 40 个真实车场没有镜像进夹具:其中 2 个坐标在省外(116.36/39.92、119.55/32.00), 放进一张图会把视野拉到北京 / 镇江,而夹具里已有 10 个精修的红山区车场。是否改成读真实接口 由后续决定。 - 「智能设备覆盖 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 回全市。
口径坑(门禁抓到过两次):
- 两个「泊位」不是一回事:16,220 是车场登记泊位数(地图不逐位画),984 是已上图的路侧泊位点位。 合成一个数就是对外发布错的资源量,所以清单必须两行分开。
- 视频设备 = 242,不是 121:
devices.kind里video(视频桩)与camera(摄像机)各 121 台, 只数video一类会少一半。副信息分开写成「视频桩 121 · 摄像机 121」。 - 告警级别以
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 个区时不显空(这是本节的主要工作量):
- KPI 4 → 6:补「今日订单」和「覆盖旗县区 3/12」,一屏 6 个数字把「全市」讲完整。
- 右栏区县态势:从「12 行」改成「3 张有数据卡片 + 数据覆盖块」。卡片按
flex: 1 1 0均分 右栏剩余高度(justify-content: space-between+ 显式align-items: stretch,否则 button 的 UA 样式会把三行缩成一小坨),内容是大号占用率 + 泊位 / 空余 + 进度条;底部「数据覆盖 3/12」 进度条 + 「其余 9 个旗县区暂无数据」+ 9 个灰色 chip。 - 重点车场 / 实时告警同样只有 6 / 5 条,行均分剩余高度;告警底部再加一块「告警分级」
(红 / 橙 / 黄条数,从同一份
alerts算),把版式收住。 - 地图取景改成数据驱动:
fitAllDistricts()只按有数据的车场坐标取景(原来按区县多边形取景, 会把 9 个空区一起框进来,导致有数据的三区缩成一小块)。缩放取Math.floor整档 —— 高德按整数级渲染,四舍五入会多放大半级,最外侧的点被挤出屏幕。 - 区县填充透明度下调:
CORE_DISTRICT_FILL_OPACITY0.74 → 0.46、OTHER0.44 → 0.34 (gov/shared/district-theme.ts),让底图和车场气泡透出来,否则三块色块把地图糊住。 - 聚合改像素网格:
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-width96 → 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;坐标按coordType1 百度 / 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 定位到两处:
- 连接顺序选错:
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 秒。 - 重复扫描:
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/...返回 200image/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 协议。
建议方案(待选型):
先挂载—— 2026-11-04 评估后关闭,见 §8.0; 取流不经过 IoT,设备台账也不需要 IoT;yudao-module-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只服务推流/拉流代理 (dataType2/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. 已知坑
- 上游
pay/member/report/...不能挂,路由和表都会撞。 - 两个
MetaObjectHandler不能同时注册。当前生效的只有 yudao 那个,它只认cn.iocoder...BaseDO;parking 的BaseDO子类(如OrgDO)INSERT 时必须自己补create_time/update_time,否则 MyBatis-Plus 会显式写 NULL 触发非空约束。OrgServiceImpl已显式处理,新增 parking 风格 MP 实体时照做。 com.clouds.common.*扫描顺序错 → 启动 NPE。- 上游全局
logic-delete只配了值、没配字段,parking 无deleted的 DO 安全。 - 菜单 id 必须错开,否则和上游 seed 冲突。
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)。pnpm ts:check还有 28 个存量页面的类型错误(18 个 EPel-tag/el-select可空 prop、2 个orgName、8 个未使用变量), 不影响pnpm dev与vite build(构建不跑 vue-tsc);新代码提交前请保证自己改的文件不新增错误。- 存量
@Scheduled默认不跑(parking.legacy.scheduler.enabled=false)。新服务上 “功能正常”不代表定时逻辑在跑;要验证调度相关需求,先显式打开开关并确认没有第二个实例 同时跑(见 §7.5)。同理,Redis db 隔离(db1)会让延迟队列在新服务上永远为空。 get-permission-info会丢掉“父链没授权”的按钮。filterDisableMenus()只保留 父级存在且已授权的菜单,所以只给角色授权按钮、不给父页面/目录时,接口能过 (@PreAuthorize查的是menu_role_ids缓存),但前端v-hasPermi拿不到这个权限串, 按钮会消失。给角色新增按钮权限时,父链要一起给(例:运营有100426/100427, 但其父页面100424挂在「系统管理」下未授权,因此这两个串不出现在权限列表里; 指挥中心页未用v-hasPermi所以不受影响)。