44 Комити c69a77ba52 ... 31de39cd9a

Аутор SHA1 Порука Датум
  lq 31de39cd9a fix: CreateVehicle和UpdateVehicle改用buildPlateOrRfidQuery——修复空RFID误判车牌已存在 пре 1 месец
  lq 75bb45ac00 fix: 顶部标题栏加暗色背景——白色文字在暗底上清晰显示 пре 1 месец
  lq 496abff1f2 fix: 标题和时钟改为纯白#ffffff,清晰可见 пре 1 месец
  lq dc15187921 fix: 标题文字提亮+时间每秒实时跳动 пре 1 месец
  lq 5ca67a784a fix: 监控大屏文字对比度优化——标签加亮加粗,暗色背景可读性提升 пре 1 месец
  lq bf8f58c093 feat: 监控大屏视觉升级——暗色控制室风格/LED读表字体/红色脉冲满位警告 пре 1 месец
  lq 418324c3d5 feat: 升级仪表盘为停车监控大屏——4统计卡片+设备状态+10秒轮询 пре 1 месец
  lq e203118a53 feat: 监控仪表盘API——GET /dashboard/monitor(PublicGroup) пре 1 месец
  lq 6eee627ee5 docs: 监控仪表盘实现计划 пре 1 месец
  lq e1414baaf3 docs: 监控仪表盘设计文档——升级首页为停车监控大屏 пре 1 месец
  lq 77ecf970c9 fix: sys_user改为sys_users——SQLite表名修正 пре 1 месец
  lq 121ed40bd4 feat: 收费记录新增停车场筛选——backend联表查询+前端下拉过滤+列表列 пре 1 месец
  lq 05f6612c84 feat: 收费记录页新增当日汇总统计——总额/现金/免密/笔数 пре 1 месец
  lq 2c2ac4bb55 feat: 收费记录显示操作员姓名——关联sys_user.nick_name пре 1 месец
  lq afba1273b1 fix: RFID出场接入收费流程——改用ExitConfirm创建payment_record пре 1 месец
  lq 2200a96da7 feat: 收费记录列表增加车辆类型列——关联vehicle+vehicle_type表 пре 1 месец
  lq e8abbf171a feat: 收费记录查询页面——报表查询→收费记录,支持车牌/支付方式/日期筛选 пре 1 месец
  lq b17e88d9e1 feat: 收费记录查询API——GET /payment/list,支持车牌/支付方式/日期筛选 пре 1 месец
  lq 70230c5fcc docs: 更新项目进度——P0-1道闸+P0-2手动页面+P0-3收费结算已全部完成 пре 1 месец
  lq 4bfbf2c3b3 fix: seed.go新增payment接口Casbin权限——/vehicle/exit/preview+confirm пре 1 месец
  lq f5c7e8b9df fix: 修复rfid_tag空字符串OR条件恒真——VehicleEntry/Exit/Preview/Confirm四处 пре 1 месец
  lq ccb1ceaf24 feat: 出场Tab增加收费结算UI——现金收款+找零+免密放行,预留扫码/POS пре 1 месец
  lq 4b45d67561 feat: 注册支付API路由——/vehicle/exit/preview + /vehicle/exit/confirm пре 1 месец
  lq 47a1745714 feat: 拆分VehicleExit——ExitPreview费用预览+ExitConfirm确认出场支付 пре 1 месец
  lq 338c218e2d fix: PaymentRecord加入AutoMigrate列表 пре 1 месец
  lq a5c83746d9 feat: 新增payment_record实体,vehicle_record加payment_method пре 1 месец
  lq 00eec25fad feat: 创建payment模块——支付服务+仓储层 пре 1 месец
  lq 4e1f2c5331 docs: 收费结算模块实现计划 пре 1 месец
  lq af0989c68f docs: 收费结算模块设计文档——现金+免密,预留POS/扫码扩展 пре 1 месец
  lq 7886251833 fix: 车牌查询空RFID导致OR条件恒真——rfid_tag=''匹配所有无标签车辆 пре 1 месец
  lq ad62816532 fix: 系统内置车辆类型隐藏编辑删除按钮——v-if替代disabled пре 1 месец
  lq 2c5fd5e0e4 refactor: 临时车判断改用is_system标记——解除对名称的硬编码依赖,支持国际化 пре 1 месец
  lq 9270028588 fix: 临时车改为名称判断——按VehicleType.Name而非硬编码ID=1;临时车类型不可编辑删除 пре 1 месец
  lq c8a27525a0 feat: 进出场操作页面支持多语言——新增30+翻译键,适配中/英/繁切换 пре 1 месец
  lq d510078a2c fix: 新增entryExit菜单翻译键——支持中英文切换显示 пре 1 месец
  lq f59ddae0af fix: entryExit.vue 修正API函数名——obtainLotList→obtainPullOverList пре 1 месец
  lq 157ad368df feat: 新增手动进出场操作页面——支持车牌/RFID查询进出场 пре 1 месец
  lq ea6d2bb926 feat: 新增进出场操作菜单和权限——MenuId=31, ParentId=20 пре 1 месец
  lq a6a21655a5 docs: 手动进出场操作页面实现计划 пре 1 месец
  lq 43864ca36f docs: 手动进出场操作页面设计文档 пре 1 месец
  lq 688b06063c refactor: 抽取进出场公共逻辑为PassageService——RFID/摄像头/手动共用同一入口 пре 1 месец
  lq 135e5fb31d feat: 启用道闸继电器控制——RFID识别后自动开闸(入场+出场) пре 1 месец
  lq d77887a773 docs: 细化P0优先级排序——P0-1道闸→P0-2手动页面→P0-3收费→P0-4打印→P0-5摄像头,LED降为P1 пре 1 месец
  lq ecffe7034d docs: 更新项目进度——补充硬件对接待实现内容(道闸/摄像头/小票/LED),标记为P0 пре 1 месец
40 измењених фајлова са 4145 додато и 326 уклоњено
  1. 73 29
      doc/项目进度.md
  2. 440 0
      docs/superpowers/plans/2026-07-16-manual-entry-exit-plan.md
  3. 1361 0
      docs/superpowers/plans/2026-07-17-payment-settlement-plan.md
  4. 266 0
      docs/superpowers/plans/2026-07-20-monitor-dashboard-plan.md
  5. 129 0
      docs/superpowers/specs/2026-07-16-manual-entry-exit-design.md
  6. 128 0
      docs/superpowers/specs/2026-07-17-payment-settlement-design.md
  7. 104 0
      docs/superpowers/specs/2026-07-20-monitor-dashboard-design.md
  8. 5 0
      frontend/src/api/dashboard.js
  9. 18 0
      frontend/src/api/payment.js
  10. 18 0
      frontend/src/api/vehicle.js
  11. 32 1
      frontend/src/lang/en.js
  12. 32 1
      frontend/src/lang/zh-CN.js
  13. 24 1
      frontend/src/lang/zh-HK.js
  14. 24 1
      frontend/src/lang/zh-TW.js
  15. 232 155
      frontend/src/view/dashboard/index.vue
  16. 12 10
      frontend/src/view/parking/carInfo.vue
  17. 298 0
      frontend/src/view/parking/entryExit.vue
  18. 138 0
      frontend/src/view/report/paymentRecord.vue
  19. 18 0
      internal/api/v1/system/sys_dashboard.go
  20. 22 0
      internal/dao/payment_record.go
  21. 1 0
      internal/dao/vehicle_record.go
  22. 3 2
      internal/dao/vehicle_type.go
  23. 1 0
      internal/initialize/gorm.go
  24. 7 1
      internal/initialize/router.go
  25. 19 1
      internal/initialize/seed.go
  26. 22 0
      internal/model/common/passage.go
  27. 7 0
      internal/model/vehicle/request/vehicle.go
  28. 19 0
      internal/model/vehicle/response/vehicle.go
  29. 88 0
      internal/modules/payment/api.go
  30. 109 0
      internal/modules/payment/repository/repo.go
  31. 67 0
      internal/modules/payment/service/service.go
  32. 4 3
      internal/router/enter.go
  33. 13 0
      internal/router/system/sys_dashboard.go
  34. 2 0
      internal/service/enter.go
  35. 5 0
      internal/service/parking/enter.go
  36. 194 0
      internal/service/parking/passage.go
  37. 50 0
      internal/service/system/sys_dashboard.go
  38. 29 112
      internal/service/uhf/reader.go
  39. 121 9
      internal/service/vehicle/vehicle.go
  40. 10 0
      internal/service/vehicle/vehicle_type.go

+ 73 - 29
doc/项目进度.md

@@ -1,6 +1,6 @@
 # 智慧停车管理系统 — 项目进度
 
-> 更新日期:2026-07-14
+> 更新日期:2026-07-17
 
 ## 技术栈
 
@@ -26,13 +26,16 @@
 | 通道管理 | ✅ | ✅ | 方向/临停开关 |
 | UHF 设备 | ✅ | ✅ | TCP/串口连接/启停 |
 | 车辆信息 | ✅ | ✅ | CRUD |
-| 车辆类型 | ✅ | ✅ | - |
+| 车辆类型 | ✅ | ✅ | 含 is_system 标记 |
 | 车主管理 | ✅ | ✅ | 含 VIP |
 | 收费配置 | ✅ | ✅ | 起步价/计费单位/每日封顶/VIP折扣 |
 | 黑白名单 | ✅ | ✅ | 白名单放行/黑名单禁止 |
 | 车辆进场 | ✅ | ✅ | RFID 自动 + 手动 |
 | 车辆出场 | ✅ | ✅ | 计费算法完整 |
 | 进出记录 | ✅ | ✅ | 查询 |
+| **道闸控制** | ✅ | ✅ | RFID识别→继电器开闸 |
+| **手动进出场** | ✅ | ✅ | entryExit.vue,双 Tab 页面 |
+| **收费结算** | ✅ | ✅ | 现金+找零/免密,payment_record表 |
 | 摄像头 | ⚠️ | ❌ | DAO 有定义,无 API/页面 |
 | 仪表盘 | ✅ | ✅ | ECharts 图表 |
 | 个人中心 | ✅ | ✅ | - |
@@ -45,20 +48,24 @@
 2. 收费规则配置 ──── 按车型/免费时长/VIP     ✅ 100%
 3. 名单管理 ──────── 白名单/黑名单           ✅ 100%
 4. 车辆进场 ──────── RFID识别→查单→开闸     ✅ 100%
-5. 车辆出场 ──────── 识别→计费→..→开闸      ⚠️ 80% (缺收费)
-6. 收费结算 ──────── 现金/扫码/免密          ❌ 0%
-7. 小票打印 ──────── ESC/POS 串口打印        ❌ 0%
-8. 交接班对账 ────── 当班统计/现金核销       ❌ 0%
-9. 报表查询 ──────── 进出记录                ⚠️ 50% (缺收入/异常)
-10. 实时监控 ─────── 车位/设备/在线车         ❌ 0%
+5. 车辆出场 ──────── 识别→计费→收费→开闸    ✅ 100%
+6. 收费结算 ──────── 现金+找零/免密          ✅ 100%
+7. 手动进出场页面 ─── 车牌/RFID输入→进出场   ✅ 100%
+8. 小票打印 ──────── ESC/POS 串口打印        ❌ 0%
+9. 摄像头识别 ────── RTSP抓拍→OCR→触发       ❌ 0%
+10. LED车位屏 ────── 余位实时显示             ❌ 0%
+11. 交接班对账 ───── 当班统计/现金核销        ❌ 0%
+12. 报表查询 ─────── 进出记录                ⚠️ 50% (缺收入/异常)
+13. 包月车管理 ───── 月卡办理/续费/到期       ❌ 0%
+14. 实时监控 ─────── 车位/设备/在线车         ❌ 0%
 ```
 
 ## 车辆进出场触发方式
 
 | 触发方式 | 状态 | 说明 |
 |---------|:--:|------|
-| UHF RFID 自动识别 | ✅ | `service/uhf/reader.go`,读标签→查车辆→进出场 |
-| 前端手动录入 | ⚠️ | 后端 API 已实现,**前端无操作页面** |
+| UHF RFID 自动识别 | ✅ | 读标签→查车辆→进出场+开闸,PassageService 统一入口 |
+| 前端手动录入 | ✅ | entryExit.vue 进出场操作页面,含收费结算 |
 | 摄像头车牌识别 | ❌ | `camera` 表已建,无拍照/OCR/触发逻辑 |
 
 ```
@@ -88,12 +95,52 @@
 
 ## 待开发模块
 
-### P0 — 核心闭环(收费 + 打印)
+### P0-1 ─ 道闸控制 ✅
+
+> 已完成:继电器 `CloseRelay1(2)` 已解除注释,`OpenGate()` 公开方法。进场/出场自动开闸。
+
+### P0-2 ─ 手动进出场页面 ✅
+
+> 已完成:`entryExit.vue` 支持车牌/RFID 输入、车辆查询、进场/出场双 Tab。含多语言支持。
+
+### P0-3 ─ 收费结算 ✅
+
+> 已完成:`payment_record` 表、`PaymentService`、现金收款+找零、免密放行。预留 POS/扫码扩展。
+
+### P0-4 ─ 小票打印
+
+> **收完钱要出票**:配合收费结算,收费完成后自动打印小票给车主。
+
+| 功能 | 后端 | 前端 | 说明 |
+|------|:--:|:--:|------|
+| 小票打印 | ❌ | ❌ | 串口 ESC/POS 指令,58mm 热敏小票打印机 |
+
+**待实现内容**:
+
+| 组件 | 说明 |
+|------|------|
+| 串口通信 | 通过串口/USB 连接 58mm 热敏打印机 |
+| ESC/POS 指令 | 封装打印指令集:文字、条码、二维码、切纸 |
+| 小票模板 | 停车小票格式:车牌号、进场时间、出场时间、费用、收款方式 |
+| 打印触发 | 收费完成后自动打印,支持补打 |
+
+### P0-5 ─ 摄像头车牌识别
+
+> **双重校验防作弊**:RFID 标签可能被拆换,车牌识别作为第二道校验。也是第三种进出场触发方式(无标签车辆)。
 
 | 功能 | 后端 | 前端 | 说明 |
 |------|:--:|:--:|------|
-| 收费结算 | ❌ | ❌ | 出场计费后无收款流程:现金/扫码支付、免密支付 |
-| 小票打印 | ❌ | ❌ | 串口 ESC/POS 指令,58mm 热敏小票 |
+| 摄像头车牌识别 | ❌ | ❌ | RTSP 视频流抓拍 + OCR 车牌识别,`camera` 表已建 |
+
+**待实现内容**:
+
+| 组件 | 说明 |
+|------|------|
+| 硬件对接 | RTSP 视频流拉取或 SDK 回调(海康/大华/宇视等) |
+| 抓拍触发 | 车辆进入识别区 → 抓拍一张图片 |
+| OCR 识别 | 车牌识别算法或第三方云服务(百度/阿里 OCR) |
+| 业务触发 | 识别到车牌 → 自动调用 `VehicleEntry()`/`VehicleExit()` |
+| 图片留存 | `VehicleRecord` 已有 `EntryImage`/`ExitImage` 字段 |
 
 ### P1 — 运营管理
 
@@ -103,6 +150,7 @@
 | 收入报表 | ❌ | ❌ | 按日/月/班次汇总收入 |
 | 异常报表 | ❌ | ⚠️ | 无牌车、手动抬杆、断网异常记录,前端空占位 |
 | 包月车管理 | ❌ | ❌ | 月卡/季卡/年卡办理、续费、到期提醒、收入报表 |
+| LED 车位显示 | ❌ | ❌ | 入口剩余车位显示屏对接(不影响车辆进出,可后续补充) |
 
 **包月车管理待实现内容**:
 
@@ -117,20 +165,12 @@
 
 > 说明:当前包月车的通行逻辑可以用 `shortlist`(白名单 + `ExpirationTime`)变通实现,但缺少管理端(办理/续费/统计)。
 
-### P0.5 — 手动进出场页面
-
-| 功能 | 后端 | 前端 | 说明 |
-|------|:--:|:--:|------|
-| 手动进出场操作页 | ✅ | ❌ | 后端 API 已有,缺前端操作页面 |
-
-### P2 — 监控与硬件
+### P2 — 监控与边缘端
 
 | 功能 | 后端 | 前端 | 说明 |
 |------|:--:|:--:|------|
 | 实时监控大屏 | ❌ | ❌ | 车位余量、在线车辆、设备状态 |
 | 在线车辆统计 | ❌ | ⚠️ | 前端空占位 |
-| 摄像头车牌识别 | ❌ | ❌ | DAO 已有 camera 表,抓拍/OCR/触发全缺 |
-| LED 车位显示 | ❌ | ❌ | 入口剩余车位屏 |
 | RK3568 边缘端 | ❌ | ❌ | edge/ 通信层(TCP协议 + 事件总线) |
 
 ## 前端页面清单(37 页)
@@ -204,10 +244,14 @@
 
 ## 下一步建议
 
-1. **手动进出场操作页面** (P0.5) — 后端 API 已就绪,只需前端页面,半天可完成
-2. **收费结算模块** (P0) — 闭环核心,优先级最高
-3. **小票打印** (P0) — 配合收费
-4. **包月车管理** (P1) — 月卡办理/续费/到期处理
-5. **摄像头车牌识别** (P2) — 第三种进出场触发方式
-6. **交接班 + 收入报表** (P1) — 运营必需
-7. **监控大屏** (P2) — 锦上添花
+| 优先级 | 功能 | 预估工时 | 状态 |
+|:--:|------|:--:|:--:|
+| ~~P0-1~~ | ~~道闸控制~~ | - | ✅ |
+| ~~P0-2~~ | ~~手动进出场页面~~ | - | ✅ |
+| ~~P0-3~~ | ~~收费结算~~ | - | ✅ |
+| **P0-4** | **小票打印** | 1-2天 | ESC/POS 串口指令封装 + 小票模板 |
+| **P0-5** | **摄像头车牌识别** | 3-5天 | RTSP+OCR,RFID+车牌双重校验 |
+| P1 | 包月车管理 | 2天 | 月卡办理/续费/到期处理 |
+| P1 | 交接班 + 收入报表 | 2天 | 运营必需 |
+| P1 | LED 车位显示 | 1天 | 不影响车辆进出,可后续补充 |
+| P2 | 监控大屏 | 2天 | 锦上添花 |

+ 440 - 0
docs/superpowers/plans/2026-07-16-manual-entry-exit-plan.md

@@ -0,0 +1,440 @@
+# 手动进出场操作页面 — 实现计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 在停车管理下新增"进出场操作"页面,支持手动输入车牌/RFID 完成车辆进场和出场操作
+
+**Architecture:** 前端新增 Vue 3 组件 `entryExit.vue`,Tab 切换进场/出场模式,复用已有 `vehicle.js` API。后端仅需在 seed.go 新增菜单记录和权限映射。
+
+**Tech Stack:** Vue 3 + Element Plus + Pinia, Go 1.25 + GIN
+
+**预估文件变更:** 2 files (1 new + 1 modified)
+
+---
+
+## 文件结构
+
+| 文件 | 操作 | 职责 |
+|------|------|------|
+| `frontend/src/view/parking/entryExit.vue` | 新建 | 进出场操作页面:输入查询、车辆详情、进场/出场操作 |
+| `internal/initialize/seed.go` | 修改 | 新增菜单记录(entryExit, ID=31) + authority_menu 映射 |
+
+---
+
+### Task 1: 新增菜单和权限映射
+
+**Files:**
+- Modify: `internal/initialize/seed.go`
+
+- [ ] **Step 1: 新增菜单记录**
+
+在 `pricing` 菜单之后插入(第 122 行之后):
+
+```go
+{MenuLevel: 0, ParentId: 20, Path: "entryExit", Name: "entryExit", Hidden: false, Component: "view/parking/entryExit.vue", Sort: 5, Meta: dao.Meta{Title: "进出场操作", Icon: "switch"}},
+```
+
+- [ ] **Step 2: 新增 authority_menu 映射**
+
+在第 173 行之后新增:
+
+```go
+// 进出场操作 (31) — admin + operator
+{MenuId: "31", AuthorityId: "618"}, {MenuId: "31", AuthorityId: "888"}, {MenuId: "31", AuthorityId: "9527"},
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd lc_garage && go build -o build/bin/smart-parking.exe .
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add internal/initialize/seed.go
+git commit -m "feat: 新增进出场操作菜单和权限——MenuId=31, ParentId=20"
+```
+
+---
+
+### Task 2: 创建进出场操作页面
+
+**Files:**
+- Create: `frontend/src/view/parking/entryExit.vue`
+
+- [ ] **Step 1: 完整文件内容(template + script + style)**
+
+```vue
+<template>
+  <div class="entry-exit-page">
+    <!-- Tab 切换进场/出场 -->
+    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+      <el-tab-pane label="车辆进场" name="entry" />
+      <el-tab-pane label="车辆出场" name="exit" />
+    </el-tabs>
+
+    <!-- 输入区:车牌号 + RFID -->
+    <el-card class="query-card">
+      <el-form :model="queryForm" inline>
+        <el-form-item label="车牌号">
+          <el-input
+            v-model="queryForm.plate_number"
+            placeholder="请输入车牌号"
+            clearable
+            style="width: 200px"
+            @keyup.enter="queryVehicle"
+          />
+        </el-form-item>
+        <el-form-item label="RFID标签">
+          <el-input
+            v-model="queryForm.rfid_tag"
+            placeholder="请输入RFID标签"
+            clearable
+            style="width: 240px"
+            @keyup.enter="queryVehicle"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="Search" @click="queryVehicle">查询</el-button>
+          <el-button icon="Refresh" @click="resetForm">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 车辆详情卡片(查询后显示) -->
+    <el-card v-if="vehicleLoaded" class="detail-card">
+      <template #header>
+        <span class="card-title">{{ activeTab === 'entry' ? '车辆信息' : '出场信息' }}</span>
+      </template>
+
+      <el-descriptions :column="3" border>
+        <el-descriptions-item label="车牌号">
+          {{ vehicleInfo.plate_number || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="RFID标签">
+          {{ vehicleInfo.rfid_tag || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="车辆类型">
+          <el-tag v-if="vehicleInfo.VehicleType" :type="vehicleInfo.VehicleType.ID === 1 ? 'warning' : 'success'">
+            {{ vehicleInfo.VehicleType.name }}
+          </el-tag>
+          <el-tag v-else type="warning">临时车</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item v-if="vehicleInfo.Owner" label="车主">
+          {{ vehicleInfo.Owner.owner_name }}
+        </el-descriptions-item>
+        <el-descriptions-item v-if="vehicleInfo.Owner && vehicleInfo.Owner.is_vip" label="VIP">
+          <el-tag type="danger">是</el-tag>
+        </el-descriptions-item>
+      </el-descriptions>
+
+      <!-- 进场特有操作区 -->
+      <div v-if="activeTab === 'entry'" class="action-area">
+        <el-divider />
+        <el-form :model="entryForm" inline>
+          <el-form-item label="选择停车场" required>
+            <el-select
+              v-model="entryForm.parking_lot_id"
+              placeholder="请选择停车场"
+              style="width: 260px"
+            >
+              <el-option
+                v-for="lot in parkingLotList"
+                :key="lot.ID"
+                :label="lot.lot_name"
+                :value="lot.ID"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button
+              type="success"
+              icon="Check"
+              :loading="submitting"
+              @click="confirmEntry"
+            >
+              确认进场
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <!-- 出场特有操作区 -->
+      <div v-if="activeTab === 'exit'" class="action-area">
+        <el-divider />
+        <p class="exit-tip">确认后系统将自动计算停车费用</p>
+        <el-button
+          type="danger"
+          icon="Check"
+          :loading="submitting"
+          @click="confirmExit"
+        >
+          确认出场并收费
+        </el-button>
+      </div>
+    </el-card>
+
+    <!-- 查询结果:未找到车辆 -->
+    <el-result
+      v-if="notFound"
+      icon="warning"
+      title="未找到车辆信息"
+      :sub-title="activeTab === 'entry' ? '将按临时车处理,请选择停车场后确认进场' : '该车辆未登记,请核实车牌号'"
+    >
+      <template v-if="activeTab === 'entry'" #extra>
+        <el-form :model="entryForm" inline>
+          <el-form-item label="选择停车场" required>
+            <el-select
+              v-model="entryForm.parking_lot_id"
+              placeholder="请选择停车场"
+              style="width: 260px"
+            >
+              <el-option
+                v-for="lot in parkingLotList"
+                :key="lot.ID"
+                :label="lot.lot_name"
+                :value="lot.ID"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button
+              type="success"
+              :loading="submitting"
+              @click="confirmEntry"
+            >
+              确认进场(临时车)
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-result>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { obtainVehicleLicense, vehicleEntry, vehicleAppear } from '@/api/vehicle'
+import { obtainLotList } from '@/api/pullOver'
+
+// ---- 状态 ----
+const activeTab = ref('entry')
+const queryForm = reactive({ plate_number: '', rfid_tag: '' })
+const vehicleInfo = ref(null)
+const vehicleLoaded = ref(false)
+const notFound = ref(false)
+const submitting = ref(false)
+
+const entryForm = reactive({ parking_lot_id: null })
+const parkingLotList = ref([])
+
+// ---- 初始化:加载停车场列表 ----
+onMounted(async () => {
+  try {
+    const res = await obtainLotList({ page: 1, pageSize: 100 })
+    parkingLotList.value = res.data?.list || []
+  } catch (e) {
+    console.error('加载停车场列表失败', e)
+  }
+})
+
+// ---- 查询车辆 ----
+async function queryVehicle() {
+  const plate = queryForm.plate_number.trim()
+  const rfid = queryForm.rfid_tag.trim()
+
+  if (!plate && !rfid) {
+    ElMessage.warning('请至少输入车牌号或RFID标签')
+    return
+  }
+
+  vehicleInfo.value = null
+  vehicleLoaded.value = false
+  notFound.value = false
+
+  try {
+    const res = await obtainVehicleLicense(plate || rfid)
+    if (res.code === 0 && res.data) {
+      vehicleInfo.value = res.data
+      vehicleLoaded.value = true
+    } else {
+      notFound.value = true
+    }
+  } catch (e) {
+    notFound.value = true
+  }
+}
+
+// ---- 确认进场 ----
+async function confirmEntry() {
+  if (!entryForm.parking_lot_id) {
+    ElMessage.warning('请选择停车场')
+    return
+  }
+
+  if (submitting.value) return
+  submitting.value = true
+
+  try {
+    const res = await vehicleEntry({
+      plate_number: queryForm.plate_number.trim(),
+      rfid_tag: queryForm.rfid_tag.trim(),
+      parking_lot_id: entryForm.parking_lot_id
+    })
+    if (res.code === 0) {
+      const plate = res.data?.data?.plate_number || queryForm.plate_number.trim()
+      ElMessage.success(`入场成功!${plate} 已进入停车场`)
+      resetForm()
+    } else {
+      ElMessage.error(res.msg || '入场失败')
+    }
+  } catch (e) {
+    ElMessage.error(e.message || '入场失败')
+  } finally {
+    submitting.value = false
+  }
+}
+
+// ---- 确认出场 ----
+async function confirmExit() {
+  const plate = vehicleInfo.value?.plate_number || queryForm.plate_number.trim()
+
+  await ElMessageBox.confirm(
+    `确认车辆 ${plate} 出场?`,
+    '确认出场',
+    { confirmButtonText: '确认出场', cancelButtonText: '取消', type: 'warning' }
+  )
+
+  if (submitting.value) return
+  submitting.value = true
+
+  try {
+    const res = await vehicleAppear({
+      plate_number: queryForm.plate_number.trim(),
+      rfid_tag: queryForm.rfid_tag.trim()
+    })
+    if (res.code === 0) {
+      const data = res.data?.data || res.data || {}
+      const fee = data.fee || 0
+      const stayTime = data.stay_time || 0
+      const entryTime = data.entry_time ? new Date(data.entry_time * 1000).toLocaleString('zh-CN') : '-'
+
+      ElMessage.success(
+        `出场成功!进场: ${entryTime} | 停留: ${formatDuration(stayTime)} | 费用: ¥${fee.toFixed(2)}`
+      )
+      resetForm()
+    } else {
+      ElMessage.error(res.msg || '出场失败')
+    }
+  } catch (e) {
+    ElMessage.error(e.message || '出场失败')
+  } finally {
+    submitting.value = false
+  }
+}
+
+// ---- 工具 ----
+function handleTabChange() {
+  resetForm()
+}
+
+function resetForm() {
+  queryForm.plate_number = ''
+  queryForm.rfid_tag = ''
+  vehicleInfo.value = null
+  vehicleLoaded.value = false
+  notFound.value = false
+  entryForm.parking_lot_id = null
+}
+
+function formatDuration(minutes) {
+  if (!minutes && minutes !== 0) return '-'
+  const h = Math.floor(minutes / 60)
+  const m = minutes % 60
+  return h > 0 ? `${h}小时${m}分钟` : `${m}分钟`
+}
+</script>
+
+<style scoped>
+.entry-exit-page {
+  padding: 0;
+}
+
+.query-card {
+  margin-bottom: 16px;
+}
+
+.detail-card {
+  margin-bottom: 16px;
+}
+
+.card-title {
+  font-weight: bold;
+  font-size: 16px;
+}
+
+.action-area {
+  margin-top: 8px;
+}
+
+.exit-tip {
+  color: #909399;
+  margin-bottom: 12px;
+}
+</style>
+```
+
+- [ ] **Step 2: 提交**
+
+```bash
+git add frontend/src/view/parking/entryExit.vue
+git commit -m "feat: 新增手动进出场操作页面——支持车牌/RFID查询进出场"
+```
+
+---
+
+### Task 3: 端到端验证
+
+- [ ] **Step 1: 启动开发模式**
+
+```bash
+cd lc_garage && wails dev
+```
+
+- [ ] **Step 2: 验证菜单显示**
+
+1. 登录(admin / 123456)
+2. 侧边栏 → 停车管理 → 确认"进出场操作"菜单出现
+3. 点击进入页面,确认 Tab 切换正常
+
+- [ ] **Step 3: 验证进场流程**
+
+1. 输入新车牌号(如"测试001")
+2. 点查询 → 确认显示"未找到车辆信息" + 停车场选择
+3. 选停车场 → 点"确认进场(临时车)"
+4. 确认绿色"入场成功"提示
+
+- [ ] **Step 4: 验证出场流程**
+
+1. 切换到"车辆出场"Tab
+2. 输入刚才的车牌号 → 点查询
+3. 确认车辆信息卡片显示
+4. 点"确认出场并收费" → 确认弹窗 → 确认
+5. 确认成功消息显示费用信息
+
+- [ ] **Step 5: 验证边界情况**
+
+| 场景 | 预期结果 |
+|------|---------|
+| 空输入点查询 | "请至少输入车牌号或RFID标签" |
+| 未选停车场点确认进场 | "请选择停车场" |
+| 已出场车辆再次出场 | "车辆未入场或已出场" |
+| 不在场车辆点出场 | "车辆未入场或已出场" |
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add -A && git commit -m "verify: 手动进出场页面端到端验证通过"
+```

Разлика између датотеке није приказан због своје велике величине
+ 1361 - 0
docs/superpowers/plans/2026-07-17-payment-settlement-plan.md


+ 266 - 0
docs/superpowers/plans/2026-07-20-monitor-dashboard-plan.md

@@ -0,0 +1,266 @@
+# 监控仪表盘 — 实现计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans.
+
+**Goal:** 升级仪表盘为停车监控大屏,4统计卡片+设备状态,10秒轮询
+
+**Architecture:** 后端 `GET /dashboard/monitor`(system 模块,SQL 聚合)。前端替换 `dashboard/index.vue`,`el-statistic` + `setInterval`。
+
+---
+
+## 文件结构
+
+| 文件 | 操作 |
+|------|------|
+| `internal/service/system/sys_dashboard.go` | 新建 |
+| `internal/api/v1/system/sys_dashboard.go` | 新建 |
+| `internal/router/system/sys_dashboard.go` | 新建 |
+| `internal/router/enter.go` | 修改 |
+| `internal/initialize/router.go` | 修改 |
+| `frontend/src/view/dashboard/index.vue` | 修改 |
+| `frontend/src/api/dashboard.js` | 修改 |
+
+---
+
+### Task 1: 后端 API
+
+- [ ] **Step 1: Service**
+
+Write `internal/service/system/sys_dashboard.go`:
+
+```go
+package system
+
+import (
+	"time"
+	"wails-app/internal/global"
+)
+
+type DeviceStatus struct {
+	ChannelName  string `json:"channel_name"`
+	Direction    string `json:"direction"`
+	DeviceName   string `json:"device_name"`
+	DeviceStatus string `json:"device_status"`
+}
+
+type MonitorData struct {
+	TotalSpaces int64          `json:"total_spaces"`
+	UsedSpaces  int64          `json:"used_spaces"`
+	InPark      int64          `json:"in_park"`
+	TodayIncome float64        `json:"today_income"`
+	TodayEntry  int64          `json:"today_entry"`
+	TodayExit   int64          `json:"today_exit"`
+	Devices     []DeviceStatus `json:"devices"`
+}
+
+type DashboardService struct{}
+
+func (s *DashboardService) GetMonitorData() (MonitorData, error) {
+	var d MonitorData
+	today := time.Now().Format("2006-01-02")
+	global.GVA_DB.Raw("SELECT COALESCE(SUM(capacity), 0) FROM parking_lot").Scan(&d.TotalSpaces)
+	global.GVA_DB.Raw("SELECT COUNT(*) FROM vehicle_record WHERE exit_time IS NULL").Scan(&d.InPark)
+	d.UsedSpaces = d.InPark
+	global.GVA_DB.Raw("SELECT COALESCE(SUM(amount), 0) FROM payment_record WHERE paid_at >= ? AND paid_at < ?", today, today+" 23:59:59").Scan(&d.TodayIncome)
+	global.GVA_DB.Raw("SELECT COUNT(*) FROM vehicle_record WHERE entry_time >= ? AND entry_time < ?", today, today+" 23:59:59").Scan(&d.TodayEntry)
+	global.GVA_DB.Raw("SELECT COUNT(*) FROM vehicle_record WHERE exit_time >= ? AND exit_time < ?", today, today+" 23:59:59").Scan(&d.TodayExit)
+	global.GVA_DB.Raw(`SELECT ch.channel_name, ch.direction, COALESCE(uhf.device_name, '-') as device_name, COALESCE(uhf.status, 'offline') as device_status FROM channel ch LEFT JOIN uhf_reader uhf ON uhf.channel_id = ch.id WHERE ch.deleted_at IS NULL ORDER BY ch.id`).Scan(&d.Devices)
+	return d, nil
+}
+```
+
+- [ ] **Step 2: Handler**
+
+Write `internal/api/v1/system/sys_dashboard.go`:
+
+```go
+package system
+
+import (
+	"github.com/gin-gonic/gin"
+	"wails-app/internal/model/common/response"
+	systemService "wails-app/internal/service/system"
+)
+
+var dashboardSvc = systemService.DashboardService{}
+
+func GetMonitor(c *gin.Context) {
+	data, err := dashboardSvc.GetMonitorData()
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	response.OkWithData(data, c)
+}
+```
+
+- [ ] **Step 3: Router**
+
+Write `internal/router/system/sys_dashboard.go`:
+
+```go
+package system
+
+import (
+	"wails-app/internal/api/v1/system"
+	"github.com/gin-gonic/gin"
+)
+
+type DashboardRouter struct{}
+
+func (s *DashboardRouter) InitDashboardRouter(Router *gin.RouterGroup) {
+	dashboardRouter := Router.Group("dashboard")
+	{
+		dashboardRouter.GET("monitor", system.GetMonitor)
+	}
+}
+```
+
+- [ ] **Step 4: 注册路由**
+
+Edit `internal/router/enter.go`, add DashboardRouter to RouterGroup:
+
+```go
+type RouterGroup struct {
+	System    system.RouterGroup
+	Example   example.RouterGroup
+	Vehicle   vehicle.RouterGroup
+	Dashboard system.DashboardRouter  // add this line
+}
+```
+
+Edit `internal/initialize/router.go`, register in PrivateGroup block:
+
+```go
+systemRouter.InitDashboardRouter(PrivateGroup)  // add this line
+```
+
+- [ ] **Step 5: Casbin**
+
+```bash
+sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', '888', '/dashboard/monitor', 'GET'), ('p', '618', '/dashboard/monitor', 'GET');"
+```
+
+- [ ] **Step 6: 编译+提交**
+
+```bash
+go build -o build/bin/smart-parking.exe .
+git add internal/service/system/sys_dashboard.go internal/api/v1/system/sys_dashboard.go internal/router/system/sys_dashboard.go internal/router/enter.go internal/initialize/router.go
+git commit -m "feat: 新增监控仪表盘API——GET /dashboard/monitor"
+```
+
+---
+
+### Task 2: 前端监控大屏
+
+- [ ] **Step 1: API**
+
+Edit `frontend/src/api/dashboard.js` (add to existing or create):
+
+```js
+import service from '@/utils/request'
+
+export const getMonitor = () => {
+  return service({ url: '/dashboard/monitor', method: 'get' })
+}
+```
+
+- [ ] **Step 2: 页面**
+
+Write `frontend/src/view/dashboard/index.vue` (replace existing):
+
+```vue
+<template>
+  <div class="monitor-page">
+    <div class="monitor-header">
+      <h2>智慧停车监控</h2>
+      <span class="time">{{ now }}</span>
+    </div>
+
+    <el-row :gutter="16" class="stat-row">
+      <el-col :span="6">
+        <el-card><el-statistic title="车位余量" :value="data.used_spaces + ' / ' + data.total_spaces">
+          <template #suffix><span :style="{ color: data.total_spaces > 0 && data.used_spaces >= data.total_spaces ? '#f56c6c' : '#67c23a' }">车位</span></template>
+        </el-statistic></el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card><el-statistic title="在场车辆" :value="data.in_park">
+          <template #suffix>台</template>
+        </el-statistic></el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card><el-statistic title="今日收入" :value="data.today_income" prefix="¥" :precision="2" /></el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card>
+          <el-statistic title="今日进出" :value="data.today_entry">
+            <template #suffix>/ {{ data.today_exit }}</template>
+          </el-statistic>
+          <span style="font-size:12px;color:#909399">进 / 出</span>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card class="device-card">
+      <template #header>设备状态</template>
+      <div class="device-list">
+        <div v-for="d in data.devices" :key="d.channel_name" class="device-item">
+          <el-tag :type="d.device_status === 'online' ? 'success' : 'danger'" size="small">{{ d.device_status === 'online' ? '在线' : '离线' }}</el-tag>
+          <span>{{ d.channel_name }}</span>
+          <span class="device-name">{{ d.device_name }} ({{ d.direction === 'in' ? '入口' : '出口' }})</span>
+        </div>
+        <el-empty v-if="!data.devices || data.devices.length === 0" description="暂无设备" :image-size="60" />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onUnmounted } from 'vue'
+import { getMonitor } from '@/api/dashboard'
+
+const data = reactive({ total_spaces: 0, used_spaces: 0, in_park: 0, today_income: 0, today_entry: 0, today_exit: 0, devices: [] })
+const now = ref('')
+let timer = null
+
+function updateTime() {
+  now.value = new Date().toLocaleString('zh-CN')
+}
+
+async function fetchData() {
+  try {
+    const res = await getMonitor()
+    if (res.code === 0) Object.assign(data, res.data)
+  } catch (e) { /* keep last data on error */ }
+}
+
+onMounted(() => {
+  updateTime()
+  fetchData()
+  timer = setInterval(() => { updateTime(); fetchData() }, 10000)
+})
+
+onUnmounted(() => clearInterval(timer))
+</script>
+
+<style scoped>
+.monitor-page { padding: 16px; }
+.monitor-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
+.monitor-header h2 { margin: 0; }
+.time { color: #909399; font-size: 14px; }
+.stat-row { margin-bottom: 16px; }
+.stat-row .el-card { text-align: center; }
+.device-card { margin-top: 16px; }
+.device-list { display: flex; flex-wrap: wrap; gap: 12px; }
+.device-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #f5f7fa; border-radius: 6px; }
+.device-name { color: #909399; font-size: 13px; }
+</style>
+```
+
+- [ ] **Step 3: 编译+提交**
+
+```bash
+cd frontend && npm run build && cd ..
+git add frontend/src/view/dashboard/index.vue frontend/src/api/dashboard.js
+git commit -m "feat: 升级仪表盘为停车监控大屏——4统计卡片+设备状态+10秒轮询"
+```

+ 129 - 0
docs/superpowers/specs/2026-07-16-manual-entry-exit-design.md

@@ -0,0 +1,129 @@
+# 手动进出场操作页面 — 设计文档
+
+> 日期:2026-07-16 | 优先级:P0-2
+
+## 1. 目标
+
+为岗亭操作员提供手动进出场操作页面,作为 RFID 自动识别的备用兜底。当 UHF 标签损坏、无标签车辆进入、或 RFID 系统故障时,操作员手动输入车牌/RFID 完成进出场。
+
+## 2. 使用场景
+
+- **主要场景**:RFID 不可用时的备用操作(低频)
+- **用户**:管理员(888)、操作员(618)
+- **菜单位置**:停车管理 → 进出场操作
+
+## 3. 页面结构
+
+```
+进出场操作
+├── Tab:车辆进场 | 车辆出场
+│
+├── 输入区
+│   ├── 车牌号输入框
+│   ├── RFID 标签输入框
+│   └── 查询按钮
+│
+├── 车辆详情卡片(查询后显示)
+│   ├── 车牌号、车辆类型、车主名
+│   ├── 当前在场状态
+│   └── 进场/出场特有信息
+│
+└── 操作区
+    ├── 进场:停车场下拉选择 + 确认进场按钮
+    └── 出场:费用信息 + 收费方式 + 确认出场按钮
+```
+
+## 4. 进场流程
+
+```
+输入车牌/RFID → 查询车辆
+  ├─ 找到车辆 → 显示详情,检查是否已入场
+  │   ├─ 已入场 → 提示"该车辆已在场内"
+  │   └─ 未入场 → 选择停车场 → 确认进场
+  └─ 未找到 → 提示"未找到车辆,将按临时车入场"
+      └─ 选择停车场 → 确认进场
+
+确认进场 → POST /vehicle/entry
+  ├─ 成功 → 提示"入场成功",显示入场时间,清空表单
+  └─ 失败 → 提示错误原因
+```
+
+### 涉及 API
+
+| 接口 | 用途 |
+|------|------|
+| `GET /vehicle/get-by-plate?plate_number=` | 根据车牌查询车辆信息 |
+| `GET /parking/lot/list` | 获取停车场下拉列表 |
+| `POST /vehicle/entry` | 执行入场 |
+
+## 5. 出场流程
+
+```
+输入车牌/RFID → 查询车辆
+  ├─ 找到车辆 → 检查是否有未出场记录
+  │   ├─ 无未出场记录 → 提示"该车辆不在场内"
+  │   └─ 有未出场记录 → 显示计费信息
+  │       ├─ 进场时间
+  │       ├─ 停留时长
+  │       ├─ 费用金额
+  │       ├─ 收费方式选择(现金/扫码/免密,本次先做现金)
+  │       └─ 确认出场
+  └─ 未找到 → 提示"未找到车辆信息"
+
+确认出场 → POST /vehicle/exit
+  ├─ 成功 → 提示"出场成功",显示费用,清空表单
+  └─ 失败 → 提示错误原因
+```
+
+### 涉及 API
+
+| 接口 | 用途 |
+|------|------|
+| `GET /vehicle/get-by-plate?plate_number=` | 根据车牌查询车辆信息 |
+| `POST /vehicle/exit` | 执行出场(返回 fee、stayTime、entryTime、exitTime) |
+
+## 6. 技术实现
+
+### 前端
+
+| 项 | 说明 |
+|------|------|
+| 框架 | Vue 3 + Composition API |
+| 组件库 | Element Plus(el-tabs、el-form、el-input、el-select、el-button、el-descriptions) |
+| API 封装 | 复用 `src/api/vehicle.js` 已有的 `vehicleEntry`、`vehicleAppear`、`obtainVehicleLicense` |
+| 文件位置 | `frontend/src/view/parking/entryExit.vue` |
+| 路由 | 后端菜单表中新增记录,前端通过动态路由自动加载 |
+
+### 后端
+
+无需修改。已有 API 全部就绪:
+- `VehicleEntry` / `VehicleExit` handler 在 `internal/api/v1/vehicle/vehicle.go`
+- 计费逻辑在 `internal/service/vehicle/vehicle.go` 的 `calculateFee()`
+- `PassageService` 已抽取公共逻辑(`internal/service/parking/passage.go`)
+
+### 涉及文件变更
+
+| 文件 | 操作 | 说明 |
+|------|------|------|
+| `frontend/src/view/parking/entryExit.vue` | 新增 | 进出场操作页面 |
+| `frontend/src/api/vehicle.js` | 不改 | 已有 entry/exit API |
+| `internal/initialize/seed.go` | 修改 | 新增菜单记录 + Casbin 规则 |
+| 后端其他文件 | 不改 | API 已就绪 |
+
+## 7. 边界情况
+
+| 场景 | 处理方式 |
+|------|---------|
+| 输入为空时点查询 | 前端校验,至少填一个(车牌或 RFID) |
+| 车牌和 RFID 都填 | 优先用车牌号查询 |
+| 查到的车辆已进场 | 进场 Tab:提示"车辆已入场,请切换到出场";出场 Tab:正常显示费用 |
+| 查到的车辆不在场内 | 进场 Tab:允许入场;出场 Tab:提示"车辆不在场内" |
+| 车辆不存在(新临时车) | 进场 Tab:自动创建临时车记录后入场;出场 Tab:提示未找到 |
+| 未选停车场点确认进场 | 前端校验,提示"请选择停车场" |
+| API 调用失败 | 显示后端返回的错误信息 |
+
+## 8. 后续扩展预留
+
+- **关联 UHF 实时扫描**:页面预留"最近扫描"区域,显示 `DeviceManager` 推送的标签列表,点击自动填入
+- **收费方式**:当前写死现金,后续对接 P0-3 收费模块后,支持扫码/免密
+- **抓拍图片**:`VehicleEntry`/`VehicleExit` 已有 `EntryImage`/`ExitImage` 字段,页面后续可加拍照上传

+ 128 - 0
docs/superpowers/specs/2026-07-17-payment-settlement-design.md

@@ -0,0 +1,128 @@
+# 收费结算模块 — 设计文档
+
+> 日期:2026-07-17 | 优先级:P0-3
+
+## 1. 目标
+
+在出场环节增加收费结算流程。临时车人工收费(现金),月租/VIP 车自动免密放行。架构上预留 POS 机、扫码支付的扩展入口。
+
+## 2. 核心改造
+
+### 出场流程
+
+```
+输入车牌 → 查询车辆
+  ├─ 月租/VIP(is_system=false)→ 点"确认出场" → 免密 → 自动放行
+  └─ 临时车(is_system=true)→ 显示费用
+       ├─ 应付金额
+       ├─ 支付方式:[现金] [扫码(预留)] [POS(预留)]
+       └─ 现金模式:
+            ├─ 收费员输入实收金额
+            ├─ 系统自动计算找零
+            └─ "确认收款" → 创建支付记录 → 出场+开闸
+```
+
+### 扩展性设计
+
+- 后端定义 `PaymentMethod` 常量(cash / free / pos / wechat / alipay)
+- 新增支付方式:加常量 + 前端加支付按钮 → 调同一接口传不同 `payment_method`
+- 前端支付按钮按 `paymentMethodList` 渲染,新增只需加数组项
+
+## 3. 数据结构
+
+### 新增表:`payment_record`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | uint | 主键 |
+| record_id | uint | 关联 vehicle_record.id |
+| payment_method | string | cash / free / pos / wechat / alipay |
+| amount | float64 | 应收金额 |
+| paid_amount | float64 | 实收金额(现金时由收费员输入)|
+| change_amount | float64 | 找零 |
+| operator_id | uint | 收费员用户ID |
+| paid_at | datetime | 支付时间 |
+| remark | string | 备注 |
+
+### vehicle_record 表新增字段
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| payment_method | string | 支付方式 |
+
+## 4. API 设计
+
+### 4.1 费用预览
+
+`POST /vehicle/exit/preview`
+
+```
+Request:  { plate_number, rfid_tag }
+Response: { plate_number, entry_time, stay_time, fee, vehicle_type, is_temp }
+```
+
+不修改数据库,仅查询进场记录 + 计算费用。
+
+### 4.2 确认出场(+支付)
+
+`POST /vehicle/exit/confirm`
+
+```
+Request:  { plate_number, rfid_tag, payment_method, paid_amount }
+Response: { plate_number, entry_time, exit_time, stay_time, fee, change_amount, payment_method }
+```
+
+1. 更新 vehicle_record(exit_time, stay_time, fee, payment_status, payment_method)
+2. 创建 payment_record
+3. 更新车辆统计
+
+**payment_method=free 时**:`paid_amount=0`,直接放行。
+
+## 5. 前端改造(entryExit.vue 出场 Tab)
+
+查询到车辆后,根据 `is_temp` 分两路:
+
+### 月租/VIP(非临时车)
+
+直接显示"确认出场"按钮 → 调 `/vehicle/exit/confirm`(payment_method=free)
+
+### 临时车
+
+显示费用信息 + 支付方式选择 + 现金输入(选现金时) + "确认收款"按钮
+
+```
+┌───────────────────────────────────┐
+│ 出场信息                          │
+│ 车牌: 粤A12345  进场: 10:30       │
+│ 停留: 2小时15分  应收: ¥15.00     │
+│                                   │
+│ 支付方式: [现金] [扫码] [POS]     │
+│                                   │
+│ 实收金额: [______] 元             │
+│ 找零: ¥5.00                       │
+│                                   │
+│ [确认收款并放行]                   │
+└───────────────────────────────────┘
+```
+
+## 6. 文件变更清单
+
+| 文件 | 操作 | 说明 |
+|------|------|------|
+| `internal/dao/payment_record.go` | 新增 | 支付记录 DAO |
+| `internal/dao/vehicle_record.go` | 修改 | 加 payment_method 字段 |
+| `internal/model/vehicle/request/vehicle.go` | 修改 | 新增 ExitPreview / ExitConfirm 请求 |
+| `internal/service/vehicle/vehicle.go` | 修改 | 拆出 Preview / Confirm 方法 |
+| `internal/api/v1/vehicle/vehicle.go` | 修改 | 新增路由 handler |
+| `internal/router/vehicle/vehicle.go` | 修改 | 注册新路由 |
+| `frontend/src/view/parking/entryExit.vue` | 修改 | 出场 Tab 增加收费 UI |
+| `frontend/src/api/vehicle.js` | 修改 | 新增 preview / confirm API |
+
+## 7. 边界情况
+
+| 场景 | 处理 |
+|------|------|
+| 现金收不够 | 前端校验:实收 < 应收,提示"金额不足" |
+| 金额输入非数字 | 前端过滤,仅允许数字和点 |
+| 已出场的车再次点出场 | 后端 preview 返回错误 |
+| 免密车类型不对 | confirm 时二次校验 is_system |

+ 104 - 0
docs/superpowers/specs/2026-07-20-monitor-dashboard-design.md

@@ -0,0 +1,104 @@
+# 监控仪表盘 — 设计文档
+
+> 日期:2026-07-20 | 优先级:P2
+
+## 1. 目标
+
+升级现有仪表盘为停车监控大屏。登录后首页展示车位余量、在场车辆、今日收入、今日进出等关键指标,以及通道设备在线状态。
+
+## 2. 页面布局
+
+```
+┌─────────────────────────────────────────────────────┐
+│  智慧停车监控                      2026-07-20 10:30  │
+├──────────┬──────────┬──────────┬────────────────────┤
+│ 车位余量  │ 在场车辆  │ 今日收入  │ 今日进出           │
+│   48/50  │    15    │ ¥1,250  │  进32 出28         │
+├──────────┴──────────┴──────────┴────────────────────┤
+│  设备状态                                           │
+│  ┌─────────────────────────────────────────────────┐│
+│  │ ● 入口通道 1  设备1 [在线]  │ ● 出口通道 1  ... ││
+│  └─────────────────────────────────────────────────┘│
+└─────────────────────────────────────────────────────┘
+```
+
+- 顶部标题 + 当前时间
+- 四个统计卡片(el-row + el-col)
+- 底部设备状态条(横向排列所有通道及其绑定设备)
+
+## 3. API 设计
+
+`GET /dashboard/monitor`
+
+```json
+{
+  "total_spaces": 50,
+  "used_spaces": 15,
+  "in_park": 12,
+  "today_income": 1250.00,
+  "today_entry": 32,
+  "today_exit": 28,
+  "devices": [
+    {
+      "channel_name": "入口通道1",
+      "direction": "in",
+      "device_name": "入口设备1",
+      "device_status": "online"
+    }
+  ]
+}
+```
+
+数据来源:
+| 字段 | SQL |
+|------|-----|
+| total_spaces | `SUM(parking_lot.capacity)` |
+| used_spaces | `total - COUNT(record WHERE exit_time IS NULL)` |
+| in_park | `COUNT(vehicle_record WHERE exit_time IS NULL)` |
+| today_income | `SUM(payment_record.amount WHERE today)` |
+| today_entry | `COUNT(vehicle_record WHERE entry_time >= today)` |
+| today_exit | `COUNT(vehicle_record WHERE exit_time >= today)` |
+| devices | `channel LEFT JOIN uhf_reader` |
+
+## 4. 前端实现
+
+### 文件变更
+
+| 文件 | 操作 | 说明 |
+|------|------|------|
+| `frontend/src/view/dashboard/index.vue` | 修改 | 替换现有仪表盘内容 |
+| `frontend/src/api/dashboard.js` | 修改/新增 | 加 getMonitor API |
+
+### 刷新策略
+
+- `setInterval` 10 秒轮询 `GET /dashboard/monitor`
+- 组件 `onUnmounted` 清除定时器
+
+### 复用
+
+- 使用 Element Plus `el-statistic` 组件(已在收费记录页用过,项目已有此组件)
+
+## 5. 后端实现
+
+### 文件变更
+
+| 文件 | 操作 | 说明 |
+|------|------|------|
+| `internal/api/v1/system/sys_dashboard.go` | 新增/修改 | GetMonitor handler |
+| `internal/service/system/sys_dashboard.go` | 新增/修改 | MonitorData service |
+| `internal/router/system/sys_dashboard.go` | 修改 | 注册路由 |
+
+放在 system 模块下(与现有仪表盘接口同位置)。
+
+## 6. 菜单
+
+不新增菜单,直接替换现有仪表盘页面(`view/dashboard/index.vue`)。原路由 `/dashboard` 不变。
+
+## 7. 边界情况
+
+| 场景 | 处理 |
+|------|------|
+| 无车辆在场 | used_spaces 和 in_park 显示 0 |
+| 设备全部离线 | 设备卡片标红,数字卡片正常显示 |
+| 无停车场数据 | total_spaces = 0,显示"未配置" |
+| API 异常 | 保留上次数据,不刷新;卡片显示 "--" |

+ 5 - 0
frontend/src/api/dashboard.js

@@ -0,0 +1,5 @@
+import service from '@/utils/request'
+
+export const getMonitor = () => {
+  return service({ url: '/dashboard/monitor', method: 'get' })
+}

+ 18 - 0
frontend/src/api/payment.js

@@ -0,0 +1,18 @@
+import service from '@/utils/request'
+
+// 收费记录列表
+export const getPaymentList = (params) => {
+  return service({
+    url: '/payment/list',
+    method: 'get',
+    params
+  })
+}
+
+// 当日收费汇总
+export const getTodaySummary = () => {
+  return service({
+    url: '/payment/today-summary',
+    method: 'get'
+  })
+}

+ 18 - 0
frontend/src/api/vehicle.js

@@ -169,3 +169,21 @@ export const getAllVehicleTypes = () => {
     method: 'get'
   })
 }
+
+// 出场费用预览
+export const exitPreview = (data) => {
+  return service({
+    url: '/vehicle/exit/preview',
+    method: 'post',
+    data
+  })
+}
+
+// 确认出场并支付
+export const exitConfirm = (data) => {
+  return service({
+    url: '/vehicle/exit/confirm',
+    method: 'post',
+    data
+  })
+}

+ 32 - 1
frontend/src/lang/en.js

@@ -127,6 +127,7 @@ export default {
   carOwner: 'OwnerManage', // 车主管理
   carInfo: 'VehicleInfo', // 车辆信息
   pricing: 'FeeConfig', // 收费配置
+  entryExit: 'Entry/Exit', // 进出场操作
   report: 'ReportQuery', // 报表查询
   enter: 'AccessRecord', // 进出记录
   sheet: 'ExceptionReport', // 异常报表
@@ -191,5 +192,35 @@ export default {
   minute: 'Minute',
   entryImg: 'Entry Image',
   exitImg: 'Exit Image',
-  recordDetail: 'Access Record Details'
+  recordDetail: 'Access Record Details',
+  entryVehicle: 'Vehicle Entry',
+  exitVehicle: 'Vehicle Exit',
+  confirmEntry: 'Confirm Entry',
+  confirmEntryTemp: 'Confirm Entry (Temp)',
+  confirmExitAndPay: 'Confirm Exit & Pay',
+  exitAutoCalcFee: 'Parking fee will be calculated automatically',
+  notFoundVehicle: 'Vehicle Not Found',
+  tempCarEntryHint: 'Will be processed as temporary vehicle. Select parking lot and confirm entry.',
+  plateNotRegHint: 'Vehicle not registered. Please verify the plate number.',
+  entrySuccessMsg: 'Entry Success',
+  exitSuccessMsg: 'Exit Success',
+  entryFailed: 'Entry Failed',
+  exitFailed: 'Exit Failed',
+  pleaseEnterPlateOrRfid: 'Please enter plate number or RFID tag',
+  vehicleDetail: 'Vehicle Info',
+  exitDetail: 'Exit Info',
+  confirmExitTitle: 'Confirm Exit',
+  confirmExitMsg: 'Confirm vehicle exit?',
+  pleaseSelectPark: 'Please select parking lot',
+  rfidInput: 'Please enter RFID tag',
+  exitRecordDetail: 'Entry: {entryTime} | Stay: {duration} | Fee: ¥{fee}',
+  paymentMethod: 'Payment Method',
+  cashPayment: 'Cash',
+  wechatPayment: 'QR Code',
+  posPayment: 'POS Card',
+  cashReceived: 'Received',
+  change: 'Change',
+  confirmPayAndExit: 'Confirm & Release',
+  insufficientAmount: 'Insufficient amount',
+  paymentRecord: 'Payment Record',
 }

+ 32 - 1
frontend/src/lang/zh-CN.js

@@ -127,6 +127,7 @@ export default {
   carOwner: '车主管理',
   carInfo: '车辆信息',
   pricing: '收费配置',
+  entryExit: '进出场操作',
   report: '报表信息查询',
   enter: '进出记录',
   sheet: '异常报表',
@@ -191,5 +192,35 @@ export default {
   minute: '分钟',
   entryImg: '入场图片',
   exitImg: '出场图片',
-  recordDetail: '进出记录详情'
+  recordDetail: '进出记录详情',
+  entryVehicle: '车辆进场',
+  exitVehicle: '车辆出场',
+  confirmEntry: '确认进场',
+  confirmEntryTemp: '确认进场(临时车)',
+  confirmExitAndPay: '确认出场并收费',
+  exitAutoCalcFee: '确认后系统将自动计算停车费用',
+  notFoundVehicle: '未找到车辆信息',
+  tempCarEntryHint: '将按临时车处理,请选择停车场后确认进场',
+  plateNotRegHint: '该车辆未登记,请核实车牌号',
+  entrySuccessMsg: '入场成功',
+  exitSuccessMsg: '出场成功',
+  entryFailed: '入场失败',
+  exitFailed: '出场失败',
+  pleaseEnterPlateOrRfid: '请至少输入车牌号或RFID标签',
+  vehicleDetail: '车辆信息',
+  exitDetail: '出场信息',
+  confirmExitTitle: '确认出场',
+  confirmExitMsg: '确认车辆出场?',
+  pleaseSelectPark: '请选择停车场',
+  rfidInput: '请输入RFID标签',
+  exitRecordDetail: '进场: {entryTime} | 停留: {duration} | 费用: ¥{fee}',
+  paymentMethod: '支付方式',
+  cashPayment: '现金',
+  wechatPayment: '扫码支付',
+  posPayment: 'POS刷卡',
+  cashReceived: '实收金额',
+  change: '找零',
+  confirmPayAndExit: '确认收款并放行',
+  insufficientAmount: '实收金额不足',
+  paymentRecord: '收费记录',
 }

+ 24 - 1
frontend/src/lang/zh-HK.js

@@ -127,6 +127,7 @@ export default {
   carOwner: '車主管理',
   carInfo: '車輛資訊',
   pricing: '收費設定',
+  entryExit: '進出場操作',
   report: '報表資訊查詢',
   enter: '出入記錄',
   sheet: '異常報表',
@@ -191,5 +192,27 @@ export default {
   minute: '分鐘',
   entryImg: '入場圖片',
   exitImg: '出場圖片',
-  recordDetail: '進出記錄詳情'
+  recordDetail: '進出記錄詳情',
+  entryVehicle: '車輛進場',
+  exitVehicle: '車輛出場',
+  confirmEntry: '確認進場',
+  confirmEntryTemp: '確認進場(臨時車)',
+  confirmExitAndPay: '確認出場並收費',
+  exitAutoCalcFee: '確認後系統將自動計算停車費用',
+  notFoundVehicle: '未找到車輛信息',
+  tempCarEntryHint: '將按臨時車處理,請選擇停車場後確認進場',
+  plateNotRegHint: '該車輛未登記,請核實車牌號',
+  entrySuccessMsg: '入場成功',
+  exitSuccessMsg: '出場成功',
+  entryFailed: '入場失敗',
+  exitFailed: '出場失敗',
+  pleaseEnterPlateOrRfid: '請至少輸入車牌號或RFID標籤',
+  vehicleDetail: '車輛信息',
+  exitDetail: '出場信息',
+  confirmExitTitle: '確認出場',
+  confirmExitMsg: '確認車輛出場?',
+  pleaseSelectPark: '請選擇停車場',
+  rfidInput: '請輸入RFID標籤',
+  exitRecordDetail: '進場: {entryTime} | 停留: {duration} | 費用: ¥{fee}',
+  paymentRecord: '收費記錄',
 }

+ 24 - 1
frontend/src/lang/zh-TW.js

@@ -127,6 +127,7 @@ export default {
   carOwner: '車主管理',
   carInfo: '車輛資訊',
   pricing: '收費設定',
+  entryExit: '進出場操作',
   report: '報表查詢',
   enter: '進出紀錄',
   sheet: '異常報表',
@@ -191,5 +192,27 @@ export default {
   minute: '分鐘',
   entryImg: '入場圖片',
   exitImg: '出場圖片',
-  recordDetail: '進出記錄詳情'
+  recordDetail: '進出記錄詳情',
+  entryVehicle: '車輛進場',
+  exitVehicle: '車輛出場',
+  confirmEntry: '確認進場',
+  confirmEntryTemp: '確認進場(臨時車)',
+  confirmExitAndPay: '確認出場並收費',
+  exitAutoCalcFee: '確認後系統將自動計算停車費用',
+  notFoundVehicle: '未找到車輛信息',
+  tempCarEntryHint: '將按臨時車處理,請選擇停車場後確認進場',
+  plateNotRegHint: '該車輛未登記,請核實車牌號',
+  entrySuccessMsg: '入場成功',
+  exitSuccessMsg: '出場成功',
+  entryFailed: '入場失敗',
+  exitFailed: '出場失敗',
+  pleaseEnterPlateOrRfid: '請至少輸入車牌號或RFID標籤',
+  vehicleDetail: '車輛信息',
+  exitDetail: '出場信息',
+  confirmExitTitle: '確認出場',
+  confirmExitMsg: '確認車輛出場?',
+  pleaseSelectPark: '請選擇停車場',
+  rfidInput: '請輸入RFID標籤',
+  exitRecordDetail: '進場: {entryTime} | 停留: {duration} | 費用: ¥{fee}',
+  paymentRecord: '收費記錄',
 }

+ 232 - 155
frontend/src/view/dashboard/index.vue

@@ -1,172 +1,249 @@
 <template>
-  <div class="page">
-    <div class="gva-card-box">
-      <div class="gva-card gva-top-card">
-        <div class="gva-top-card-left">
-          <div class="gva-top-card-left-title">早安,管理员,请开始一天的工作吧</div>
-          <div class="gva-top-card-left-dot">{{ weatherInfo }}</div>
-        </div>
-        <img
-          src="@/assets/dashboard.png"
-          class="gva-top-card-right"
-          alt
-        >
+  <div class="monitor" :class="{ 'is-critical': isFull }">
+    <!-- 顶部状态栏 -->
+    <header class="monitor-bar">
+      <div class="bar-left">
+        <span class="bar-dot" />
+        <span class="bar-label">智慧停车监控</span>
+      </div>
+      <div class="bar-right">
+        <span class="bar-status">{{ isFull ? '已满' : '运营中' }}</span>
+        <span class="bar-divider">|</span>
+        <span class="bar-time">{{ now }}</span>
+      </div>
+    </header>
+
+    <!-- 核心读表区 -->
+    <section class="readouts">
+      <div class="readout-card spaces" :class="{ full: isFull }">
+        <p class="readout-label">车位余量</p>
+        <p class="readout-value">
+          <span class="readout-num used">{{ data.used_spaces }}</span>
+          <span class="readout-sep">/</span>
+          <span class="readout-num total">{{ data.total_spaces }}</span>
+        </p>
+        <p class="readout-meta" v-if="isFull">全场满</p>
+        <p class="readout-meta" v-else>{{ data.total_spaces - data.used_spaces }} 空位</p>
+      </div>
+      <div class="readout-card park">
+        <p class="readout-label">在场车辆</p>
+        <p class="readout-value">{{ data.in_park }} <span class="readout-unit">台</span></p>
       </div>
-    </div>
-    
-    <div class="gva-card-box">
-      <div class="gva-card">
-        <div class="gva-card-title">数据统计</div>
-        <div class="p-4">
-          <el-row :gutter="20">
-            <el-col
-              :xs="24"
-              :sm="18"
-            >
-              <echarts-line />
-            </el-col>
-            <el-col
-              :xs="24"
-              :sm="6"
-            >
-              <dashboard-table />
-            </el-col>
-          </el-row>
+      <div class="readout-card income">
+        <p class="readout-label">今日收入</p>
+        <p class="readout-value">
+          <span class="readout-currency">¥</span>{{ formatNum(data.today_income) }}
+        </p>
+      </div>
+      <div class="readout-card flow">
+        <p class="readout-label">今日进出</p>
+        <p class="readout-value">
+          <span class="readout-num entry">{{ data.today_entry }}</span>
+          <span class="readout-arrow">→</span>
+          <span class="readout-num exit">{{ data.today_exit }}</span>
+        </p>
+      </div>
+    </section>
+
+    <!-- 设备状态 -->
+    <section class="devices-panel">
+      <h3 class="panel-title">设备状态</h3>
+      <div class="device-grid">
+        <div
+          v-for="d in data.devices"
+          :key="d.channel_name"
+          class="device-row"
+          :class="{ offline: d.device_status !== 'online' }"
+        >
+          <span class="device-indicator" :class="d.device_status" />
+          <span class="device-channel">{{ d.channel_name }}</span>
+          <span class="device-dir">{{ d.direction === 'in' ? '入口' : '出口' }}</span>
+          <span class="device-name">{{ d.device_name }}</span>
+        </div>
+        <div v-if="!data.devices || data.devices.length === 0" class="device-empty">
+          暂无设备连接
         </div>
       </div>
-    </div>
+    </section>
   </div>
 </template>
 
 <script setup>
-import EchartsLine from '@/view/dashboard/dashboardCharts/echartsLine.vue'
-import DashboardTable from '@/view/dashboard/dashboardTable/dashboardTable.vue'
-import { ref } from 'vue'
-import { useRouter } from 'vue-router'
-import { useWeatherInfo } from '@/view/dashboard/weather.js'
-import SelectImage from '@/components/selectImage/selectImage.vue'
-
-defineOptions({
-  name: 'Dashboard'
+import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
+import { getMonitor } from '@/api/dashboard'
+
+const data = reactive({
+  total_spaces: 0, used_spaces: 0, in_park: 0,
+  today_income: 0, today_entry: 0, today_exit: 0,
+  devices: []
 })
+const now = ref('')
+let dataTimer = null
+let clockTimer = null
+
+const isFull = computed(() => data.total_spaces > 0 && data.used_spaces >= data.total_spaces)
+
+function formatNum(n) { return (n || 0).toFixed(2) }
 
-const weatherInfo = useWeatherInfo()
-
-const toolCards = ref([
-  {
-    label: '用户管理',
-    icon: 'monitor',
-    name: 'user',
-    color: '#ff9c6e',
-    bg: 'rgba(255, 156, 110,.3)'
-  },
-  {
-    label: '角色管理',
-    icon: 'setting',
-    name: 'authority',
-    color: '#69c0ff',
-    bg: 'rgba(105, 192, 255,.3)'
-  },
-  {
-    label: '菜单管理',
-    icon: 'menu',
-    name: 'menu',
-    color: '#b37feb',
-    bg: 'rgba(179, 127, 235,.3)'
-  },
-  {
-    label: '代码生成器',
-    icon: 'cpu',
-    name: 'autoCode',
-    color: '#ffd666',
-    bg: 'rgba(255, 214, 102,.3)'
-  },
-  {
-    label: '表单生成器',
-    icon: 'document-checked',
-    name: 'formCreate',
-    color: '#ff85c0',
-    bg: 'rgba(255, 133, 192,.3)'
-  },
-  {
-    label: '关于我们',
-    icon: 'user',
-    name: 'about',
-    color: '#5cdbd3',
-    bg: 'rgba(92, 219, 211,.3)'
-  }
-])
-
-const router = useRouter()
-
-const toTarget = (name) => {
-  router.push({ name })
+function updateClock() {
+  now.value = new Date().toLocaleString('zh-CN', {
+    hour: '2-digit', minute: '2-digit', second: '2-digit'
+  })
 }
 
+async function fetchData() {
+  try {
+    const res = await getMonitor()
+    if (res.code === 0) Object.assign(data, res.data)
+  } catch (e) { /* keep last state */ }
+}
+
+onMounted(() => {
+  updateClock()
+  fetchData()
+  clockTimer = setInterval(updateClock, 1000)
+  dataTimer = setInterval(fetchData, 10000)
+})
+
+onUnmounted(() => {
+  clearInterval(clockTimer)
+  clearInterval(dataTimer)
+})
 </script>
 
-<style lang="scss" scoped>
-.page {
-    @apply p-0;
-    .gva-card-box{
-      @apply p-4;
-      &+.gva-card-box{
-        @apply pt-0;
-      }
-    }
-    .gva-card {
-      @apply box-border bg-white rounded h-auto px-6 py-8 overflow-hidden shadow-sm;
-      .gva-card-title{
-        @apply pb-5 border-t-0 border-l-0 border-r-0 border-b border-solid border-gray-100;
-      }
-    }
-    .gva-top-card {
-        @apply h-72 flex items-center justify-between text-gray-500;
-        &-left {
-          @apply h-full flex flex-col w-auto;
-            &-title {
-              @apply text-3xl text-gray-600;
-            }
-            &-dot {
-              @apply mt-4 text-gray-600 text-lg;
-            }
-            &-item{
-              +.gva-top-card-left-item{
-                margin-top: 24px;
-              }
-              margin-top: 14px;
-            }
-        }
-        &-right {
-            height: 600px;
-            width: 600px;
-            margin-top: 28px;
-        }
-    }
-     ::v-deep(.el-card__header){
-          @apply p-0  border-gray-200;
-        }
-        .card-header{
-          @apply pb-5 border-b border-solid border-gray-200 border-t-0 border-l-0 border-r-0;
-        }
-    .quick-entrance-items {
-      @apply flex items-center justify-center text-center text-gray-800;
-        .quick-entrance-item {
-          @apply px-8 py-6 flex items-center flex-col transition-all duration-100 ease-in-out rounded-lg cursor-pointer;
-          &:hover{
-            @apply shadow-lg;
-          }
-            &-icon {
-              @apply flex items-center h-16 w-16 rounded-lg justify-center mx-0 my-auto text-2xl;
-            }
-            p {
-                @apply mt-2.5;
-            }
-        }
-    }
-}
-.dashboard-icon {
-  @apply flex items-center text-xl mr-2 text-blue-400;
+<style scoped>
+/* === Theme tokens === */
+.monitor {
+  --bg:    #0c1922;
+  --panel: #152534;
+  --text:  #e2ecf5;
+  --muted: #8ca8c4;
+  --green: #00e6a0;
+  --amber: #fca33b;
+  --red:   #ff4757;
+
+  background: var(--bg);
+  min-height: 100vh;
+  padding: 24px 32px;
+  color: var(--text);
+  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+  transition: background .6s;
+}
+.monitor.is-critical {
+  --bg: #1a0f0f;
+  --panel: #241414;
 }
 
+/* === Bar === */
+.monitor-bar {
+  display: flex; justify-content: space-between; align-items: center;
+  padding: 16px 20px; margin: -24px -32px 28px;
+  background: var(--panel);
+  border-bottom: 1px solid #1e3240;
+}
+.bar-left { display: flex; align-items: center; gap: 10px; }
+.bar-dot {
+  width: 8px; height: 8px; border-radius: 50%; background: var(--green);
+  box-shadow: 0 0 6px var(--green);
+}
+.is-critical .bar-dot { background: var(--red); box-shadow: 0 0 6px var(--red); }
+.bar-label { font-size: 18px; font-weight: 700; letter-spacing: .04em; color: #ffffff; }
+.bar-right { display: flex; gap: 12px; align-items: center; font-size: 13px; color: #b0c8de; }
+.bar-status { color: var(--green); }
+.is-critical .bar-status { color: var(--red); }
+.bar-divider { color: #2a3d4e; }
+.bar-time { font-variant-numeric: tabular-nums; color: #ffffff; font-size: 15px; font-weight: 600; }
+
+/* === Readouts === */
+.readouts {
+  display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 28px;
+}
+.readout-card {
+  background: var(--panel);
+  border: 1px solid #1a2e3c;
+  border-radius: 10px;
+  padding: 24px 20px 20px;
+  text-align: center;
+  transition: border-color .4s, box-shadow .4s;
+}
+.readout-card.full {
+  border-color: var(--red);
+  box-shadow: inset 0 0 40px rgba(255,71,87,.06), 0 0 20px rgba(255,71,87,.08);
+  animation: pulse-red 2s ease-in-out infinite;
+}
+@keyframes pulse-red {
+  0%, 100% { box-shadow: inset 0 0 30px rgba(255,71,87,.04), 0 0 16px rgba(255,71,87,.06); }
+  50%      { box-shadow: inset 0 0 50px rgba(255,71,87,.08), 0 0 28px rgba(255,71,87,.1); }
+}
+
+/* Label */
+.readout-label {
+  font-size: 13px; text-transform: uppercase; letter-spacing: .08em;
+  color: #a0bedb; margin: 0 0 10px; font-weight: 600;
+}
+
+/* Value */
+.readout-value {
+  margin: 0;
+  font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
+  font-size: 36px; font-weight: 600; letter-spacing: -.02em;
+  color: #eaf4fb;
+  line-height: 1.1;
+}
+.spaces .readout-num.used { color: #eaf4fb; }
+.full .spaces .readout-num.used { color: var(--red); text-shadow: 0 0 12px rgba(255,71,87,.4); }
+.readout-sep { font-size: 28px; color: #3a5068; margin: 0 4px; font-weight: 400; }
+.readout-num.total { color: var(--muted); font-weight: 400; }
+.readout-unit { font-size: 16px; color: var(--muted); font-weight: 400; letter-spacing: 0; }
+.readout-currency { font-size: 20px; color: var(--green); margin-right: 2px; font-weight: 400; }
+.readout-arrow { color: #3a5068; margin: 0 6px; font-size: 22px; }
+.readout-num.entry { color: var(--green); }
+.readout-num.exit { color: var(--amber); }
+.readout-meta { margin: 8px 0 0; font-size: 12px; color: var(--muted); }
+.full .readout-meta { color: var(--red); font-weight: 500; }
+
+/* Income glow */
+.income .readout-value {
+  color: var(--green);
+  text-shadow: 0 0 18px rgba(0,214,143,.15);
+}
+
+/* === Devices === */
+.devices-panel {
+  background: var(--panel); border: 1px solid #1a2e3c; border-radius: 10px;
+  padding: 20px 24px 16px;
+}
+.panel-title {
+  margin: 0 0 14px; font-size: 13px; text-transform: uppercase;
+  letter-spacing: .08em; color: #a0bedb; font-weight: 600;
+}
+.device-grid {
+  display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px;
+}
+.device-row {
+  display: flex; align-items: center; gap: 10px;
+  padding: 10px 14px;
+  background: #0e1a23; border-radius: 6px;
+  border-left: 3px solid var(--green);
+  transition: border-color .3s;
+}
+.device-row.offline { border-left-color: var(--red); }
+.device-indicator {
+  width: 7px; height: 7px; border-radius: 50%; background: var(--green);
+  box-shadow: 0 0 5px var(--green); flex-shrink: 0;
+}
+.device-indicator.offline { background: var(--red); box-shadow: 0 0 5px var(--red); }
+.device-channel { font-weight: 600; font-size: 13px; min-width: 80px; color: #dce8f2; }
+.device-dir {
+  font-size: 11px; color: #8aa8c2; background: #162330;
+  padding: 2px 8px; border-radius: 3px;
+}
+.device-name { color: #849eb6; font-size: 13px; margin-left: auto; }
+.device-empty { color: #8aa8c2; font-size: 13px; padding: 8px; }
+
+/* === Responsive === */
+@media (max-width: 900px) {
+  .readouts { grid-template-columns: repeat(2, 1fr); }
+  .readout-value { font-size: 28px; }
+}
 </style>

+ 12 - 10
frontend/src/view/parking/carInfo.vue

@@ -263,16 +263,18 @@
         />
         <el-table-column :label="$t.value.Action">
           <template #default="scope">
-            <el-button
-              text
-              type="primary"
-              @click="openTypeForm($t.value.modify, scope.row)"
-            >{{ $t.value.modify }}</el-button>
-            <el-button
-              text
-              type="danger"
-              @click="deleteType(scope.row.ID)"
-            >{{ $t.value.remove }}</el-button>
+            <template v-if="!scope.row.is_system">
+              <el-button
+                text
+                type="primary"
+                @click="openTypeForm($t.value.modify, scope.row)"
+              >{{ $t.value.modify }}</el-button>
+              <el-button
+                text
+                type="danger"
+                @click="deleteType(scope.row.ID)"
+              >{{ $t.value.remove }}</el-button>
+            </template>
           </template>
         </el-table-column>
       </el-table>

+ 298 - 0
frontend/src/view/parking/entryExit.vue

@@ -0,0 +1,298 @@
+<template>
+  <div class="entry-exit-page">
+    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+      <el-tab-pane :label="$t.value.entryVehicle" name="entry" />
+      <el-tab-pane :label="$t.value.exitVehicle" name="exit" />
+    </el-tabs>
+
+    <el-card class="query-card">
+      <el-form :model="queryForm" inline>
+        <el-form-item :label="$t.value.LicensePlateNo">
+          <el-input v-model="queryForm.plate_number" :placeholder="$t.value.carPlateInput" clearable style="width: 200px" @keyup.enter="queryVehicle" />
+        </el-form-item>
+        <el-form-item :label="$t.value.RFID_Tag">
+          <el-input v-model="queryForm.rfid_tag" :placeholder="$t.value.rfidInput" clearable style="width: 240px" @keyup.enter="queryVehicle" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="Search" @click="queryVehicle">{{ $t.value.Search }}</el-button>
+          <el-button icon="Refresh" @click="resetForm">{{ $t.value.Reset }}</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-card v-if="vehicleLoaded" class="detail-card">
+      <template #header>
+        <span class="card-title">{{ activeTab === 'entry' ? $t.value.vehicleDetail : $t.value.exitDetail }}</span>
+      </template>
+
+      <el-descriptions :column="3" border>
+        <el-descriptions-item :label="$t.value.LicensePlateNo">{{ vehicleInfo.plate_number || '-' }}</el-descriptions-item>
+        <el-descriptions-item :label="$t.value.RFID_Tag">{{ vehicleInfo.rfid_tag || '-' }}</el-descriptions-item>
+        <el-descriptions-item :label="$t.value.VehicleType">
+          <el-tag v-if="vehicleInfo.VehicleType" :type="vehicleInfo.VehicleType.is_system ? 'warning' : 'success'">{{ vehicleInfo.VehicleType.name }}</el-tag>
+          <el-tag v-else type="warning">{{ $t.value.tempCar }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item v-if="vehicleInfo.Owner" :label="$t.value.CarOwner">{{ vehicleInfo.Owner.owner_name }}</el-descriptions-item>
+        <el-descriptions-item v-if="vehicleInfo.Owner && vehicleInfo.Owner.is_vip" label="VIP"><el-tag type="danger">{{ $t.value.yes }}</el-tag></el-descriptions-item>
+      </el-descriptions>
+
+      <!-- 进场特有操作区 -->
+      <div v-if="activeTab === 'entry'" class="action-area">
+        <el-divider />
+        <el-form :model="entryForm" inline>
+          <el-form-item :label="$t.value.selectParkArea" required>
+            <el-select v-model="entryForm.parking_lot_id" :placeholder="$t.value.pleaseSelectPark" style="width: 260px">
+              <el-option v-for="lot in parkingLotList" :key="lot.ID" :label="lot.lot_name" :value="lot.ID" />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="success" icon="Check" :loading="submitting" @click="confirmEntry">{{ $t.value.confirmEntry }}</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <!-- 出场操作区 -->
+      <div v-if="activeTab === 'exit' && exitData" class="action-area">
+        <el-divider />
+        <el-descriptions :column="2" border>
+          <el-descriptions-item :label="$t.value.EntryTime">
+            {{ formatTime(exitData.entry_time) }}
+          </el-descriptions-item>
+          <el-descriptions-item :label="$t.value.StayDuration">
+            {{ formatDuration(exitData.stay_time) }}
+          </el-descriptions-item>
+          <el-descriptions-item :label="$t.value.Fee">
+            <span class="fee-amount">¥{{ exitData.fee.toFixed(2) }}</span>
+          </el-descriptions-item>
+          <el-descriptions-item :label="$t.value.VehicleType">
+            {{ exitData.vehicle_type || '-' }}
+          </el-descriptions-item>
+        </el-descriptions>
+
+        <!-- 临时车:支付方式选择 -->
+        <div v-if="exitData.is_temp" class="payment-section">
+          <p class="section-label">{{ $t.value.paymentMethod }}:</p>
+          <el-radio-group v-model="paymentMethod">
+            <el-radio-button value="cash">{{ $t.value.cashPayment }}</el-radio-button>
+            <el-radio-button value="wechat" disabled>{{ $t.value.wechatPayment }}</el-radio-button>
+            <el-radio-button value="pos" disabled>{{ $t.value.posPayment }}</el-radio-button>
+          </el-radio-group>
+
+          <div v-if="paymentMethod === 'cash'" class="cash-input">
+            <span class="cash-label">{{ $t.value.cashReceived }}:</span>
+            <el-input-number v-model="paidAmount" :min="0" :precision="1" style="width: 160px" />
+            <span class="change-label">{{ $t.value.change }}: ¥{{ Math.max(0, paidAmount - exitData.fee).toFixed(1) }}</span>
+          </div>
+        </div>
+
+        <div style="margin-top: 16px">
+          <el-button type="danger" icon="Check" :loading="submitting" @click="confirmExit">
+            {{ exitData.is_temp ? $t.value.confirmPayAndExit : $t.value.confirmExitAndPay }}
+          </el-button>
+        </div>
+      </div>
+    </el-card>
+
+    <el-result v-if="notFound" icon="warning" :title="$t.value.notFoundVehicle" :sub-title="activeTab === 'entry' ? $t.value.tempCarEntryHint : $t.value.plateNotRegHint">
+      <template v-if="activeTab === 'entry'" #extra>
+        <el-form :model="entryForm" inline>
+          <el-form-item :label="$t.value.selectParkArea" required>
+            <el-select v-model="entryForm.parking_lot_id" :placeholder="$t.value.pleaseSelectPark" style="width: 260px">
+              <el-option v-for="lot in parkingLotList" :key="lot.ID" :label="lot.lot_name" :value="lot.ID" />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="success" :loading="submitting" @click="confirmEntry">{{ $t.value.confirmEntryTemp }}</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-result>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { obtainVehicleLicense, vehicleEntry, exitPreview, exitConfirm } from '@/api/vehicle'
+import { obtainPullOverList } from '@/api/pullOver'
+import { t } from '@/lang/index.js'
+
+const activeTab = ref('entry')
+const queryForm = reactive({ plate_number: '', rfid_tag: '' })
+const vehicleInfo = ref(null)
+const vehicleLoaded = ref(false)
+const notFound = ref(false)
+const submitting = ref(false)
+const entryForm = reactive({ parking_lot_id: null })
+const parkingLotList = ref([])
+
+// 出场收费状态
+const exitData = ref(null)
+const paymentMethod = ref('cash')
+const paidAmount = ref(0)
+
+onMounted(async () => {
+  try {
+    const res = await obtainPullOverList({ page: 1, pageSize: 100 })
+    parkingLotList.value = res.data?.list || []
+  } catch (e) {
+    console.error('Failed to load parking lots', e)
+  }
+})
+
+async function queryVehicle() {
+  const plate = queryForm.plate_number.trim()
+  const rfid = queryForm.rfid_tag.trim()
+  if (!plate && !rfid) {
+    ElMessage.warning(t.value.pleaseEnterPlateOrRfid)
+    return
+  }
+  vehicleInfo.value = null
+  vehicleLoaded.value = false
+  notFound.value = false
+  exitData.value = null
+  try {
+    const res = await obtainVehicleLicense(plate || rfid)
+    if (res.code === 0 && res.data) {
+      vehicleInfo.value = res.data
+      vehicleLoaded.value = true
+      // 出场模式:预览费用
+      if (activeTab.value === 'exit') {
+        await previewExitFee()
+      }
+    } else {
+      notFound.value = true
+    }
+  } catch (e) {
+    notFound.value = true
+  }
+}
+
+async function previewExitFee() {
+  try {
+    const res = await exitPreview({
+      plate_number: queryForm.plate_number.trim(),
+      rfid_tag: queryForm.rfid_tag.trim()
+    })
+    if (res.code === 0) {
+      exitData.value = res.data
+      paidAmount.value = 0
+      paymentMethod.value = 'cash'
+    }
+  } catch (e) {
+    // preview 失败不影响,confirmExit 会再次校验
+    console.error('Preview failed', e)
+  }
+}
+
+async function confirmEntry() {
+  if (!entryForm.parking_lot_id) {
+    ElMessage.warning(t.value.pleaseSelectPark)
+    return
+  }
+  if (submitting.value) return
+  submitting.value = true
+  try {
+    const res = await vehicleEntry({
+      plate_number: queryForm.plate_number.trim(),
+      rfid_tag: queryForm.rfid_tag.trim(),
+      parking_lot_id: entryForm.parking_lot_id
+    })
+    if (res.code === 0) {
+      const plate = res.data?.data?.plate_number || queryForm.plate_number.trim()
+      ElMessage.success(`${t.value.entrySuccessMsg}!${plate}`)
+      resetForm()
+    } else {
+      ElMessage.error(res.msg || t.value.entryFailed)
+    }
+  } catch (e) {
+    ElMessage.error(e.message || t.value.entryFailed)
+  } finally {
+    submitting.value = false
+  }
+}
+
+async function confirmExit() {
+  // 临时车现金:校验实收金额
+  if (exitData.value?.is_temp && paymentMethod.value === 'cash') {
+    if (paidAmount.value < exitData.value.fee) {
+      ElMessage.warning(t.value.insufficientAmount)
+      return
+    }
+  }
+
+  await ElMessageBox.confirm(
+    t.value.confirmExitMsg,
+    t.value.confirmExitTitle,
+    { confirmButtonText: t.value.Confirm, cancelButtonText: t.value.Cancel, type: 'warning' }
+  )
+  if (submitting.value) return
+  submitting.value = true
+  try {
+    const pm = exitData.value?.is_temp ? paymentMethod.value : 'free'
+    const pa = exitData.value?.is_temp ? paidAmount.value : 0
+    const res = await exitConfirm({
+      plate_number: queryForm.plate_number.trim(),
+      rfid_tag: queryForm.rfid_tag.trim(),
+      payment_method: pm,
+      paid_amount: pa
+    })
+    if (res.code === 0) {
+      const d = res.data
+      const detail = t.value.exitRecordDetail
+        .replace('{entryTime}', new Date(d.entry_time * 1000).toLocaleString('zh-CN'))
+        .replace('{duration}', formatDuration(d.stay_time))
+        .replace('{fee}', d.fee.toFixed(2))
+      ElMessage.success(`${t.value.exitSuccessMsg}!${detail}`)
+      resetForm()
+    } else {
+      ElMessage.error(res.msg || t.value.exitFailed)
+    }
+  } catch (e) {
+    ElMessage.error(e.message || t.value.exitFailed)
+  } finally {
+    submitting.value = false
+  }
+}
+
+function handleTabChange() { resetForm() }
+
+function resetForm() {
+  queryForm.plate_number = ''
+  queryForm.rfid_tag = ''
+  vehicleInfo.value = null
+  vehicleLoaded.value = false
+  notFound.value = false
+  exitData.value = null
+  entryForm.parking_lot_id = null
+  paidAmount.value = 0
+  paymentMethod.value = 'cash'
+}
+
+function formatTime(ts) {
+  if (!ts) return '-'
+  return new Date(ts * 1000).toLocaleString('zh-CN')
+}
+
+function formatDuration(minutes) {
+  if (!minutes && minutes !== 0) return '-'
+  const h = Math.floor(minutes / 60)
+  const m = minutes % 60
+  return h > 0 ? `${h}h ${m}${t.value.minute}` : `${m}${t.value.minute}`
+}
+</script>
+
+<style scoped>
+.entry-exit-page { padding: 0; }
+.query-card { margin-bottom: 16px; }
+.detail-card { margin-bottom: 16px; }
+.card-title { font-weight: bold; font-size: 16px; }
+.action-area { margin-top: 8px; }
+.exit-tip { color: #909399; margin-bottom: 12px; }
+.fee-amount { color: #e6a23c; font-size: 20px; font-weight: bold; }
+.payment-section { margin-top: 16px; }
+.section-label { font-weight: bold; margin-bottom: 8px; }
+.cash-input { margin-top: 12px; }
+.cash-label { margin-right: 8px; }
+.change-label { margin-left: 16px; color: #67c23a; font-weight: bold; }
+</style>

+ 138 - 0
frontend/src/view/report/paymentRecord.vue

@@ -0,0 +1,138 @@
+<template>
+  <div>
+    <el-row :gutter="16" class="summary-row">
+      <el-col :span="6">
+        <el-statistic title="今日收款总额" :value="summary.total_amount" prefix="¥" :precision="2" />
+      </el-col>
+      <el-col :span="6">
+        <el-statistic title="现金收款" :value="summary.cash_amount" prefix="¥" :precision="2" />
+      </el-col>
+      <el-col :span="6">
+        <el-statistic title="免密放行" :value="summary.free_amount" prefix="¥" :precision="2" />
+      </el-col>
+      <el-col :span="6">
+        <el-statistic title="收款笔数" :value="summary.total_count" />
+      </el-col>
+    </el-row>
+
+    <el-form inline style="margin-top:16px">
+      <el-form-item :label="$t.value.LicensePlateNo">
+        <el-input v-model="queryForm.plate_number" :placeholder="$t.value.carPlateInput" clearable />
+      </el-form-item>
+      <el-form-item label="停车场">
+        <el-select v-model="queryForm.parking_lot_id" clearable placeholder="全部" style="width:160px">
+          <el-option v-for="lot in lotList" :key="lot.ID" :label="lot.lot_name" :value="lot.ID" />
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="$t.value.paymentMethod">
+        <el-select v-model="queryForm.payment_method" clearable :placeholder="$t.value.pleaseSelect" style="width:140px">
+          <el-option label="现金" value="cash" />
+          <el-option label="免密" value="free" />
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="$t.value.EntryTime">
+        <el-date-picker v-model="dateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" value-format="YYYY-MM-DD" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="fetchList">{{ $t.value.Search }}</el-button>
+        <el-button @click="resetQuery">{{ $t.value.Reset }}</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table :data="list" height="605" border>
+      <el-table-column label="停车场" prop="parking_lot" align="center" width="120" />
+      <el-table-column :label="$t.value.licensePlate" prop="plate_number" align="center" width="120" />
+      <el-table-column :label="$t.value.VehicleType" prop="vehicle_type" align="center" width="100" />
+      <el-table-column :label="$t.value.paymentMethod" align="center" width="100">
+        <template #default="{ row }">
+          <el-tag :type="row.payment_method === 'cash' ? '' : 'success'">{{ row.payment_method === 'cash' ? '现金' : '免密' }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t.value.Fee" prop="amount" align="center" width="100">
+        <template #default="{ row }">¥{{ row.amount.toFixed(2) }}</template>
+      </el-table-column>
+      <el-table-column label="实收" align="center" width="100">
+        <template #default="{ row }">¥{{ (row.paid_amount || 0).toFixed(2) }}</template>
+      </el-table-column>
+      <el-table-column label="找零" align="center" width="100">
+        <template #default="{ row }">¥{{ (row.change_amount || 0).toFixed(2) }}</template>
+      </el-table-column>
+      <el-table-column :label="$t.value.EntryTime" align="center" width="170">
+        <template #default="{ row }">{{ row.entry_time }}</template>
+      </el-table-column>
+      <el-table-column :label="$t.value.ExitTime" align="center" width="170">
+        <template #default="{ row }">{{ row.exit_time }}</template>
+      </el-table-column>
+      <el-table-column label="操作员" prop="operator_name" align="center" width="100" />
+      <el-table-column label="收款时间" align="center" width="170">
+        <template #default="{ row }">{{ row.paid_at }}</template>
+      </el-table-column>
+    </el-table>
+
+    <el-pagination
+      v-model:current-page="queryForm.page"
+      :page-size="queryForm.page_size"
+      :total="total"
+      layout="total, prev, pager, next"
+      style="margin-top:12px; justify-content:flex-end"
+      @current-change="fetchList"
+    />
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { getPaymentList, getTodaySummary } from '@/api/payment'
+import { obtainPullOverList } from '@/api/pullOver'
+
+const queryForm = reactive({ plate_number: '', payment_method: '', parking_lot_id: null, page: 1, page_size: 10, start_date: '', end_date: '' })
+const dateRange = ref([])
+const list = ref([])
+const total = ref(0)
+const lotList = ref([])
+const summary = reactive({ total_amount: 0, cash_amount: 0, free_amount: 0, total_count: 0 })
+
+onMounted(() => { fetchList(); fetchSummary(); loadLotList() })
+
+function loadLotList() {
+  obtainPullOverList({ page: 1, pageSize: 100 }).then(res => {
+    if (res.code === 0) lotList.value = res.data?.list || []
+  })
+}
+
+function fetchSummary() {
+  getTodaySummary().then(res => {
+    if (res.code === 0) Object.assign(summary, res.data)
+  })
+}
+
+function fetchList() {
+  if (dateRange.value && dateRange.value.length === 2) {
+    queryForm.start_date = dateRange.value[0]
+    queryForm.end_date = dateRange.value[1]
+  } else {
+    queryForm.start_date = ''
+    queryForm.end_date = ''
+  }
+  getPaymentList(queryForm).then(res => {
+    if (res.code === 0) {
+      list.value = res.data.list || []
+      total.value = res.data.total || 0
+    }
+  })
+}
+
+function resetQuery() {
+  queryForm.plate_number = ''
+  queryForm.payment_method = ''
+  queryForm.parking_lot_id = null
+  queryForm.page = 1
+  dateRange.value = []
+  fetchList()
+}
+</script>
+
+<style scoped>
+.summary-row { margin-bottom: 16px; }
+.summary-row .el-statistic { background: #f5f7fa; padding: 16px; border-radius: 8px; }
+</style>

+ 18 - 0
internal/api/v1/system/sys_dashboard.go

@@ -0,0 +1,18 @@
+package system
+
+import (
+	"github.com/gin-gonic/gin"
+	"wails-app/internal/model/common/response"
+	systemService "wails-app/internal/service/system"
+)
+
+var dashboardSvc = systemService.DashboardService{}
+
+func GetMonitor(c *gin.Context) {
+	data, err := dashboardSvc.GetMonitorData()
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	response.OkWithData(data, c)
+}

+ 22 - 0
internal/dao/payment_record.go

@@ -0,0 +1,22 @@
+package dao
+
+import (
+	"time"
+	"wails-app/internal/global"
+)
+
+type PaymentRecord struct {
+	global.GVA_MODEL
+	RecordID      uint      `gorm:"index" json:"record_id"`
+	PaymentMethod string    `gorm:"size:20" json:"payment_method"`
+	Amount        float64   `json:"amount"`
+	PaidAmount    float64   `json:"paid_amount"`
+	ChangeAmount  float64   `json:"change_amount"`
+	OperatorID    uint      `json:"operator_id"`
+	PaidAt        time.Time `json:"paid_at"`
+	Remark        string    `gorm:"size:200" json:"remark"`
+}
+
+func (PaymentRecord) TableName() string {
+	return "payment_record"
+}

+ 1 - 0
internal/dao/vehicle_record.go

@@ -14,6 +14,7 @@ type VehicleRecord struct {
 	StayTime       int64      `json:"stay_time"`                                      // 停留时间(分钟)
 	Fee            float64    `json:"fee"`                                            // 费用
 	PaymentStatus  string     `gorm:"size:20;default:'unpaid'" json:"payment_status"` // 支付状态
+		PaymentMethod  string     `gorm:"size:20" json:"payment_method"`
 	EntryImage     string     `gorm:"size:255" json:"entry_image"`                    // 入场图片
 	ExitImage      string     `gorm:"size:255" json:"exit_image"`                     // 出场图片
 	ParkingLotID   uint       `json:"parking_lot_id"`                                 // 停车场ID

+ 3 - 2
internal/dao/vehicle_type.go

@@ -4,8 +4,9 @@ import "wails-app/internal/global"
 
 type VehicleType struct {
 	global.GVA_MODEL
-	Name    string `form:"name" json:"name" gorm:"comment:名称"`
-	Remarks string `form:"remarks" json:"remarks" gorm:"comment:备注"`
+	Name     string `form:"name" json:"name" gorm:"comment:名称"`
+	Remarks  string `form:"remarks" json:"remarks" gorm:"comment:备注"`
+	IsSystem bool   `form:"is_system" json:"is_system" gorm:"comment:系统内置标记"`
 }
 
 func (VehicleType) TableName() string {

+ 1 - 0
internal/initialize/gorm.go

@@ -55,6 +55,7 @@ func RegisterTables() {
 		dao.Channel{},
 		dao.ParkingLot{},
 		dao.VehicleRecord{},
+		dao.PaymentRecord{},
 		dao.UHFReader{},
 		dao.Camera{},
 		dao.Shortlist{},

+ 7 - 1
internal/initialize/router.go

@@ -8,6 +8,7 @@ import (
 	"wails-app/internal/middleware"
 	"wails-app/internal/router"
 	"wails-app/internal/router/owner"
+	"wails-app/internal/modules/payment"
 	"wails-app/internal/router/parking"
 	"wails-app/internal/router/vehicle"
 
@@ -47,6 +48,7 @@ func Routers() *gin.Engine {
 	systemRouter := router.RouterGroupApp.System
 	exampleRouter := router.RouterGroupApp.Example
 	vehicleRouter := router.RouterGroupApp.Vehicle
+	dashboardRouter := router.RouterGroupApp.Dashboard
 
 	// 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的
 	// VUE_APP_BASE_API = /
@@ -74,7 +76,8 @@ func Routers() *gin.Engine {
 		})
 	}
 	{
-		systemRouter.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权
+		systemRouter.InitBaseRouter(PublicGroup)    // 注册基础功能路由 不做鉴权
+		dashboardRouter.InitDashboardRouter(PublicGroup) // 监控仪表盘 不做鉴权
 	}
 	PrivateGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix)
 	PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
@@ -103,6 +106,9 @@ func Routers() *gin.Engine {
 		// 停车相关路由
 		parking.SetupParkingRouter(PrivateGroup)
 
+		// 支付相关路由
+		payment.SetupPaymentRouter(PrivateGroup)
+
 		// UHF读写器路由
 		//uhf.SetupUHFRouter(PrivateGroup)
 	}

+ 19 - 1
internal/initialize/seed.go

@@ -120,10 +120,12 @@ func SeedSystemData() {
 		{MenuLevel: 0, ParentId: 20, Path: "carOwner", Name: "carOwner", Hidden: false, Component: "view/parking/carOwner.vue", Sort: 2, Meta: dao.Meta{Title: "车主管理", Icon: "stamp"}},
 		{MenuLevel: 0, ParentId: 20, Path: "roster", Name: "roster", Hidden: false, Component: "view/parking/roster.vue", Sort: 3, Meta: dao.Meta{Title: "黑白名单", Icon: "memo"}},
 		{MenuLevel: 0, ParentId: 20, Path: "pricing", Name: "pricing", Hidden: false, Component: "view/parking/pricing.vue", Sort: 4, Meta: dao.Meta{Title: "收费配置", Icon: "office-building"}},
-		// 报表查询 (ID=25)
+		{MenuLevel: 0, ParentId: 20, Path: "entryExit", Name: "entryExit", Hidden: false, Component: "view/parking/entryExit.vue", Sort: 5, Meta: dao.Meta{Title: "进出场操作", Icon: "switch"}},
+		// 报表查询 (ID=26)
 		{MenuLevel: 0, ParentId: 0, Path: "report", Name: "report", Hidden: false, Component: "", Sort: 3, Meta: dao.Meta{Title: "报表查询", Icon: "data-analysis"}},
 		{MenuLevel: 0, ParentId: 25, Path: "enter", Name: "enter", Hidden: false, Component: "view/report/enter.vue", Sort: 1, Meta: dao.Meta{Title: "进出记录", Icon: "sort"}},
 		{MenuLevel: 0, ParentId: 25, Path: "sheet", Name: "sheet", Hidden: false, Component: "view/report/sheet.vue", Sort: 2, Meta: dao.Meta{Title: "异常报表", Icon: "document-copy"}},
+		{MenuLevel: 0, ParentId: 25, Path: "paymentRecord", Name: "paymentRecord", Hidden: false, Component: "view/report/paymentRecord.vue", Sort: 3, Meta: dao.Meta{Title: "收费记录", Icon: "money"}},
 		// 统计查询 (ID=28)
 		{MenuLevel: 0, ParentId: 0, Path: "statistics", Name: "statistics", Hidden: false, Component: "", Sort: 4, Meta: dao.Meta{Title: "统计查询", Icon: "chat-line-round"}},
 		{MenuLevel: 0, ParentId: 28, Path: "vehicle", Name: "vehicle", Hidden: false, Component: "view/statistics/vehicle.vue", Sort: 1, Meta: dao.Meta{Title: "在线车辆统计", Icon: "van"}},
@@ -168,11 +170,19 @@ func SeedSystemData() {
 		{MenuId: "25", AuthorityId: "618"}, {MenuId: "25", AuthorityId: "888"}, {MenuId: "25", AuthorityId: "9527"},
 		{MenuId: "26", AuthorityId: "618"}, {MenuId: "26", AuthorityId: "888"}, {MenuId: "26", AuthorityId: "9527"},
 		{MenuId: "27", AuthorityId: "618"}, {MenuId: "27", AuthorityId: "888"}, {MenuId: "27", AuthorityId: "9527"},
+		// 收费记录 (32) — admin + operator
+		{MenuId: "32", AuthorityId: "618"}, {MenuId: "32", AuthorityId: "888"}, {MenuId: "32", AuthorityId: "9527"},
 		// 统计查询 + 子菜单 (28-29) — admin + operator
 		{MenuId: "28", AuthorityId: "618"}, {MenuId: "28", AuthorityId: "888"}, {MenuId: "28", AuthorityId: "9527"},
 		{MenuId: "29", AuthorityId: "618"}, {MenuId: "29", AuthorityId: "888"}, {MenuId: "29", AuthorityId: "9527"},
+		// 进出场操作 (31) — admin + operator
+		{MenuId: "31", AuthorityId: "618"}, {MenuId: "31", AuthorityId: "888"}, {MenuId: "31", AuthorityId: "9527"},
 	}
 	db.Create(&authMenus)
+		// --- vehicle_type (默认车辆类型:临时车,标记为系统内置) ---
+		db.FirstOrCreate(&dao.VehicleType{},
+			dao.VehicleType{Name: "临时车", IsSystem: true, Remarks: "系统内置"},
+		)
 
 	// --- sys_apis (active only, skipping soft-deleted ids 44-66,84,90-98) ---
 	apis := []dao.SysApi{
@@ -400,6 +410,14 @@ func SeedSystemData() {
 		{"p", "9527", "/authorityBtn/setAuthorityBtn", "POST", "", "", ""},
 		{"p", "9527", "/authorityBtn/getAuthorityBtn", "POST", "", "", ""},
 		{"p", "9527", "/authorityBtn/canRemoveAuthorityBtn", "POST", "", "", ""},
+		// 收费结算 (vehicle/exit/preview, vehicle/exit/confirm) — admin + operator
+		{"p", "888", "/vehicle/exit/preview", "POST", "", "", ""},
+		{"p", "618", "/vehicle/exit/preview", "POST", "", "", ""},
+		{"p", "888", "/vehicle/exit/confirm", "POST", "", "", ""},
+		{"p", "618", "/vehicle/exit/confirm", "POST", "", "", ""},
+		// 收费记录查询
+		{"p", "888", "/payment/list", "GET", "", "", ""},
+		{"p", "618", "/payment/list", "GET", "", "", ""},
 	}
 	// Ensure casbin_rule table exists before seeding
 	db.Exec(`CREATE TABLE IF NOT EXISTS casbin_rule (

+ 22 - 0
internal/model/common/passage.go

@@ -0,0 +1,22 @@
+package common
+
+// PassageRequest 统一的进出场请求(所有触发方式共用:RFID、摄像头、手动录入)
+type PassageRequest struct {
+	RFIDTag     string // RFID 标签(UHF 读卡器触发时)
+	PlateNumber string // 车牌号(摄像头/手动触发时)
+	DeviceCode  string // UHF 设备编码(用于定位通道 + 控制道闸)
+	Image       string // 抓拍图片路径
+}
+
+// PassageResult 进出场结果
+type PassageResult struct {
+	Success     bool    `json:"success"`
+	Message     string  `json:"message"`
+	Direction   string  `json:"direction"`    // in / out
+	PlateNumber string  `json:"plate_number"` // 车牌号
+	RFIDTag     string  `json:"rfid_tag"`     // RFID 标签
+	Fee         float64 `json:"fee"`          // 出场费用(入场时为0)
+	StayTime    int64   `json:"stay_time"`    // 停留时长(分钟)
+	EntryTime   int64   `json:"entry_time"`   // 入场时间戳
+	ExitTime    int64   `json:"exit_time"`    // 出场时间戳
+}

+ 7 - 0
internal/model/vehicle/request/vehicle.go

@@ -89,3 +89,10 @@ type VehicleRecordRequest struct {
 	Page        int    `form:"page,default=1" json:"page"`
 	PageSize    int    `form:"page_size,default=10" json:"page_size"`
 }
+
+type ExitConfirmRequest struct {
+	PlateNumber   string  `json:"plate_number"`
+	RFIDTag       string  `json:"rfid_tag"`
+	PaymentMethod string  `json:"payment_method"`
+	PaidAmount    float64 `json:"paid_amount"`
+}

+ 19 - 0
internal/model/vehicle/response/vehicle.go

@@ -28,3 +28,22 @@ type FeeConfigList struct {
 	Total int64           `json:"total"`
 	List  []dao.FeeConfig `json:"list"`
 }
+
+type ExitPreviewResponse struct {
+	PlateNumber string  `json:"plate_number"`
+	EntryTime   int64   `json:"entry_time"`
+	StayTime    int64   `json:"stay_time"`
+	Fee         float64 `json:"fee"`
+	VehicleType string  `json:"vehicle_type"`
+	IsTemp      bool    `json:"is_temp"`
+}
+
+type ExitConfirmResponse struct {
+	PlateNumber   string  `json:"plate_number"`
+	EntryTime     int64   `json:"entry_time"`
+	ExitTime      int64   `json:"exit_time"`
+	StayTime      int64   `json:"stay_time"`
+	Fee           float64 `json:"fee"`
+	ChangeAmount  float64 `json:"change_amount"`
+	PaymentMethod string  `json:"payment_method"`
+}

+ 88 - 0
internal/modules/payment/api.go

@@ -0,0 +1,88 @@
+package payment
+
+import (
+	"github.com/gin-gonic/gin"
+	"wails-app/internal/model/common/response"
+	"wails-app/internal/model/vehicle/request"
+	"wails-app/internal/modules/payment/repository"
+	paymentService "wails-app/internal/modules/payment/service"
+	utils "wails-app/internal/pkg"
+	"wails-app/internal/service"
+)
+
+var vehicleSvc = service.ServiceGroupApp.VehicleServiceGroup.VehicleService
+var paymentSvc = paymentService.NewPaymentService()
+
+// ExitPreview 费用预览(只读,不修改数据库)
+func ExitPreview(c *gin.Context) {
+	var req request.VehicleEntry
+	if err := c.ShouldBindJSON(&req); err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	resp, err := vehicleSvc.ExitPreview(req)
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	response.OkWithData(resp, c)
+}
+
+// ExitConfirm 确认出场 + 支付
+func ExitConfirm(c *gin.Context) {
+	var req request.ExitConfirmRequest
+	if err := c.ShouldBindJSON(&req); err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	resp, err := vehicleSvc.ExitConfirm(req, utils.GetUserID(c))
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	response.OkWithData(resp, c)
+}
+
+// ListPayments 收费记录列表
+func ListPayments(c *gin.Context) {
+	var q repository.PaymentListQuery
+	if err := c.ShouldBindQuery(&q); err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	list, total, err := paymentSvc.QueryPayments(q)
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	response.OkWithDetailed(response.PageResult{
+		List:     list,
+		Total:    total,
+		Page:     q.Page,
+		PageSize: q.PageSize,
+	}, "查询成功", c)
+}
+
+// TodaySummary 当日收费汇总
+func TodaySummary(c *gin.Context) {
+	s, err := paymentSvc.GetTodaySummary()
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	response.OkWithData(s, c)
+}
+
+// SetupPaymentRouter 注册支付相关路由
+func SetupPaymentRouter(router *gin.RouterGroup) {
+	pg := router.Group("/payment")
+	{
+		pg.GET("/list", ListPayments)
+		pg.GET("/today-summary", TodaySummary)
+	}
+	eg := router.Group("/vehicle")
+	{
+		eg.POST("/exit/preview", ExitPreview)
+		eg.POST("/exit/confirm", ExitConfirm)
+	}
+}

+ 109 - 0
internal/modules/payment/repository/repo.go

@@ -0,0 +1,109 @@
+package repository
+
+import (
+	"time"
+	"wails-app/internal/dao"
+	"wails-app/internal/global"
+)
+
+type PaymentRepository struct{}
+
+func (r *PaymentRepository) Create(record *dao.PaymentRecord) error {
+	return global.GVA_DB.Create(record).Error
+}
+
+// PaymentListQuery 收费记录查询参数
+type PaymentListQuery struct {
+	PlateNumber   string `form:"plate_number"`
+	PaymentMethod string `form:"payment_method"`
+	ParkingLotID  uint   `form:"parking_lot_id"`
+	StartDate     string `form:"start_date"`
+	EndDate       string `form:"end_date"`
+	Page          int    `form:"page"`
+	PageSize      int    `form:"page_size"`
+}
+
+// PaymentListResult 收费记录列表项
+type PaymentListResult struct {
+	ID            uint    `json:"id"`
+	PlateNumber   string  `json:"plate_number"`
+	VehicleType   string  `json:"vehicle_type"`
+	PaymentMethod string  `json:"payment_method"`
+	Amount        float64 `json:"amount"`
+	PaidAmount    float64 `json:"paid_amount"`
+	ChangeAmount  float64 `json:"change_amount"`
+	OperatorID    uint    `json:"operator_id"`
+	OperatorName  string  `json:"operator_name"`
+	ParkingLot    string  `json:"parking_lot"`
+	PaidAt        string  `json:"paid_at"`
+	EntryTime     string  `json:"entry_time"`
+	ExitTime      string  `json:"exit_time"`
+	StayTime      int64   `json:"stay_time"`
+}
+
+// List 查询收费记录列表(关联 vehicle_record + vehicle + vehicle_type)
+func (r *PaymentRepository) List(q PaymentListQuery) ([]PaymentListResult, int64, error) {
+	db := global.GVA_DB.Table("payment_record").
+		Select(`payment_record.id, vehicle_record.plate_number, vehicle_type.name as vehicle_type, payment_record.payment_method,
+			payment_record.amount, payment_record.paid_amount, payment_record.change_amount,
+			payment_record.operator_id, sys_users.nick_name as operator_name, vehicle_record.parking_lot_id, parking_lot.lot_name as parking_lot, payment_record.paid_at,
+			vehicle_record.entry_time, vehicle_record.exit_time, vehicle_record.stay_time`).
+		Joins("LEFT JOIN vehicle_record ON vehicle_record.id = payment_record.record_id").
+		Joins("LEFT JOIN vehicle ON vehicle.plate_number = vehicle_record.plate_number").
+		Joins("LEFT JOIN vehicle_type ON vehicle_type.id = vehicle.vehicle_type_id").
+		Joins("LEFT JOIN sys_users ON sys_users.id = payment_record.operator_id").
+		Joins("LEFT JOIN parking_lot ON parking_lot.id = vehicle_record.parking_lot_id")
+
+	if q.PlateNumber != "" {
+		db = db.Where("vehicle_record.plate_number LIKE ?", "%"+q.PlateNumber+"%")
+	}
+	if q.ParkingLotID > 0 {
+		db = db.Where("vehicle_record.parking_lot_id = ?", q.ParkingLotID)
+	}
+	if q.PaymentMethod != "" {
+		db = db.Where("payment_record.payment_method = ?", q.PaymentMethod)
+	}
+	if q.StartDate != "" {
+		db = db.Where("payment_record.paid_at >= ?", q.StartDate)
+	}
+	if q.EndDate != "" {
+		db = db.Where("payment_record.paid_at <= ?", q.EndDate+" 23:59:59")
+	}
+
+	var total int64
+	db.Count(&total)
+
+	if q.Page <= 0 {
+		q.Page = 1
+	}
+	if q.PageSize <= 0 {
+		q.PageSize = 10
+	}
+	offset := (q.Page - 1) * q.PageSize
+
+	var results []PaymentListResult
+	err := db.Order("payment_record.paid_at DESC").Offset(offset).Limit(q.PageSize).Scan(&results).Error
+	return results, total, err
+}
+
+// TodaySummary 当日收费汇总
+type TodaySummary struct {
+	TotalAmount float64 `json:"total_amount"`
+	CashAmount  float64 `json:"cash_amount"`
+	FreeAmount  float64 `json:"free_amount"`
+	TotalCount  int64   `json:"total_count"`
+}
+
+// GetTodaySummary 获取当日收费汇总
+func (r *PaymentRepository) GetTodaySummary() (TodaySummary, error) {
+	var s TodaySummary
+	today := time.Now().Format("2006-01-02")
+	err := global.GVA_DB.Table("payment_record").
+		Select(`COALESCE(SUM(amount), 0) as total_amount,
+			COALESCE(SUM(CASE WHEN payment_method = 'cash' THEN amount ELSE 0 END), 0) as cash_amount,
+			COALESCE(SUM(CASE WHEN payment_method = 'free' THEN amount ELSE 0 END), 0) as free_amount,
+			COUNT(*) as total_count`).
+		Where("paid_at >= ? AND paid_at < ?", today, today+" 23:59:59").
+		Scan(&s).Error
+	return s, err
+}

+ 67 - 0
internal/modules/payment/service/service.go

@@ -0,0 +1,67 @@
+package service
+
+import (
+	"fmt"
+	"time"
+	"wails-app/internal/dao"
+	"wails-app/internal/global"
+	"wails-app/internal/modules/payment/repository"
+)
+
+// Payment method constants — add new methods here for POS/Wechat/Alipay
+const (
+	PaymentCash   = "cash"
+	PaymentFree   = "free"
+	PaymentPOS    = "pos"
+	PaymentWechat = "wechat"
+	PaymentAlipay = "alipay"
+)
+
+type PaymentService struct {
+	repo *repository.PaymentRepository
+}
+
+func NewPaymentService() *PaymentService {
+	return &PaymentService{repo: &repository.PaymentRepository{}}
+}
+
+// ProcessPayment creates a payment_record and updates vehicle_record payment status
+func (s *PaymentService) ProcessPayment(recordID uint, paymentMethod string, amount float64, paidAmount float64, operatorID uint) (*dao.PaymentRecord, error) {
+	changeAmount := 0.0
+	if paymentMethod == PaymentCash && paidAmount > amount {
+		changeAmount = paidAmount - amount
+	}
+
+	now := time.Now()
+	pr := &dao.PaymentRecord{
+		RecordID:      recordID,
+		PaymentMethod: paymentMethod,
+		Amount:        amount,
+		PaidAmount:    paidAmount,
+		ChangeAmount:  changeAmount,
+		OperatorID:    operatorID,
+		PaidAt:        now,
+	}
+
+	if err := s.repo.Create(pr); err != nil {
+		return nil, fmt.Errorf("创建支付记录失败: %w", err)
+	}
+
+	// Update vehicle_record payment status
+	global.GVA_DB.Model(&dao.VehicleRecord{}).Where("id = ?", recordID).Updates(map[string]interface{}{
+		"payment_status": "paid",
+		"payment_method": paymentMethod,
+	})
+
+	return pr, nil
+}
+
+// QueryPayments 查询收费记录列表
+func (s *PaymentService) QueryPayments(q repository.PaymentListQuery) ([]repository.PaymentListResult, int64, error) {
+	return s.repo.List(q)
+}
+
+// GetTodaySummary 获取当日收费汇总
+func (s *PaymentService) GetTodaySummary() (repository.TodaySummary, error) {
+	return s.repo.GetTodaySummary()
+}

+ 4 - 3
internal/router/enter.go

@@ -7,9 +7,10 @@ import (
 )
 
 type RouterGroup struct {
-	System  system.RouterGroup
-	Example example.RouterGroup
-	Vehicle vehicle.RouterGroup
+	System    system.RouterGroup
+	Example   example.RouterGroup
+	Vehicle   vehicle.RouterGroup
+	Dashboard system.DashboardRouter
 }
 
 var RouterGroupApp = new(RouterGroup)

+ 13 - 0
internal/router/system/sys_dashboard.go

@@ -0,0 +1,13 @@
+package system
+
+import (
+	"wails-app/internal/api/v1/system"
+	"github.com/gin-gonic/gin"
+)
+
+type DashboardRouter struct{}
+
+func (s *DashboardRouter) InitDashboardRouter(Router *gin.RouterGroup) {
+	dashboardRouter := Router.Group("dashboard")
+	dashboardRouter.GET("monitor", system.GetMonitor)
+}

+ 2 - 0
internal/service/enter.go

@@ -2,6 +2,7 @@ package service
 
 import (
 	"wails-app/internal/service/example"
+	"wails-app/internal/service/parking"
 	"wails-app/internal/service/system"
 	"wails-app/internal/service/vehicle"
 )
@@ -10,6 +11,7 @@ type ServiceGroup struct {
 	SystemServiceGroup  system.ServiceGroup
 	ExampleServiceGroup example.ServiceGroup
 	VehicleServiceGroup vehicle.ServiceGroup
+	ParkingServiceGroup parking.ServiceGroup
 }
 
 var ServiceGroupApp = new(ServiceGroup)

+ 5 - 0
internal/service/parking/enter.go

@@ -0,0 +1,5 @@
+package parking
+
+type ServiceGroup struct {
+	PassageService
+}

+ 194 - 0
internal/service/parking/passage.go

@@ -0,0 +1,194 @@
+package parking
+
+import (
+	"fmt"
+	"sync"
+	"time"
+	"wails-app/internal/dao"
+	"wails-app/internal/global"
+	common "wails-app/internal/model/common"
+	"wails-app/internal/model/vehicle/request"
+	"wails-app/internal/service/vehicle"
+)
+
+// GateOpener 道闸控制函数(由 UHF 模块在初始化时注入,避免循环依赖)
+var GateOpener func(deviceCode string, validTime byte) error
+
+// PassageService 统一的进出场服务(所有触发方式共用:RFID、摄像头、手动录入)
+type PassageService struct{}
+
+// ===================== 防抖与状态 =====================
+var (
+	passageLastTime    = make(map[string]int64)
+	passageStatus      = make(map[string]bool) // true=在场内
+	passageMutex       sync.Mutex
+	passageDebounceSec = int64(3) // 3秒防抖
+)
+
+// getIdentifier 从请求中提取唯一标识(优先 RFID,其次车牌)
+func getIdentifier(req common.PassageRequest) string {
+	if req.RFIDTag != "" {
+		return req.RFIDTag
+	}
+	return req.PlateNumber
+}
+
+// HandlePassage 统一的进出场处理入口
+// 调用方只需传入车辆标识 + 设备编码,其余逻辑全部在此处理:
+//
+//	查询设备/通道 → 查询车辆 → 黑名单检查 → 临时车规则 → 进场/出场 → 开闸
+func (s *PassageService) HandlePassage(req common.PassageRequest) (*common.PassageResult, error) {
+	identifier := getIdentifier(req)
+	if identifier == "" {
+		return nil, fmt.Errorf("缺少车辆标识(RFIDTag 或 PlateNumber)")
+	}
+
+	// ===================== 1. 防抖 =====================
+	passageMutex.Lock()
+	if last, ok := passageLastTime[identifier]; ok && time.Now().Unix()-last < passageDebounceSec {
+		passageMutex.Unlock()
+		return nil, fmt.Errorf("防抖中: %s", identifier)
+	}
+	passageLastTime[identifier] = time.Now().Unix()
+	passageMutex.Unlock()
+
+	// ===================== 2. 查询设备 + 通道 =====================
+	var device dao.UHFReader
+	if err := global.GVA_DB.Preload("Channel").First(&device, "device_code = ?", req.DeviceCode).Error; err != nil {
+		return nil, fmt.Errorf("设备不存在: %s", req.DeviceCode)
+	}
+	channel := device.Channel
+	direction := channel.Direction
+	allowTemporary := channel.AllowTemporary
+
+	// ===================== 3. 查询车辆 =====================
+	vehicleSvc := &vehicle.VehicleService{}
+	shortlistSvc := &vehicle.ShortlistService{}
+
+	v, err := vehicleSvc.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
+	isTempVehicle := false
+	if err != nil || v.ID == 0 {
+		isTempVehicle = true
+	} else if v.VehicleType != nil {
+		isTempVehicle = v.VehicleType.IsSystem
+	} else {
+		isTempVehicle = true
+	}
+
+	// ===================== 4. 黑名单检查 =====================
+	isBlack, _ := shortlistSvc.CheckVehicleShortlist(req.RFIDTag)
+	if isBlack {
+		fmt.Printf("🚫 禁止通行 | 车辆在黑名单中 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
+		return nil, fmt.Errorf("车辆在黑名单中")
+	}
+
+	// ===================== 5. 临时车规则 =====================
+	if isTempVehicle && !allowTemporary {
+		fmt.Printf("🚫 禁止通行 | 临时车不允许此通道 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
+		return nil, fmt.Errorf("临时车不允许此通道")
+	}
+
+	// ===================== 6. 根据方向处理 =====================
+	switch direction {
+	case "in":
+		return s.handleEntry(req, identifier, channel.ParkingLotID)
+	case "out":
+		return s.handleExit(req, identifier)
+	default:
+		return nil, fmt.Errorf("未知通道方向: %s", direction)
+	}
+}
+
+// handleEntry 处理入场
+func (s *PassageService) handleEntry(req common.PassageRequest, identifier string, parkingLotID uint) (*common.PassageResult, error) {
+	passageMutex.Lock()
+	canIn := !passageStatus[identifier]
+	passageMutex.Unlock()
+
+	if !canIn {
+		return nil, fmt.Errorf("车辆已在场内: %s", identifier)
+	}
+
+	fmt.Printf("🟢 入口开闸 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
+
+	// 开闸
+	if GateOpener != nil {
+		if err := GateOpener(req.DeviceCode, 2); err != nil {
+			fmt.Printf("⚠️ 开闸失败: %v\n", err)
+		}
+	}
+
+	// 执行入场
+	vehicleSvc := &vehicle.VehicleService{}
+	resp, err := vehicleSvc.VehicleEntry(request.VehicleEntry{
+		PlateNumber:  req.PlateNumber,
+		RFIDTag:      req.RFIDTag,
+		ParkingLotID: parkingLotID,
+		EntryImage:   req.Image,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	passageMutex.Lock()
+	passageStatus[identifier] = true
+	passageMutex.Unlock()
+
+	return &common.PassageResult{
+		Success:     true,
+		Message:     "入场成功",
+		Direction:   "in",
+		PlateNumber: resp.Data.PlateNumber,
+		RFIDTag:     req.RFIDTag,
+		EntryTime:   resp.Data.EntryTime,
+	}, nil
+}
+
+// handleExit 处理出场(使用新的收费流程 ExitConfirm)
+func (s *PassageService) handleExit(req common.PassageRequest, identifier string) (*common.PassageResult, error) {
+	fmt.Printf("🔴 出口开闸 | 设备:%s 车牌:%s RFID:%s\n", req.DeviceCode, req.PlateNumber, req.RFIDTag)
+
+	// 开闸
+	if GateOpener != nil {
+		if err := GateOpener(req.DeviceCode, 2); err != nil {
+			fmt.Printf("⚠️ 开闸失败: %v\n", err)
+		}
+	}
+
+	// 执行出场(RFID自动出场统一走免密,无人工收费)
+	vehicleSvc := &vehicle.VehicleService{}
+	confirmReq := request.ExitConfirmRequest{
+		PlateNumber:   req.PlateNumber,
+		RFIDTag:       req.RFIDTag,
+		PaymentMethod: "free",
+		PaidAmount:    0,
+	}
+	resp, err := vehicleSvc.ExitConfirm(confirmReq, 0)
+	if err != nil {
+		return nil, err
+	}
+
+	passageMutex.Lock()
+	passageStatus[identifier] = false
+	passageMutex.Unlock()
+
+	return &common.PassageResult{
+		Success:     true,
+		Message:     "出场成功",
+		Direction:   "out",
+		PlateNumber: req.PlateNumber,
+		RFIDTag:     req.RFIDTag,
+		Fee:         resp.Fee,
+		StayTime:    resp.StayTime,
+		EntryTime:   resp.EntryTime,
+		ExitTime:    resp.ExitTime,
+	}, nil
+}
+
+// SetEpcExit 手动重置离场状态
+func SetEpcExit(identifier string) {
+	passageMutex.Lock()
+	defer passageMutex.Unlock()
+	passageStatus[identifier] = false
+	fmt.Printf("🚙 手动重置离场 | 标识: %s\n", identifier)
+}

+ 50 - 0
internal/service/system/sys_dashboard.go

@@ -0,0 +1,50 @@
+package system
+
+import (
+	"time"
+	"wails-app/internal/global"
+)
+
+type DeviceStatus struct {
+	ChannelName  string `json:"channel_name"`
+	Direction    string `json:"direction"`
+	DeviceName   string `json:"device_name"`
+	DeviceStatus string `json:"device_status"`
+}
+
+type MonitorData struct {
+	TotalSpaces int64          `json:"total_spaces"`
+	UsedSpaces  int64          `json:"used_spaces"`
+	InPark      int64          `json:"in_park"`
+	TodayIncome float64        `json:"today_income"`
+	TodayEntry  int64          `json:"today_entry"`
+	TodayExit   int64          `json:"today_exit"`
+	Devices     []DeviceStatus `json:"devices"`
+}
+
+type DashboardService struct{}
+
+func (s *DashboardService) GetMonitorData() (MonitorData, error) {
+	var d MonitorData
+	today := time.Now().Format("2006-01-02")
+
+	global.GVA_DB.Raw("SELECT COALESCE(SUM(capacity), 0) FROM parking_lot").Scan(&d.TotalSpaces)
+	global.GVA_DB.Raw("SELECT COUNT(*) FROM vehicle_record WHERE exit_time IS NULL").Scan(&d.InPark)
+	d.UsedSpaces = d.InPark
+
+	global.GVA_DB.Raw("SELECT COALESCE(SUM(amount), 0) FROM payment_record WHERE paid_at >= ? AND paid_at < ?",
+		today, today+" 23:59:59").Scan(&d.TodayIncome)
+
+	global.GVA_DB.Raw("SELECT COUNT(*) FROM vehicle_record WHERE entry_time >= ? AND entry_time < ?",
+		today, today+" 23:59:59").Scan(&d.TodayEntry)
+	global.GVA_DB.Raw("SELECT COUNT(*) FROM vehicle_record WHERE exit_time >= ? AND exit_time < ?",
+		today, today+" 23:59:59").Scan(&d.TodayExit)
+
+	global.GVA_DB.Raw(`SELECT ch.channel_name, ch.direction,
+		COALESCE(uhf.device_name, '-') as device_name,
+		COALESCE(uhf.status, 'offline') as device_status
+		FROM channel ch LEFT JOIN uhf_reader uhf ON uhf.channel_id = ch.id
+		WHERE ch.deleted_at IS NULL ORDER BY ch.id`).Scan(&d.Devices)
+
+	return d, nil
+}

+ 29 - 112
internal/service/uhf/reader.go

@@ -8,8 +8,9 @@ import (
 	"fmt"
 	"wails-app/internal/dao"
 	"wails-app/internal/global"
-	"wails-app/internal/model/vehicle/request"
+	common "wails-app/internal/model/common"
 	"wails-app/internal/service"
+	"wails-app/internal/service/parking"
 	"sync"
 	"time"
 )
@@ -44,6 +45,11 @@ var DeviceManager = &Manager{
 	devices: make(map[string]*DeviceHandler),
 }
 
+// init 注入道闸控制回调(避免循环依赖)
+func init() {
+	parking.GateOpener = DeviceManager.OpenGateByDeviceCode
+}
+
 type Manager struct {
 	mu      sync.RWMutex
 	devices map[string]*DeviceHandler
@@ -81,6 +87,20 @@ func (m *Manager) Get(code string) (*DeviceHandler, bool) {
 	return h, ok
 }
 
+// OpenGate 开闸(公开方法,供外部触发源调用,如摄像头识别、手动进出场)
+func (h *DeviceHandler) OpenGate(validTime byte) error {
+	return h.reader.CloseRelay1(validTime)
+}
+
+// OpenGateByDeviceCode 通过设备编码直接开闸(便捷方法)
+func (m *Manager) OpenGateByDeviceCode(deviceCode string, validTime byte) error {
+	h, ok := m.Get(deviceCode)
+	if !ok {
+		return fmt.Errorf("设备未连接: %s", deviceCode)
+	}
+	return h.OpenGate(validTime)
+}
+
 // ===================== 复用原有CRC和解析函数(无需修改)=====================
 func uiCrc16Cal(pucY []byte, ucX uint8) uint16 {
 	const PRESET_VALUE = 0xFFFF
@@ -226,15 +246,7 @@ func parseReportData(deviceCode string, buf []byte) (*ReportData, error) {
 	}, nil
 }
 
-// 防抖与状态
-var (
-	epcLastTime    = make(map[string]int64)
-	epcStatus      = make(map[string]bool)
-	epcMutex       sync.Mutex
-	debounceSecond = int64(3)
-)
-
-// handleReportData 处理UHF读取到的标签 + 完整权限判断
+// handleReportData 处理UHF读取到的标签(精简版:只做解析+防抖,业务逻辑委托给 PassageService)
 func handleReportData(report *ReportData) {
 	if len(report.Epcs) == 0 {
 		return
@@ -242,113 +254,18 @@ func handleReportData(report *ReportData) {
 
 	epc := report.Epcs[0]
 	fmt.Println("<UNK>", epc)
-	now := time.Now().Unix()
 
-	// ===================== 【修复】锁只包防抖,绝不包业务!=====================
-	epcMutex.Lock()
-	// 3秒防抖
-	if lastTime, ok := epcLastTime[epc]; ok && now-lastTime < debounceSecond {
-		epcMutex.Unlock()
-		return
-	}
-	epcLastTime[epc] = now
-	epcMutex.Unlock()
-	// ===================== 锁已释放,后面逻辑不会阻塞 =====================
-
-	// 获取设备处理器
-	_, exists := DeviceManager.Get(report.DeviceCode)
-	if !exists {
-		return
-	}
-
-	// ===================== 查询设备 + 通道 =====================
-	var device dao.UHFReader
-	err := global.GVA_DB.Preload("Channel").First(&device, "device_code = ?", report.DeviceCode).Error
+	// 委托给统一的进出场服务(所有触发方式共用同一入口)
+	result, err := service.ServiceGroupApp.ParkingServiceGroup.PassageService.HandlePassage(common.PassageRequest{
+		RFIDTag:    epc,
+		DeviceCode: report.DeviceCode,
+	})
 	if err != nil {
+		fmt.Printf("⚠️ 进出场处理失败 | 设备:%s EPC:%s 错误:%v\n", report.DeviceCode, epc, err)
 		return
 	}
-	channel := device.Channel
-	direction := channel.Direction
-	allowTemporary := channel.AllowTemporary
-
-	vehicleService := service.ServiceGroupApp.VehicleServiceGroup.VehicleService
-	shortlistService := service.ServiceGroupApp.VehicleServiceGroup.ShortlistService
-
-	// ===================== 1. 根据EPC查询车辆 =====================
-	vehicle, err := vehicleService.GetVehicleByPlateNumber("", epc)
-	isTempVehicle := false
-	if err != nil || vehicle.ID == 0 {
-		isTempVehicle = true
-	} else {
-		if vehicle.VehicleType != nil {
-			isTempVehicle = (vehicle.VehicleTypeID == 1)
-		} else {
-			isTempVehicle = true
-		}
-	}
-
-	// ===================== 2. 查询黑白名单 =====================
-	isBlack, _ := shortlistService.CheckVehicleShortlist(epc)
-
-	// ===================== 3. 黑名单优先判断 =====================
-	if isBlack {
-		fmt.Printf("🚫 禁止通行 | 车辆在黑名单中 | 设备:%s EPC:%s\n", report.DeviceCode, epc)
-		return
-	}
-
-	// ===================== 4. 临时车规则 =====================
-	// 临时车:只看通道是否允许临停
-	if isTempVehicle && !allowTemporary {
-		fmt.Printf("🚫 禁止通行 | 临时车不允许此通道 | 设备:%s EPC:%s\n", report.DeviceCode, epc)
-		return
-	}
-
-	// ===================== 5. 入口 =====================
-	if direction == "in" {
-		epcMutex.Lock()
-		canIn := !epcStatus[epc]
-		epcMutex.Unlock()
-
-		if canIn {
-			fmt.Printf("🟢 入口开闸 | 设备:%s EPC:%s\n", report.DeviceCode, epc)
-			//handler.reader.CloseRelay1(2)
-
-			var vehicleEntry request.VehicleEntry
-			vehicleEntry.RFIDTag = epc
-			vehicleEntry.ParkingLotID = channel.ParkingLotID
-			_, err := vehicleService.VehicleEntry(vehicleEntry)
-
-			if err == nil {
-				epcMutex.Lock()
-				epcStatus[epc] = true
-				epcMutex.Unlock()
-			}
-		}
-	}
-
-	// ===================== 6. 出口 =====================
-	if direction == "out" {
-		fmt.Printf("🔴 出口开闸 | 设备:%s EPC:%s\n", report.DeviceCode, epc)
-		//handler.reader.CloseRelay1(2)
-
-		var vehicleExit request.VehicleExit
-		vehicleExit.RFIDTag = epc
-		_, err := vehicleService.VehicleExit(vehicleExit)
-
-		if err == nil {
-			epcMutex.Lock()
-			epcStatus[epc] = false
-			epcMutex.Unlock()
-		}
-	}
-}
 
-// ===================== 手动重置离场 =====================
-func SetEpcExit(epc string) {
-	epcMutex.Lock()
-	defer epcMutex.Unlock()
-	epcStatus[epc] = false
-	fmt.Printf("🚙 手动重置离场 | EPC: %s\n", epc)
+	fmt.Printf("✅ 进出场成功 | 方向:%s 车牌:%s 费用:%.2f\n", result.Direction, result.PlateNumber, result.Fee)
 }
 
 // ==============================

+ 121 - 9
internal/service/vehicle/vehicle.go

@@ -7,15 +7,27 @@ import (
 	"wails-app/internal/dao"
 	"wails-app/internal/global"
 	"wails-app/internal/model/vehicle/request"
+	vehicleResponse "wails-app/internal/model/vehicle/response"
+	paymentService "wails-app/internal/modules/payment/service"
+
+	"gorm.io/gorm"
 )
 
 type VehicleService struct{}
 
+// buildPlateOrRfidQuery 按车牌或RFID查询,RFID为空时仅用车牌(避免 rfid_tag='' 匹配所有无标签车辆)
+func buildPlateOrRfidQuery(db *gorm.DB, plateNumber, rfidTag string) *gorm.DB {
+	if rfidTag != "" {
+		return db.Where("plate_number = ? OR rfid_tag = ?", plateNumber, rfidTag)
+	}
+	return db.Where("plate_number = ?", plateNumber)
+}
+
 // CreateVehicle 创建车辆信息
 func (s *VehicleService) CreateVehicle(req request.VehicleCreate) error {
 	// 检查车牌号是否已存在
 	var existingVehicle dao.Vehicle
-	result := global.GVA_DB.Where("plate_number = ? OR rfid_tag = ?", req.PlateNumber, req.RFIDTag).First(&existingVehicle)
+	result := buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).First(&existingVehicle)
 	if result.RowsAffected > 0 {
 		return errors.New("车牌号已存在")
 	}
@@ -52,7 +64,7 @@ func (s *VehicleService) UpdateVehicle(req request.VehicleUpdate) error {
 
 	// 检查车牌号是否被其他车辆使用
 	var existingVehicle dao.Vehicle
-	result = global.GVA_DB.Where("(plate_number = ? OR rfid_tag = ?) AND id != ?", req.PlateNumber, req.RFIDTag, req.ID).First(&existingVehicle)
+	result = buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).Where("id != ?", req.ID).First(&existingVehicle)
 	fmt.Println("result", result.RowsAffected)
 	if result.RowsAffected > 0 {
 		return errors.New("车牌号已被其他车辆使用")
@@ -94,10 +106,16 @@ func (s *VehicleService) GetVehicleByID(id uint) (dao.Vehicle, error) {
 	return vehicle, nil
 }
 
-// GetVehicleByPlateNumber 根据车牌号获取车辆信息
+// GetVehicleByPlateNumber 根据车牌号或RFID获取车辆信息
 func (s *VehicleService) GetVehicleByPlateNumber(plateNumber, RFIDTag string) (dao.Vehicle, error) {
 	var vehicle dao.Vehicle
-	result := global.GVA_DB.Where("plate_number = ? or rfid_tag = ?", plateNumber, RFIDTag).Preload("VehicleType").First(&vehicle)
+	query := global.GVA_DB.Preload("VehicleType")
+	if RFIDTag != "" {
+		query = query.Where("plate_number = ? OR rfid_tag = ?", plateNumber, RFIDTag)
+	} else {
+		query = query.Where("plate_number = ?", plateNumber)
+	}
+	result := query.First(&vehicle)
 	if result.RowsAffected == 0 {
 		return dao.Vehicle{}, errors.New("车辆不存在")
 	}
@@ -159,11 +177,15 @@ func (s *VehicleService) VehicleEntry(req request.VehicleEntry) (request.Vehicle
 	// 检查车辆是否存在
 	vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
 	if err != nil {
-		// 如果车辆不存在,创建新车辆,默认使用ID为1的车辆类型(临时车)
+		// 如果车辆不存在,创建新车辆,自动归类为"临时车"
+		var tempType dao.VehicleType
+		if global.GVA_DB.Where("is_system = ?", true).First(&tempType).Error != nil {
+			return request.VehicleEntryResponse{}, errors.New("系统错误:临时车类型未配置")
+		}
 		createReq := request.VehicleCreate{
 			PlateNumber:   req.PlateNumber,
-			VehicleTypeID: 1, // 默认类型ID为1(临时车)
-			OwnerId:       0, // 默认无车主
+			VehicleTypeID: tempType.ID,
+			OwnerId:       0,
 		}
 		err = s.CreateVehicle(createReq)
 		if err != nil {
@@ -174,7 +196,7 @@ func (s *VehicleService) VehicleEntry(req request.VehicleEntry) (request.Vehicle
 
 	// 检查是否有未出场的记录
 	var existingRecord dao.VehicleRecord
-	result := global.GVA_DB.Where("(plate_number = ? OR rfid_tag = ?) AND exit_time IS NULL", req.PlateNumber, req.RFIDTag).First(&existingRecord)
+	result := buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).Where("exit_time IS NULL").First(&existingRecord)
 	if result.RowsAffected > 0 {
 		return request.VehicleEntryResponse{}, errors.New("车辆已入场,未出场")
 	}
@@ -216,7 +238,7 @@ func (s *VehicleService) VehicleEntry(req request.VehicleEntry) (request.Vehicle
 func (s *VehicleService) VehicleExit(req request.VehicleExit) (request.VehicleExitResponse, error) {
 	// 1. 查询未出场记录
 	var record dao.VehicleRecord
-	if err := global.GVA_DB.Where("(plate_number = ? OR rfid_tag = ?) AND exit_time IS NULL", req.PlateNumber, req.RFIDTag).First(&record).Error; err != nil {
+	if err := buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).Where("exit_time IS NULL").First(&record).Error; err != nil {
 		return request.VehicleExitResponse{}, errors.New("车辆未入场或已出场")
 	}
 
@@ -265,6 +287,96 @@ func (s *VehicleService) VehicleExit(req request.VehicleExit) (request.VehicleEx
 	return response, nil
 }
 
+// ExitPreview 费用预览(不修改数据库,仅查询+计算费用)
+func (s *VehicleService) ExitPreview(req request.VehicleEntry) (vehicleResponse.ExitPreviewResponse, error) {
+	var record dao.VehicleRecord
+	err := buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).Where("exit_time IS NULL").First(&record).Error
+	if err != nil {
+		return vehicleResponse.ExitPreviewResponse{}, errors.New("车辆未入场或已出场")
+	}
+
+	vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
+	if err != nil {
+		return vehicleResponse.ExitPreviewResponse{}, err
+	}
+
+	now := time.Now()
+	stayTime := int64(now.Sub(record.EntryTime).Minutes())
+	if stayTime < 0 {
+		stayTime = 0
+	}
+	fee := s.calculateFee(&vehicle, stayTime)
+
+	typeName := ""
+	isTemp := false
+	if vehicle.VehicleType != nil {
+		typeName = vehicle.VehicleType.Name
+		isTemp = vehicle.VehicleType.IsSystem
+	}
+
+	return vehicleResponse.ExitPreviewResponse{
+		PlateNumber: vehicle.PlateNumber,
+		EntryTime:   record.EntryTime.Unix(),
+		StayTime:    stayTime,
+		Fee:         fee,
+		VehicleType: typeName,
+		IsTemp:      isTemp,
+	}, nil
+}
+
+// ExitConfirm 确认出场 + 支付(更新车辆记录 + 创建支付记录)
+func (s *VehicleService) ExitConfirm(req request.ExitConfirmRequest, operatorID uint) (vehicleResponse.ExitConfirmResponse, error) {
+var record dao.VehicleRecord
+	err := buildPlateOrRfidQuery(global.GVA_DB, req.PlateNumber, req.RFIDTag).Where("exit_time IS NULL").First(&record).Error
+	if err != nil {
+		return vehicleResponse.ExitConfirmResponse{}, errors.New("车辆未入场或已出场")
+	}
+
+	vehicle, err := s.GetVehicleByPlateNumber(req.PlateNumber, req.RFIDTag)
+	if err != nil {
+		return vehicleResponse.ExitConfirmResponse{}, err
+	}
+
+	exitTime := time.Now()
+	stayTime := int64(exitTime.Sub(record.EntryTime).Minutes())
+	if stayTime < 0 {
+		stayTime = 0
+	}
+	fee := s.calculateFee(&vehicle, stayTime)
+
+	record.ExitTime = &exitTime
+	record.StayTime = stayTime
+	record.Fee = fee
+	record.PaymentMethod = req.PaymentMethod
+	global.GVA_DB.Save(&record)
+
+	vehicle.LastExitTime = &exitTime
+	vehicle.TotalStayTime += stayTime
+	vehicle.TotalFee += fee
+	global.GVA_DB.Save(&vehicle)
+
+	paymentSvc := paymentService.NewPaymentService()
+	pr, err := paymentSvc.ProcessPayment(record.ID, req.PaymentMethod, fee, req.PaidAmount, operatorID)
+	if err != nil {
+		return vehicleResponse.ExitConfirmResponse{}, err
+	}
+
+	changeAmount := 0.0
+	if pr != nil {
+		changeAmount = pr.ChangeAmount
+	}
+
+	return vehicleResponse.ExitConfirmResponse{
+		PlateNumber:   vehicle.PlateNumber,
+		EntryTime:     record.EntryTime.Unix(),
+		ExitTime:      exitTime.Unix(),
+		StayTime:      stayTime,
+		Fee:           fee,
+		ChangeAmount:  changeAmount,
+		PaymentMethod: req.PaymentMethod,
+	}, nil
+}
+
 // calculateFee 计算停车费用
 func (s *VehicleService) calculateFee(vehicle *dao.Vehicle, stayTime int64) float64 {
 	// 1. 根据车辆类型ID获取收费配置(唯一)

+ 10 - 0
internal/service/vehicle/vehicle_type.go

@@ -33,6 +33,11 @@ func (s *VehicleTypeService) UpdateVehicleType(req request.VehicleTypeUpdate) er
 		return errors.New("车辆类型不存在")
 	}
 
+	// 禁止编辑系统内置类型
+	if vehicleType.IsSystem {
+		return errors.New("系统内置类型,不可编辑")
+	}
+
 	var existingType dao.VehicleType
 	result = global.GVA_DB.Where("name = ? AND id != ?", req.Name, req.ID).First(&existingType)
 	if result.RowsAffected > 0 {
@@ -87,6 +92,11 @@ func (s *VehicleTypeService) DeleteVehicleType(id uint) error {
 		return errors.New("车辆类型不存在")
 	}
 
+	// 禁止删除系统内置类型
+	if vehicleType.IsSystem {
+		return errors.New("系统内置类型,不可删除")
+	}
+
 	var vehicleCount int64
 	global.GVA_DB.Model(&dao.Vehicle{}).Where("vehicle_type_id = ?", id).Count(&vehicleCount)
 	if vehicleCount > 0 {