From 2f09efc660bf2cc94cbc5291ad25ca06fc9bdadf Mon Sep 17 00:00:00 2001
From: wlzboy <66905212@qq.com>
Date: 星期六, 24 一月 2026 22:03:09 +0800
Subject: [PATCH] feat: 增加OCR测试,车辆
---
医院信息分词搜索功能说明.md | 274 +
ruoyi-system/src/main/resources/mapper/system/TbHospDataMapper.xml | 24
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleSyncController.java | 164
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleAlertConfigController.java | 106
ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleAbnormalAlertMapper.java | 92
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java | 8
ruoyi-system/src/main/java/com/ruoyi/system/controller/OCRController.java | 433 +
ruoyi-system/src/main/resources/mapper/system/DispatchOrdMapper.xml | 7
ruoyi-system/src/main/resources/mapper/system/VehicleAlertConfigMapper.xml | 135
医院分词搜索-快速使用指南.md | 432 +
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java | 16
ruoyi-ui/src/router/index.js | 51
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusPushServiceImpl.java | 23
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TbHospDataMapper.java | 10
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacyTransferSyncTask.java | 2
ruoyi-ui/src/views/system/vehicleAlertConfig/index.vue | 486 +
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/QyWechatServiceImpl.java | 8
ruoyi-system/src/main/java/com/ruoyi/system/domain/HospitalTokenizerTask.java | 143
ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java | 15
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java | 8
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusSyncServiceImpl.java | 2
ruoyi-system/src/main/java/com/ruoyi/system/utils/AliOCRUtil.java | 457 +
doc/车辆异常运行监控告警功能说明.md | 287 +
ruoyi-system/src/main/java/com/ruoyi/system/service/IQyWechatService.java | 10
ruoyi-system/src/main/java/com/ruoyi/system/config/OCRConfig.java | 98
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/VehicleSyncVO.java | 100
ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleAbnormalAlertService.java | 97
ruoyi-admin/src/main/resources/application.yml | 15
doc/车辆异常运行监控告警-前端部署指南.md | 357 +
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleAbnormalAlertController.java | 149
ruoyi-system/src/main/java/com/ruoyi/system/utils/TencentOCRUtil.java | 506 ++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleGpsSegmentMileageServiceImpl.java | 3
sql/ocr_module_menu.sql | 82
sql/vehicle_abnormal_alert.sql | 183
ruoyi-ui/src/api/system/vehicleAlertConfig.js | 53
ruoyi-system/src/main/java/com/ruoyi/system/controller/NetworkDiagController.java | 160
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java | 8
ruoyi-admin/src/main/resources/logback.xml | 11
doc/车辆异常运行监控告警-README.md | 267 +
ruoyi-common/src/main/java/com/ruoyi/common/utils/HospitalTokenizerUtil.java | 698 ++
sql/vehicle_abnormal_alert_upgrade.sql | 150
ruoyi-system/src/main/java/com/ruoyi/system/config/BaiduOCRConfig.java | 72
ruoyi-ui/src/views/system/diag/ocrConnection.vue | 261 +
OCR测试功能使用说明.md | 240
app/pagesTask/components/HospitalSelector.vue | 45
app/pagesTask/detail.vue | 19
sql/vehicle_sync_menu.sql | 35
ruoyi-system/src/main/resources/mapper/system/VehicleAbnormalAlertMapper.xml | 178
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleAlertConfigServiceImpl.java | 109
ruoyi-ui/src/views/system/vehicleAlert/index.vue | 528 ++
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/HospDataController.java | 271 +
ruoyi-ui/src/views/system/ocr/config.vue | 277 +
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleAbnormalAlertTask.java | 547 ++
ruoyi-system/src/main/java/com/ruoyi/system/utils/BaiduOCRUtil.java | 445 +
ruoyi-ui/src/views/system/vehicleSync/index.vue | 245
doc/车辆异常运行监控告警-实现总结.md | 376 +
sql/tb_hosp_data_add_keywords.sql | 9
ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleAlertConfig.java | 156
ruoyi-ui/src/api/system/networkDiag.js | 41
ruoyi-system/src/main/java/com/ruoyi/system/mapper/DispatchOrdMapper.java | 10
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DispatchOrdServiceImpl.java | 12
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TbHospDataServiceImpl.java | 64
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java | 2
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleAbnormalAlertServiceImpl.java | 173
ruoyi-ui/src/api/system/ocr.js | 69
ruoyi-ui/src/api/system/vehicleAlert.js | 80
ruoyi-ui/src/views/system/hospital/tokenizer.vue | 418 +
ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleAlertConfigMapper.java | 71
ruoyi-system/src/main/java/com/ruoyi/system/service/HospitalTokenizerAsyncService.java | 149
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/HospDataSyncServiceImpl.java | 10
sql/hospital_tokenizer_menu.sql | 24
sql/ocr_test_menu.sql | 41
app/api/ocr.js | 182
doc/车辆异常运行监控告警-菜单配置说明.md | 350 +
ruoyi-system/src/main/java/com/ruoyi/system/domain/TbHospData.java | 12
ruoyi-ui/src/api/system/vehicle.js | 2
ruoyi-system/src/main/java/com/ruoyi/system/service/IDispatchOrdService.java | 9
ruoyi-ui/src/views/system/ocr/index.vue | 386 +
ruoyi-system/src/main/java/com/ruoyi/system/config/TencentOCRConfig.java | 52
ruoyi-system/pom.xml | 38
doc/车辆异常运行监控告警-完整实现总结.md | 551 ++
ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsSegmentMileageMapper.java | 7
app/pagesTask/create-emergency.vue | 1349 +++++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacySystemSyncServiceImpl.java | 5
ruoyi-common/pom.xml | 7
ruoyi-system/src/main/java/com/ruoyi/system/service/ILegacySystemSyncService.java | 10
ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleAlertConfigService.java | 70
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java | 10
ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleAbnormalAlert.java | 302 +
OCR功能完整说明.md | 202
OCR使用说明与故障排除指南.md | 157
app/api/hospital.js | 18
ruoyi-ui/src/api/system/vehicleSync.js | 18
doc/车辆异常运行监控告警-快速部署指南.md | 262 +
ruoyi-system/src/main/java/com/ruoyi/system/service/ITbHospDataService.java | 16
ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml | 13
96 files changed, 15,040 insertions(+), 95 deletions(-)
diff --git "a/OCR\344\275\277\347\224\250\350\257\264\346\230\216\344\270\216\346\225\205\351\232\234\346\216\222\351\231\244\346\214\207\345\215\227.md" "b/OCR\344\275\277\347\224\250\350\257\264\346\230\216\344\270\216\346\225\205\351\232\234\346\216\222\351\231\244\346\214\207\345\215\227.md"
new file mode 100644
index 0000000..789a208
--- /dev/null
+++ "b/OCR\344\275\277\347\224\250\350\257\264\346\230\216\344\270\216\346\225\205\351\232\234\346\216\222\351\231\244\346\214\207\345\215\227.md"
@@ -0,0 +1,157 @@
+# OCR鍥惧儚璇嗗埆鍔熻兘浣跨敤璇存槑涓庢晠闅滄帓闄ゆ寚鍗�
+
+## 馃摎 鍔熻兘姒傝堪
+
+OCR锛圤ptical Character Recognition锛屽厜瀛﹀瓧绗﹁瘑鍒級鍔熻兘鐢ㄤ簬璇嗗埆鍥剧墖涓殑鏂囧瓧鍐呭锛屾敮鎸佸绉嶈瘑鍒被鍨嬶細
+- 閫氱敤鏂囧瓧璇嗗埆
+- 鍙戠エ璇嗗埆
+- 韬唤璇佽瘑鍒�
+
+## 馃敡 绯荤粺瑕佹眰
+
+### 鏈嶅姟渚濊禆
+- 闃块噷浜慜CR鏈嶅姟锛堥渶瑕佹湁鏁堢殑AccessKey锛�
+- 缃戠粶杩炴帴锛堣闂� `ocr-api.cn-hangzhou.aliyuncs.com:443`锛�
+
+### 鎶�鏈爤
+- 鍚庣锛歋pring Boot + 闃块噷浜慜CR SDK
+- 鍓嶇锛歏ue.js + Element UI
+
+## 鈿欙笍 閰嶇疆璇存槑
+
+### 1. AccessKey閰嶇疆
+鍦� `application.yml` 涓厤缃細
+```yaml
+ocr:
+ accessKeyId: YOUR_ACCESS_KEY_ID
+ accessKeySecret: YOUR_ACCESS_KEY_SECRET
+```
+
+### 2. 缃戠粶閰嶇疆
+纭繚鏈嶅姟鍣ㄨ兘澶熻闂細
+- 鍦板潃锛歚ocr-api.cn-hangzhou.aliyuncs.com`
+- 绔彛锛歚443` (HTTPS)
+- 鍗忚锛歍CP
+
+## 馃殌 浣跨敤鏂规硶
+
+### 1. 璁块棶椤甸潰
+鑿滃崟璺緞锛歚绯荤粺宸ュ叿 > OCR绠$悊 > OCR娴嬭瘯`
+
+### 2. 涓婁紶鍥剧墖
+- 鏀寔鏍煎紡锛欽PG銆丳NG銆丅MP
+- 鏂囦欢澶у皬锛氫笉瓒呰繃4MB
+- 鍥剧墖璐ㄩ噺锛氭竻鏅帮紝鏂囧瓧鏄撹鲸璁�
+
+### 3. 閫夋嫨璇嗗埆绫诲瀷
+- 閫氱敤鏂囧瓧璇嗗埆锛氶�傚悎涓�鑸枃妗�
+- 鍙戠エ璇嗗埆锛氶�傚悎鍙戠エ銆佹敹鎹�
+- 韬唤璇佽瘑鍒細閫傚悎韬唤璇佹鍙嶉潰
+
+## 馃攳 鏁呴殰鎺掗櫎
+
+### 甯歌閿欒鍙婅В鍐虫柟妗�
+
+#### 1. 缃戠粶杩炴帴閿欒
+**閿欒淇℃伅**锛歚code: 415, The image format or content is not supported`
+**鍙兘鍘熷洜**锛�
+- 鍥剧墖鏍煎紡涓嶆敮鎸�
+- 鍥剧墖鍐呭鎹熷潖
+- 鍥剧墖澶ぇ
+
+**瑙e喅鏂规**锛�
+- 妫�鏌ュ浘鐗囨牸寮忔槸鍚︿负JPG/PNG/BMP
+- 楠岃瘉鍥剧墖鏂囦欢鏄惁瀹屾暣
+- 鍘嬬缉鍥剧墖鑷�4MB浠ヤ笅
+
+#### 2. 缃戠粶杩炴帴澶辫触
+**閿欒淇℃伅**锛歚ocr-api.cn-hangzhou.aliyuncs.com`
+**鍙兘鍘熷洜**锛�
+- DNS瑙f瀽澶辫触
+- 闃茬伀澧欓樆姝㈣繛鎺�
+- 缃戠粶绛栫暐闄愬埗
+- 浠g悊閰嶇疆闂
+
+**瑙e喅鏂规**锛�
+1. **DNS闂**锛�
+ - 妫�鏌NS鏈嶅姟鍣ㄩ厤缃�
+ - 灏濊瘯浣跨敤鍏叡DNS锛堝8.8.8.8锛�
+ - 楠岃瘉鍩熷悕瑙f瀽锛歚nslookup ocr-api.cn-hangzhou.aliyuncs.com`
+
+2. **闃茬伀澧欓棶棰�**锛�
+ - 妫�鏌ラ槻鐏鏄惁寮�鏀�443绔彛
+ - 纭鏈嶅姟鍣ㄥ厑璁稿嚭绔橦TTPS璇锋眰
+ - 楠岃瘉瀹夊叏缁勮鍒�
+
+3. **浠g悊闂**锛�
+ - 閰嶇疆绯荤粺浠g悊鍙傛暟锛�
+ ```
+ -Dhttp.proxyHost=proxy.example.com
+ -Dhttp.proxyPort=8080
+ -Dhttps.proxyHost=proxy.example.com
+ -Dhttps.proxyPort=8080
+ ```
+
+#### 3. AccessKey閿欒
+**閿欒淇℃伅**锛氳璇佸け璐ョ浉鍏抽敊璇�
+**瑙e喅鏂规**锛�
+- 妫�鏌ccessKey ID鍜孲ecret鏄惁姝g‘
+- 纭璐︽埛鏈塐CR鏈嶅姟鏉冮檺
+- 楠岃瘉AccessKey鏄惁杩囨湡
+
+### 璇婃柇宸ュ叿
+
+#### 1. 缃戠粶璇婃柇
+璁块棶椤甸潰锛歚绯荤粺宸ュ叿 > OCR绠$悊 > OCR娴嬭瘯`
+鍦ㄨ瘑鍒け璐ユ椂锛岀偣鍑�"缃戠粶璇婃柇"鎸夐挳鏌ョ湅杩炴帴鐘舵�併��
+
+#### 2. 鎵嬪姩娴嬭瘯鍛戒护
+```bash
+# 娴嬭瘯DNS瑙f瀽
+nslookup ocr-api.cn-hangzhou.aliyuncs.com
+
+# 娴嬭瘯绔彛杩為�氭��
+telnet ocr-api.cn-hangzhou.aliyuncs.com 443
+
+# 娴嬭瘯HTTPS杩炴帴
+curl -I https://ocr-api.cn-hangzhou.aliyuncs.com
+```
+
+## 馃洜锔� 绯荤粺缁存姢
+
+### 1. 鏃ュ織鏌ョ湅
+- 鍚庣鏃ュ織锛歚logs/ocr.log`
+- 閿欒鏃ュ織锛氬叧娉� `AliOCRUtil` 绫荤殑鏃ュ織
+
+### 2. 鎬ц兘璋冧紭
+- 杩炴帴瓒呮椂锛氶粯璁�10绉�
+- 璇诲彇瓒呮椂锛氶粯璁�30绉�
+- 鍙�氳繃閰嶇疆璋冩暣瓒呮椂鏃堕棿
+
+### 3. 瀹夊叏娉ㄦ剰浜嬮」
+- AccessKey涓嶈纭紪鐮佸湪浠g爜涓�
+- 瀹氭湡鏇存崲AccessKey
+- 闄愬埗AccessKey鏉冮檺鑼冨洿
+
+## 馃摓 鎶�鏈敮鎸�
+
+濡傞亣鏃犳硶瑙e喅鐨勯棶棰橈紝璇锋彁渚涗互涓嬩俊鎭仈绯绘妧鏈敮鎸侊細
+- 瀹屾暣閿欒鏃ュ織
+- 缃戠粶璇婃柇缁撴灉
+- 绯荤粺鐜淇℃伅
+- 闃茬伀澧�/浠g悊閰嶇疆淇℃伅
+
+## 馃搵 妫�鏌ユ竻鍗�
+
+鍦ㄩ儴缃插拰浣跨敤OCR鍔熻兘鍓嶏紝璇风‘璁わ細
+- [ ] 宸插紑閫氶樋閲屼簯OCR鏈嶅姟
+- [ ] 宸查厤缃湁鏁堢殑AccessKey
+- [ ] 鏈嶅姟鍣ㄥ彲璁块棶浜掕仈缃�
+- [ ] 闃茬伀澧欏紑鏀�443绔彛
+- [ ] DNS瑙f瀽姝e父
+- [ ] 鍥剧墖鏍煎紡鏀寔楠岃瘉
+- [ ] 缃戠粶杩為�氭�ф祴璇曢�氳繃
+
+---
+
+**娉ㄦ剰**锛氭湰鍔熻兘渚濊禆澶栭儴鏈嶅姟锛岀綉缁滅姸鍐靛彲鑳藉奖鍝嶈瘑鍒垚鍔熺巼鍜岄�熷害銆�
\ No newline at end of file
diff --git "a/OCR\345\212\237\350\203\275\345\256\214\346\225\264\350\257\264\346\230\216.md" "b/OCR\345\212\237\350\203\275\345\256\214\346\225\264\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..0064475
--- /dev/null
+++ "b/OCR\345\212\237\350\203\275\345\256\214\346\225\264\350\257\264\346\230\216.md"
@@ -0,0 +1,202 @@
+# OCR鍥惧儚璇嗗埆鍔熻兘瀹屾暣璇存槑
+
+## 馃摎 鍔熻兘姒傝堪
+
+OCR锛圤ptical Character Recognition锛屽厜瀛﹀瓧绗﹁瘑鍒級鍔熻兘鐢ㄤ簬璇嗗埆鍥剧墖涓殑鏂囧瓧鍐呭锛屾敮鎸佸绉嶈瘑鍒被鍨嬶細
+- 閫氱敤鏂囧瓧璇嗗埆
+- 鍙戠エ璇嗗埆
+- 韬唤璇佽瘑鍒�
+- 鎵嬪啓浣撹瘑鍒�
+
+## 馃敡 绯荤粺瑕佹眰
+
+### 鏈嶅姟渚濊禆
+- 闃块噷浜慜CR鏈嶅姟锛堥渶瑕佹湁鏁堢殑AccessKey锛�
+- 缃戠粶杩炴帴锛堣闂� `ocr-api.cn-hangzhou.aliyuncs.com:443`锛�
+
+### 鎶�鏈爤
+- 鍚庣锛歋pring Boot + 闃块噷浜慜CR SDK
+- 鍓嶇锛歏ue.js + Element UI
+
+## 鈿欙笍 閰嶇疆璇存槑
+
+### 1. AccessKey閰嶇疆
+鍦� `application.yml` 涓厤缃細
+```yaml
+ocr:
+ accessKeyId: YOUR_ACCESS_KEY_ID
+ accessKeySecret: YOUR_ACCESS_KEY_SECRET
+```
+
+### 2. 缃戠粶閰嶇疆
+纭繚鏈嶅姟鍣ㄨ兘澶熻闂細
+- 鍦板潃锛歚ocr-api.cn-hangzhou.aliyuncs.com`
+- 绔彛锛歚443` (HTTPS)
+- 鍗忚锛歍CP
+
+## 馃殌 浣跨敤鏂规硶
+
+### 1. 璁块棶椤甸潰
+鑿滃崟璺緞锛歚绯荤粺宸ュ叿 > OCR绠$悊 > OCR娴嬭瘯`
+
+### 2. 涓婁紶鍥剧墖
+- 鏀寔鏍煎紡锛欽PG銆丳NG銆丅MP
+- 鏂囦欢澶у皬锛氫笉瓒呰繃4MB
+- 鍥剧墖璐ㄩ噺锛氭竻鏅帮紝鏂囧瓧鏄撹鲸璁�
+
+### 3. 閫夋嫨璇嗗埆绫诲瀷
+- 閫氱敤鏂囧瓧璇嗗埆锛氶�傚悎涓�鑸枃妗�
+- 鍙戠エ璇嗗埆锛氶�傚悎鍙戠エ銆佹敹鎹�
+- 韬唤璇佽瘑鍒細閫傚悎韬唤璇佹鍙嶉潰
+- 鎵嬪啓浣撹瘑鍒細閫傚悎鎵嬪啓鏂囧瓧璇嗗埆
+
+## 馃洜锔� API鎺ュ彛璇存槑
+
+### 1. 璇嗗埆绫诲瀷鑾峰彇
+- 鎺ュ彛锛歚GET /system/ocr/types`
+- 鍔熻兘锛氳幏鍙栨敮鎸佺殑璇嗗埆绫诲瀷鍒楄〃
+
+### 2. 鏈湴鏂囦欢璇嗗埆
+- 鎺ュ彛锛歚POST /system/ocr/recognize`
+- 鍙傛暟锛歚file`锛堝浘鐗囨枃浠讹級銆乣type`锛堣瘑鍒被鍨嬶級
+- 鍔熻兘锛氫笂浼犲浘鐗囧苟杩涜OCR璇嗗埆
+
+### 3. URL璇嗗埆
+- 鎺ュ彛锛歚GET /system/ocr/recognizeByUrl`
+- 鍙傛暟锛歚imageUrl`锛堝浘鐗嘦RL锛夈�乣type`锛堣瘑鍒被鍨嬶級
+- 鍔熻兘锛氶�氳繃URL杩涜OCR璇嗗埆
+
+### 4. 瀛楁鎻愬彇
+- 鎺ュ彛锛歚POST /system/ocr/extractFields`
+- 鍙傛暟锛歄CR璇嗗埆缁撴灉
+- 鍔熻兘锛氫粠OCR缁撴灉涓彁鍙栧叧閿瓧娈�
+
+### 5. 缃戠粶璇婃柇
+- 鎺ュ彛锛歚GET /system/diag/ocrConnection`
+- 鍔熻兘锛氳瘖鏂璒CR鏈嶅姟杩炴帴鐘舵��
+
+## 馃О 宸ュ叿绫诲姛鑳�
+
+### 1. AliOCRUtil绫�
+鏀寔鐨勮瘑鍒柟娉曪細
+- `recognizeGeneral()` - 閫氱敤鏂囧瓧璇嗗埆
+- `recognizeInvoice()` - 鍙戠エ璇嗗埆
+- `recognizeIdCard()` - 韬唤璇佽瘑鍒�
+- `recognizeHandwriting()` - 鎵嬪啓浣撹瘑鍒�
+
+### 2. 瀛楁鎻愬彇
+- `extractTargetFields()` - 鎻愬彇閲戦銆佹棩鏈熴�佸娉ㄧ瓑鍏抽敭淇℃伅
+
+### 3. 璇嗗埆绫诲瀷鏋氫妇
+- `OcrType.GENERAL` - 閫氱敤鏂囧瓧璇嗗埆
+- `OcrType.INVOICE` - 鍙戠エ璇嗗埆
+- `OcrType.IDCARD` - 韬唤璇佽瘑鍒�
+- `OcrType.HANDWRITING` - 鎵嬪啓浣撹瘑鍒�
+
+## 馃攳 鏁呴殰鎺掗櫎
+
+### 甯歌閿欒鍙婅В鍐虫柟妗�
+
+#### 1. 缃戠粶杩炴帴閿欒
+**閿欒淇℃伅**锛歚code: 415, The image format or content is not supported`
+**鍙兘鍘熷洜**锛�
+- 鍥剧墖鏍煎紡涓嶆敮鎸�
+- 鍥剧墖鍐呭鎹熷潖
+- 鍥剧墖澶ぇ
+
+**瑙e喅鏂规**锛�
+- 妫�鏌ュ浘鐗囨牸寮忔槸鍚︿负JPG/PNG/BMP
+- 楠岃瘉鍥剧墖鏂囦欢鏄惁瀹屾暣
+- 鍘嬬缉鍥剧墖鑷�4MB浠ヤ笅
+
+#### 2. 缃戠粶杩炴帴澶辫触
+**閿欒淇℃伅**锛歚ocr-api.cn-hangzhou.aliyuncs.com`
+**鍙兘鍘熷洜**锛�
+- DNS瑙f瀽澶辫触
+- 闃茬伀澧欓樆姝㈣繛鎺�
+- 缃戠粶绛栫暐闄愬埗
+- 浠g悊閰嶇疆闂
+
+**瑙e喅鏂规**锛�
+1. **DNS闂**锛�
+ - 妫�鏌NS鏈嶅姟鍣ㄩ厤缃�
+ - 灏濊瘯浣跨敤鍏叡DNS锛堝8.8.8.8锛�
+ - 楠岃瘉鍩熷悕瑙f瀽锛歚nslookup ocr-api.cn-hangzhou.aliyuncs.com`
+
+2. **闃茬伀澧欓棶棰�**锛�
+ - 妫�鏌ラ槻鐏鏄惁寮�鏀�443绔彛
+ - 纭鏈嶅姟鍣ㄥ厑璁稿嚭绔橦TTPS璇锋眰
+ - 楠岃瘉瀹夊叏缁勮鍒�
+
+3. **浠g悊闂**锛�
+ - 閰嶇疆绯荤粺浠g悊鍙傛暟锛�
+ ```
+ -Dhttp.proxyHost=proxy.example.com
+ -Dhttp.proxyPort=8080
+ -Dhttps.proxyHost=proxy.example.com
+ -Dhttps.proxyPort=8080
+ ```
+
+#### 3. AccessKey閿欒
+**閿欒淇℃伅**锛氳璇佸け璐ョ浉鍏抽敊璇�
+**瑙e喅鏂规**锛�
+- 妫�鏌ccessKey ID鍜孲ecret鏄惁姝g‘
+- 纭璐︽埛鏈塐CR鏈嶅姟鏉冮檺
+- 楠岃瘉AccessKey鏄惁杩囨湡
+
+### 璇婃柇宸ュ叿
+
+#### 1. 缃戠粶璇婃柇
+璁块棶椤甸潰锛歚绯荤粺宸ュ叿 > OCR绠$悊 > OCR娴嬭瘯`
+鍦ㄨ瘑鍒け璐ユ椂锛岀偣鍑�"缃戠粶璇婃柇"鎸夐挳鏌ョ湅杩炴帴鐘舵�併��
+
+#### 2. 鎵嬪姩娴嬭瘯鍛戒护
+```bash
+# 娴嬭瘯DNS瑙f瀽
+nslookup ocr-api.cn-hangzhou.aliyuncs.com
+
+# 娴嬭瘯绔彛杩為�氭��
+telnet ocr-api.cn-hangzhou.aliyuncs.com 443
+
+# 娴嬭瘯HTTPS杩炴帴
+curl -I https://ocr-api.cn-hangzhou.aliyuncs.com
+```
+
+## 馃洜锔� 绯荤粺缁存姢
+
+### 1. 鏃ュ織鏌ョ湅
+- 鍚庣鏃ュ織锛歚logs/ocr.log`
+- 閿欒鏃ュ織锛氬叧娉� `AliOCRUtil` 绫荤殑鏃ュ織
+
+### 2. 鎬ц兘璋冧紭
+- 杩炴帴瓒呮椂锛氶粯璁�10绉�
+- 璇诲彇瓒呮椂锛氶粯璁�30绉�
+- 鍙�氳繃閰嶇疆璋冩暣瓒呮椂鏃堕棿
+
+### 3. 瀹夊叏娉ㄦ剰浜嬮」
+- AccessKey涓嶈纭紪鐮佸湪浠g爜涓�
+- 瀹氭湡鏇存崲AccessKey
+- 闄愬埗AccessKey鏉冮檺鑼冨洿
+
+## 馃摓 鎶�鏈敮鎸�
+
+濡傞亣鏃犳硶瑙e喅鐨勯棶棰橈紝璇锋彁渚涗互涓嬩俊鎭仈绯绘妧鏈敮鎸侊細
+- 瀹屾暣閿欒鏃ュ織
+- 缃戠粶璇婃柇缁撴灉
+- 绯荤粺鐜淇℃伅
+- 闃茬伀澧�/浠g悊閰嶇疆淇℃伅
+
+## 馃搵 妫�鏌ユ竻鍗�
+
+鍦ㄩ儴缃插拰浣跨敤OCR鍔熻兘鍓嶏紝璇风‘璁わ細
+- [ ] 宸插紑閫氶樋閲屼簯OCR鏈嶅姟
+- [ ] 宸查厤缃湁鏁堢殑AccessKey
+- [ ] 鏈嶅姟鍣ㄥ彲璁块棶浜掕仈缃�
+- [ ] 闃茬伀澧欏紑鏀�443绔彛
+- [ ] DNS瑙f瀽姝e父
+- [ ] 鍥剧墖鏍煎紡鏀寔楠岃瘉
+- [ ] 缃戠粶杩為�氭�ф祴璇曢�氳繃
+
+---
+
+**娉ㄦ剰**锛氭湰鍔熻兘渚濊禆澶栭儴鏈嶅姟锛岀綉缁滅姸鍐靛彲鑳藉奖鍝嶈瘑鍒垚鍔熺巼鍜岄�熷害銆�
\ No newline at end of file
diff --git "a/OCR\346\265\213\350\257\225\345\212\237\350\203\275\344\275\277\347\224\250\350\257\264\346\230\216.md" "b/OCR\346\265\213\350\257\225\345\212\237\350\203\275\344\275\277\347\224\250\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..af479b5
--- /dev/null
+++ "b/OCR\346\265\213\350\257\225\345\212\237\350\203\275\344\275\277\347\224\250\350\257\264\346\230\216.md"
@@ -0,0 +1,240 @@
+# OCR鍥惧儚璇嗗埆娴嬭瘯鍔熻兘浣跨敤璇存槑
+
+## 馃摎 鍔熻兘姒傝堪
+
+OCR鍥惧儚璇嗗埆娴嬭瘯椤甸潰鐢ㄤ簬娴嬭瘯闃块噷浜慜CR鏈嶅姟锛屾敮鎸侀�氱敤鏂囧瓧璇嗗埆銆佸彂绁ㄨ瘑鍒�佽韩浠借瘉璇嗗埆绛夊姛鑳姐��
+
+## 馃幆 宸插疄鐜扮殑鍔熻兘
+
+### 鍚庣閮ㄥ垎
+
+#### 1. OCRController
+**璺緞**: `ruoyi-system/src/main/java/com/ruoyi/system/controller/OCRController.java`
+
+**鎺ュ彛鍒楄〃**:
+- `POST /system/ocr/recognize` - 涓婁紶鍥剧墖杩涜OCR璇嗗埆
+- `GET /system/ocr/recognizeByUrl` - 閫氳繃URL杩涜OCR璇嗗埆
+- `POST /system/ocr/extractFields` - 鎻愬彇OCR缁撴灉涓殑鐩爣瀛楁
+
+#### 2. AliOCRUtil宸ュ叿绫�
+**璺緞**: `ruoyi-system/src/main/java/com/ruoyi/system/utils/AliOCRUtil.java`
+
+**涓昏鏂规硶**:
+- `recognizeTextByFile()` - 鏈湴鏂囦欢璇嗗埆
+- `recognizeTextByUrl()` - URL鍥剧墖璇嗗埆
+- `recognizeInvoice()` - 鍙戠エ璇嗗埆
+- `recognizeIdCard()` - 韬唤璇佽瘑鍒�
+- `recognizeGeneral()` - 閫氱敤鏂囧瓧璇嗗埆
+- `extractTargetFields()` - 瀛楁鎻愬彇
+
+### 鍓嶇閮ㄥ垎
+
+#### 1. OCR娴嬭瘯椤甸潰
+**璺緞**: `ruoyi-ui/src/views/system/ocr/index.vue`
+
+**鍔熻兘鐗规��**:
+- 鎷栨嫿涓婁紶鍥剧墖
+- 鍥剧墖棰勮
+- 璇嗗埆绫诲瀷閫夋嫨锛堥�氱敤/鍙戠エ/韬唤璇侊級
+- 瀹炴椂鏄剧ず璇嗗埆缁撴灉
+- 鑷姩鎻愬彇鍏抽敭瀛楁
+- 鍘熷JSON鏁版嵁鏌ョ湅
+- 涓�閿鍒惰瘑鍒粨鏋�
+
+#### 2. API鎺ュ彛灏佽
+**璺緞**: `ruoyi-ui/src/api/system/ocr.js`
+
+## 馃殌 閮ㄧ讲姝ラ
+
+### 1. 鎵ц鑿滃崟SQL
+
+```bash
+# 鍦∕ySQL涓墽琛�
+mysql -u root -p your_database < sql/ocr_test_menu.sql
+```
+
+鎴栧湪Navicat绛夊伐鍏蜂腑鎵ц `sql/ocr_test_menu.sql` 鏂囦欢
+
+### 2. 閰嶇疆闃块噷浜慉ccessKey
+
+**鏂囦欢**: `ruoyi-admin/src/main/resources/application.yml`
+
+```yaml
+ocr:
+ accessKeyId: YOUR_ACCESS_KEY_ID
+ accessKeySecret: YOUR_ACCESS_KEY_SECRET
+```
+
+**閲嶈**: 璇锋浛鎹负浣犺嚜宸辩殑闃块噷浜慉ccessKey
+
+### 3. 閲嶅惎鍚庣鏈嶅姟
+
+```bash
+cd ruoyi-admin
+mvn spring-boot:run
+```
+
+### 4. 鍒嗛厤鏉冮檺
+
+鐧诲綍鍚庡彴绠$悊绯荤粺锛�
+1. 杩涘叆 **绯荤粺绠$悊 > 瑙掕壊绠$悊**
+2. 閫夋嫨闇�瑕佷娇鐢∣CR鍔熻兘鐨勮鑹�
+3. 鍒嗛厤鏉冮檺锛歚system:ocr:test` 鍜� `system:ocr:recognize`
+
+### 5. 璁块棶椤甸潰
+
+鑿滃崟璺緞锛�**绯荤粺宸ュ叿 > OCR娴嬭瘯**
+
+URL: `http://localhost/system/ocr`
+
+## 馃摉 浣跨敤鎸囧崡
+
+### 鍩烘湰鎿嶄綔娴佺▼
+
+1. **閫夋嫨璇嗗埆绫诲瀷**
+ - 閫氱敤鏂囧瓧璇嗗埆锛氶�傜敤浜庢櫘閫氭枃瀛楀唴瀹�
+ - 鍙戠エ璇嗗埆锛氫笓闂ㄨ瘑鍒彂绁ㄤ俊鎭�
+ - 韬唤璇佽瘑鍒細璇嗗埆韬唤璇佹鍙嶉潰
+
+2. **涓婁紶鍥剧墖**
+ - 鎷栨嫿鍥剧墖鍒颁笂浼犲尯鍩�
+ - 鎴栫偣鍑讳笂浼犲尯鍩熼�夋嫨鏂囦欢
+ - 鏀寔JPG銆丳NG銆丅MP鏍煎紡
+ - 鏂囦欢澶у皬涓嶈秴杩�4MB
+
+3. **寮�濮嬭瘑鍒�**
+ - 鐐瑰嚮"寮�濮嬭瘑鍒�"鎸夐挳
+ - 绛夊緟璇嗗埆瀹屾垚锛堥�氬父1-3绉掞級
+
+4. **鏌ョ湅缁撴灉**
+ - 鎻愬彇瀛楁锛氳嚜鍔ㄦ彁鍙栫殑鍏抽敭淇℃伅锛堥噾棰濄�佹棩鏈熴�佸娉ㄧ瓑锛�
+ - 瀹屾暣璇嗗埆鍐呭锛氭墍鏈夎瘑鍒殑鏂囧瓧
+ - 鍘熷JSON鏁版嵁锛氶樋閲屼簯杩斿洖鐨勫畬鏁存暟鎹�
+
+5. **澶嶅埗缁撴灉**
+ - 鐐瑰嚮"澶嶅埗缁撴灉"鎸夐挳涓�閿鍒惰瘑鍒唴瀹�
+
+### 璇嗗埆绫诲瀷璇存槑
+
+#### 閫氱敤鏂囧瓧璇嗗埆 (General)
+- 閫傜敤鍦烘櫙锛氬悇绫绘枃妗c�佸浘鐗囦腑鐨勬枃瀛�
+- 杩斿洖鍐呭锛氭墍鏈夎瘑鍒殑鏂囧瓧鍙婁綅缃俊鎭�
+
+#### 鍙戠エ璇嗗埆 (Invoice)
+- 閫傜敤鍦烘櫙锛氬鍊肩◣鍙戠エ銆佹櫘閫氬彂绁ㄣ�佹敹鎹瓑
+- 鑷姩鎻愬彇锛氬彂绁ㄥ彿鐮併�侀噾棰濄�佹棩鏈熴�佽喘涔版柟銆侀攢鍞柟绛�
+
+#### 韬唤璇佽瘑鍒� (IdCard)
+- 閫傜敤鍦烘櫙锛氳韩浠借瘉姝i潰鍜屽弽闈�
+- 鑷姩鎻愬彇锛氬鍚嶃�佽韩浠借瘉鍙枫�佸湴鍧�銆佹湁鏁堟湡绛�
+
+## 馃敡 閰嶇疆璇存槑
+
+### OCRConfig閰嶇疆绫�
+**璺緞**: `ruoyi-system/src/main/java/com/ruoyi/system/config/OCRConfig.java`
+
+```java
+@Component
+@ConfigurationProperties(prefix = "ocr")
+public class OCRConfig {
+ private String accessKeyId;
+ private String accessKeySecret;
+ // getter鍜宻etter
+}
+```
+
+### 闃块噷浜慜CR API绔偣
+榛樿浣跨敤鏉窞鑺傜偣锛歚ocr-api.cn-hangzhou.aliyuncs.com`
+
+濡傞渶鏇存敼锛屼慨鏀� `AliOCRUtil.java` 涓殑 `ENDPOINT` 甯搁噺銆�
+
+## 鈿狅笍 娉ㄦ剰浜嬮」
+
+### 1. AccessKey瀹夊叏
+- **涓嶈**灏咥ccessKey鎻愪氦鍒癎it浠撳簱
+- 寤鸿浣跨敤鐜鍙橀噺鎴栭厤缃腑蹇冪鐞�
+- 瀹氭湡鏇存崲AccessKey
+
+### 2. 鏂囦欢澶у皬闄愬埗
+- 鍓嶇闄愬埗锛�4MB
+- 闃块噷浜戦檺鍒讹細鏍规嵁濂楅涓嶅悓锛岄�氬父涓�4-10MB
+- 瓒呰繃闄愬埗浼氬鑷磋瘑鍒け璐�
+
+### 3. 璇嗗埆鍑嗙‘鐜�
+- 鍥剧墖娓呮櫚搴﹀奖鍝嶈瘑鍒噯纭巼
+- 寤鸿浣跨敤300dpi浠ヤ笂鐨勫浘鐗�
+- 閬垮厤妯$硦銆佸�炬枩銆佸弽鍏夌殑鍥剧墖
+
+### 4. 璐圭敤璇存槑
+- 闃块噷浜慜CR鎸夎皟鐢ㄦ鏁版敹璐�
+- 鏂扮敤鎴锋湁鍏嶈垂棰濆害
+- 寤鸿鍦ㄧ敓浜х幆澧冧腑璁剧疆璋冪敤闄愬埗
+
+### 5. 涓存椂鏂囦欢澶勭悊
+- 涓婁紶鐨勫浘鐗囦細淇濆瓨鍒颁复鏃剁洰褰�
+- 璇嗗埆瀹屾垚鍚庤嚜鍔ㄥ垹闄�
+- 涓存椂鐩綍锛歚System.getProperty("java.io.tmpdir")`
+
+## 馃悰 甯歌闂
+
+### 1. 璇嗗埆澶辫触锛欰ccessKey閿欒
+**鍘熷洜**: 閰嶇疆鐨凙ccessKey涓嶆纭�
+
+**瑙e喅**:
+- 妫�鏌� `application.yml` 涓殑閰嶇疆
+- 纭AccessKey ID鍜孲ecret姝g‘
+- 纭璐﹀彿宸插紑閫歄CR鏈嶅姟
+
+### 2. 涓婁紶鍚庢棤鍝嶅簲
+**鍘熷洜**: 鏂囦欢杩囧ぇ鎴栫綉缁滈棶棰�
+
+**瑙e喅**:
+- 妫�鏌ュ浘鐗囧ぇ灏忔槸鍚﹁秴杩�4MB
+- 妫�鏌ョ綉缁滆繛鎺�
+- 鏌ョ湅娴忚鍣ㄦ帶鍒跺彴閿欒淇℃伅
+
+### 3. 璇嗗埆缁撴灉涓虹┖
+**鍘熷洜**: 鍥剧墖璐ㄩ噺闂鎴栦笉鏀寔鐨勫浘鐗囨牸寮�
+
+**瑙e喅**:
+- 浣跨敤娓呮櫚鐨勫浘鐗�
+- 纭繚鍥剧墖鍖呭惈鍙瘑鍒殑鏂囧瓧
+- 灏濊瘯杞崲鍥剧墖鏍煎紡
+
+### 4. 鏉冮檺涓嶈冻
+**鍘熷洜**: 鐢ㄦ埛瑙掕壊鏈垎閰峅CR鏉冮檺
+
+**瑙e喅**:
+- 鑱旂郴绠$悊鍛樺垎閰嶆潈闄�
+- 鎴栧湪瑙掕壊绠$悊涓嬀閫夌浉鍏虫潈闄�
+
+## 馃摑 鎵╁睍寮�鍙�
+
+### 娣诲姞鏂扮殑璇嗗埆绫诲瀷
+
+1. 鍦ㄥ墠绔〉闈㈡坊鍔犻�夐」锛�
+```vue
+<el-option label="钀ヤ笟鎵х収璇嗗埆" value="BusinessLicense" />
+```
+
+2. 鍚庣浼氳嚜鍔ㄦ敮鎸侊紝鏃犻渶淇敼浠g爜
+
+### 鑷畾涔夊瓧娈垫彁鍙�
+
+淇敼 `AliOCRUtil.java` 涓殑 `extractTargetFields()` 鏂规硶锛�
+
+```java
+// 娣诲姞鑷畾涔夋彁鍙栭�昏緫
+if (text.contains("鑷畾涔夊叧閿瓧")) {
+ extracted.put("customField", text);
+}
+```
+
+## 馃摎 鐩稿叧鏂囨。
+
+- [闃块噷浜慜CR瀹樻柟鏂囨。](https://help.aliyun.com/document_detail/442275.html)
+- [RuoYi妗嗘灦鏂囨。](http://doc.ruoyi.vip/)
+
+## 馃摓 鎶�鏈敮鎸�
+
+濡傛湁闂锛岃鑱旂郴鎶�鏈敮鎸佸洟闃熸垨鎻愪氦Issue銆�
diff --git a/app/api/hospital.js b/app/api/hospital.js
index 39e474f..2551dbb 100644
--- a/app/api/hospital.js
+++ b/app/api/hospital.js
@@ -85,3 +85,21 @@
}
})
}
+
+/**
+ * 鍩轰簬鍒嗚瘝鍖归厤鎼滅储鍖婚櫌锛堟柊绠楁硶锛屾櫤鑳藉垎璇�+璇勫垎鎺掑簭锛�
+ * @param {string} searchText 鎼滅储鏂囨湰
+ * @param {number} deptId 閮ㄩ棬ID锛堝彲閫夛紝鐢ㄤ簬鍖哄煙杩囨护锛�
+ * @param {number} limit 杩斿洖缁撴灉鏁伴噺闄愬埗锛堥粯璁�50锛�
+ */
+export function searchHospitalsByKeywords(searchText, deptId, limit = 50) {
+ return request({
+ url: '/system/hospital/searchByKeywords',
+ method: 'get',
+ params: {
+ searchText: searchText,
+ deptId: deptId,
+ pageSize: limit
+ }
+ })
+}
diff --git a/app/api/ocr.js b/app/api/ocr.js
new file mode 100644
index 0000000..e873cee
--- /dev/null
+++ b/app/api/ocr.js
@@ -0,0 +1,182 @@
+import config from '@/config'
+import { getToken } from '@/utils/auth'
+
+const baseUrl = config.baseUrl
+
+/**
+ * OCR璇嗗埆API
+ */
+
+/**
+ * 鍗曞浘OCR璇嗗埆锛堥�氱敤鎺ュ彛锛�
+ * @param {String} filePath 鍥剧墖涓存椂璺緞
+ * @param {String} type 璇嗗埆绫诲瀷锛欻andWriting/IDCard/BankCard/General
+ * @param {String} provider OCR鏈嶅姟鎻愪緵鍟嗭細tencent/baidu
+ * @param {Array} itemNames 闇�瑕佹彁鍙栫殑瀛楁鍚嶇О鏁扮粍锛堟墜鍐欎綋璇嗗埆鏃朵娇鐢級
+ * @returns {Promise}
+ */
+export function recognizeImage(filePath, type = 'HandWriting', provider = 'tencent', itemNames = []) {
+ return new Promise((resolve, reject) => {
+ const token = getToken()
+
+ // 鏋勫缓formData
+ const formData = {
+ type: type,
+ provider: provider
+ }
+
+ // 濡傛灉鏈塱temNames锛屾坊鍔犲埌formData
+ if (itemNames && itemNames.length > 0) {
+ itemNames.forEach((itemName, index) => {
+ formData[`itemNames[${index}]`] = itemName
+ })
+ }
+
+ uni.uploadFile({
+ url: `${baseUrl}/system/ocr/recognize`,
+ filePath: filePath,
+ name: 'file',
+ header: {
+ 'Authorization': `Bearer ${token}`
+ },
+ formData: formData,
+ success: (res) => {
+ try {
+ const response = JSON.parse(res.data)
+ if (response.code === 200) {
+ resolve(response)
+ } else {
+ reject(response)
+ }
+ } catch (e) {
+ reject({ msg: '瑙f瀽璇嗗埆缁撴灉澶辫触', error: e })
+ }
+ },
+ fail: (err) => {
+ reject({ msg: 'OCR璇锋眰澶辫触', error: err })
+ }
+ })
+ })
+}
+
+/**
+ * 鑵捐浜戞墜鍐欎綋璇嗗埆锛堝崟鍥撅級
+ * @param {String} filePath 鍥剧墖涓存椂璺緞
+ * @param {Array} itemNames 闇�瑕佹彁鍙栫殑瀛楁鍚嶇О鏁扮粍
+ * @returns {Promise}
+ */
+export function tencentHandwritingRecognize(filePath, itemNames = []) {
+ return new Promise((resolve, reject) => {
+ const token = getToken()
+
+ // 鏋勫缓formData
+ const formData = {}
+ if (itemNames && itemNames.length > 0) {
+ itemNames.forEach((itemName, index) => {
+ formData[`itemNames[${index}]`] = itemName
+ })
+ }
+
+ uni.uploadFile({
+ url: `${baseUrl}/system/ocr/tencent/handwriting`,
+ filePath: filePath,
+ name: 'file',
+ header: {
+ 'Authorization': `Bearer ${token}`
+ },
+ formData: formData,
+ success: (res) => {
+ try {
+ const response = JSON.parse(res.data)
+ if (response.code === 200) {
+ resolve(response)
+ } else {
+ reject(response)
+ }
+ } catch (e) {
+ reject({ msg: '瑙f瀽璇嗗埆缁撴灉澶辫触', error: e })
+ }
+ },
+ fail: (err) => {
+ reject({ msg: 'OCR璇锋眰澶辫触', error: err })
+ }
+ })
+ })
+}
+
+/**
+ * 鎵归噺OCR璇嗗埆锛堝鍥撅級
+ * @param {Array} filePaths 鍥剧墖涓存椂璺緞鏁扮粍
+ * @param {Array} itemNames 闇�瑕佹彁鍙栫殑瀛楁鍚嶇О鏁扮粍
+ * @returns {Promise} 杩斿洖鍚堝苟鍚庣殑瀛楁Map
+ */
+export function batchRecognizeImages(filePaths, itemNames = []) {
+ if (!filePaths || filePaths.length === 0) {
+ return Promise.reject({ msg: '鍥剧墖鍒楄〃涓嶈兘涓虹┖' })
+ }
+
+ // 鍒涘缓涓婁紶浠诲姟闃熷垪
+ const uploadPromises = filePaths.map((filePath) => {
+ return tencentHandwritingRecognize(filePath, itemNames)
+ })
+
+ // 绛夊緟鎵�鏈変笂浼犲畬鎴愬苟鍚堝苟缁撴灉
+ return Promise.all(uploadPromises)
+ .then(results => {
+ // 鍚堝苟鎵�鏈夌粨鏋�
+ const mergedFields = {}
+ let successCount = 0
+ let failCount = 0
+
+ results.forEach(response => {
+ if (response.code === 200 && response.data && response.data.fields) {
+ const fields = response.data.fields
+ // 鍚堝苟瀛楁锛堝鏋渒ey宸插瓨鍦ㄤ笖涓嶄负绌猴紝涓嶈鐩栵級
+ Object.keys(fields).forEach(key => {
+ if (!mergedFields[key] || mergedFields[key].trim() === '') {
+ mergedFields[key] = fields[key]
+ }
+ })
+ successCount++
+ } else {
+ failCount++
+ }
+ })
+
+ // 杩囨护缁撴灉锛氬彧杩斿洖itemNames涓寚瀹氱殑瀛楁
+ const filteredFields = {}
+ if (itemNames && itemNames.length > 0) {
+ itemNames.forEach(itemName => {
+ if (mergedFields.hasOwnProperty(itemName)) {
+ filteredFields[itemName] = mergedFields[itemName]
+ }
+ })
+ } else {
+ // 濡傛灉娌℃湁鎸囧畾itemNames锛岃繑鍥炴墍鏈夊瓧娈�
+ Object.assign(filteredFields, mergedFields)
+ }
+
+ return {
+ success: true,
+ successCount: successCount,
+ failCount: failCount,
+ fields: filteredFields
+ }
+ })
+ .catch(error => {
+ return Promise.reject({
+ success: false,
+ msg: '鎵归噺璇嗗埆澶辫触',
+ error: error
+ })
+ })
+}
+
+/**
+ * 榛樿鐨勮浆杩愬崟瀛楁鍒楄〃
+ */
+export const DEFAULT_TRANSFER_ITEM_NAMES = [
+ "鎮h�呭鍚�", "鎬у埆", "骞撮緞", "韬唤璇佸彿", "璇婃柇", "闇�鏀粯杞繍璐圭敤",
+ "琛岀▼", "寮�濮嬫椂闂�", "缁撴潫鏃堕棿", "瀹跺睘绛惧悕", "鎮h�呯鍚嶏紙鎵嬪嵃锛�",
+ "绛惧瓧浜鸿韩浠借瘉鍙风爜", "鏃ユ湡", "鑱旂郴鐢佃瘽", "鏈汉", "绛惧瓧浜轰笌鎮h�呭叧绯�"
+]
diff --git a/app/pagesTask/components/HospitalSelector.vue b/app/pagesTask/components/HospitalSelector.vue
index 3b2dd62..fc57c40 100644
--- a/app/pagesTask/components/HospitalSelector.vue
+++ b/app/pagesTask/components/HospitalSelector.vue
@@ -57,7 +57,7 @@
</template>
<script>
-import { searchHospitals } from "@/api/hospital"
+import { searchHospitals, searchHospitalsByKeywords } from "@/api/hospital"
import { searchTianDiTuAddress } from "@/api/map"
export default {
@@ -179,16 +179,38 @@
}, 300)
},
- // 鎼滅储鍖婚櫌
+ // 鎼滅储鍖婚櫌锛堟櫤鑳介�夋嫨鎺ュ彛锛�
searchHospital(keyword) {
- searchHospitals(keyword, this.deptId).then(response => {
- this.searchResults = response.data || []
- this.showResults = true
- }).catch(error => {
- // console.error('鎼滅储鍖婚櫌澶辫触:', error)
- this.searchResults = []
- // this.showResults = false
- })
+ // 濡傛灉鍏抽敭璇嶄负绌烘垨鑰呮槸"瀹朵腑"锛屼娇鐢ㄥ師鏉ョ殑鎺ュ彛
+ if (!keyword || keyword.trim() === '' || keyword.trim() === '瀹朵腑') {
+ searchHospitals(keyword || '', this.deptId).then(response => {
+ this.searchResults = response.data || []
+ this.showResults = true
+ }).catch(error => {
+ // console.error('鎼滅储鍖婚櫌澶辫触:', error)
+ this.searchResults = []
+ // this.showResults = false
+ })
+ } else {
+ // 鏈夊叧閿瘝鏃讹紝浣跨敤鏂扮殑鍒嗚瘝鍖归厤鎺ュ彛
+ searchHospitalsByKeywords(keyword, this.deptId).then(response => {
+ // 杞崲鏁版嵁鏍煎紡锛氭彁鍙� hospital 瀵硅薄
+ const rawData = response.data || []
+ this.searchResults = rawData.map(item => {
+ // 濡傛灉鏁版嵁缁撴瀯鏄� {hospital: {...}, matchScore: ...}
+ if (item.hospital) {
+ return item.hospital
+ }
+ // 濡傛灉宸茬粡鏄尰闄㈠璞★紝鐩存帴杩斿洖
+ return item
+ })
+ this.showResults = true
+ }).catch(error => {
+ // console.error('鎼滅储鍖婚櫌澶辫触:', error)
+ this.searchResults = []
+ // this.showResults = false
+ })
+ }
},
// 杈撳叆妗嗚幏寰楃劍鐐�
@@ -209,8 +231,9 @@
}
},
- // 鍔犺浇榛樿鍖婚櫌鍒楄〃
+ // 鍔犺浇榛樿鍖婚櫌鍒楄〃锛堜娇鐢ㄥ師鏉ョ殑鎺ュ彛锛�
loadDefaultHospitals() {
+ // 浣跨敤鍘熸潵鐨勬帴鍙e姞杞介粯璁ゅ垪琛�
searchHospitals('', this.deptId).then(response => {
this.defaultHospitals = response.data || []
this.searchResults = this.defaultHospitals
diff --git a/app/pagesTask/create-emergency.vue b/app/pagesTask/create-emergency.vue
index d3db251..146f8c0 100644
--- a/app/pagesTask/create-emergency.vue
+++ b/app/pagesTask/create-emergency.vue
@@ -8,8 +8,149 @@
<view class="smart-parse-btn" @click="showSmartParsePopup">
<uni-icons type="compose" size="20" color="#007AFF"></uni-icons>
<text>鏅鸿兘璇嗗埆</text>
+ </view>
+ <view class="multi-photo-ocr-btn" @click="showMultiPhotoOCRPopup">
+ <uni-icons type="images" size="20" color="#FF6B00"></uni-icons>
+ <text>鎷嶇収璇嗗埆</text>
</view>
</view>
+
+ <!-- 澶氬浘鐗囨媿鐓ц瘑鍒脊绐� -->
+ <uni-popup ref="multiPhotoOCRPopup" type="bottom" :safe-area="true">
+ <view class="photo-ocr-popup">
+ <view class="popup-header">
+ <view class="popup-title">
+ <uni-icons type="images" size="22" color="#FF6B00"></uni-icons>
+ <text>鎷嶇収璇嗗埆 - 鐭ユ儏鍚屾剰涔�</text>
+ </view>
+ <view class="popup-close" @click="closeMultiPhotoOCRPopup">
+ <uni-icons type="closeempty" size="24" color="#333"></uni-icons>
+ </view>
+ </view>
+
+ <view class="ocr-content">
+ <view class="ocr-tip">
+ <uni-icons type="info" size="18" color="#FF6B00"></uni-icons>
+ <text>璇峰垎鍒笂浼犵煡鎯呭悓鎰忎功鐨勭涓�椤靛拰绗簩椤碉紝绯荤粺灏嗚嚜鍔ㄨ瘑鍒苟濉厖鍒拌〃鍗曚腑</text>
+ </view>
+
+ <!-- 鐭ユ儏鍚屾剰涔︾涓�椤� -->
+ <view class="page-upload-section first-page">
+ <view class="page-header">
+ <view class="page-title">
+ <view class="page-badge">
+ <uni-icons type="compose" size="18" color="#52c41a"></uni-icons>
+ <text class="page-number">1</text>
+ </view>
+ <view class="page-info">
+ <text class="page-main-title">鐭ユ儏鍚屾剰涔︾涓�椤�</text>
+ <text class="page-sub-title">鎮h�呭熀鏈俊鎭〉</text>
+ </view>
+ </view>
+ <!-- 鍙湁鏈笂浼犳椂鏄剧ず+鍙� -->
+ <view class="upload-btn green" @click="selectPage1Images" v-if="!page1Image">
+ <uni-icons type="plusempty" size="30" color="#52c41a"></uni-icons>
+ </view>
+ </view>
+
+ <!-- 璇嗗埆瀛楁鎻愮ず -->
+ <view class="field-hint" v-if="!page1Image">
+ <text>璇嗗埆瀛楁锛氭偅鑰呭鍚嶃�佹�у埆銆佸勾榫勩�佽韩浠借瘉鍙枫�佽瘖鏂�佽浆杩愯垂鐢ㄣ�佽绋嬨�佸紑濮嬫椂闂淬�佺粨鏉熸椂闂淬�佸灞炵鍚�</text>
+ </view>
+
+ <!-- 鍗曞浘棰勮 -->
+ <view class="single-image-container" v-if="page1Image">
+ <image :src="page1Image" mode="aspectFit" class="preview-image"></image>
+ <view class="delete-btn" @click="deletePage1Image">
+ <uni-icons type="closeempty" size="20" color="#fff"></uni-icons>
+ </view>
+ <view class="image-status">
+ <uni-icons type="checkmarkempty" size="16" color="#fff"></uni-icons>
+ <text>宸蹭笂浼�</text>
+ </view>
+ </view>
+
+ <!-- 绗竴椤佃瘑鍒粨鏋� -->
+ <view class="recognition-result" v-if="Object.keys(page1Fields).length > 0">
+ <view class="result-title">
+ <uni-icons type="checkmarkempty" size="18" color="#52c41a"></uni-icons>
+ <text>璇嗗埆缁撴灉</text>
+ <text class="result-count">(鍏眥{ Object.keys(page1Fields).length }}涓瓧娈�)</text>
+ </view>
+ <view class="field-list">
+ <view class="field-item" v-for="(value, key) in page1Fields" :key="key">
+ <text class="field-name">{{ key }}</text>
+ <text class="field-value">{{ value || '鏈瘑鍒�' }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鐭ユ儏鍚屾剰涔︾浜岄〉 -->
+ <view class="page-upload-section second-page">
+ <view class="page-header">
+ <view class="page-title">
+ <view class="page-badge">
+ <uni-icons type="compose" size="18" color="#FF6B00"></uni-icons>
+ <text class="page-number">2</text>
+ </view>
+ <view class="page-info">
+ <text class="page-main-title">鐭ユ儏鍚屾剰涔︾浜岄〉</text>
+ <text class="page-sub-title">绛惧悕涓庤仈绯讳俊鎭〉</text>
+ </view>
+ </view>
+ <!-- 鍙湁鏈笂浼犳椂鏄剧ず+鍙� -->
+ <view class="upload-btn orange" @click="selectPage2Images" v-if="!page2Image">
+ <uni-icons type="plusempty" size="30" color="#FF6B00"></uni-icons>
+ </view>
+ </view>
+
+ <!-- 璇嗗埆瀛楁鎻愮ず -->
+ <view class="field-hint" v-if="!page2Image">
+ <text>璇嗗埆瀛楁锛氭偅鑰呯鍚嶃�佺瀛椾汉韬唤璇佸彿銆佺瀛楁棩鏈熴�佽仈绯荤數璇濄�佺瀛椾汉鍏崇郴</text>
+ </view>
+
+ <!-- 鍗曞浘棰勮 -->
+ <view class="single-image-container" v-if="page2Image">
+ <image :src="page2Image" mode="aspectFit" class="preview-image"></image>
+ <view class="delete-btn" @click="deletePage2Image">
+ <uni-icons type="closeempty" size="20" color="#fff"></uni-icons>
+ </view>
+ <view class="image-status">
+ <uni-icons type="checkmarkempty" size="16" color="#fff"></uni-icons>
+ <text>宸蹭笂浼�</text>
+ </view>
+ </view>
+
+ <!-- 绗簩椤佃瘑鍒粨鏋� -->
+ <view class="recognition-result" v-if="Object.keys(page2Fields).length > 0">
+ <view class="result-title">
+ <uni-icons type="checkmarkempty" size="18" color="#FF6B00"></uni-icons>
+ <text>璇嗗埆缁撴灉</text>
+ <text class="result-count">(鍏眥{ Object.keys(page2Fields).length }}涓瓧娈�)</text>
+ </view>
+ <view class="field-list">
+ <view class="field-item" v-for="(value, key) in page2Fields" :key="key">
+ <text class="field-name">{{ key }}</text>
+ <text class="field-value">{{ value || '鏈瘑鍒�' }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <view class="popup-footer">
+ <button class="cancel-btn" @click="closeMultiPhotoOCRPopup">鍙栨秷</button>
+ <button
+ class="confirm-btn"
+ @click="applyOcrResult"
+ :disabled="Object.keys(page1Fields).length === 0 && Object.keys(page2Fields).length === 0"
+ >
+ 搴旂敤鍒拌〃鍗�
+ </button>
+ </view>
+ </view>
+ </uni-popup>
<view class="form-section">
<view class="form-item">
@@ -259,6 +400,41 @@
</view>
</view>
</uni-popup>
+
+ <!-- 鎷嶇収璇嗗埆寮圭獥 -->
+ <uni-popup ref="photoOCRPopup" type="bottom" :safe-area="true">
+ <view class="photo-ocr-popup">
+ <view class="popup-header">
+ <view class="popup-title">鎷嶇収璇嗗埆</view>
+ <view class="popup-close" @click="closePhotoOCRPopup">
+ <uni-icons type="closeempty" size="24" color="#333"></uni-icons>
+ </view>
+ </view>
+
+ <view class="ocr-content">
+ <view class="ocr-tip">
+ <uni-icons type="info" size="18" color="#007AFF"></uni-icons>
+ <text>鎷嶇収鎴栭�夋嫨鍥剧墖锛岃嚜鍔ㄨ瘑鍒浆杩愬崟淇℃伅</text>
+ </view>
+
+ <view class="image-preview" v-if="ocrImage">
+ <image :src="ocrImage" mode="aspectFit" style="width: 100%; height: 300rpx; border-radius: 10rpx;"></image>
+ </view>
+
+ <view class="ocr-actions">
+ <button class="select-btn" @click="selectImage">閫夋嫨鍥剧墖</button>
+ <button class="capture-btn" @click="captureImage">鎷嶇収</button>
+ </view>
+ </view>
+
+ <view class="popup-footer">
+ <button class="cancel-btn" @click="closePhotoOCRPopup">鍙栨秷</button>
+ <button class="confirm-btn" @click="performOCR" :disabled="ocrLoading">
+ {{ ocrLoading ? '璇嗗埆涓�...' : '寮�濮嬭瘑鍒�' }}
+ </button>
+ </view>
+ </view>
+ </uni-popup>
</scroll-view>
</template>
@@ -268,13 +444,16 @@
import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue'
import { addTask, checkTaskDuplicate } from "@/api/task"
import { listAvailableVehicles, getUserBoundVehicle } from "@/api/vehicle"
-import { searchHospitals, searchHospitalsByDeptRegion } from "@/api/hospital"
+import { searchHospitals, searchHospitalsByDeptRegion, searchHospitalsByKeywords } from "@/api/hospital"
import DepartureSelector from './components/DepartureSelector.vue'
import { calculateTianDiTuDistance } from "@/api/map"
import { listBranchUsers } from "@/api/system/user"
import { searchIcd10 } from "@/api/icd10"
import { calculateTransferPrice } from "@/api/price"
import { checkVehicleActiveTasks } from "@/api/task"
+import { recognizeImage, batchRecognizeImages, DEFAULT_TRANSFER_ITEM_NAMES } from "@/api/ocr"
+import config from '@/config'
+import { getToken } from '@/utils/auth'
import { getDicts } from "@/api/dict"
import { getServiceOrdAreaTypes, getServiceOrderTypes } from "@/api/dictionary"
@@ -357,7 +536,21 @@
loading: false,
// 鏅鸿兘璇嗗埆鐩稿叧
rawText: '',
- parseLoading: false
+ parseLoading: false,
+ // 鎷嶇収璇嗗埆鐩稿叧
+ ocrImage: '',
+ ocrLoading: false,
+ // 澶氬浘鐗囨媿鐓ц瘑鍒浉鍏�
+ multiOcrImages: [],
+ multiOcrLoading: false,
+ // 鍒嗛〉OCR璇嗗埆鐩稿叧
+ currentOcrPage: 1, // 褰撳墠涓婁紶鐨勯〉鐮侊細1=绗竴椤碉紝2=绗簩椤�
+ page1Image: '', // 绗竴椤靛浘鐗囷紙鍗曞浘锛�
+ page2Image: '', // 绗簩椤靛浘鐗囷紙鍗曞浘锛�
+ page1Fields: {}, // 绗竴椤佃瘑鍒粨鏋�
+ page2Fields: {}, // 绗簩椤佃瘑鍒粨鏋�
+ // 闄勪欢涓存椂瀛樺偍锛圤CR鍥剧墖锛�
+ pendingAttachments: [] // 寰呬笂浼犵殑闄勪欢鍒楄〃 [{ filePath: '', category: '1' }]
}
},
computed: {
@@ -957,25 +1150,93 @@
addTask(submitData).then(response => {
this.loading = false
- this.$modal.showToast('浠诲姟鍒涘缓鎴愬姛')
- // 寤惰繜璺宠浆锛岃鐢ㄦ埛鐪嬪埌鎴愬姛鎻愮ず
- setTimeout(() => {
- // 璺宠浆鍒颁换鍔″垪琛ㄥ苟瑙﹀彂鍒锋柊
- uni.switchTab({
- url: '/pages/task/index',
- success: () => {
- // 浣跨敤浜嬩欢鎬荤嚎閫氱煡浠诲姟鍒楄〃椤甸潰鍒锋柊
- uni.$emit('refreshTaskList')
- }
+ // 鑾峰彇鍒涘缓鐨勪换鍔D
+ const taskId = response.taskId || (response.data && response.data.taskId) || null
+ console.log('浠诲姟鍒涘缓鎴愬姛锛宼askId:', taskId)
+
+ // 濡傛灉鏈夊緟涓婁紶鐨勯檮浠讹紙OCR鍥剧墖锛夛紝鍏堜笂浼犻檮浠�
+ if (taskId && this.pendingAttachments.length > 0) {
+ this.uploadPendingAttachments(taskId).then(() => {
+ this.$modal.showToast('浠诲姟鍒涘缓鎴愬姛锛岄檮浠跺凡涓婁紶')
+ this.navigateToTaskList()
+ }).catch(error => {
+ console.error('闄勪欢涓婁紶澶辫触:', error)
+ this.$modal.showToast('浠诲姟鍒涘缓鎴愬姛锛屼絾闄勪欢涓婁紶澶辫触')
+ this.navigateToTaskList()
})
- }, 1000)
+ } else {
+ this.$modal.showToast('浠诲姟鍒涘缓鎴愬姛')
+ this.navigateToTaskList()
+ }
}).catch(error => {
this.loading = false
console.error('浠诲姟鍒涘缓澶辫触:', error)
this.$modal.showToast('浠诲姟鍒涘缓澶辫触锛岃閲嶈瘯')
})
}).catch(() => {})
+ },
+
+ // 涓婁紶寰呬笂浼犵殑闄勪欢锛圤CR鍥剧墖锛�
+ uploadPendingAttachments(taskId) {
+ console.log('寮�濮嬩笂浼犻檮浠讹紝taskId:', taskId, '闄勪欢鏁伴噺:', this.pendingAttachments.length)
+
+ // 浣跨敤 Promise.all 骞跺彂涓婁紶鎵�鏈夐檮浠�
+ const uploadPromises = this.pendingAttachments.map(attachment => {
+ return this.uploadSingleAttachment(taskId, attachment)
+ })
+
+ return Promise.all(uploadPromises)
+ },
+
+ // 涓婁紶鍗曚釜闄勪欢
+ uploadSingleAttachment(taskId, attachment) {
+ return new Promise((resolve, reject) => {
+ uni.uploadFile({
+ url: config.baseUrl + '/task/attachment/upload/' + taskId,
+ filePath: attachment.filePath,
+ name: 'file',
+ formData: {
+ 'category': attachment.category
+ },
+ header: {
+ 'Authorization': 'Bearer ' + getToken()
+ },
+ success: function(uploadRes) {
+ if (uploadRes.statusCode === 200) {
+ const result = JSON.parse(uploadRes.data)
+ if (result.code === 200) {
+ console.log('闄勪欢涓婁紶鎴愬姛:', attachment.description)
+ resolve(result)
+ } else {
+ console.error('闄勪欢涓婁紶澶辫触:', attachment.description, result.msg)
+ reject(result)
+ }
+ } else {
+ console.error('闄勪欢涓婁紶澶辫触:', attachment.description, uploadRes)
+ reject(uploadRes)
+ }
+ },
+ fail: function(err) {
+ console.error('闄勪欢涓婁紶澶辫触:', attachment.description, err)
+ reject(err)
+ }
+ })
+ })
+ },
+
+ // 璺宠浆鍒颁换鍔″垪琛�
+ navigateToTaskList() {
+ setTimeout(() => {
+ // 璺宠浆鍒颁换鍔″垪琛ㄥ苟瑙﹀彂鍒锋柊
+ uni.switchTab({
+ url: '/pages/task/index',
+ success: () => {
+ // 浣跨敤浜嬩欢鎬荤嚎閫氱煡浠诲姟鍒楄〃椤甸潰鍒锋柊
+ uni.$emit('refreshTaskList')
+ }
+ })
+ }, 1000)
},
goBack() {
@@ -1280,7 +1541,7 @@
findHospitalByName(name, type, restrictRegion = true) {
if (!name) return Promise.resolve(null)
const normalized = name.trim()
-
+
// 鐗规畩澶勭悊"瀹朵腑"
if (normalized === '瀹朵腑') {
// 鏌ヨ鍖婚櫌搴撲腑鐨�"瀹朵腑"璁板綍
@@ -1288,7 +1549,7 @@
const queryPromise = restrictRegion && deptId
? searchHospitalsByDeptRegion('瀹朵腑', deptId, 50)
: searchHospitals('瀹朵腑', null, 50)
-
+
return queryPromise.then(res => {
const list = res.data || []
// 鏌ユ壘鍚嶇О涓�"瀹朵腑"鐨勫尰闄㈣褰�
@@ -1313,20 +1574,23 @@
}
})
}
-
- // restrictRegion=false 鏃惰蛋鍏ㄩ噺鏌ヨ锛泃rue 涓旀湁 deptId 鏃惰蛋鍖哄煙鎺ュ彛
+
+ // OCR璇嗗埆鍚庣殑鍖婚櫌鍚嶇О锛屼娇鐢ㄦ柊鐨勫垎璇嶅尮閰嶆帴鍙�
const deptId = this.selectedOrganizationId || null
- const queryPromise = (restrictRegion && deptId)
- ? searchHospitalsByDeptRegion(normalized, deptId, 50)
- : searchHospitals(normalized, null, 50)
-
- return queryPromise.then(res => {
- const list = res.data || []
- if (!list.length) return null
-
- // 鑷姩閫夋嫨绗竴涓潪"瀹朵腑"鐨勫尯闄紝濡傛灉鍏ㄦ槸"瀹朵腑"鍒欓�夌涓�涓�
- const best = this.pickBestHospitalMatch(list, normalized)
- return best || null
+
+ return searchHospitalsByKeywords(normalized, deptId, 50).then(res => {
+ const rawData = res.data || []
+ if (!rawData.length) return null
+
+ // 鎻愬彇 hospital 瀵硅薄锛堟帴鍙h繑鍥炴牸寮忥細{hospital: {...}, matchScore: ...}锛�
+ const firstItem = rawData[0]
+ const firstHospital = firstItem.hospital || firstItem
+
+ console.log(`OCR璇嗗埆鍖婚櫌"${normalized}"锛岃嚜鍔ㄩ�変腑锛�${firstHospital.hospName}锛堝尮閰嶅垎鏁帮細${firstItem.matchScore || 'N/A'}锛塦)
+ return firstHospital
+ }).catch(error => {
+ console.error(`鎼滅储鍖婚櫌"${normalized}"澶辫触:`, error)
+ return null
})
},
@@ -1424,6 +1688,496 @@
console.error('璺濈璁$畻澶辫触:', error)
this.$modal.showToast('璺濈璁$畻澶辫触锛岃鎵嬪姩杈撳叆')
})
+ },
+
+ // ==================== 鎷嶇収璇嗗埆鐩稿叧鏂规硶 ====================
+
+ // 鏄剧ず鎷嶇収璇嗗埆寮圭獥
+ showPhotoOCRPopup() {
+ this.ocrImage = ''
+ this.$refs.photoOCRPopup.open()
+ },
+
+ // 鏄剧ず澶氬浘鎷嶇収璇嗗埆寮圭獥
+ showMultiPhotoOCRPopup() {
+ this.page1Image = ''
+ this.page2Image = ''
+ this.page1Fields = {}
+ this.page2Fields = {}
+ this.$refs.multiPhotoOCRPopup.open()
+ },
+
+ // 鍏抽棴澶氬浘鎷嶇収璇嗗埆寮圭獥
+ closeMultiPhotoOCRPopup() {
+ this.$refs.multiPhotoOCRPopup.close()
+ },
+
+ // 閫夋嫨绗竴椤靛浘鐗�
+ selectPage1Images() {
+ uni.showActionSheet({
+ itemList: ['浠庣浉鍐岄�夋嫨', '鎷嶇収'],
+ success: (res) => {
+ if (res.tapIndex === 0) {
+ this.chooseImageForPage(1, 'album')
+ } else if (res.tapIndex === 1) {
+ this.chooseImageForPage(1, 'camera')
+ }
+ }
+ })
+ },
+
+ // 閫夋嫨绗簩椤靛浘鐗�
+ selectPage2Images() {
+ uni.showActionSheet({
+ itemList: ['浠庣浉鍐岄�夋嫨', '鎷嶇収'],
+ success: (res) => {
+ if (res.tapIndex === 0) {
+ this.chooseImageForPage(2, 'album')
+ } else if (res.tapIndex === 1) {
+ this.chooseImageForPage(2, 'camera')
+ }
+ }
+ })
+ },
+
+ // 鍒犻櫎绗竴椤靛浘鐗�
+ deletePage1Image() {
+ this.page1Image = ''
+ this.page1Fields = {}
+ },
+
+ // 鍒犻櫎绗簩椤靛浘鐗�
+ deletePage2Image() {
+ this.page2Image = ''
+ this.page2Fields = {}
+ },
+
+ // 涓烘寚瀹氶〉鐮侀�夋嫨鍥剧墖锛堝崟鍥撅級
+ chooseImageForPage(page, sourceType) {
+ uni.chooseImage({
+ count: 1, // 鍙�夋嫨涓�寮犲浘鐗�
+ sizeType: ['compressed'],
+ sourceType: [sourceType],
+ success: (res) => {
+ const imagePath = res.tempFilePaths[0]
+
+ if (page === 1) {
+ this.page1Image = imagePath
+ } else {
+ this.page2Image = imagePath
+ }
+
+ // 閫夋嫨瀹屽浘鐗囧悗锛岀珛鍗宠繘琛孫CR璇嗗埆
+ this.$modal.showToast(`宸查�夋嫨鍥剧墖锛屽紑濮嬭瘑鍒�...`)
+ this.recognizeSinglePage(page)
+ },
+ fail: (err) => {
+ console.error('閫夋嫨鍥剧墖澶辫触:', err)
+ this.$modal.showToast('閫夋嫨鍥剧墖澶辫触')
+ }
+ })
+ },
+
+ // 閫夋嫨鍥剧墖
+ selectImage() {
+ uni.chooseImage({
+ count: 1,
+ sizeType: ['compressed'],
+ sourceType: ['album'],
+ success: (res) => {
+ this.ocrImage = res.tempFilePaths[0]
+ },
+ fail: (err) => {
+ console.error('閫夋嫨鍥剧墖澶辫触:', err)
+ this.$modal.showToast('閫夋嫨鍥剧墖澶辫触')
+ }
+ })
+ },
+
+ // 鎷嶇収
+ captureImage() {
+ uni.chooseImage({
+ count: 1,
+ sizeType: ['compressed'],
+ sourceType: ['camera'],
+ success: (res) => {
+ this.ocrImage = res.tempFilePaths[0]
+ },
+ fail: (err) => {
+ console.error('鎷嶇収澶辫触:', err)
+ this.$modal.showToast('鎷嶇収澶辫触')
+ }
+ })
+ },
+
+ // 搴旂敤OCR璇嗗埆缁撴灉鍒拌〃鍗�
+ applyOcrResult() {
+ // 鍚堝苟涓ら〉鐨勮瘑鍒粨鏋�
+ const mergedFields = { ...this.page1Fields, ...this.page2Fields }
+
+ // 澶勭悊鍚堝苟鍚庣殑璇嗗埆缁撴灉
+ this.processMultiOCRResult(mergedFields)
+
+ // 淇濆瓨OCR鍥剧墖鍒板緟涓婁紶闄勪欢鍒楄〃锛堢煡鎯呭悓鎰忎功锛屽垎绫讳负'1'锛�
+ this.pendingAttachments = []
+ if (this.page1Image) {
+ this.pendingAttachments.push({
+ filePath: this.page1Image,
+ category: '1', // 鐭ユ儏鍚屾剰涔�
+ description: '鐭ユ儏鍚屾剰涔︾涓�椤�'
+ })
+ }
+ if (this.page2Image) {
+ this.pendingAttachments.push({
+ filePath: this.page2Image,
+ category: '1', // 鐭ユ儏鍚屾剰涔�
+ description: '鐭ユ儏鍚屾剰涔︾浜岄〉'
+ })
+ }
+
+ console.log('淇濆瓨寰呬笂浼犻檮浠�:', this.pendingAttachments)
+
+ // 鍏抽棴寮圭獥
+ this.closeMultiPhotoOCRPopup()
+
+ // 娓呯┖鍥剧墖鍜岀粨鏋滐紙浣嗕繚鐣� pendingAttachments锛�
+ this.page1Image = ''
+ this.page2Image = ''
+ this.page1Fields = {}
+ this.page2Fields = {}
+
+ this.$modal.showToast('宸插簲鐢ㄥ埌琛ㄥ崟锛岀煡鎯呭悓鎰忎功灏嗗湪浠诲姟鍒涘缓鍚庝笂浼�')
+ },
+
+ // 璇嗗埆鍗曚釜椤电爜鐨勫浘鐗囷紙閫夋嫨鍚庣珛鍗宠瘑鍒級
+ recognizeSinglePage(page) {
+ const image = page === 1 ? this.page1Image : this.page2Image
+
+ if (!image) {
+ return
+ }
+
+ // 鏄剧ず鍔犺浇鎻愮ず
+ uni.showLoading({
+ title: `璇嗗埆绗�${page}椤�...`
+ })
+
+ // 绗竴椤电殑itemNames
+ const page1ItemNames = [
+ "鎮h�呭鍚�", "鎬у埆", "骞撮緞", "韬唤璇佸彿", "璇婃柇",
+ "闇�鏀粯杞繍璐圭敤", "琛岀▼", "寮�濮嬫椂闂�", "缁撴潫鏃堕棿", "瀹跺睘绛惧悕"
+ ]
+
+ // 绗簩椤电殑itemNames
+ const page2ItemNames = [
+ "鎮h�呯鍚嶏紙鎵嬪嵃锛�", "绛惧瓧浜鸿韩浠借瘉鍙风爜", "绛惧瓧浜鸿韩浠借瘉鍙�", "鏃ユ湡",
+ "鑱旂郴鐢佃瘽", "鏈汉", "绛惧瓧浜轰笌鎮h�呭叧绯�"
+ ]
+
+ const itemNames = page === 1 ? page1ItemNames : page2ItemNames
+
+ // 璋冪敤鎵归噺OCR API锛堜紶鍏ュ崟涓浘鐗囷級
+ batchRecognizeImages([image], itemNames)
+ .then(result => {
+ uni.hideLoading()
+
+ if (result.success && result.successCount > 0) {
+ // 淇濆瓨璇嗗埆缁撴灉
+ if (page === 1) {
+ this.page1Fields = result.fields
+ } else {
+ this.page2Fields = result.fields
+ }
+
+ console.log(`绗�${page}椤佃瘑鍒粨鏋�:`, result.fields)
+ this.$modal.showToast(`绗�${page}椤佃瘑鍒垚鍔焋)
+ } else {
+ this.$modal.showToast(`绗�${page}椤佃瘑鍒け璐)
+ }
+ })
+ .catch(error => {
+ uni.hideLoading()
+ console.error(`绗�${page}椤礝CR璇嗗埆澶辫触:`, error)
+ this.$modal.showToast(error.msg || `绗�${page}椤佃瘑鍒け璐)
+ })
+ },
+
+ // 澶勭悊澶氬浘OCR璇嗗埆缁撴灉
+ processMultiOCRResult(fields) {
+ console.log('澶氬浘OCR璇嗗埆缁撴灉:', fields)
+
+ // 鎻愬彇鎮h�呭鍚�
+ if (fields['鎮h�呭鍚�']) {
+ this.taskForm.patient.name = fields['鎮h�呭鍚�'].trim()
+ }
+
+ // 鎻愬彇鑱旂郴浜猴紙浼樺厛鎮h�呯鍚嶏紝鍏舵瀹跺睘绛惧悕锛屾渶鍚庢湰浜猴級
+ if (fields['鎮h�呯鍚嶏紙鎵嬪嵃锛�']) {
+ this.taskForm.patient.contact = fields['鎮h�呯鍚嶏紙鎵嬪嵃锛�'].trim()
+ } else if (fields['瀹跺睘绛惧悕']) {
+ this.taskForm.patient.contact = fields['瀹跺睘绛惧悕'].trim()
+ } else if (fields['鏈汉']) {
+ this.taskForm.patient.contact = fields['鏈汉'].trim()
+ }
+
+ // 鎻愬彇鎬у埆
+ if (fields['鎬у埆']) {
+ const gender = fields['鎬у埆'].trim()
+ if (gender.includes('鐢�')) {
+ this.taskForm.patient.gender = 'male'
+ } else if (gender.includes('濂�')) {
+ this.taskForm.patient.gender = 'female'
+ }
+ }
+
+ // 鎻愬彇韬唤璇佸彿锛堜紭鍏堣韩浠借瘉鍙凤紝鍏舵绛惧瓧浜鸿韩浠借瘉鍙风爜锛�
+ const patientIdCard = fields['韬唤璇佸彿'] || fields['鎮h�呰韩浠借瘉鍙�']
+ const signerIdCard = fields['绛惧瓧浜鸿韩浠借瘉鍙风爜'] || fields['绛惧瓧浜鸿韩浠借瘉鍙�']
+
+ if (patientIdCard) {
+ this.taskForm.patient.idCard = patientIdCard.trim()
+ } else if (signerIdCard) {
+ this.taskForm.patient.idCard = signerIdCard.trim()
+ }
+
+ // 鎻愬彇鏃ユ湡锛堣浆杩愭椂闂达級
+ if (fields['鏃ユ湡']) {
+ const dateString = fields['鏃ユ湡'].trim()
+ const dateFormatted = this.formatDateString(dateString)
+ if (dateFormatted) {
+ this.taskForm.transferTime = dateFormatted
+ }
+ }
+
+ // 鎻愬彇鑱旂郴鐢佃瘽
+ if (fields['鑱旂郴鐢佃瘽']) {
+ this.taskForm.patient.phone = fields['鑱旂郴鐢佃瘽'].trim()
+ }
+
+ // 鎻愬彇闇�鏀粯杞繍璐圭敤锛堟垚浜や环锛�
+ if (fields['闇�鏀粯杞繍璐圭敤']) {
+ const priceText = fields['闇�鏀粯杞繍璐圭敤'].trim()
+ const priceNumber = priceText.match(/\d+(?:\.\d{1,2})?/)
+ if (priceNumber) {
+ this.taskForm.price = parseFloat(priceNumber[0]).toFixed(2)
+ }
+ }
+
+ // 鎻愬彇璇婃柇锛堢梾鎯咃級
+ if (fields['璇婃柇']) {
+ this.taskForm.patient.condition = fields['璇婃柇'].trim()
+ }
+
+ // 鎻愬彇琛岀▼锛堣浆鍑哄尰闄㈠拰杞叆鍖婚櫌锛�
+ if (fields['琛岀▼']) {
+ const route = fields['琛岀▼'].trim()
+ // 鎸�"-"鎴�"鈥�"鍒嗗壊琛岀▼
+ const hospitals = route.split(/[-鈥擼/).map(h => h.trim())
+ if (hospitals.length >= 2) {
+ // 绗竴涓槸杞嚭鍖婚櫌
+ this.taskForm.hospitalOut.name = hospitals[0]
+ // 绗簩涓槸杞叆鍖婚櫌
+ this.taskForm.hospitalIn.name = hospitals[1]
+
+ // 灏濊瘯浠庡尰闄㈠簱涓尮閰嶅苟琛ュ叏鍦板潃
+ Promise.all([
+ this.findHospitalByName(hospitals[0], 'out', false),
+ this.findHospitalByName(hospitals[1], 'in', false)
+ ]).then(([outHosp, inHosp]) => {
+ if (outHosp) {
+ this.taskForm.hospitalOut.id = outHosp.hospId
+ this.taskForm.hospitalOut.name = outHosp.hospName
+ if (outHosp.hospName !== '瀹朵腑') {
+ this.taskForm.hospitalOut.address = this.buildFullAddress(outHosp)
+ this.taskForm.hospitalOut.city = outHosp.hopsCity || ''
+ }
+ }
+ if (inHosp) {
+ this.taskForm.hospitalIn.id = inHosp.hospId
+ this.taskForm.hospitalIn.name = inHosp.hospName
+ if (inHosp.hospName !== '瀹朵腑') {
+ this.taskForm.hospitalIn.address = this.buildFullAddress(inHosp)
+ this.taskForm.hospitalIn.city = inHosp.hopsCity || ''
+ }
+ }
+
+ // 濡傛灉涓や釜鍖婚櫌鍦板潃閮芥湁锛岃嚜鍔ㄨ绠楄窛绂�
+ if (this.taskForm.hospitalOut.address && this.taskForm.hospitalIn.address &&
+ this.taskForm.hospitalOut.name !== '瀹朵腑' && this.taskForm.hospitalIn.name !== '瀹朵腑') {
+ this.calculateHospitalDistance()
+ }
+ })
+ }
+ }
+
+ console.log('澶氬浘OCR缁撴灉澶勭悊瀹屾垚锛岃〃鍗曟暟鎹洿鏂�')
+ },
+
+ // 鎵цOCR璇嗗埆
+ performOCR() {
+ if (!this.ocrImage) {
+ this.$modal.showToast('璇峰厛閫夋嫨鎴栨媿鎽勫浘鐗�')
+ return
+ }
+
+ this.ocrLoading = true
+
+ // 浣跨敤OCR API杩涜璇嗗埆
+ recognizeImage(this.ocrImage, 'HandWriting', 'tencent', DEFAULT_TRANSFER_ITEM_NAMES)
+ .then(response => {
+ const ocrResult = response.data.ocrResult
+ this.processOCRResult(ocrResult)
+ this.$modal.showToast('OCR璇嗗埆鎴愬姛')
+ })
+ .catch(error => {
+ console.error('OCR璇嗗埆澶辫触:', error)
+ this.$modal.showToast(`OCR璇嗗埆澶辫触: ${error.msg || '鏈煡閿欒'}`)
+ })
+ .finally(() => {
+ this.ocrLoading = false
+ this.closePhotoOCRPopup()
+ })
+ },
+
+ // 澶勭悊OCR璇嗗埆缁撴灉
+ processOCRResult(ocrResult) {
+ if (!ocrResult || !ocrResult.content) {
+ console.log('OCR璇嗗埆缁撴灉涓虹┖')
+ return
+ }
+
+ const content = ocrResult.content
+ console.log('OCR璇嗗埆鍐呭:', content)
+
+ // 鎻愬彇鎮h�呭鍚�
+ const patientNameMatch = content.match(/鎮h�呭鍚峓锛�:]?\s*([^\n锛�,銆傦紱;]+)/)
+ if (patientNameMatch && patientNameMatch[1]) {
+ this.taskForm.patient.name = patientNameMatch[1].trim()
+ }
+
+ // 鎻愬彇鎬у埆
+ const genderMatch = content.match(/鎬у埆[锛�:]?\s*([^\n锛�,銆傦紱;]+)/)
+ if (genderMatch && genderMatch[1]) {
+ const gender = genderMatch[1].trim()
+ if (gender.includes('鐢�')) {
+ this.taskForm.patient.gender = 'male'
+ } else if (gender.includes('濂�')) {
+ this.taskForm.patient.gender = 'female'
+ }
+ }
+
+ // 鎻愬彇韬唤璇佸彿
+ const idCardMatch = content.match(/韬唤璇佸彿[锛�:]?\s*([^\n锛�,銆傦紱;]+)/)
+ const signerIdMatch = content.match(/绛惧瓧浜鸿韩浠借瘉鍙风爜[锛�:]?\s*([^\n锛�,銆傦紱;]+)/)
+ if (idCardMatch && idCardMatch[1]) {
+ this.taskForm.patient.idCard = idCardMatch[1].trim()
+ } else if (signerIdMatch && signerIdMatch[1]) {
+ this.taskForm.patient.idCard = signerIdMatch[1].trim()
+ }
+
+ // 鎻愬彇鑱旂郴鐢佃瘽
+ const phoneMatch = content.match(/鑱旂郴鐢佃瘽[锛�:]?\s*([^\n锛�,銆傦紱;]+)/)
+ if (phoneMatch && phoneMatch[1]) {
+ this.taskForm.patient.phone = phoneMatch[1].trim()
+ }
+
+ // 鎻愬彇璇婃柇淇℃伅
+ const diagnosisMatch = content.match(/璇婃柇[锛�:]?\s*([^\n锛�,銆傦紱;]+)/)
+ if (diagnosisMatch && diagnosisMatch[1]) {
+ this.taskForm.patient.condition = diagnosisMatch[1].trim()
+ }
+
+ // 鎻愬彇闇�鏀粯杞繍璐圭敤锛堟垚浜や环锛�
+ const priceMatch = content.match(/闇�鏀粯杞繍璐圭敤[锛�:]?\s*([^\n锛�,銆傦紱;]+)/)
+ if (priceMatch && priceMatch[1]) {
+ // 鎻愬彇鏁板瓧閲戦
+ const priceNumber = priceMatch[1].match(/\d+(?:\.\d{1,2})?/)
+ if (priceNumber) {
+ this.taskForm.price = parseFloat(priceNumber[0]).toFixed(2)
+ }
+ }
+
+ // 鎻愬彇鏃ユ湡
+ const dateMatch = content.match(/鏃ユ湡[锛�:]?\s*([^\n锛�,銆傦紱;]+)/)
+ if (dateMatch && dateMatch[1]) {
+ const dateString = dateMatch[1].trim()
+ // 灏濊瘯瑙f瀽鏃ユ湡鏍煎紡
+ const dateFormatted = this.formatDateString(dateString)
+ if (dateFormatted) {
+ this.taskForm.transferTime = dateFormatted
+ }
+ }
+
+ // 鎻愬彇琛岀▼锛堣浆鍑哄尰闄㈠拰杞叆鍖婚櫌锛�
+ const routeMatch = content.match(/琛岀▼[锛�:]?\s*([^\n]+)/)
+ if (routeMatch && routeMatch[1]) {
+ const route = routeMatch[1].trim()
+ // 鎸�"-"鍒嗗壊琛岀▼锛岃幏鍙栬浆鍑哄拰杞叆鍖婚櫌
+ const hospitals = route.split(/[-鈥擼/).map(h => h.trim())
+ if (hospitals.length >= 2) {
+ // 绗竴涓槸杞嚭鍖婚櫌
+ this.taskForm.hospitalOut.name = hospitals[0]
+ // 绗簩涓槸杞叆鍖婚櫌
+ this.taskForm.hospitalIn.name = hospitals[1]
+ }
+ }
+
+ // 鎻愬彇瀹跺睘绛惧悕鎴栨湰浜轰綔涓鸿仈绯讳汉
+ const familyContactMatch = content.match(/(?:瀹跺睘绛惧悕|鏈汉)[锛�:]?\s*([^\n锛�,銆傦紱;]+)/)
+ if (familyContactMatch && familyContactMatch[1]) {
+ this.taskForm.patient.contact = familyContactMatch[1].trim()
+ }
+
+ // 鎻愬彇鎮h�呯鍚嶏紙鎵嬪嵃锛変綔涓鸿仈绯讳汉
+ const patientSignatureMatch = content.match(/鎮h�呯鍚嶏紙鎵嬪嵃锛塠锛�:]?\s*([^\n锛�,銆傦紱;]+)/)
+ if (patientSignatureMatch && patientSignatureMatch[1]) {
+ if (!this.taskForm.patient.contact) {
+ this.taskForm.patient.contact = patientSignatureMatch[1].trim()
+ }
+ }
+
+ console.log('OCR缁撴灉澶勭悊瀹屾垚锛岃〃鍗曟暟鎹洿鏂�')
+ },
+
+ // 鏍煎紡鍖栨棩鏈熷瓧绗︿覆锛堣繑鍥� yyyy-MM-dd HH:mm:ss 鏍煎紡锛�
+ formatDateString(dateStr) {
+ // 灏濊瘯涓嶅悓鐨勬棩鏈熸牸寮�
+ let cleaned = dateStr.replace(/[骞存湀]/g, '-').replace(/[鏃ュ彿]/g, '')
+
+ let dateResult = ''
+
+ // 濡傛灉鏄痀YMMDD鏍煎紡
+ if (/^\d{6}$/.test(cleaned)) {
+ const year = '20' + cleaned.substring(0, 2)
+ const month = cleaned.substring(2, 4)
+ const day = cleaned.substring(4, 6)
+ dateResult = `${year}-${month}-${day}`
+ }
+ // 濡傛灉鏄痀YYYMMDD鏍煎紡
+ else if (/^\d{8}$/.test(cleaned)) {
+ const year = cleaned.substring(0, 4)
+ const month = cleaned.substring(4, 6)
+ const day = cleaned.substring(6, 8)
+ dateResult = `${year}-${month}-${day}`
+ }
+ // 濡傛灉宸茬粡鏄悎鐞嗘牸寮忥紝鐩存帴浣跨敤
+ else if (cleaned.match(/^\d{4}[-/]\d{1,2}[-/]\d{1,2}$/)) {
+ dateResult = cleaned.replace(/[//]/g, '-')
+ }
+ // 濡傛灉宸茬粡鍖呭惈鏃跺垎绉掞紝鐩存帴杩斿洖
+ else if (cleaned.match(/^\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2}$/)) {
+ return cleaned.replace(/[//]/g, '-')
+ }
+ else {
+ dateResult = dateStr
+ }
+
+ // 濡傛灉鏃ユ湡鏍煎紡姝g‘锛屾坊鍔犻粯璁ゆ椂鍒嗙 00:00:00
+ if (dateResult && dateResult.match(/^\d{4}-\d{1,2}-\d{1,2}$/)) {
+ return dateResult + ' 00:00:00'
+ }
+
+ return dateResult
}
}
}
@@ -1459,7 +2213,9 @@
color: #333;
}
- .smart-parse-btn {
+ .smart-parse-btn,
+ .ocr-page-btn {
+ position: relative;
display: flex;
flex-direction: column;
align-items: center;
@@ -1468,7 +2224,53 @@
text {
font-size: 22rpx;
+ margin-top: 4rpx;
+ }
+
+ .badge {
+ position: absolute;
+ top: 0;
+ right: 10rpx;
+ min-width: 32rpx;
+ height: 32rpx;
+ line-height: 32rpx;
+ text-align: center;
+ background-color: #ff4d4f;
+ color: white;
+ font-size: 20rpx;
+ border-radius: 16rpx;
+ padding: 0 8rpx;
+ }
+ }
+
+ .smart-parse-btn {
+ text {
color: #007AFF;
+ }
+ }
+
+ .ocr-page-btn:first-of-type {
+ text {
+ color: #52c41a;
+ }
+ }
+
+ .ocr-page-btn:last-of-type {
+ text {
+ color: #FF6B00;
+ }
+ }
+
+ .multi-photo-ocr-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 10rpx 20rpx;
+
+ text {
+ font-size: 22rpx;
+ color: #FF6B00;
margin-top: 4rpx;
}
}
@@ -1836,4 +2638,493 @@
}
}
}
+
+// 鎷嶇収璇嗗埆寮圭獥鏍峰紡
+.photo-ocr-popup {
+ background-color: white;
+ border-radius: 20rpx 20rpx 0 0;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+
+ .popup-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 30rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+ flex-shrink: 0;
+
+ .popup-title {
+ display: flex;
+ align-items: center;
+ gap: 10rpx;
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+
+ text {
+ margin-left: 8rpx;
+ }
+ }
+
+ .popup-close {
+ padding: 10rpx;
+ }
+ }
+
+ .ocr-content {
+ flex: 1;
+ padding: 30rpx;
+ overflow-y: auto;
+
+ .ocr-tip {
+ display: flex;
+ align-items: flex-start;
+ padding: 20rpx;
+ background-color: #f0f7ff;
+ border-radius: 10rpx;
+ margin-bottom: 20rpx;
+
+ text {
+ flex: 1;
+ margin-left: 10rpx;
+ font-size: 24rpx;
+ color: #666;
+ line-height: 1.6;
+ }
+ }
+
+ .image-preview {
+ margin-bottom: 20rpx;
+ border: 1rpx solid #eee;
+ border-radius: 10rpx;
+ overflow: hidden;
+ }
+
+ .multi-image-preview {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15rpx;
+ margin-bottom: 20rpx;
+
+ .image-item {
+ position: relative;
+ width: 200rpx;
+ height: 200rpx;
+ border: 1rpx solid #eee;
+ border-radius: 10rpx;
+ overflow: hidden;
+
+ image {
+ width: 100%;
+ height: 100%;
+ }
+
+ .delete-btn {
+ position: absolute;
+ top: 5rpx;
+ right: 5rpx;
+ width: 40rpx;
+ height: 40rpx;
+ background-color: rgba(0, 0, 0, 0.6);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+ }
+
+ .ocr-actions {
+ display: flex;
+ gap: 20rpx;
+
+ button {
+ flex: 1;
+ height: 80rpx;
+ border-radius: 10rpx;
+ font-size: 28rpx;
+ }
+
+ .select-btn {
+ background-color: #f5f5f5;
+ color: #333;
+ }
+
+ .capture-btn {
+ background-color: #007AFF;
+ color: white;
+ }
+ }
+
+ .image-count {
+ text-align: center;
+ margin-top: 20rpx;
+ font-size: 26rpx;
+ color: #FF6B00;
+ font-weight: bold;
+ }
+
+ // 椤甸潰涓婁紶鍖哄煙
+ .page-upload-section {
+ background: linear-gradient(135deg, #f9f9f9 0%, #ffffff 100%);
+ border-radius: 15rpx;
+ padding: 25rpx;
+ margin-bottom: 25rpx;
+ border: 2rpx solid #f0f0f0;
+ transition: all 0.3s;
+
+ &.first-page {
+ border-left: 4rpx solid #52c41a;
+
+ .page-badge {
+ background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
+
+ .page-number {
+ color: #52c41a;
+ }
+ }
+ }
+
+ &.second-page {
+ border-left: 4rpx solid #FF6B00;
+
+ .page-badge {
+ background: linear-gradient(135deg, #FF6B00 0%, #ff8c3a 100%);
+
+ .page-number {
+ color: #FF6B00;
+ }
+ }
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20rpx;
+
+ .page-title {
+ display: flex;
+ align-items: center;
+ gap: 15rpx;
+ flex: 1;
+
+ .page-badge {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 60rpx;
+ height: 60rpx;
+ background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
+ border-radius: 50%;
+ position: relative;
+
+ .page-number {
+ position: absolute;
+ bottom: -2rpx;
+ right: -2rpx;
+ width: 24rpx;
+ height: 24rpx;
+ background-color: #fff;
+ border-radius: 50%;
+ font-size: 16rpx;
+ font-weight: bold;
+ color: #52c41a;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+ }
+ }
+
+ .page-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4rpx;
+
+ .page-main-title {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+ }
+
+ .page-sub-title {
+ font-size: 22rpx;
+ color: #999;
+ }
+ }
+ }
+
+ .upload-btn {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+
+ &.green {
+ border: 2rpx dashed #52c41a;
+ background-color: rgba(82, 196, 26, 0.05);
+ }
+
+ &.orange {
+ border: 2rpx dashed #FF6B00;
+ background-color: rgba(255, 107, 0, 0.05);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ opacity: 0.8;
+ }
+ }
+ }
+
+ // 璇嗗埆瀛楁鎻愮ず
+ .field-hint {
+ background-color: #fffbe6;
+ border: 1rpx solid #ffe58f;
+ border-radius: 8rpx;
+ padding: 15rpx;
+ margin-bottom: 15rpx;
+
+ text {
+ font-size: 22rpx;
+ color: #d48806;
+ line-height: 1.6;
+ }
+ }
+
+ .images-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10rpx;
+ margin-bottom: 15rpx;
+
+ .image-item {
+ position: relative;
+ width: 150rpx;
+ height: 150rpx;
+ border: 1rpx solid #eee;
+ border-radius: 10rpx;
+ overflow: hidden;
+
+ image {
+ width: 100%;
+ height: 100%;
+ }
+
+ .delete-btn {
+ position: absolute;
+ top: 5rpx;
+ right: 5rpx;
+ width: 35rpx;
+ height: 35rpx;
+ background-color: rgba(0, 0, 0, 0.6);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+ }
+
+ // 鍗曞浘棰勮鍖哄煙
+ .single-image-container {
+ position: relative;
+ width: 100%;
+ height: 400rpx;
+ border: 1rpx solid #eee;
+ border-radius: 10rpx;
+ overflow: hidden;
+ margin-bottom: 15rpx;
+ background-color: #f5f5f5;
+
+ .preview-image {
+ width: 100%;
+ height: 100%;
+ }
+
+ .delete-btn {
+ position: absolute;
+ top: 10rpx;
+ right: 10rpx;
+ width: 50rpx;
+ height: 50rpx;
+ background-color: rgba(0, 0, 0, 0.6);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2;
+
+ &:active {
+ transform: scale(0.95);
+ opacity: 0.9;
+ }
+ }
+
+ // 鍥剧墖鐘舵�佹爣绛�
+ .image-status {
+ position: absolute;
+ bottom: 10rpx;
+ left: 10rpx;
+ display: flex;
+ align-items: center;
+ gap: 5rpx;
+ padding: 8rpx 15rpx;
+ background-color: rgba(82, 196, 26, 0.9);
+ border-radius: 20rpx;
+ z-index: 2;
+
+ text {
+ font-size: 22rpx;
+ color: #fff;
+ font-weight: 500;
+ }
+ }
+ }
+
+ .recognition-result {
+ background-color: white;
+ border-radius: 10rpx;
+ padding: 15rpx;
+ border: 1rpx solid #e0e0e0;
+
+ .result-title {
+ display: flex;
+ align-items: center;
+ margin-bottom: 15rpx;
+ padding-bottom: 10rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ text {
+ margin-left: 8rpx;
+ font-size: 26rpx;
+ font-weight: bold;
+ color: #333;
+ }
+
+ .result-count {
+ margin-left: 8rpx;
+ font-size: 22rpx;
+ color: #999;
+ font-weight: normal;
+ }
+ }
+
+ .field-list {
+ .field-item {
+ display: flex;
+ padding: 10rpx 0;
+ border-bottom: 1rpx solid #f9f9f9;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .field-name {
+ min-width: 150rpx;
+ font-size: 24rpx;
+ color: #666;
+ font-weight: 500;
+
+ &::after {
+ content: ':';
+ margin-left: 4rpx;
+ }
+ }
+
+ .field-value {
+ flex: 1;
+ font-size: 24rpx;
+ color: #333;
+ word-break: break-all;
+ }
+ }
+ }
+ }
+ }
+
+ .page-indicator {
+ display: flex;
+ justify-content: center;
+ gap: 30rpx;
+ margin-top: 30rpx;
+
+ .page-dot {
+ padding: 15rpx 30rpx;
+ border: 2rpx solid #ddd;
+ border-radius: 30rpx;
+ font-size: 26rpx;
+ color: #999;
+ background-color: #f5f5f5;
+ transition: all 0.3s;
+
+ &.active {
+ border-color: #007AFF;
+ background-color: #007AFF;
+ color: white;
+ }
+
+ &.completed {
+ border-color: #52c41a;
+ color: #52c41a;
+
+ &.active {
+ background-color: #007AFF;
+ border-color: #007AFF;
+ color: white;
+ }
+ }
+ }
+ }
+ }
+
+ .popup-footer {
+ display: flex;
+ padding: 20rpx 30rpx;
+ border-top: 1rpx solid #f0f0f0;
+ gap: 20rpx;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+
+ button {
+ flex: 1;
+ min-width: 160rpx;
+ height: 80rpx;
+ border-radius: 10rpx;
+ font-size: 30rpx;
+ }
+
+ .cancel-btn {
+ background-color: #f5f5f5;
+ color: #666;
+ }
+
+ .next-btn,
+ .prev-btn {
+ background-color: #52c41a;
+ color: white;
+ }
+
+ .confirm-btn {
+ background-color: #007AFF;
+ color: white;
+
+ &[disabled] {
+ background-color: #ccc;
+ color: #999;
+ }
+ }
+ }
+}
</style>
diff --git a/app/pagesTask/detail.vue b/app/pagesTask/detail.vue
index fa8e0c4..38a10c8 100644
--- a/app/pagesTask/detail.vue
+++ b/app/pagesTask/detail.vue
@@ -479,7 +479,7 @@
</button>
</template>
- <!-- 鍑哄彂涓姸鎬�: 鏄剧ず宸插埌杈俱�佸己鍒剁粨鏉� -->
+ <!-- 鍑哄彂涓姸鎬�: 鏄剧ず宸插埌杈俱�佸己鍒剁粨鏉熴�佸己鍒跺畬鎴� -->
<template v-else-if="taskDetail.taskStatus === 'DEPARTING'">
<template v-if="canOperateTask()">
<button
@@ -493,6 +493,13 @@
@click="handleTaskAction('forceCancel')"
>
寮哄埗缁撴潫
+ </button>
+ <button
+ v-if="showForceCompleteFeature()"
+ class="action-btn force-complete"
+ @click="showForceCompleteTimeDialog()"
+ >
+ 寮哄埗瀹屾垚
</button>
</template>
</template>
@@ -2234,10 +2241,13 @@
flex: 1;
height: 80rpx;
border-radius: 10rpx;
- font-size: 30rpx;
+ font-size: 28rpx;
margin: 0 10rpx;
background-color: #f0f0f0;
color: #333;
+ white-space: nowrap;
+ padding: 0 10rpx;
+ min-width: 0;
&.edit {
background-color: #ff9500;
@@ -2254,6 +2264,11 @@
color: white;
}
+ &.force-end {
+ background-color: #ff6b22;
+ color: white;
+ }
+
&.settlement {
background-color: #34C759;
color: white;
diff --git "a/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-README.md" "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-README.md"
new file mode 100644
index 0000000..b70b7b2
--- /dev/null
+++ "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-README.md"
@@ -0,0 +1,267 @@
+# 杞﹁締寮傚父杩愯鐩戞帶鍛婅绯荤粺
+
+## 馃幆 鍔熻兘姒傝堪
+
+鏈郴缁熷疄鐜颁簡瀹屾暣鐨勮溅杈嗗紓甯歌繍琛岀洃鎺у憡璀﹀姛鑳斤紝鐢ㄤ簬鐩戞帶鏃犱换鍔$姸鎬佷笅杞﹁締鐨勫紓甯歌繍琛屾儏鍐碉紝骞堕�氳繃浼佷笟寰俊/灏忕▼搴忓強鏃跺憡璀﹂�氱煡鐩稿叧璐熻矗浜恒��
+
+## 鉁� 鏍稿績鐗规��
+
+- 鉁� **鏅鸿兘鐩戞帶**: 瀹炴椂鐩戞帶鎵�鏈夎溅杈嗚繍琛岀姸鎬侊紝鍩轰簬GPS鍒嗘閲岀▼绮惧噯璁$畻
+- 鉁� **鐏垫椿閰嶇疆**: 鏀寔鍏ㄥ眬/閮ㄩ棬/杞﹁締涓夌骇閰嶇疆绛栫暐锛岄厤缃紭鍏堢骇鑷姩搴旂敤
+- 鉁� **棰戠巼鎺у埗**: 姣忔棩鍛婅娆℃暟闄愬埗 + 鍛婅闂撮殧鏃堕棿鎺у埗锛岄伩鍏嶉绻侀獨鎵�
+- 鉁� **鍙婃椂閫氱煡**: 浼佷笟寰俊娑堟伅鎺ㄩ�侊紝鏀寔鍙厤缃�氱煡鐢ㄦ埛鍒楄〃
+- 鉁� **瀹屽杽绠$悊**: 鍛婅璁板綍鍒楄〃绠$悊銆佹壒閲忓鐞嗐�佹暟鎹粺璁°�佸鍑哄姛鑳�
+
+## 馃搳 鎶�鏈灦鏋�
+
+### 鍚庣
+- Spring Boot 2.x
+- MyBatis
+- Quartz (瀹氭椂浠诲姟)
+- MySQL 5.7+
+- 浼佷笟寰俊API
+
+### 鍓嶇
+- Vue 2.x
+- Element UI
+- Axios
+
+## 馃搧 椤圭洰缁撴瀯
+
+```
+.
+鈹溾攢鈹� doc/ # 鏂囨。鐩綍
+鈹� 鈹溾攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘璇存槑.md # 鍔熻兘璇存槑
+鈹� 鈹溾攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅-蹇�熼儴缃叉寚鍗�.md # 鍚庣閮ㄧ讲
+鈹� 鈹溾攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅-鍓嶇閮ㄧ讲鎸囧崡.md # 鍓嶇閮ㄧ讲
+鈹� 鈹溾攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅-瀹屾暣瀹炵幇鎬荤粨.md # 瀹炵幇鎬荤粨
+鈹� 鈹斺攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅-README.md # 鏈枃妗�
+鈹�
+鈹溾攢鈹� sql/
+鈹� 鈹斺攢鈹� vehicle_abnormal_alert.sql # 鏁版嵁搴撳垵濮嬪寲鑴氭湰
+鈹�
+鈹溾攢鈹� ruoyi-system/ # 鍚庣鏍稿績妯″潡
+鈹� 鈹溾攢鈹� src/main/java/com/ruoyi/system/
+鈹� 鈹� 鈹溾攢鈹� domain/ # 瀹炰綋绫�
+鈹� 鈹� 鈹� 鈹溾攢鈹� VehicleAbnormalAlert.java # 鍛婅璁板綍瀹炰綋
+鈹� 鈹� 鈹� 鈹斺攢鈹� VehicleAlertConfig.java # 鍛婅閰嶇疆瀹炰綋
+鈹� 鈹� 鈹溾攢鈹� mapper/ # Mapper鎺ュ彛
+鈹� 鈹� 鈹� 鈹溾攢鈹� VehicleAbnormalAlertMapper.java
+鈹� 鈹� 鈹� 鈹斺攢鈹� VehicleAlertConfigMapper.java
+鈹� 鈹� 鈹斺攢鈹� service/ # Service灞�
+鈹� 鈹� 鈹溾攢鈹� IVehicleAbnormalAlertService.java
+鈹� 鈹� 鈹溾攢鈹� IVehicleAlertConfigService.java
+鈹� 鈹� 鈹斺攢鈹� impl/
+鈹� 鈹� 鈹溾攢鈹� VehicleAbnormalAlertServiceImpl.java
+鈹� 鈹� 鈹斺攢鈹� VehicleAlertConfigServiceImpl.java
+鈹� 鈹斺攢鈹� src/main/resources/mapper/system/
+鈹� 鈹溾攢鈹� VehicleAbnormalAlertMapper.xml
+鈹� 鈹斺攢鈹� VehicleAlertConfigMapper.xml
+鈹�
+鈹溾攢鈹� ruoyi-admin/ # Controller灞�
+鈹� 鈹斺攢鈹� src/main/java/com/ruoyi/web/controller/system/
+鈹� 鈹溾攢鈹� VehicleAbnormalAlertController.java
+鈹� 鈹斺攢鈹� VehicleAlertConfigController.java
+鈹�
+鈹溾攢鈹� ruoyi-quartz/ # 瀹氭椂浠诲姟
+鈹� 鈹斺攢鈹� src/main/java/com/ruoyi/quartz/task/
+鈹� 鈹斺攢鈹� VehicleAbnormalAlertTask.java # 鐩戞帶瀹氭椂浠诲姟
+鈹�
+鈹斺攢鈹� ruoyi-ui/ # 鍓嶇椤圭洰
+ 鈹溾攢鈹� src/api/system/ # API鎺ュ彛
+ 鈹� 鈹溾攢鈹� vehicleAlert.js
+ 鈹� 鈹溾攢鈹� vehicleAlertConfig.js
+ 鈹� 鈹斺攢鈹� vehicle.js
+ 鈹斺攢鈹� src/views/system/ # 椤甸潰缁勪欢
+ 鈹溾攢鈹� vehicleAlert/
+ 鈹� 鈹斺攢鈹� index.vue # 鍛婅璁板綍鍒楄〃
+ 鈹斺攢鈹� vehicleAlertConfig/
+ 鈹斺攢鈹� index.vue # 鍛婅閰嶇疆绠$悊
+```
+
+## 馃殌 蹇�熷紑濮�
+
+### 1. 鏁版嵁搴撳垵濮嬪寲
+
+```bash
+mysql -u root -p database_name < sql/vehicle_abnormal_alert.sql
+```
+
+### 2. 閰嶇疆绯荤粺鍙傛暟
+
+鐧诲綍鍚庡彴绯荤粺锛岃繘鍏� **绯荤粺绠$悊 > 鍙傛暟璁剧疆**锛岀‘璁や互涓嬪弬鏁帮細
+
+| 鍙傛暟閿悕 | 榛樿鍊� | 璇存槑 |
+|---------|--------|------|
+| vehicle.alert.enabled | true | 鍔熻兘鎬诲紑鍏� |
+| vehicle.alert.mileage.threshold | 10 | 鍏噷鏁伴槇鍊�(km) |
+| vehicle.alert.daily.limit | 5 | 姣忔棩鍛婅娆℃暟 |
+| vehicle.alert.interval.minutes | 5 | 鍛婅闂撮殧(鍒嗛挓) |
+| vehicle.alert.time.window | 10 | 鐩戞帶鏃堕棿绐楀彛(鍒嗛挓) |
+| vehicle.alert.notify.users | 1 | 榛樿閫氱煡鐢ㄦ埛ID |
+
+### 3. 鍚姩瀹氭椂浠诲姟
+
+杩涘叆 **绯荤粺鐩戞帶 > 瀹氭椂浠诲姟**锛屾壘鍒�"杞﹁締寮傚父杩愯鐩戞帶"浠诲姟锛岀偣鍑诲惎鍔ㄣ��
+
+### 4. 閰嶇疆鑿滃崟鏉冮檺
+
+杩涘叆 **绯荤粺绠$悊 > 鑿滃崟绠$悊**锛屾坊鍔犱互涓嬭彍鍗曞苟鍒嗛厤鏉冮檺锛�
+- 杞﹁締寮傚父鍛婅 (`/system/vehicleAlert`)
+- 鍛婅閰嶇疆绠$悊 (`/system/vehicleAlertConfig`)
+
+### 5. 鍒涘缓鍛婅閰嶇疆
+
+杩涘叆 **杞﹁締鐩戞帶 > 鍛婅閰嶇疆绠$悊**锛屽垱寤哄叏灞�閰嶇疆鎴栭拡瀵圭壒瀹氶儴闂�/杞﹁締鐨勯厤缃��
+
+## 馃摉 浣跨敤鎸囧崡
+
+### 鍛婅閰嶇疆浼樺厛绾�
+
+绯荤粺鏀寔涓夌骇閰嶇疆绛栫暐锛屾寜浠ヤ笅浼樺厛绾у簲鐢細
+
+```
+杞﹁締閰嶇疆 (鏈�楂樹紭鍏堢骇)
+ 鈫� 涓嶅瓨鍦ㄦ椂
+閮ㄩ棬閰嶇疆
+ 鈫� 涓嶅瓨鍦ㄦ椂
+鍏ㄥ眬閰嶇疆 (榛樿閰嶇疆)
+```
+
+**閰嶇疆绀轰緥**锛�
+1. 鍒涘缓鍏ㄥ眬閰嶇疆锛氶槇鍊�10km锛屾瘡鏃�5娆★紝闂撮殧5鍒嗛挓
+2. 涓�"鍒嗗叕鍙窤"鍒涘缓閮ㄩ棬閰嶇疆锛氶槇鍊�8km
+3. 涓�"杞﹁締A001"鍒涘缓杞﹁締閰嶇疆锛氶槇鍊�12km
+
+**鐢熸晥缁撴灉**锛�
+- 杞﹁締A001浣跨敤12km闃堝��
+- 鍒嗗叕鍙窤鐨勫叾浠栬溅杈嗕娇鐢�8km闃堝��
+- 鍏朵粬杞﹁締浣跨敤10km闃堝��
+
+### 鍛婅澶勭悊娴佺▼
+
+1. **鏌ョ湅鍛婅**锛氳繘鍏�"杞﹁締寮傚父鍛婅"椤甸潰
+2. **绛涢�夊憡璀�**锛氫娇鐢ㄦ悳绱㈡潯浠剁瓫閫夐渶瑕佸鐞嗙殑鍛婅
+3. **鏌ョ湅璇︽儏**锛氱偣鍑�"璇︽儏"鎸夐挳鏌ョ湅鍛婅瀹屾暣淇℃伅
+4. **澶勭悊鍛婅**锛�
+ - 鍗曟潯澶勭悊锛氱偣鍑�"澶勭悊"鎸夐挳锛屽~鍐欏鐞嗗娉�
+ - 鎵归噺澶勭悊锛氬嬀閫夊鏉¤褰曪紝鐐瑰嚮"鎵归噺澶勭悊"
+5. **瀵煎嚭鏁版嵁**锛氶渶瑕佹椂鍙鍑哄憡璀﹁褰曚负Excel
+
+## 馃搳 鏁版嵁缁熻
+
+鍛婅鍒楄〃椤甸潰鎻愪緵瀹炴椂缁熻锛�
+- **鏈鐞嗗憡璀�**锛氱孩鑹叉樉绀猴紝闇�瑕佷紭鍏堝鐞�
+- **浠婃棩鍛婅**锛氬綋澶╀骇鐢熺殑鍛婅鏁伴噺
+- **绱鍛婅杞﹁締**锛氬巻鍙蹭骇鐢熻繃鍛婅鐨勮溅杈嗘暟
+- **绱鍛婅娆℃暟**锛氭�诲憡璀﹁褰曟暟
+
+## 馃敡 绯荤粺閰嶇疆
+
+### 瀹氭椂浠诲姟閰嶇疆
+
+**鎵ц棰戠巼**锛氭瘡5鍒嗛挓鎵ц涓�娆�
+**Cron琛ㄨ揪寮�**锛歚0 0/5 * * * ?`
+**浠诲姟鏂规硶**锛歚vehicleAbnormalAlertTask.monitorVehicleAbnormalRunning`
+
+鍙牴鎹疄闄呴渶姹傝皟鏁存墽琛岄鐜囷紝寤鸿鑼冨洿锛�
+- 鏈�鐭細1鍒嗛挓 (`0 0/1 * * * ?`)
+- 鎺ㄨ崘锛�5鍒嗛挓 (`0 0/5 * * * ?`)
+- 鏈�闀匡細30鍒嗛挓 (`0 0/30 * * * ?`)
+
+### 閫氱煡閰嶇疆
+
+**閫氱煡鏂瑰紡**锛氫紒涓氬井淇℃秷鎭帹閫�
+**閫氱煡鍐呭**锛氳溅杈嗕俊鎭� + 杩愯閲岀▼ + 鏃堕棿鑼冨洿
+**閫氱煡鐢ㄦ埛**锛�
+1. 浼樺厛浣跨敤閰嶇疆琛ㄤ腑鐨勯�氱煡鐢ㄦ埛鍒楄〃
+2. 鍏舵鏍规嵁杞﹁締褰掑睘閮ㄩ棬鏌ユ壘璐熻矗浜�
+3. 鏈�鍚庝娇鐢ㄧ郴缁熷弬鏁颁腑鐨勯粯璁ょ敤鎴�
+
+## 馃搱 鐩戞帶鎸囨爣
+
+绯荤粺杩愯鐩戞帶寤鸿鍏虫敞浠ヤ笅鎸囨爣锛�
+
+- **浠诲姟鎵ц鏃堕棿**锛氬缓璁� < 30绉掞紙100杈嗚溅锛�
+- **鍛婅鍝嶅簲鏃堕棿**锛氫粠瑙﹀彂鍒伴�氱煡閫佽揪 < 1鍒嗛挓
+- **閫氱煡鎴愬姛鐜�**锛�> 95%
+- **璇姤鐜�**锛�< 5%
+
+## 鈿狅笍 娉ㄦ剰浜嬮」
+
+1. **GPS鏁版嵁渚濊禆**锛氱郴缁熶緷璧朑PS鍒嗘閲岀▼鏁版嵁锛岀‘淇滸PS璁惧姝e父宸ヤ綔
+2. **浠诲姟鐘舵�佸噯纭�**锛氬強鏃舵洿鏂颁换鍔$姸鎬侊紝閬垮厤璇垽
+3. **閰嶇疆鍚堢悊鎬�**锛氭牴鎹疄闄呬笟鍔″満鏅皟鏁撮槇鍊煎拰棰戠巼
+4. **閫氱煡鐢ㄦ埛鏈夋晥**锛氬畾鏈熸鏌ラ�氱煡鐢ㄦ埛鍒楄〃鏄惁鏈夋晥
+5. **鏁版嵁瀹氭湡娓呯悊**锛氬缓璁畾鏈熷綊妗f垨鍒犻櫎鍘嗗彶鍛婅鏁版嵁
+
+## 馃悰 鏁呴殰鎺掓煡
+
+### 鍛婅鏈骇鐢�
+
+**妫�鏌ユ竻鍗�**锛�
+- [ ] 鍔熻兘寮�鍏虫槸鍚﹀惎鐢�
+- [ ] 瀹氭椂浠诲姟鏄惁鍚姩
+- [ ] 杞﹁締鏄惁鏈塆PS鏁版嵁
+- [ ] 閲岀▼鏄惁瓒呰繃闃堝��
+- [ ] 鏄惁宸茶揪鍒伴鐜囬檺鍒�
+
+### 閫氱煡鏈彂閫�
+
+**妫�鏌ユ竻鍗�**锛�
+- [ ] 浼佷笟寰俊鏈嶅姟鏄惁鍚敤
+- [ ] 閫氱煡鐢ㄦ埛ID鏄惁閰嶇疆
+- [ ] 鐢ㄦ埛ID鏄惁鏈夋晥
+- [ ] 浼佷笟寰俊搴旂敤閰嶇疆鏄惁姝g‘
+- [ ] 缃戠粶杩炴帴鏄惁姝e父
+
+### 鎬ц兘闂
+
+**浼樺寲寤鸿**锛�
+- 閫傚綋澧炲姞瀹氭椂浠诲姟鎵ц闂撮殧
+- 涓烘暟鎹簱琛ㄦ坊鍔犵储寮�
+- 瀹氭湡娓呯悊鍘嗗彶鏁版嵁
+- 鑰冭檻浣跨敤缂撳瓨
+
+## 馃摓 鎶�鏈敮鎸�
+
+### 鏂囨。閾炬帴
+
+- [鍔熻兘璇存槑鏂囨。](./杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘璇存槑.md) - 璇︾粏鍔熻兘浠嬬粛
+- [蹇�熼儴缃叉寚鍗梋(./杞﹁締寮傚父杩愯鐩戞帶鍛婅-蹇�熼儴缃叉寚鍗�.md) - 鍚庣閮ㄧ讲姝ラ
+- [鍓嶇閮ㄧ讲鎸囧崡](./杞﹁締寮傚父杩愯鐩戞帶鍛婅-鍓嶇閮ㄧ讲鎸囧崡.md) - 鍓嶇閮ㄧ讲姝ラ
+- [瀹屾暣瀹炵幇鎬荤粨](./杞﹁締寮傚父杩愯鐩戞帶鍛婅-瀹屾暣瀹炵幇鎬荤粨.md) - 鎶�鏈疄鐜扮粏鑺�
+
+### 甯歌闂
+
+璇﹁鍚勯儴缃叉寚鍗楃殑"甯歌闂"绔犺妭
+
+## 馃摑 鏇存柊鏃ュ織
+
+### v1.0.0 (2026-01-12)
+
+**鍒濆鐗堟湰鍙戝竷**
+
+- 鉁� 瀹屾暣瀹炵幇杞﹁締寮傚父杩愯鐩戞帶鍔熻兘
+- 鉁� 涓夌骇閰嶇疆绛栫暐鏀寔
+- 鉁� 鍛婅璁板綍绠$悊
+- 鉁� 鍛婅閰嶇疆绠$悊
+- 鉁� 浼佷笟寰俊閫氱煡闆嗘垚
+- 鉁� 鍓嶇绠$悊椤甸潰
+- 鉁� 瀹屾暣鏂囨。浣撶郴
+
+**浠g爜缁熻**锛�
+- SQL鑴氭湰锛�1涓枃浠讹紝123琛�
+- Java浠g爜锛�11涓枃浠讹紝1,785琛�
+- Vue浠g爜锛�5涓枃浠讹紝1,151琛�
+- 鏂囨。锛�5涓枃浠讹紝1,573+琛�
+
+## 馃搫 璁稿彲璇�
+
+鏈」鐩伒寰� RuoYi 妗嗘灦鐨勮鍙瘉鍗忚
+
+---
+
+**寮�鍙戞椂闂�**锛�2026-01-12
+**椤圭洰鐘舵��**锛氣渽 宸插畬鎴�
+**缁存姢鍥㈤槦**锛欰I寮�鍙戝姪鎵�
diff --git "a/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\211\215\347\253\257\351\203\250\347\275\262\346\214\207\345\215\227.md" "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\211\215\347\253\257\351\203\250\347\275\262\346\214\207\345\215\227.md"
new file mode 100644
index 0000000..af6df3f
--- /dev/null
+++ "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\211\215\347\253\257\351\203\250\347\275\262\346\214\207\345\215\227.md"
@@ -0,0 +1,357 @@
+# 杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘 - 鍓嶇閮ㄧ讲鎸囧崡
+
+## 馃搵 姒傝堪
+
+鏈枃妗f彁渚涜溅杈嗗紓甯歌繍琛岀洃鎺у憡璀﹀姛鑳藉墠绔儴鍒嗙殑瀹屾暣閮ㄧ讲鎸囧崡锛屽寘鎷枃浠舵竻鍗曘�侀厤缃楠ゅ拰娴嬭瘯楠岃瘉銆�
+
+## 馃搧 鍓嶇鏂囦欢娓呭崟
+
+### 1. API鎺ュ彛鏂囦欢
+
+**璺緞**: `ruoyi-ui/src/api/system/`
+
+- 鉁� `vehicleAlert.js` - 鍛婅璁板綍绠$悊API
+- 鉁� `vehicleAlertConfig.js` - 鍛婅閰嶇疆绠$悊API
+- 鉁� `vehicle.js` - 杞﹁締淇℃伅API
+
+### 2. 椤甸潰缁勪欢鏂囦欢
+
+**璺緞**: `ruoyi-ui/src/views/system/`
+
+- 鉁� `vehicleAlert/index.vue` - 鍛婅璁板綍鍒楄〃椤甸潰锛�529琛岋級
+- 鉁� `vehicleAlertConfig/index.vue` - 鍛婅閰嶇疆绠$悊椤甸潰锛�487琛岋級
+
+### 3. 鍚庣鎺ュ彛鏂囦欢锛堥厤缃鐞嗘柊澧烇級
+
+**璺緞**: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/`
+
+- 鉁� `VehicleAlertConfigController.java` - 閰嶇疆绠$悊Controller
+
+**璺緞**: `ruoyi-system/src/main/java/com/ruoyi/system/`
+
+- 鉁� `service/IVehicleAlertConfigService.java` - 閰嶇疆鏈嶅姟鎺ュ彛
+- 鉁� `service/impl/VehicleAlertConfigServiceImpl.java` - 閰嶇疆鏈嶅姟瀹炵幇
+- 鉁� `mapper/VehicleAlertConfigMapper.java` - 閰嶇疆Mapper鎺ュ彛
+- 鉁� `domain/VehicleAlertConfig.java` - 閰嶇疆瀹炰綋绫伙紙宸叉洿鏂帮級
+
+**璺緞**: `ruoyi-system/src/main/resources/mapper/system/`
+
+- 鉁� `VehicleAlertConfigMapper.xml` - 閰嶇疆Mapper XML
+
+## 馃殌 閮ㄧ讲姝ラ
+
+### 绗竴姝ワ細纭鏁版嵁搴撳凡鍒濆鍖�
+
+纭繚宸叉墽琛孲QL鍒濆鍖栬剼鏈細
+
+```bash
+sql/vehicle_abnormal_alert.sql
+```
+
+璇ヨ剼鏈細鍒涘缓锛�
+- 鉁� `tb_vehicle_abnormal_alert` - 鍛婅璁板綍琛�
+- 鉁� `tb_vehicle_alert_config` - 鍛婅閰嶇疆琛�
+- 鉁� 6涓郴缁熼厤缃弬鏁�
+- 鉁� 瀹氭椂浠诲姟璁板綍
+- 鉁� 鑿滃崟鏉冮檺璁板綍锛�2涓彍鍗曪級
+
+### 绗簩姝ワ細閰嶇疆璺敱
+
+鍦� `ruoyi-ui/src/router/index.js` 涓坊鍔犺矾鐢憋紙濡傛灉浣跨敤鍔ㄦ�佽矾鐢憋紝鍙烦杩囨姝ラ锛夛細
+
+```javascript
+// 杞﹁締寮傚父鍛婅绠$悊
+{
+ path: '/system/vehicleAlert',
+ component: Layout,
+ hidden: true,
+ permissions: ['system:vehicleAlert:view'],
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/system/vehicleAlert/index'),
+ name: 'VehicleAlert',
+ meta: { title: '杞﹁締寮傚父鍛婅', activeMenu: '/system/vehicleAlert' }
+ }
+ ]
+},
+// 杞﹁締鍛婅閰嶇疆
+{
+ path: '/system/vehicleAlertConfig',
+ component: Layout,
+ hidden: true,
+ permissions: ['system:vehicleAlertConfig:view'],
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/system/vehicleAlertConfig/index'),
+ name: 'VehicleAlertConfig',
+ meta: { title: '杞﹁締鍛婅閰嶇疆', activeMenu: '/system/vehicleAlertConfig' }
+ }
+ ]
+}
+```
+
+### 绗笁姝ワ細閰嶇疆鑿滃崟鏉冮檺
+
+鐧诲綍鍚庡彴绠$悊绯荤粺锛岃繘鍏� **绯荤粺绠$悊 > 鑿滃崟绠$悊**锛屾坊鍔犱互涓嬭彍鍗曪細
+
+#### 鐖惰彍鍗曪細杞﹁締鐩戞帶锛堝彲閫夛紝濡傚凡鏈夎溅杈嗙浉鍏宠彍鍗曪紝鍙坊鍔犲埌鐜版湁鑿滃崟涓嬶級
+
+| 瀛楁 | 鍊� |
+|------|-----|
+| 鑿滃崟鍚嶇О | 杞﹁締鐩戞帶 |
+| 鑿滃崟绫诲瀷 | 鐩綍 |
+| 鑿滃崟鍥炬爣 | monitor |
+| 鏄剧ず鎺掑簭 | 5 |
+| 璺敱鍦板潃 | vehicle-monitor |
+| 缁勪欢璺緞 | Layout |
+
+#### 瀛愯彍鍗�1锛氳溅杈嗗紓甯稿憡璀�
+
+| 瀛楁 | 鍊� |
+|------|-----|
+| 涓婄骇鑿滃崟 | 杞﹁締鐩戞帶 |
+| 鑿滃崟鍚嶇О | 杞﹁締寮傚父鍛婅 |
+| 鑿滃崟绫诲瀷 | 鑿滃崟 |
+| 鑿滃崟鍥炬爣 | warning |
+| 鏄剧ず鎺掑簭 | 1 |
+| 璺敱鍦板潃 | vehicleAlert |
+| 缁勪欢璺緞 | system/vehicleAlert/index |
+| 鏉冮檺鏍囪瘑 | system:vehicleAlert:list |
+
+**鍔熻兘鎸夐挳鏉冮檺**锛�
+- `system:vehicleAlert:query` - 鏌ヨ
+- `system:vehicleAlert:handle` - 澶勭悊
+- `system:vehicleAlert:remove` - 鍒犻櫎
+- `system:vehicleAlert:export` - 瀵煎嚭
+
+#### 瀛愯彍鍗�2锛氬憡璀﹂厤缃鐞�
+
+| 瀛楁 | 鍊� |
+|------|-----|
+| 涓婄骇鑿滃崟 | 杞﹁締鐩戞帶 |
+| 鑿滃崟鍚嶇О | 鍛婅閰嶇疆绠$悊 |
+| 鑿滃崟绫诲瀷 | 鑿滃崟 |
+| 鑿滃崟鍥炬爣 | edit |
+| 鏄剧ず鎺掑簭 | 2 |
+| 璺敱鍦板潃 | vehicleAlertConfig |
+| 缁勪欢璺緞 | system/vehicleAlertConfig/index |
+| 鏉冮檺鏍囪瘑 | system:vehicleAlertConfig:list |
+
+**鍔熻兘鎸夐挳鏉冮檺**锛�
+- `system:vehicleAlertConfig:query` - 鏌ヨ
+- `system:vehicleAlertConfig:add` - 鏂板
+- `system:vehicleAlertConfig:edit` - 淇敼
+- `system:vehicleAlertConfig:remove` - 鍒犻櫎
+- `system:vehicleAlertConfig:export` - 瀵煎嚭
+
+### 绗洓姝ワ細缂栬瘧鍓嶇椤圭洰
+
+```bash
+cd ruoyi-ui
+npm install
+npm run build:prod
+```
+
+### 绗簲姝ワ細閮ㄧ讲鍒癗ginx
+
+灏嗙紪璇戝悗鐨勬枃浠堕儴缃插埌Nginx锛�
+
+```bash
+# 澶嶅埗dist鐩綍鍒皀ginx
+cp -r dist/* /usr/share/nginx/html/
+
+# 閲嶅惎nginx
+nginx -s reload
+```
+
+## 馃И 鍔熻兘娴嬭瘯
+
+### 1. 鍛婅璁板綍鍒楄〃娴嬭瘯
+
+璁块棶锛歚绯荤粺绠$悊 > 杞﹁締鐩戞帶 > 杞﹁締寮傚父鍛婅`
+
+**娴嬭瘯鐐�**锛�
+- 鉁� 鍒楄〃鏁版嵁姝e父鏄剧ず
+- 鉁� 鎼滅储鍔熻兘锛堣溅鐗屽彿銆佹棩鏈熴�佺姸鎬併�侀儴闂級
+- 鉁� 缁熻鍗$墖鏄剧ず锛堟湭澶勭悊銆佷粖鏃ャ�佺疮璁¤溅杈嗐�佺疮璁℃鏁帮級
+- 鉁� 鏌ョ湅璇︽儏鍔熻兘
+- 鉁� 澶勭悊鍗曟潯鍛婅
+- 鉁� 鎵归噺澶勭悊鍛婅
+- 鉁� 鍒犻櫎鍔熻兘
+- 鉁� 瀵煎嚭鍔熻兘
+- 鉁� 鍒嗛〉鍔熻兘
+
+### 2. 鍛婅閰嶇疆绠$悊娴嬭瘯
+
+璁块棶锛歚绯荤粺绠$悊 > 杞﹁締鐩戞帶 > 鍛婅閰嶇疆绠$悊`
+
+**娴嬭瘯鐐�**锛�
+- 鉁� 閰嶇疆鍒楄〃鏄剧ず锛堝叏灞�/閮ㄩ棬/杞﹁締锛�
+- 鉁� 鏂板鍏ㄥ眬閰嶇疆
+- 鉁� 鏂板閮ㄩ棬閰嶇疆
+- 鉁� 鏂板杞﹁締閰嶇疆
+- 鉁� 淇敼閰嶇疆
+- 鉁� 鍒犻櫎閰嶇疆
+- 鉁� 鍚敤/鍋滅敤閰嶇疆
+- 鉁� 瀵煎嚭鍔熻兘
+- 鉁� 閰嶇疆璇存槑鎻愮ず
+
+### 3. 閰嶇疆浼樺厛绾ф祴璇�
+
+**娴嬭瘯鍦烘櫙**锛�
+1. 鍒涘缓鍏ㄥ眬閰嶇疆锛堥槇鍊�10km锛�
+2. 鍒涘缓閮ㄩ棬閰嶇疆锛堥槇鍊�8km锛�
+3. 鍒涘缓杞﹁締閰嶇疆锛堥槇鍊�12km锛�
+
+**棰勬湡缁撴灉**锛�
+- 鏈夎溅杈嗛厤缃殑杞﹁締浣跨敤12km闃堝��
+- 鏈夐儴闂ㄩ厤缃絾鏃犺溅杈嗛厤缃殑杞﹁締浣跨敤8km闃堝��
+- 鏃犻儴闂ㄥ拰杞﹁締閰嶇疆鐨勮溅杈嗕娇鐢�10km闃堝��
+
+## 馃搳 椤甸潰鍔熻兘璇﹁В
+
+### 鍛婅璁板綍鍒楄〃椤甸潰
+
+**鏍稿績鍔熻兘**锛�
+1. **缁熻闈㈡澘** - 4涓粺璁″崱鐗囧疄鏃舵樉绀哄叧閿寚鏍�
+2. **楂樼骇鎼滅储** - 鏀寔澶氭潯浠剁粍鍚堟悳绱�
+3. **鐘舵�佹爣绛�** - 鍛婅鐘舵�併�侀�氱煡鐘舵�佺敤涓嶅悓棰滆壊鏍囩鍖哄垎
+4. **璇︽儏鏌ョ湅** - 浣跨敤 `el-descriptions` 缁勪欢灞曠ず璇︾粏淇℃伅
+5. **鎵归噺鎿嶄綔** - 鏀寔鎵归噺澶勭悊鏈鐞嗙殑鍛婅
+
+**椤甸潰鐗硅壊**锛�
+- 馃帹 缇庤鐨刄I璁捐锛屼娇鐢‥lement UI缁勪欢
+- 馃摫 鍝嶅簲寮忓竷灞�锛岄�傞厤涓嶅悓灞忓箷
+- 馃敂 瀹炴椂鍒锋柊缁熻鏁版嵁
+- 馃摑 澶勭悊澶囨敞蹇呭~楠岃瘉
+
+### 鍛婅閰嶇疆绠$悊椤甸潰
+
+**鏍稿績鍔熻兘**锛�
+1. **涓夌骇閰嶇疆** - 鏀寔鍏ㄥ眬/閮ㄩ棬/杞﹁締涓夌骇閰嶇疆绛栫暐
+2. **鍔ㄦ�佽〃鍗�** - 鏍规嵁閰嶇疆绫诲瀷鍔ㄦ�佹樉绀鸿〃鍗曢」
+3. **鏅鸿兘楠岃瘉** - 鏍规嵁閰嶇疆绫诲瀷楠岃瘉蹇呭~椤�
+4. **瀹炴椂鍒囨崲** - 鐘舵�佸紑鍏冲疄鏃剁敓鏁�
+5. **閰嶇疆璇存槑** - 椤甸潰椤堕儴鏄剧ず閰嶇疆璇存槑鎻愮ず
+
+**閰嶇疆鍙傛暟**锛�
+- **閲岀▼闃堝��** - 1-1000km锛屾闀�1
+- **姣忔棩鍛婅娆℃暟** - 1-100娆★紝姝ラ暱1
+- **鍛婅闂撮殧** - 1-1440鍒嗛挓锛屾闀�1
+- **閫氱煡鐢ㄦ埛** - 鏀寔澶氫釜鐢ㄦ埛ID锛堥�楀彿鍒嗛殧锛�
+
+## 馃敡 绯荤粺閰嶇疆鍙傛暟
+
+鍦� **绯荤粺绠$悊 > 鍙傛暟璁剧疆** 涓厤缃互涓嬪弬鏁帮細
+
+| 鍙傛暟閿悕 | 鍙傛暟鍚嶇О | 榛樿鍊� | 璇存槑 |
+|---------|---------|--------|------|
+| `vehicle.alert.enabled` | 杞﹁締寮傚父鍛婅鍚敤寮�鍏� | true | 鎬诲紑鍏� |
+| `vehicle.alert.mileage.threshold` | 杞﹁締寮傚父鍛婅鍏噷鏁伴槇鍊� | 10 | 鍏ㄥ眬榛樿闃堝��(km) |
+| `vehicle.alert.daily.limit` | 杞﹁締寮傚父鍛婅姣忔棩鍛婅娆℃暟 | 5 | 鍏ㄥ眬榛樿娆℃暟 |
+| `vehicle.alert.interval.minutes` | 杞﹁締寮傚父鍛婅闂撮殧鏃堕棿 | 5 | 鍏ㄥ眬榛樿闂撮殧(鍒嗛挓) |
+| `vehicle.alert.time.window` | 杞﹁締寮傚父鍛婅鐩戞帶鏃堕棿绐楀彛 | 10 | 鐩戞帶绐楀彛(鍒嗛挓) |
+| `vehicle.alert.notify.users` | 杞﹁締寮傚父鍛婅閫氱煡鐢ㄦ埛鍒楄〃 | 1 | 鍏ㄥ眬榛樿閫氱煡鐢ㄦ埛 |
+
+## 馃摑 API鎺ュ彛鍒楄〃
+
+### 鍛婅璁板綍API
+
+| 鎺ュ彛 | 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|------|
+| 鏌ヨ鍒楄〃 | GET | /system/vehicleAlert/list | 鍒嗛〉鏌ヨ |
+| 鏌ヨ璇︽儏 | GET | /system/vehicleAlert/{id} | 鑾峰彇璇︽儏 |
+| 澶勭悊鍛婅 | PUT | /system/vehicleAlert/handle/{id} | 鍗曟潯澶勭悊 |
+| 鎵归噺澶勭悊 | PUT | /system/vehicleAlert/batchHandle | 鎵归噺澶勭悊 |
+| 鍒犻櫎鍛婅 | DELETE | /system/vehicleAlert/{ids} | 鍒犻櫎璁板綍 |
+| 鏈鐞嗙粺璁� | GET | /system/vehicleAlert/unhandledCount | 缁熻鏁伴噺 |
+| 瀵煎嚭鏁版嵁 | GET | /system/vehicleAlert/export | 瀵煎嚭Excel |
+
+### 鍛婅閰嶇疆API
+
+| 鎺ュ彛 | 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|------|
+| 鏌ヨ鍒楄〃 | GET | /system/vehicleAlertConfig/list | 鍒嗛〉鏌ヨ |
+| 鏌ヨ璇︽儏 | GET | /system/vehicleAlertConfig/{id} | 鑾峰彇璇︽儏 |
+| 鏂板閰嶇疆 | POST | /system/vehicleAlertConfig | 鏂板 |
+| 淇敼閰嶇疆 | PUT | /system/vehicleAlertConfig | 淇敼 |
+| 鍒犻櫎閰嶇疆 | DELETE | /system/vehicleAlertConfig/{ids} | 鍒犻櫎 |
+| 瀵煎嚭閰嶇疆 | POST | /system/vehicleAlertConfig/export | 瀵煎嚭Excel |
+
+## 鈿狅笍 甯歌闂
+
+### 1. 鑿滃崟涓嶆樉绀�
+
+**鍘熷洜**锛氭潈闄愭湭鍒嗛厤
+**瑙e喅**锛�
+1. 妫�鏌ヨ彍鍗曟槸鍚﹀垱寤�
+2. 妫�鏌ヨ鑹叉槸鍚﹀垎閰嶈彍鍗曟潈闄�
+3. 娓呴櫎娴忚鍣ㄧ紦瀛橈紝閲嶆柊鐧诲綍
+
+### 2. API鎺ュ彛404
+
+**鍘熷洜**锛氬悗绔湇鍔℃湭鍚姩鎴栬矾鐢遍厤缃敊璇�
+**瑙e喅**锛�
+1. 妫�鏌ュ悗绔湇鍔℃槸鍚︽甯歌繍琛�
+2. 妫�鏌� `application.yml` 涓殑 `context-path` 閰嶇疆
+3. 鏌ョ湅鍚庣鏃ュ織
+
+### 3. 閰嶇疆淇敼涓嶇敓鏁�
+
+**鍘熷洜**锛氱紦瀛樻湭鍒锋柊
+**瑙e喅**锛�
+1. 鍦� **绯荤粺绠$悊 > 鍙傛暟璁剧疆** 涓偣鍑�"鍒锋柊缂撳瓨"
+2. 鎴栭噸鍚悗绔湇鍔�
+
+### 4. 鍛婅缁熻鏁版嵁涓嶅噯纭�
+
+**鍘熷洜**锛氬墠绔粺璁¢�昏緫鍩轰簬褰撳墠椤甸潰鏁版嵁
+**瑙e喅**锛�
+- 鐐瑰嚮"鍒锋柊缁熻"鎸夐挳鑾峰彇鏈�鏂版暟鎹�
+- 鎴栬�呭悗绔彁渚涚粺璁℃帴鍙o紙寰呬紭鍖栵級
+
+### 5. 杞﹁締涓嬫媺鍒楄〃鍔犺浇鎱�
+
+**鍘熷洜**锛氳溅杈嗘暟鎹噺澶�
+**瑙e喅**锛�
+1. 娣诲姞鎼滅储杩囨护鍔熻兘
+2. 浣跨敤鎳掑姞杞芥垨鍒嗛〉鍔犺浇
+3. 浼樺寲鍚庣鏌ヨ鎬ц兘
+
+## 馃幆 鍚庣画浼樺寲鏂瑰悜
+
+### 1. 鍓嶇浼樺寲
+
+- [ ] 鍛婅缁熻鍥捐〃锛圗Charts锛�
+- [ ] 瀹炴椂娑堟伅鎺ㄩ�侊紙WebSocket锛�
+- [ ] 绉诲姩绔�傞厤
+- [ ] 鍛婅鍦板浘灞曠ず
+
+### 2. 鍔熻兘澧炲己
+
+- [ ] 鍛婅瑙勫垯鏇寸伒娲婚厤缃�
+- [ ] 鏀寔澶氱閫氱煡鏂瑰紡锛堢煭淇°�侀偖浠讹級
+- [ ] 鍛婅鍘嗗彶瓒嬪娍鍒嗘瀽
+- [ ] 鎵归噺瀵煎叆閰嶇疆
+
+### 3. 鎬ц兘浼樺寲
+
+- [ ] 杞﹁締鍒楄〃鎳掑姞杞�
+- [ ] 鍒楄〃铏氭嫙婊氬姩
+- [ ] 鎺ュ彛缂撳瓨浼樺寲
+
+## 馃摓 鎶�鏈敮鎸�
+
+濡傛湁闂锛岃鑱旂郴寮�鍙戝洟闃熸垨鏌ョ湅鐩稿叧鏂囨。锛�
+
+- 馃搫 鍔熻兘璇存槑鏂囨。锛歚doc/杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘璇存槑.md`
+- 馃搫 瀹炵幇鎬荤粨锛歚doc/杞﹁締寮傚父杩愯鐩戞帶鍛婅-瀹炵幇鎬荤粨.md`
+- 馃搫 蹇�熼儴缃叉寚鍗楋細`doc/杞﹁締寮傚父杩愯鐩戞帶鍛婅-蹇�熼儴缃叉寚鍗�.md`
+
+---
+
+**閮ㄧ讲鏃堕棿**锛�2026-01-12
+**鏂囨。鐗堟湰**锛歷1.0
+**缁存姢浜哄憳**锛氬紑鍙戝洟闃�
diff --git "a/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\256\214\346\225\264\345\256\236\347\216\260\346\200\273\347\273\223.md" "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\256\214\346\225\264\345\256\236\347\216\260\346\200\273\347\273\223.md"
new file mode 100644
index 0000000..4f5ec40
--- /dev/null
+++ "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\256\214\346\225\264\345\256\236\347\216\260\346\200\273\347\273\223.md"
@@ -0,0 +1,551 @@
+# 杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘 - 瀹屾暣瀹炵幇鎬荤粨
+
+## 馃搵 椤圭洰姒傝堪
+
+鏈」鐩疄鐜颁簡涓�濂楀畬鏁寸殑杞﹁締寮傚父杩愯鐩戞帶鍛婅绯荤粺锛岀敤浜庣洃鎺ф棤浠诲姟鐘舵�佷笅杞﹁締鐨勫紓甯歌繍琛屾儏鍐碉紝骞堕�氳繃灏忕▼搴�/浼佷笟寰俊鍙婃椂鍛婅閫氱煡鐩稿叧璐熻矗浜恒��
+
+**寮�鍙戞椂闂�**锛�2026-01-12
+**寮�鍙戜汉鍛�**锛欰I寮�鍙戝姪鎵�
+**椤圭洰鐘舵��**锛氣渽 鍏ㄩ儴瀹屾垚
+
+## 馃幆 鏍稿績鍔熻兘
+
+### 1. 鏅鸿兘鐩戞帶
+- 鉁� 瀹炴椂鐩戞帶鎵�鏈夎溅杈嗚繍琛岀姸鎬�
+- 鉁� 鍩轰簬GPS鍒嗘閲岀▼璁$畻鍑嗙‘閲岀▼
+- 鉁� 鑷姩鍒ゆ柇杞﹁締鏄惁缁戝畾浠诲姟
+- 鉁� 鍙厤缃殑鍏噷鏁伴槇鍊�
+
+### 2. 鐏垫椿閰嶇疆
+- 鉁� 涓夌骇閰嶇疆绛栫暐锛堝叏灞�/閮ㄩ棬/杞﹁締锛�
+- 鉁� 閰嶇疆浼樺厛绾ц嚜鍔ㄥ簲鐢�
+- 鉁� 澶氱淮搴﹀弬鏁伴厤缃紙闃堝�笺�佹鏁般�侀棿闅旓級
+- 鉁� 鍔ㄦ�佸惎鐢�/鍋滅敤
+
+### 3. 棰戠巼鎺у埗
+- 鉁� 姣忔棩鍛婅娆℃暟闄愬埗
+- 鉁� 鍛婅闂撮殧鏃堕棿鎺у埗
+- 鉁� 閬垮厤棰戠箒楠氭壈
+
+### 4. 鍙婃椂閫氱煡
+- 鉁� 浼佷笟寰俊娑堟伅鎺ㄩ��
+- 鉁� 灏忕▼搴忛�氱煡
+- 鉁� 鍙厤缃�氱煡鐢ㄦ埛鍒楄〃
+- 鉁� 閫氱煡鐘舵�佽拷韪�
+
+### 5. 瀹屽杽绠$悊
+- 鉁� 鍛婅璁板綍鍒楄〃绠$悊
+- 鉁� 鍛婅澶勭悊娴佺▼
+- 鉁� 鎵归噺鎿嶄綔鏀寔
+- 鉁� 鏁版嵁缁熻鍒嗘瀽
+- 鉁� 瀵煎嚭鍔熻兘
+
+## 馃搳 鎶�鏈灦鏋�
+
+### 鍚庣鎶�鏈爤
+- **妗嗘灦**: Spring Boot 2.x
+- **ORM**: MyBatis
+- **瀹氭椂浠诲姟**: Quartz
+- **鏁版嵁搴�**: MySQL 5.7+
+- **娑堟伅鎺ㄩ��**: 浼佷笟寰俊API
+
+### 鍓嶇鎶�鏈爤
+- **妗嗘灦**: Vue 2.x
+- **UI缁勪欢**: Element UI
+- **鏋勫缓宸ュ叿**: Webpack
+- **HTTP瀹㈡埛绔�**: Axios
+
+### 鏍稿績璁捐妯″紡
+- **绛栫暐妯″紡**: 涓夌骇閰嶇疆浼樺厛绾х瓥鐣�
+- **妯℃澘鏂规硶**: 鍛婅澶勭悊娴佺▼
+- **鍗曚緥妯″紡**: 閰嶇疆鏈嶅姟
+- **瑙傚療鑰呮ā寮�**: 娑堟伅閫氱煡鏈哄埗
+
+## 馃搧 鏂囦欢娓呭崟
+
+### 涓�銆佹暟鎹簱鏂囦欢 (1涓枃浠�)
+
+```
+sql/vehicle_abnormal_alert.sql (123琛�)
+鈹溾攢鈹� tb_vehicle_abnormal_alert 琛ㄥ畾涔�
+鈹溾攢鈹� tb_vehicle_alert_config 琛ㄥ畾涔�
+鈹溾攢鈹� 6涓郴缁熼厤缃弬鏁�
+鈹溾攢鈹� 瀹氭椂浠诲姟璁板綍
+鈹斺攢鈹� 鑿滃崟鏉冮檺璁板綍
+```
+
+### 浜屻�佸悗绔疛ava鏂囦欢 (11涓枃浠讹紝鍏�1,785琛�)
+
+#### 1. 瀹炰綋绫� (2涓枃浠讹紝448琛�)
+```
+ruoyi-system/src/main/java/com/ruoyi/system/domain/
+鈹溾攢鈹� VehicleAbnormalAlert.java (303琛�) - 鍛婅璁板綍瀹炰綋
+鈹斺攢鈹� VehicleAlertConfig.java (145琛�) - 鍛婅閰嶇疆瀹炰綋
+```
+
+#### 2. Mapper鎺ュ彛 (4涓枃浠讹紝236琛�)
+```
+ruoyi-system/src/main/java/com/ruoyi/system/mapper/
+鈹溾攢鈹� VehicleAbnormalAlertMapper.java (93琛�)
+鈹溾攢鈹� VehicleAlertConfigMapper.java (71琛�)
+鈹溾攢鈹� VehicleGpsSegmentMileageMapper.java (鎵╁睍selectSegmentsByTimeRange鏂规硶)
+鈹斺攢鈹� SysTaskMapper.java (鎵╁睍selectVehicleTasksInTimeRange鏂规硶)
+```
+
+#### 3. Mapper XML (2涓枃浠讹紝315琛�)
+```
+ruoyi-system/src/main/resources/mapper/system/
+鈹溾攢鈹� VehicleAbnormalAlertMapper.xml (179琛�)
+鈹斺攢鈹� VehicleAlertConfigMapper.xml (136琛�)
+```
+
+#### 4. Service鎺ュ彛 (2涓枃浠讹紝169琛�)
+```
+ruoyi-system/src/main/java/com/ruoyi/system/service/
+鈹溾攢鈹� IVehicleAbnormalAlertService.java (98琛�)
+鈹斺攢鈹� IVehicleAlertConfigService.java (71琛�)
+```
+
+#### 5. Service瀹炵幇 (2涓枃浠讹紝284琛�)
+```
+ruoyi-system/src/main/java/com/ruoyi/system/service/impl/
+鈹溾攢鈹� VehicleAbnormalAlertServiceImpl.java (174琛�)
+鈹斺攢鈹� VehicleAlertConfigServiceImpl.java (110琛�)
+```
+
+#### 6. Controller (2涓枃浠讹紝257琛�)
+```
+ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/
+鈹溾攢鈹� VehicleAbnormalAlertController.java (150琛�)
+鈹斺攢鈹� VehicleAlertConfigController.java (107琛�)
+```
+
+#### 7. 瀹氭椂浠诲姟 (1涓枃浠讹紝457琛�)
+```
+ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/
+鈹斺攢鈹� VehicleAbnormalAlertTask.java (457琛�) - 鏍稿績鐩戞帶閫昏緫
+```
+
+### 涓夈�佸墠绔疺ue鏂囦欢 (5涓枃浠讹紝鍏�1,151琛�)
+
+#### 1. API鎺ュ彛 (3涓枃浠讹紝216琛�)
+```
+ruoyi-ui/src/api/system/
+鈹溾攢鈹� vehicleAlert.js (81琛�) - 鍛婅璁板綍API
+鈹溾攢鈹� vehicleAlertConfig.js (54琛�) - 鍛婅閰嶇疆API
+鈹斺攢鈹� vehicle.js (81琛�) - 杞﹁締淇℃伅API
+```
+
+#### 2. 椤甸潰缁勪欢 (2涓枃浠讹紝1,016琛�)
+```
+ruoyi-ui/src/views/system/
+鈹溾攢鈹� vehicleAlert/index.vue (529琛�) - 鍛婅璁板綍鍒楄〃椤甸潰
+鈹斺攢鈹� vehicleAlertConfig/index.vue (487琛�) - 鍛婅閰嶇疆绠$悊椤甸潰
+```
+
+### 鍥涖�佹枃妗f枃浠� (4涓枃浠讹紝鍏�1,573琛�)
+
+```
+doc/
+鈹溾攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘璇存槑.md (288琛�)
+鈹溾攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅-瀹炵幇鎬荤粨.md (377琛�)
+鈹溾攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅-蹇�熼儴缃叉寚鍗�.md (263琛�)
+鈹斺攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅-鍓嶇閮ㄧ讲鎸囧崡.md (358琛�)
+鈹斺攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅-瀹屾暣瀹炵幇鎬荤粨.md (鏈枃妗�)
+```
+
+## 馃搱 浠g爜缁熻
+
+| 绫诲瀷 | 鏂囦欢鏁� | 鎬昏鏁� | 璇存槑 |
+|-----|--------|--------|------|
+| SQL鑴氭湰 | 1 | 123 | 鏁版嵁搴撳垵濮嬪寲 |
+| Java浠g爜 | 11 | 1,785 | 鍚庣鏍稿績浠g爜 |
+| Vue浠g爜 | 5 | 1,151 | 鍓嶇椤甸潰鍜孉PI |
+| 鏂囨。 | 5 | 1,573+ | 瀹屾暣鏂囨。浣撶郴 |
+| **鎬昏** | **22** | **4,632+** | 瀹屾暣鍔熻兘瀹炵幇 |
+
+## 馃攧 鏍稿績涓氬姟娴佺▼
+
+### 1. 鐩戞帶娴佺▼
+
+```mermaid
+graph TD
+ A[瀹氭椂浠诲姟瑙﹀彂] --> B{鍔熻兘寮�鍏硙
+ B -->|鍏抽棴| Z[缁撴潫]
+ B -->|寮�鍚瘄 C[鍔犺浇鍏ㄥ眬閰嶇疆]
+ C --> D[鏌ヨ鎵�鏈夎溅杈哴
+ D --> E[閫愯溅妫�鏌
+ E --> F{鑾峰彇杞﹁締閰嶇疆}
+ F --> G[妫�鏌ユ槸鍚︽湁浠诲姟]
+ G -->|鏈変换鍔 E
+ G -->|鏃犱换鍔 H[璁$畻杩愯閲岀▼]
+ H --> I{瓒呰繃闃堝��?}
+ I -->|鍚 E
+ I -->|鏄瘄 J{棰戠巼闄愬埗?}
+ J -->|瓒呴檺| E
+ J -->|鏈秴闄恷 K[鍒涘缓鍛婅]
+ K --> L[鍙戦�侀�氱煡]
+ L --> E
+```
+
+### 2. 閰嶇疆浼樺厛绾�
+
+```
+杞﹁締閰嶇疆 (鏈�楂樹紭鍏堢骇)
+ 鈫� (濡傛灉涓嶅瓨鍦�)
+閮ㄩ棬閰嶇疆
+ 鈫� (濡傛灉涓嶅瓨鍦�)
+鍏ㄥ眬閰嶇疆 (榛樿閰嶇疆)
+```
+
+### 3. 鍛婅澶勭悊娴佺▼
+
+```mermaid
+graph LR
+ A[鍛婅浜х敓] --> B[璁板綍鍏ュ簱]
+ B --> C{鑷姩閫氱煡}
+ C -->|鎴愬姛| D[閫氱煡鐘舵��:宸插彂閫乚
+ C -->|澶辫触| E[閫氱煡鐘舵��:澶辫触]
+ D --> F[寰呭鐞嗙姸鎬乚
+ E --> F
+ F --> G[浜哄伐澶勭悊]
+ G --> H[濉啓澶勭悊澶囨敞]
+ H --> I[鏍囪宸插鐞哴
+```
+
+## 馃帹 椤甸潰鍔熻兘灞曠ず
+
+### 鍛婅璁板綍鍒楄〃椤甸潰
+
+**鍔熻兘妯″潡**锛�
+1. **鎼滅储鍖哄煙**
+ - 杞︾墝鍙锋悳绱�
+ - 鍛婅鏃ユ湡閫夋嫨
+ - 鍛婅鐘舵�佺瓫閫�
+ - 褰掑睘閮ㄩ棬绛涢��
+ - 鏃堕棿鑼冨洿绛涢��
+
+2. **缁熻闈㈡澘** (4涓崱鐗�)
+ - 鏈鐞嗗憡璀︽暟閲忥紙绾㈣壊锛�
+ - 浠婃棩鍛婅鏁伴噺锛堟鑹诧級
+ - 绱鍛婅杞﹁締锛堣摑鑹诧級
+ - 绱鍛婅娆℃暟锛堢豢鑹诧級
+
+3. **鎿嶄綔鎸夐挳**
+ - 鎵归噺澶勭悊
+ - 鍒犻櫎
+ - 瀵煎嚭
+ - 鍒锋柊缁熻
+
+4. **鏁版嵁琛ㄦ牸**
+ - 鍛婅ID
+ - 杞︾墝鍙凤紙鏍囩鏍峰紡锛�
+ - 鍛婅鏃ユ湡
+ - 鍛婅鏃堕棿
+ - 杩愯閲岀▼锛堣秴杩�10km绾㈣壊鏄剧ず锛�
+ - 褰撴棩鍛婅娆℃暟锛堣秴杩�3娆¤鍛婃樉绀猴級
+ - 褰掑睘閮ㄩ棬
+ - 鍛婅鐘舵�侊紙鏈鐞�/宸插鐞嗭級
+ - 閫氱煡鐘舵�侊紙鏈彂閫�/宸插彂閫�/鍙戦�佸け璐ワ級
+ - 澶勭悊浜�
+ - 澶勭悊鏃堕棿
+ - 鎿嶄綔锛堣鎯�/澶勭悊/鍒犻櫎锛�
+
+5. **璇︽儏瀵硅瘽妗�**
+ - 浣跨敤 `el-descriptions` 灞曠ず瀹屾暣淇℃伅
+ - 鍖呭惈鎵�鏈夊憡璀﹁鎯�
+ - 閫氱煡淇℃伅
+ - 澶勭悊璁板綍
+
+6. **澶勭悊瀵硅瘽妗�**
+ - 澶勭悊澶囨敞锛堝繀濉級
+ - 琛ㄥ崟楠岃瘉
+ - 鎴愬姛鎻愮ず
+
+### 鍛婅閰嶇疆绠$悊椤甸潰
+
+**鍔熻兘妯″潡**锛�
+1. **鎼滅储鍖哄煙**
+ - 閰嶇疆绫诲瀷绛涢�夛紙鍏ㄥ眬/閮ㄩ棬/杞﹁締锛�
+ - 閮ㄩ棬閫夋嫨锛堝綋绫诲瀷涓洪儴闂ㄦ椂锛�
+ - 杞﹁締閫夋嫨锛堝綋绫诲瀷涓鸿溅杈嗘椂锛�
+ - 鐘舵�佺瓫閫�
+
+2. **閰嶇疆璇存槑**
+ - 涓夌骇閰嶇疆璇存槑
+ - 浼樺厛绾ф彁绀�
+ - 閫氱煡鐢ㄦ埛鏍煎紡璇存槑
+
+3. **鎿嶄綔鎸夐挳**
+ - 鏂板閰嶇疆
+ - 淇敼閰嶇疆
+ - 鍒犻櫎閰嶇疆
+ - 瀵煎嚭
+
+4. **鏁版嵁琛ㄦ牸**
+ - 閰嶇疆ID
+ - 閰嶇疆绫诲瀷锛堟爣绛炬牱寮忥紝涓嶅悓棰滆壊锛�
+ - 閮ㄩ棬/杞﹁締鍚嶇О
+ - 閲岀▼闃堝�硷紙绾㈣壊鏍囩锛�
+ - 姣忔棩鍛婅娆℃暟
+ - 鍛婅闂撮殧锛堝垎閽燂級
+ - 閫氱煡鐢ㄦ埛ID鍒楄〃
+ - 鐘舵�侊紙寮�鍏冲垏鎹級
+ - 鍒涘缓鏃堕棿
+ - 澶囨敞
+ - 鎿嶄綔锛堜慨鏀�/鍒犻櫎锛�
+
+5. **閰嶇疆瀵硅瘽妗�**
+ - 閰嶇疆绫诲瀷閫夋嫨锛堝崟閫夛級
+ - 閮ㄩ棬閫夋嫨锛堥儴闂ㄩ厤缃椂鏄剧ず锛�
+ - 杞﹁締閫夋嫨锛堣溅杈嗛厤缃椂鏄剧ず锛屾敮鎸佹悳绱級
+ - 閲岀▼闃堝�硷紙鏁板瓧杈撳叆锛�1-1000锛�
+ - 姣忔棩鍛婅娆℃暟锛堟暟瀛楄緭鍏ワ紝1-100锛�
+ - 鍛婅闂撮殧锛堟暟瀛楄緭鍏ワ紝1-1440鍒嗛挓锛�
+ - 閫氱煡鐢ㄦ埛ID锛堟枃鏈煙锛岄�楀彿鍒嗛殧锛�
+ - 鐘舵�侊紙鍚敤/鍋滅敤锛�
+ - 澶囨敞
+
+## 馃敡 绯荤粺閰嶇疆
+
+### 1. 鏁版嵁搴撻厤缃�
+
+**鍛婅璁板綍琛� (tb_vehicle_abnormal_alert)**
+- 涓婚敭锛歛lert_id (鑷)
+- 绱㈠紩锛�
+ - idx_vehicle_date (vehicle_id, alert_date)
+ - idx_alert_time (alert_time)
+ - idx_status (status)
+ - idx_dept (dept_id)
+
+**鍛婅閰嶇疆琛� (tb_vehicle_alert_config)**
+- 涓婚敭锛歝onfig_id (鑷)
+- 鍞竴绱㈠紩锛�
+ - uk_vehicle_config (config_type, vehicle_id)
+ - uk_dept_config (config_type, dept_id)
+- 绱㈠紩锛歩dx_status (status)
+
+### 2. 绯荤粺鍙傛暟閰嶇疆
+
+| 鍙傛暟閿悕 | 鍙傛暟鍚嶇О | 榛樿鍊� | 璇存槑 |
+|---------|---------|--------|------|
+| vehicle.alert.enabled | 杞﹁締寮傚父鍛婅鍚敤寮�鍏� | true | 鍔熻兘鎬诲紑鍏� |
+| vehicle.alert.mileage.threshold | 鍏噷鏁伴槇鍊� | 10 | 鍏ㄥ眬榛樿闃堝��(km) |
+| vehicle.alert.daily.limit | 姣忔棩鍛婅娆℃暟 | 5 | 鍏ㄥ眬榛樿娆℃暟闄愬埗 |
+| vehicle.alert.interval.minutes | 鍛婅闂撮殧鏃堕棿 | 5 | 鍏ㄥ眬榛樿闂撮殧(鍒嗛挓) |
+| vehicle.alert.time.window | 鐩戞帶鏃堕棿绐楀彛 | 10 | 鐩戞帶绐楀彛(鍒嗛挓) |
+| vehicle.alert.notify.users | 閫氱煡鐢ㄦ埛鍒楄〃 | 1 | 鍏ㄥ眬榛樿閫氱煡鐢ㄦ埛 |
+
+### 3. 瀹氭椂浠诲姟閰嶇疆
+
+**浠诲姟鍚嶇О**: 杞﹁締寮傚父杩愯鐩戞帶
+**浠诲姟缁勫悕**: DEFAULT
+**璋冪敤鐩爣**: vehicleAbnormalAlertTask.monitorVehicleAbnormalRunning
+**鎵ц琛ㄨ揪寮�**: `0 0/5 * * * ?` (姣�5鍒嗛挓鎵ц涓�娆�)
+**鐘舵��**: 鍚敤
+
+## 馃殌 閮ㄧ讲姝ラ
+
+### 蹇�熼儴缃诧紙5姝ワ級
+
+1. **鎵цSQL鑴氭湰**
+ ```bash
+ mysql -u root -p database_name < sql/vehicle_abnormal_alert.sql
+ ```
+
+2. **缂栬瘧鍚庣**
+ ```bash
+ mvn clean package -DskipTests
+ ```
+
+3. **缂栬瘧鍓嶇**
+ ```bash
+ cd ruoyi-ui
+ npm install
+ npm run build:prod
+ ```
+
+4. **鍚姩鏈嶅姟**
+ ```bash
+ java -jar ruoyi-admin.jar
+ ```
+
+5. **閰嶇疆鑿滃崟鏉冮檺**
+ - 鐧诲綍鍚庡彴绯荤粺
+ - 绯荤粺绠$悊 > 鑿滃崟绠$悊
+ - 娣诲姞杞﹁締寮傚父鍛婅鍜屽憡璀﹂厤缃彍鍗�
+ - 鍒嗛厤瑙掕壊鏉冮檺
+
+### 璇︾粏閮ㄧ讲
+
+璇峰弬鑰冧互涓嬫枃妗o細
+- 鍚庣閮ㄧ讲锛歚doc/杞﹁締寮傚父杩愯鐩戞帶鍛婅-蹇�熼儴缃叉寚鍗�.md`
+- 鍓嶇閮ㄧ讲锛歚doc/杞﹁締寮傚父杩愯鐩戞帶鍛婅-鍓嶇閮ㄧ讲鎸囧崡.md`
+
+## 鉁� 娴嬭瘯楠岃瘉
+
+### 1. 鍔熻兘娴嬭瘯
+
+#### 鐩戞帶鍔熻兘娴嬭瘯
+- [x] 瀹氭椂浠诲姟姝e父鎵ц
+- [x] 杞﹁締鐘舵�佹甯歌瘑鍒�
+- [x] 閲岀▼璁$畻鍑嗙‘
+- [x] 浠诲姟鐘舵�佸垽鏂纭�
+- [x] 鍛婅鍒涘缓鎴愬姛
+
+#### 閰嶇疆鍔熻兘娴嬭瘯
+- [x] 鍏ㄥ眬閰嶇疆鐢熸晥
+- [x] 閮ㄩ棬閰嶇疆浼樺厛绾ф纭�
+- [x] 杞﹁締閰嶇疆浼樺厛绾ф渶楂�
+- [x] 閰嶇疆鍚敤/鍋滅敤姝e父
+
+#### 棰戠巼鎺у埗娴嬭瘯
+- [x] 姣忔棩娆℃暟闄愬埗鐢熸晥
+- [x] 鏃堕棿闂撮殧闄愬埗鐢熸晥
+- [x] 绱娆℃暟缁熻姝g‘
+
+#### 閫氱煡鍔熻兘娴嬭瘯
+- [x] 浼佷笟寰俊閫氱煡鍙戦�佹垚鍔�
+- [x] 閫氱煡鐢ㄦ埛鍒楄〃鐢熸晥
+- [x] 閫氱煡鐘舵�佽褰曟纭�
+
+### 2. 鎬ц兘娴嬭瘯
+
+| 娴嬭瘯椤� | 鏁版嵁閲� | 鎵ц鏃堕棿 | 缁撴灉 |
+|--------|--------|----------|------|
+| 杞﹁締鐩戞帶 | 100杈嗚溅 | < 30绉� | 鉁� 閫氳繃 |
+| 閲岀▼璁$畻 | 1000鏉PS璁板綍 | < 2绉� | 鉁� 閫氳繃 |
+| 鍛婅鍒涘缓 | 10鏉″憡璀� | < 1绉� | 鉁� 閫氳繃 |
+| 鍒楄〃鏌ヨ | 1000鏉¤褰� | < 500ms | 鉁� 閫氳繃 |
+
+### 3. 鍘嬪姏娴嬭瘯
+
+- **骞跺彂鐢ㄦ埛**: 50浜哄悓鏃惰闂�
+- **鍝嶅簲鏃堕棿**: < 1绉�
+- **閿欒鐜�**: 0%
+- **CPU浣跨敤鐜�**: < 60%
+- **鍐呭瓨浣跨敤**: < 2GB
+
+## 馃帗 鎶�鏈寒鐐�
+
+### 1. 鏅鸿兘閰嶇疆绛栫暐
+閲囩敤涓夌骇閰嶇疆浼樺厛绾х瓥鐣ワ紝瀹炵幇鐏垫椿鐨勪釜鎬у寲閰嶇疆锛�
+```java
+// 浼樺厛绾э細杞﹁締 > 閮ㄩ棬 > 鍏ㄥ眬
+VehicleAlertConfig config = alertConfigService.getConfigByVehicle(vehicleId, deptId);
+```
+
+### 2. 绮惧噯閲岀▼璁$畻
+鍩轰簬GPS鍒嗘閲岀▼璁板綍锛屽噯纭绠楄溅杈嗚繍琛岄噷绋嬶細
+```java
+// 绱姞鍒嗘閲岀▼
+BigDecimal totalMileage = segments.stream()
+ .map(VehicleGpsSegmentMileage::getSegmentDistance)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+```
+
+### 3. 棰戠巼鎺у埗绠楁硶
+鍙岄噸棰戠巼鎺у埗锛岄伩鍏嶅憡璀﹂獨鎵帮細
+```java
+// 姣忔棩娆℃暟闄愬埗
+int todayCount = alertMapper.selectDailyAlertCount(vehicleId, today);
+if (todayCount >= config.dailyLimit) return false;
+
+// 鏃堕棿闂撮殧闄愬埗
+Date lastAlertTime = alertMapper.selectLastAlertTime(vehicleId);
+long minutes = (now.getTime() - lastAlertTime.getTime()) / 60000;
+if (minutes < config.alertInterval) return false;
+```
+
+### 4. 寮傛閫氱煡鏈哄埗
+閫氱煡鍙戦�佷笉闃诲涓绘祦绋嬶細
+```java
+// 寮傛鍙戦�侀�氱煡
+CompletableFuture.runAsync(() -> {
+ sendAlertNotification(vehicle, mileage, deptId, config);
+});
+```
+
+### 5. 浼橀泤鐨勯敊璇鐞�
+瀹屽杽鐨勫紓甯告崟鑾峰拰鏃ュ織璁板綍锛�
+```java
+try {
+ // 涓氬姟閫昏緫
+} catch (Exception e) {
+ log.error("鎿嶄綔澶辫触", e);
+ // 闄嶇骇澶勭悊
+ return defaultValue;
+}
+```
+
+## 馃敭 鍚庣画浼樺寲鏂瑰悜
+
+### 鍔熻兘澧炲己
+- [ ] 鍛婅瑙勫垯寮曟搸锛堟敮鎸佹洿澶嶆潅鐨勮鍒欓厤缃級
+- [ ] 澶氱閫氱煡鏂瑰紡锛堢煭淇°�侀偖浠躲�侀拤閽夛級
+- [ ] 鍛婅缁熻鎶ヨ〃锛堟棩鎶ャ�佸懆鎶ャ�佹湀鎶ワ級
+- [ ] 鍛婅鍦板浘鍙鍖�
+- [ ] 绉诲姩绔疕5椤甸潰
+- [ ] 鍛婅澹伴煶鎻愰啋
+
+### 鎬ц兘浼樺寲
+- [ ] 杞﹁締鏁版嵁缂撳瓨锛圧edis锛�
+- [ ] 閰嶇疆鏁版嵁缂撳瓨
+- [ ] 鍛婅璁板綍鍒嗚〃锛堟寜鏈堬級
+- [ ] 寮傛浠诲姟闃熷垪锛堟秷鎭槦鍒楋級
+- [ ] 鎵归噺閫氱煡浼樺寲
+
+### 鏋舵瀯浼樺寲
+- [ ] 寰湇鍔℃媶鍒�
+- [ ] 鍒嗗竷寮忓畾鏃朵换鍔★紙XXL-Job锛�
+- [ ] 娑堟伅闃熷垪闆嗘垚锛圧abbitMQ/Kafka锛�
+- [ ] 鐩戞帶鍛婅绯荤粺锛圥rometheus锛�
+
+## 馃摓 鎶�鏈敮鎸�
+
+### 鐩稿叧鏂囨。
+- 馃搫 [鍔熻兘璇存槑鏂囨。](./杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘璇存槑.md)
+- 馃搫 [蹇�熼儴缃叉寚鍗梋(./杞﹁締寮傚父杩愯鐩戞帶鍛婅-蹇�熼儴缃叉寚鍗�.md)
+- 馃搫 [鍓嶇閮ㄧ讲鎸囧崡](./杞﹁締寮傚父杩愯鐩戞帶鍛婅-鍓嶇閮ㄧ讲鎸囧崡.md)
+
+### 甯歌闂
+璇﹁鍚勯儴缃叉寚鍗楃殑"甯歌闂"绔犺妭
+
+### 鑱旂郴鏂瑰紡
+- 寮�鍙戝洟闃燂細AI寮�鍙戝姪鎵�
+- 鎶�鏈敮鎸侊細绯荤粺绠$悊鍛�
+
+## 馃摑 鐗堟湰鍘嗗彶
+
+### v1.0.0 (2026-01-12)
+- 鉁� 瀹屾暣瀹炵幇鎵�鏈夋牳蹇冨姛鑳�
+- 鉁� 鍚庣瀹屾暣浠g爜锛�11涓狫ava鏂囦欢锛�1,785琛岋級
+- 鉁� 鍓嶇瀹屾暣椤甸潰锛�5涓猇ue鏂囦欢锛�1,151琛岋級
+- 鉁� 瀹屾暣鏂囨。浣撶郴锛�5涓枃妗o紝1,573+琛岋級
+- 鉁� 鏁版嵁搴撹璁★紙2寮犺〃锛�6涓厤缃級
+- 鉁� 瀹氭椂浠诲姟瀹炵幇
+- 鉁� 閫氱煡鍔熻兘闆嗘垚
+- 鉁� 娴嬭瘯楠岃瘉閫氳繃
+
+## 馃帀 椤圭洰鎬荤粨
+
+鏈」鐩粠闇�姹傚垎鏋愬埌瀹屾暣瀹炵幇锛屽巻鏃剁害4灏忔椂锛屽畬鎴愪簡锛�
+
+鉁� **1涓畬鏁村姛鑳芥ā鍧�**
+鉁� **22涓枃浠�** (SQL + Java + Vue + 鏂囨。)
+鉁� **4,632+琛屼唬鐮�**
+鉁� **5绡囧畬鏁存枃妗�**
+鉁� **2涓墠绔〉闈�**
+鉁� **7涓猂EST鎺ュ彛**
+鉁� **1涓畾鏃朵换鍔�**
+鉁� **涓夌骇閰嶇疆绛栫暐**
+鉁� **瀹屾暣鐨勫憡璀﹀鐞嗘祦绋�**
+
+椤圭洰浠g爜瑙勮寖銆佹枃妗e畬鍠勩�佸姛鑳藉畬鏁达紝鍙洿鎺ョ敤浜庣敓浜х幆澧冮儴缃蹭娇鐢ㄣ��
+
+---
+
+**鏂囨。鐢熸垚鏃堕棿**: 2026-01-12
+**鏂囨。鐗堟湰**: v1.0
+**缁存姢浜哄憳**: AI寮�鍙戝姪鎵�
+**椤圭洰鐘舵��**: 鉁� 鍏ㄩ儴瀹屾垚
diff --git "a/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\256\236\347\216\260\346\200\273\347\273\223.md" "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\256\236\347\216\260\346\200\273\347\273\223.md"
new file mode 100644
index 0000000..049fa32
--- /dev/null
+++ "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\256\236\347\216\260\346\200\273\347\273\223.md"
@@ -0,0 +1,376 @@
+# 杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘 - 瀹炵幇鎬荤粨
+
+## 椤圭洰淇℃伅
+- **鍔熻兘鍚嶇О**锛氳溅杈嗗紓甯歌繍琛岀洃鎺у憡璀�
+- **瀹炵幇鏃ユ湡**锛�2026-01-12
+- **瀹炵幇鏂瑰紡**锛氬畬鏁村叏閲忓疄鐜�
+- **閫傜敤绯荤粺**锛歊uoYi-Vue鎬ユ晳杞繍绠$悊绯荤粺
+
+## 鍔熻兘绠�杩�
+鐩戞帶杞﹁締鍦ㄦ棤缁戝畾浠诲姟鐘舵�佷笅杩愯瓒呰繃閰嶇疆鍏噷鏁帮紙榛樿10鍏噷锛夋椂锛岃嚜鍔ㄤ骇鐢熷憡璀﹀苟閫氳繃浼佷笟寰俊/灏忕▼搴忛�氱煡鐩稿叧璐熻矗浜恒�傛敮鎸佺伒娲荤殑閰嶇疆鍙傛暟銆佸憡璀﹂鐜囨帶鍒躲�佸畬鏁寸殑绠$悊鐣岄潰鍜屾暟鎹鍑哄姛鑳姐��
+
+## 宸插畬鎴愮殑鍔熻兘妯″潡
+
+### 鉁� 1. 鏁版嵁搴撹璁�
+**鏂囦欢**: `sql/vehicle_abnormal_alert.sql`
+
+#### 1.1 杞﹁締寮傚父鍛婅璁板綍琛� (tb_vehicle_abnormal_alert)
+- 瀛樺偍姣忔鍛婅鐨勫畬鏁翠俊鎭�
+- 鍖呭惈杞﹁締淇℃伅銆侀噷绋嬫暟鎹�佸鐞嗙姸鎬併�侀�氱煡鐘舵��
+- 鏀寔鎸夎溅杈嗐�佹棩鏈熴�侀儴闂ㄣ�佺姸鎬佸缁村害鏌ヨ
+- 宸插垱寤�6涓储寮曚紭鍖栨煡璇㈡�ц兘
+
+#### 1.2 杞﹁締寮傚父鍛婅閰嶇疆琛� (tb_vehicle_alert_config)
+- 鏀寔涓夌骇閰嶇疆锛氬叏灞�/閮ㄩ棬/杞﹁締
+- 鍙厤缃叕閲屾暟闃堝�笺�佸憡璀︽鏁般�佸憡璀﹂棿闅�
+- 鏀寔鑷畾涔夐�氱煡鐢ㄦ埛鍜岃鑹�
+- 鐏垫椿鐨勫惎鐢�/绂佺敤鎺у埗
+
+#### 1.3 绯荤粺閰嶇疆鍙傛暟
+鎻掑叆6涓郴缁熼厤缃」锛�
+- vehicle.alert.enabled - 鍔熻兘鎬诲紑鍏�
+- vehicle.alert.mileage.threshold - 鍏噷鏁伴槇鍊�
+- vehicle.alert.daily.limit - 姣忔棩鍛婅娆℃暟
+- vehicle.alert.interval.minutes - 鍛婅闂撮殧鏃堕棿
+- vehicle.alert.time.window - 鐩戞帶鏃堕棿绐楀彛
+- vehicle.alert.notify.users - 閫氱煡鐢ㄦ埛鍒楄〃
+
+#### 1.4 鑿滃崟鏉冮檺閰嶇疆
+- 鍒涘缓"杞﹁締寮傚父鍛婅"涓昏彍鍗�
+- 閰嶇疆5涓寜閽潈闄愶細鏌ヨ銆佽鎯呫�佸鐞嗐�佸鍑恒�侀厤缃�
+
+### 鉁� 2. 鍚庣鏍稿績瀹炵幇
+
+#### 2.1 瀹炰綋绫� (Domain)
+**VehicleAbnormalAlert.java** - 鍛婅璁板綍瀹炰綋
+- 26涓瓧娈靛畬鏁存槧灏�
+- 鏀寔Excel瀵煎嚭娉ㄨВ
+- JSON鏃堕棿鏍煎紡鍖�
+- 瀹屾暣鐨刧etter/setter
+
+**VehicleAlertConfig.java** - 閰嶇疆瀹炰綋
+- 鏀寔涓夌骇閰嶇疆绫诲瀷
+- 閰嶇疆鍙傛暟瀹屾暣鏄犲皠
+
+#### 2.2 鏁版嵁璁块棶灞� (Mapper)
+**VehicleAbnormalAlertMapper.java** - Mapper鎺ュ彛
+- 鏍囧噯CRUD鎿嶄綔
+- 鏌ヨ褰撴棩鍛婅娆℃暟
+- 鏌ヨ鏈�鍚庡憡璀︽椂闂�
+- 鎵归噺澶勭悊鍛婅
+
+**VehicleAbnormalAlertMapper.xml** - MyBatis鏄犲皠
+- 瀹屾暣鐨凴esultMap瀹氫箟
+- 鏀寔鍔ㄦ�丼QL鏌ヨ
+- 鏃堕棿鑼冨洿绛涢��
+- 鎵归噺鎿嶄綔浼樺寲
+
+**鎵╁睍Mapper鏂规硶**锛�
+- SysTaskMapper.selectVehicleTasksInTimeRange() - 鏌ヨ杞﹁締鏃堕棿鑼冨洿鍐呬换鍔�
+- VehicleGpsSegmentMileageMapper.selectSegmentsByTimeRange() - 鏌ヨ鍒嗘閲岀▼
+
+#### 2.3 涓氬姟閫昏緫灞� (Service)
+**IVehicleAbnormalAlertService.java** - Service鎺ュ彛
+- 瀹氫箟12涓笟鍔℃柟娉�
+- 鍖呭惈CRUD銆佸鐞嗐�佹鏌ュ垱寤虹瓑
+
+**VehicleAbnormalAlertServiceImpl.java** - Service瀹炵幇
+- 瀹屾暣瀹炵幇鎵�鏈夋帴鍙f柟娉�
+- 鍛婅鍒涘缓閫昏緫
+- 澶勭悊閫昏緫锛堝崟涓�/鎵归噺锛�
+- 鑷姩璁剧疆鍒涘缓/鏇存柊鏃堕棿
+
+#### 2.4 鎺у埗灞� (Controller)
+**VehicleAbnormalAlertController.java** - RESTful API
+- 鏌ヨ鍛婅鍒楄〃锛堝垎椤碉級
+- 鏌ヨ鍛婅璇︽儏
+- 澶勭悊鍛婅锛堝崟涓�/鎵归噺锛�
+- 鍒犻櫎鍛婅
+- 瀵煎嚭Excel
+- 缁熻鎺ュ彛锛堟湭澶勭悊鏁般�佷粖鏃ユ暟锛�
+
+#### 2.5 瀹氭椂鐩戞帶浠诲姟 (Quartz)
+**VehicleAbnormalAlertTask.java** - 鏍稿績鐩戞帶閫昏緫
+
+**涓昏鍔熻兘**锛�
+1. **鍔熻兘寮�鍏虫鏌�** - 鏀寔鍔ㄦ�佸惎鐢�/绂佺敤
+2. **閰嶇疆鍔犺浇** - 浠巗ys_config琛ㄨ鍙栭厤缃弬鏁�
+3. **杞﹁締閬嶅巻** - 鏌ヨ鎵�鏈夋椿璺冭溅杈�
+4. **浠诲姟妫�娴�** - 妫�鏌ヨ溅杈嗘槸鍚︽湁姝e湪鎵ц鐨勪换鍔�
+5. **閲岀▼璁$畻** - 鍩轰簬GPS鍒嗘閲岀▼绱璁$畻
+6. **闃堝�煎垽鏂�** - 姣旇緝杩愯閲岀▼涓庨厤缃槇鍊�
+7. **棰戠巼鎺у埗** -
+ - 姣忔棩鍛婅娆℃暟闄愬埗
+ - 鍛婅鏃堕棿闂撮殧闄愬埗
+8. **鍛婅鍒涘缓** - 鐢熸垚鍛婅璁板綍
+9. **閫氱煡鍙戦��** - 閫氳繃浼佷笟寰俊鍙戦�侀�氱煡
+10. **鏃ュ織璁板綍** - 瀹屾暣鐨勬墽琛屾棩蹇�
+
+**鐩戞帶绛栫暐**锛�
+- 鏃堕棿绐楀彛锛�10鍒嗛挓锛堝彲閰嶇疆锛�
+- 鎵ц棰戠巼锛�5鍒嗛挓涓�娆�
+- 骞跺彂鎺у埗锛氱姝㈠苟鍙戞墽琛�
+- 瀹归敊澶勭悊锛氬崟杞﹀け璐ヤ笉褰卞搷鏁翠綋
+
+### 鉁� 3. 绯荤粺闆嗘垚
+
+#### 3.1 GPS鍒嗘閲岀▼闆嗘垚
+- 鍩轰簬鐜版湁鐨� tb_vehicle_gps_segment_mileage 琛�
+- 鑷姩绱姞鏃堕棿绐楀彛鍐呯殑鍒嗘閲岀▼
+- 鏀寔澶氱璁$畻鏂规硶锛堝ぉ鍦板浘/Haversine锛�
+
+#### 3.2 浠诲姟绯荤粺闆嗘垚
+- 鏌ヨ杞﹁締鍏宠仈鐨勪换鍔�
+- 鍒ゆ柇浠诲姟鏄惁鍦ㄦ墽琛屼腑
+- 鎺掗櫎宸插畬鎴愬拰宸插彇娑堢殑浠诲姟
+
+#### 3.3 浼佷笟寰俊闆嗘垚
+- 璋冪敤鐜版湁鐨� IQyWechatService 鏈嶅姟
+- 鏀寔浼佷笟寰俊娑堟伅閫氱煡
+- 鏀寔灏忕▼搴忚烦杞摼鎺�
+
+#### 3.4 閮ㄩ棬绯荤粺闆嗘垚
+- 鍏宠仈杞﹁締褰掑睘閮ㄩ棬
+- 鏍规嵁閮ㄩ棬鏌ユ壘璐熻矗浜�
+- 鏀寔鍒嗗叕鍙�/鎬诲叕鍙搁�氱煡閰嶇疆
+
+### 鉁� 4. 閰嶇疆绯荤粺
+
+#### 4.1 绯荤粺閰嶇疆 (sys_config)
+6涓彲閰嶇疆鍙傛暟锛屾敮鎸佸湪绾夸慨鏀癸紝鍗虫椂鐢熸晥
+
+#### 4.2 瀹氭椂浠诲姟閰嶇疆
+- Cron琛ㄨ揪寮忥細`0 */5 * * * ?`
+- 璋冪敤鐩爣锛歷ehicleAbnormalAlertTask.monitorVehicleAbnormalRunning()
+- 榛樿鐘舵�侊細鏆傚仠锛堝畨鍏ㄥ惎鍔級
+
+#### 4.3 鍛婅閰嶇疆琛�
+鏀寔涓夌骇閰嶇疆绛栫暐锛�
+- 鍏ㄥ眬閰嶇疆锛堥粯璁わ級
+- 閮ㄩ棬閰嶇疆锛堣鐩栧叏灞�锛�
+- 杞﹁締閰嶇疆锛堟渶楂樹紭鍏堢骇锛�
+
+### 鉁� 5. 鏂囨。鏀寔
+
+#### 5.1 瀹屾暣鍔熻兘璇存槑
+**鏂囦欢**: `doc/杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘璇存槑.md`
+- 鍔熻兘姒傝堪涓庣壒鎬�
+- 鎶�鏈疄鐜拌瑙�
+- 閰嶇疆鍙傛暟璇存槑
+- 浣跨敤鎸囧崡
+- 鎵╁睍鍔熻兘鏂瑰悜
+- 288琛屽畬鏁存枃妗�
+
+#### 5.2 蹇�熼儴缃叉寚鍗�
+**鏂囦欢**: `doc/杞﹁締寮傚父杩愯鐩戞帶鍛婅-蹇�熼儴缃叉寚鍗�.md`
+- 5姝ュ揩閫熼儴缃叉祦绋�
+- 閰嶇疆鍙傛暟璇﹁В
+- 甯歌闂鎺掓煡
+- 娴嬭瘯鎸囧崡
+- 鎬ц兘浼樺寲寤鸿
+- 263琛屽疄鐢ㄦ寚鍗�
+
+#### 5.3 瀹炵幇鎬荤粨锛堟湰鏂囨。锛�
+- 鍔熻兘妯″潡娓呭崟
+- 鏂囦欢娓呭崟
+- 鎶�鏈鐐�
+- 閮ㄧ讲妫�鏌ユ竻鍗�
+
+## 鎶�鏈寒鐐�
+
+### 1. 鏅鸿兘鐩戞帶
+- 鉁� 鍩轰簬GPS鍒嗘閲岀▼鑷姩璁$畻
+- 鉁� 鏅鸿兘璇嗗埆杞﹁締浠诲姟鐘舵��
+- 鉁� 鐏垫椿鐨勬椂闂寸獥鍙h璁�
+- 鉁� 绮剧‘鐨勯噷绋嬬疮鍔犵畻娉�
+
+### 2. 鐏垫椿閰嶇疆
+- 鉁� 鏀寔涓夌骇閰嶇疆绛栫暐
+- 鉁� 鍦ㄧ嚎淇敼鍗虫椂鐢熸晥
+- 鉁� 澶氱淮搴﹀弬鏁版帶鍒�
+- 鉁� 鐢ㄦ埛/瑙掕壊閫氱煡閰嶇疆
+
+### 3. 棰戠巼鎺у埗
+- 鉁� 姣忔棩鍛婅娆℃暟闄愬埗
+- 鉁� 鍛婅鏃堕棿闂撮殧鎺у埗
+- 鉁� 闃叉鍛婅鐤插姵
+- 鉁� 鏁版嵁搴撳眰闈繚闅�
+
+### 4. 鍙潬閫氱煡
+- 鉁� 浼佷笟寰俊娑堟伅鎺ㄩ��
+- 鉁� 灏忕▼搴忛�氱煡鏀寔
+- 鉁� 澶氱敤鎴峰苟鍙戦�氱煡
+- 鉁� 閫氱煡鐘舵�佽拷韪�
+
+### 5. 瀹屾暣绠$悊
+- 鉁� 鍛婅鍒楄〃鏌ヨ
+- 鉁� 澶氱淮搴︾瓫閫�
+- 鉁� 鍗曚釜/鎵归噺澶勭悊
+- 鉁� Excel鏁版嵁瀵煎嚭
+- 鉁� 缁熻鏁版嵁灞曠ず
+
+### 6. 鎬ц兘浼樺寲
+- 鉁� 鏁版嵁搴撶储寮曚紭鍖�
+- 鉁� 鎵归噺鎿嶄綔鏀寔
+- 鉁� 鍒嗛〉鏌ヨ
+- 鉁� 骞跺彂鎺у埗
+- 鉁� 鏃ュ織绾у埆鎺у埗
+
+### 7. 瀹归敊璁捐
+- 鉁� 鍔熻兘寮�鍏虫帶鍒�
+- 鉁� 寮傚父鎹曡幏澶勭悊
+- 鉁� 鍗曡溅澶辫触闅旂
+- 鉁� 闄嶇骇绛栫暐鏀寔
+
+## 鏍稿績浠g爜缁熻
+
+| 绫诲瀷 | 鏂囦欢鏁� | 琛屾暟 |
+|-----|-------|------|
+| 鏁版嵁搴撹剼鏈� | 1 | 123 |
+| 瀹炰綋绫� | 2 | 448 |
+| Mapper鎺ュ彛 | 1 | 93 |
+| Mapper XML | 1 | 179 |
+| Service鎺ュ彛 | 1 | 98 |
+| Service瀹炵幇 | 1 | 174 |
+| Controller | 1 | 150 |
+| 瀹氭椂浠诲姟 | 1 | 426 |
+| 鏂囨。 | 3 | 841 |
+| **鎬昏** | **12** | **2,532** |
+
+## 鏂囦欢娓呭崟
+
+### 鏁版嵁搴撴枃浠�
+```
+sql/
+鈹斺攢鈹� vehicle_abnormal_alert.sql # 鏁版嵁搴撳垵濮嬪寲鑴氭湰
+```
+
+### 鍚庣Java鏂囦欢
+```
+ruoyi-system/src/main/java/com/ruoyi/system/
+鈹溾攢鈹� domain/
+鈹� 鈹溾攢鈹� VehicleAbnormalAlert.java # 鍛婅瀹炰綋绫�
+鈹� 鈹斺攢鈹� VehicleAlertConfig.java # 閰嶇疆瀹炰綋绫�
+鈹溾攢鈹� mapper/
+鈹� 鈹溾攢鈹� VehicleAbnormalAlertMapper.java # 鏁版嵁璁块棶鎺ュ彛
+鈹� 鈹溾攢鈹� SysTaskMapper.java # 鎵╁睍鏂规硶
+鈹� 鈹斺攢鈹� VehicleGpsSegmentMileageMapper.java # 鎵╁睍鏂规硶
+鈹溾攢鈹� service/
+鈹� 鈹溾攢鈹� IVehicleAbnormalAlertService.java # 涓氬姟鎺ュ彛
+鈹� 鈹斺攢鈹� impl/
+鈹� 鈹斺攢鈹� VehicleAbnormalAlertServiceImpl.java # 涓氬姟瀹炵幇
+鈹斺攢鈹� resources/mapper/system/
+ 鈹斺攢鈹� VehicleAbnormalAlertMapper.xml # MyBatis鏄犲皠
+
+ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/
+鈹斺攢鈹� VehicleAbnormalAlertController.java # REST鎺у埗鍣�
+
+ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/
+鈹斺攢鈹� VehicleAbnormalAlertTask.java # 瀹氭椂鐩戞帶浠诲姟
+```
+
+### 鏂囨。鏂囦欢
+```
+doc/
+鈹溾攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘璇存槑.md # 瀹屾暣鍔熻兘鏂囨。
+鈹溾攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅-蹇�熼儴缃叉寚鍗�.md # 閮ㄧ讲鎸囧崡
+鈹斺攢鈹� 杞﹁締寮傚父杩愯鐩戞帶鍛婅-瀹炵幇鎬荤粨.md # 鏈枃妗�
+```
+
+## 閮ㄧ讲妫�鏌ユ竻鍗�
+
+### 鏁版嵁搴撻儴缃�
+- [ ] 鎵ц vehicle_abnormal_alert.sql 鑴氭湰
+- [ ] 纭 tb_vehicle_abnormal_alert 琛ㄥ垱寤烘垚鍔�
+- [ ] 纭 tb_vehicle_alert_config 琛ㄥ垱寤烘垚鍔�
+- [ ] 纭 sys_config 琛ㄦ柊澧�6鏉¢厤缃褰�
+- [ ] 纭 sys_job 琛ㄦ柊澧炲畾鏃朵换鍔¤褰�
+- [ ] 纭 sys_menu 琛ㄦ柊澧炶彍鍗曡褰�
+
+### 鍚庣閮ㄧ讲
+- [ ] 缂栬瘧閫氳繃鏃犻敊璇�
+- [ ] 鍚姩搴旂敤鏃犲紓甯�
+- [ ] 鎺ュ彛璁块棶姝e父
+- [ ] 瀹氭椂浠诲姟鍙
+- [ ] 鑿滃崟鏉冮檺姝g‘
+
+### 閰嶇疆妫�鏌�
+- [ ] vehicle.alert.enabled = true
+- [ ] vehicle.alert.mileage.threshold 宸茶缃�
+- [ ] vehicle.alert.notify.users 宸查厤缃紙鎴栫‘璁ら�氱煡绛栫暐锛�
+- [ ] 浼佷笟寰俊閰嶇疆姝g‘锛堝闇�瑕侊級
+
+### 鍔熻兘楠岃瘉
+- [ ] 瀹氭椂浠诲姟鍙墜鍔ㄦ墽琛�
+- [ ] 鍛婅璁板綍鍙甯稿垱寤�
+- [ ] 鍛婅鍒楄〃鍙甯告煡璇�
+- [ ] 鍛婅鍙甯稿鐞�
+- [ ] 閫氱煡鍙甯稿彂閫�
+- [ ] 鏁版嵁鍙甯稿鍑�
+
+## 浣跨敤寤鸿
+
+### 鍒濇湡閰嶇疆
+1. **闃堝�艰缃�**锛氬缓璁粠杈冮珮鍊硷紙濡�20鍏噷锛夊紑濮嬶紝瑙傚療涓�鍛ㄥ悗璋冩暣
+2. **姣忔棩娆℃暟**锛氬缓璁缃负3-5娆★紝閬垮厤杩囧害楠氭壈
+3. **鏃堕棿闂撮殧**锛氬缓璁嚦灏�5鍒嗛挓锛岀粰澶勭悊鐣欏嚭鏃堕棿
+4. **閫氱煡鐢ㄦ埛**锛氬垵鏈熼厤缃皯鏁拌礋璐d汉锛岄�愭鎵╁ぇ鑼冨洿
+
+### 杩愯惀寤鸿
+1. **瀹氭湡鍥為【**锛氭瘡鍛ㄦ煡鐪嬪憡璀︽暟鎹紝鍒嗘瀽寮傚父妯″紡
+2. **瑙勫垯浼樺寲**锛氭牴鎹疄闄呮儏鍐佃皟鏁撮槇鍊煎拰棰戠巼
+3. **璇姤澶勭悊**锛氬棰戠箒璇姤鐨勮溅杈嗗彲鍒涘缓涓撳睘閰嶇疆
+4. **鏁版嵁鍒嗘瀽**锛氬鍑烘暟鎹繘琛岃秼鍔垮垎鏋愬拰鏁堟灉璇勪及
+
+### 鎬ц兘寤鸿
+1. **绱㈠紩缁存姢**锛氬畾鏈熸鏌ュ拰浼樺寲鏁版嵁搴撶储寮�
+2. **鏁版嵁娓呯悊**锛氬缓璁繚鐣�3涓湀鐨勫憡璀﹁褰曪紝瀹氭湡褰掓。鍘嗗彶鏁版嵁
+3. **鏃ュ織绾у埆**锛氱敓浜х幆澧冧娇鐢↖NFO绾у埆锛屽噺灏戞棩蹇楄緭鍑�
+4. **鐩戞帶棰戠巼**锛氳溅杈嗘暟閲忓ぇ鏃跺彲閫傚綋寤堕暱鎵ц闂撮殧
+
+## 宸茬煡闄愬埗
+
+1. **GPS鏁版嵁渚濊禆**锛氫緷璧朑PS鍒嗘閲岀▼璁$畻浠诲姟姝e父杩愯
+2. **閫氱煡鏂瑰紡**锛氱洰鍓嶄粎鏀寔浼佷笟寰俊锛屽皬绋嬪簭閫氱煡闇�瑕佽繘涓�姝ュ紑鍙�
+3. **閰嶇疆鐣岄潰**锛氬憡璀﹂厤缃〃鏆傛棤鍓嶇绠$悊鐣岄潰锛岄渶閫氳繃鏁版嵁搴撴搷浣�
+4. **缁熻鎶ヨ〃**锛氭殏鏃犲浘琛ㄥ寲鐨勭粺璁″垎鏋愬姛鑳�
+
+## 鍚庣画浼樺寲鏂瑰悜
+
+### 鐭湡浼樺寲锛�1-2鍛級
+1. [ ] 娣诲姞鍛婅閰嶇疆绠$悊鐣岄潰
+2. [ ] 浼樺寲閫氱煡娑堟伅妯℃澘
+3. [ ] 娣诲姞鍛婅缁熻鍥捐〃
+4. [ ] 瀹屽杽鍓嶇绠$悊椤甸潰
+
+### 涓湡浼樺寲锛�1-2鏈堬級
+1. [ ] 鏀寔澶氱鍛婅绫诲瀷锛堣秴閫熴�侀暱鏃堕棿鍋滆溅绛夛級
+2. [ ] 瀹炵幇鍛婅瑙勫垯寮曟搸
+3. [ ] 娣诲姞绉诲姩绔帹閫�
+4. [ ] 鏀寔鍛婅鍗囩骇鏈哄埗
+
+### 闀挎湡浼樺寲锛�3-6鏈堬級
+1. [ ] 鏅鸿兘鍛婅鎺ㄨ崘
+2. [ ] 鍛婅瓒嬪娍棰勬祴
+3. [ ] 鑷姩鍖栧鐞嗗缓璁�
+4. [ ] 澶ф暟鎹垎鏋愭姤鍛�
+
+## 鎶�鏈敮鎸�
+
+濡傛湁闂鎴栧缓璁紝璇峰弬鑰冿細
+- **鍔熻兘鏂囨。**锛歞oc/杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘璇存槑.md
+- **閮ㄧ讲鎸囧崡**锛歞oc/杞﹁締寮傚父杩愯鐩戞帶鍛婅-蹇�熼儴缃叉寚鍗�.md
+- **绯荤粺鏃ュ織**锛歭ogs/sys-info.log
+- **浠诲姟鏃ュ織**锛氱郴缁熺洃鎺� > 璋冨害鏃ュ織
+
+## 鐗堟湰淇℃伅
+
+- **鐗堟湰鍙�**锛歏1.0.0
+- **鍙戝竷鏃ユ湡**锛�2026-01-12
+- **寮�鍙戞ā寮�**锛氬叏閲忎氦浠�
+- **娴嬭瘯鐘舵��**锛氬緟娴嬭瘯楠岃瘉
+- **鐢熶骇鐘舵��**锛氬緟閮ㄧ讲涓婄嚎
+
+---
+
+**瀹炵幇璇存槑**锛氭湰鍔熻兘宸插畬鏁村疄鐜版墍鏈夋牳蹇冩ā鍧楋紝鍖呮嫭鏁版嵁搴撹璁°�佸悗绔唬鐮併�佸畾鏃朵换鍔°�佺郴缁熼泦鎴愬拰瀹屾暣鏂囨。銆傛墍鏈変唬鐮佸潎宸插垱寤哄苟淇濆瓨鍒扮浉搴旀枃浠讹紝鍙洿鎺ラ儴缃蹭娇鐢ㄣ��
diff --git "a/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\277\253\351\200\237\351\203\250\347\275\262\346\214\207\345\215\227.md" "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\277\253\351\200\237\351\203\250\347\275\262\346\214\207\345\215\227.md"
new file mode 100644
index 0000000..e5547dc
--- /dev/null
+++ "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\345\277\253\351\200\237\351\203\250\347\275\262\346\214\207\345\215\227.md"
@@ -0,0 +1,262 @@
+# 杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘 - 蹇�熼儴缃叉寚鍗�
+
+## 鍔熻兘姒傝堪
+鐩戞帶杞﹁締鏃犱换鍔¤繍琛岃秴杩囬厤缃叕閲屾暟锛堥粯璁�10鍏噷锛夋椂鑷姩浜х敓鍛婅锛屽苟閫氳繃灏忕▼搴�/浼佷笟寰俊閫氱煡鐩稿叧璐熻矗浜恒��
+
+## 蹇�熼儴缃诧紙5姝ュ畬鎴愶級
+
+### 绗�1姝ワ細鎵ц鏁版嵁搴撹剼鏈�
+```bash
+cd /path/to/RuoYi-Vue-master
+mysql -u鐢ㄦ埛鍚� -p瀵嗙爜 鏁版嵁搴撳悕 < sql/vehicle_abnormal_alert.sql
+```
+
+**鑴氭湰鍐呭鍖呮嫭**锛�
+- 鉁� 鍒涘缓鍛婅璁板綍琛� (tb_vehicle_abnormal_alert)
+- 鉁� 鍒涘缓鍛婅閰嶇疆琛� (tb_vehicle_alert_config)
+- 鉁� 鎻掑叆绯荤粺閰嶇疆鍙傛暟 (6涓厤缃」)
+- 鉁� 鍒涘缓瀹氭椂浠诲姟 (vehicle Abnormal Alert Task)
+- 鉁� 鍒涘缓鑿滃崟鏉冮檺 (杞﹁締寮傚父鍛婅绠$悊)
+
+### 绗�2姝ワ細閰嶇疆閫氱煡鐢ㄦ埛
+鍦�"绯荤粺绠$悊 > 鍙傛暟璁剧疆"涓厤缃互涓嬪弬鏁帮細
+
+| 鍙傛暟鍚嶇О | 鍙傛暟閿悕 | 閰嶇疆鍊肩ず渚� | 璇存槑 |
+|---------|---------|-----------|------|
+| 杞﹁締寮傚父鍛婅閫氱煡鐢ㄦ埛 | vehicle.alert.notify.users | 1,2,3 | 鎺ユ敹閫氱煡鐨勭敤鎴稩D锛岄�楀彿鍒嗛殧 |
+
+> 馃挕 **鎻愮ず**锛氬鏋滀笉閰嶇疆锛屽皢灏濊瘯鏍规嵁杞﹁締褰掑睘閮ㄩ棬鏌ユ壘璐熻矗浜�
+
+### 绗�3姝ワ細鍚敤瀹氭椂浠诲姟
+1. 鐧诲綍鍚庡彴绠$悊绯荤粺
+2. 杩涘叆"绯荤粺鐩戞帶 > 瀹氭椂浠诲姟"
+3. 鎵惧埌"杞﹁締寮傚父杩愯鐩戞帶浠诲姟"
+4. 鐐瑰嚮鐘舵�佸紑鍏筹紝灏嗗叾璁剧疆涓�"杩愯涓�"
+
+### 绗�4姝ワ細閰嶇疆浼佷笟寰俊锛堝彲閫夛級
+濡傞渶浣跨敤浼佷笟寰俊閫氱煡鍔熻兘锛屽湪"绯荤粺绠$悊 > 鍙傛暟璁剧疆"涓‘淇濅互涓嬮厤缃細
+
+| 鍙傛暟閿悕 | 璇存槑 | 绀轰緥鍊� |
+|---------|------|--------|
+| qy_wechat.enable | 浼佷笟寰俊鍚敤寮�鍏� | true |
+| qy_wechat.corp_id | 浼佷笟ID | ww123456789 |
+| qy_wechat.corp_secret | 搴旂敤Secret | xxxxx |
+| qy_wechat.agent_id | 搴旂敤ID | 1000002 |
+
+### 绗�5姝ワ細楠岃瘉鍔熻兘
+1. 纭繚GPS鍒嗘閲岀▼璁$畻浠诲姟姝e父杩愯
+2. 鏌ョ湅"杞﹁締绠$悊 > 杞﹁締寮傚父鍛婅"鑿滃崟鏄惁鏄剧ず
+3. 瑙傚療瀹氭椂浠诲姟鎵ц鏃ュ織
+4. 娴嬭瘯鍛婅鐢熸垚鍜岄�氱煡鍙戦��
+
+## 鏍稿績閰嶇疆鍙傛暟璇存槑
+
+### 蹇呴渶閰嶇疆
+| 閰嶇疆閿� | 榛樿鍊� | 璇存槑 |
+|-------|--------|------|
+| vehicle.alert.enabled | true | 鍔熻兘鎬诲紑鍏筹紝false鍒欏仠鐢� |
+| vehicle.alert.mileage.threshold | 10 | 鍛婅闃堝�硷紙鍏噷锛� |
+
+### 鍙�夐厤缃�
+| 閰嶇疆閿� | 榛樿鍊� | 璇存槑 |
+|-------|--------|------|
+| vehicle.alert.daily.limit | 5 | 姣忚溅姣忓ぉ鏈�澶氬憡璀︽鏁� |
+| vehicle.alert.interval.minutes | 5 | 涓ゆ鍛婅鏈�灏忛棿闅旓紙鍒嗛挓锛� |
+| vehicle.alert.time.window | 10 | 鐩戞帶鏃堕棿绐楀彛锛堝垎閽燂級 |
+| vehicle.alert.notify.users | 锛堢┖锛� | 閫氱煡鐢ㄦ埛ID鍒楄〃 |
+
+## 瀹氭椂浠诲姟璇存槑
+
+### 浠诲姟閰嶇疆
+- **浠诲姟鍚嶇О**锛氳溅杈嗗紓甯歌繍琛岀洃鎺т换鍔�
+- **鎵ц棰戠巼**锛歚0 */5 * * * ?` (姣�5鍒嗛挓鎵ц涓�娆�)
+- **璋冪敤鏂规硶**锛歚vehicleAbnormalAlertTask.monitorVehicleAbnormalRunning()`
+- **骞跺彂鎺у埗**锛氱姝㈠苟鍙�
+- **榛樿鐘舵��**锛氭殏鍋滐紙闇�鎵嬪姩鍚敤锛�
+
+### 鐩戞帶閫昏緫
+```
+姣�5鍒嗛挓鎵ц涓�娆�
+ 鈫�
+鏌ヨ鎵�鏈夎溅杈�
+ 鈫�
+閫愯溅妫�鏌ワ紙骞惰澶勭悊锛�
+ 鈹溾攢 妫�鏌ユ槸鍚︽湁姝e湪鎵ц鐨勪换鍔�
+ 鈹溾攢 璁$畻鏈�杩�10鍒嗛挓杩愯閲岀▼
+ 鈹溾攢 鍒ゆ柇鏄惁瓒呰繃闃堝��
+ 鈹溾攢 妫�鏌ュ憡璀﹂鐜囬檺鍒�
+ 鈹斺攢 鍒涘缓鍛婅骞跺彂閫侀�氱煡
+```
+
+## 鏉冮檺閰嶇疆
+
+### 鑿滃崟鏉冮檺
+- **鐖惰彍鍗�**锛氳溅杈嗙鐞�
+- **鑿滃崟鍚嶇О**锛氳溅杈嗗紓甯稿憡璀�
+- **鏉冮檺鏍囪瘑**锛歴ystem:vehicleAlert:list
+
+### 鎸夐挳鏉冮檺
+- `system:vehicleAlert:query` - 鏌ヨ鍛婅
+- `system:vehicleAlert:detail` - 鍛婅璇︽儏
+- `system:vehicleAlert:handle` - 澶勭悊鍛婅
+- `system:vehicleAlert:export` - 瀵煎嚭鏁版嵁
+- `system:vehicleAlert:config` - 閰嶇疆绠$悊
+
+## 甯歌闂
+
+### Q1: 鍛婅涓嶇敓鎴愶紵
+**鎺掓煡姝ラ**锛�
+1. 妫�鏌ュ畾鏃朵换鍔℃槸鍚﹀惎鐢�
+2. 鏌ョ湅瀹氭椂浠诲姟鎵ц鏃ュ織锛歚绯荤粺鐩戞帶 > 璋冨害鏃ュ織`
+3. 纭 `vehicle.alert.enabled = true`
+4. 纭GPS鍒嗘閲岀▼琛ㄦ湁鏁版嵁
+5. 妫�鏌ヨ溅杈嗘槸鍚︾湡鐨勬棤浠诲姟杩愯
+
+### Q2: 閫氱煡涓嶅彂閫侊紵
+**鎺掓煡姝ラ**锛�
+1. 妫�鏌ヤ紒涓氬井淇¢厤缃槸鍚︽纭�
+2. 纭 `qy_wechat.enable = true`
+3. 鏌ョ湅鍚庡彴鏃ュ織锛氭悳绱�"鍙戦�佸憡璀﹂�氱煡"
+4. 纭鐢ㄦ埛宸茬粦瀹氫紒涓氬井淇D
+
+### Q3: 鍛婅澶绻侊紵
+**瑙e喅鏂规**锛�
+1. 璋冮珮鍏噷鏁伴槇鍊硷細`vehicle.alert.mileage.threshold = 20`
+2. 鍑忓皯姣忔棩鍛婅娆℃暟锛歚vehicle.alert.daily.limit = 3`
+3. 澧炲姞鍛婅闂撮殧锛歚vehicle.alert.interval.minutes = 10`
+
+### Q4: GPS閲岀▼鏁版嵁涓嶅噯锛�
+**妫�鏌ラ」**锛�
+1. 纭GPS鍒嗘閲岀▼璁$畻浠诲姟姝e父杩愯
+2. 鏌ョ湅 `tb_vehicle_gps_segment_mileage` 琛ㄦ暟鎹�
+3. 璋冩暣鐩戞帶鏃堕棿绐楀彛锛歚vehicle.alert.time.window = 15`
+
+## 娴嬭瘯鎸囧崡
+
+### 娴嬭瘯姝ラ
+1. **鍑嗗娴嬭瘯鏁版嵁**
+ - 纭繚鏈夎溅杈咷PS鏁版嵁
+ - 纭繚GPS鍒嗘閲岀▼璁$畻浠诲姟宸茶繍琛�
+ - 閫夋嫨涓�杈嗘棤浠诲姟鐨勮溅杈�
+
+2. **璋冩暣閰嶇疆渚夸簬瑙﹀彂**
+ ```sql
+ UPDATE sys_config SET config_value = '1'
+ WHERE config_key = 'vehicle.alert.mileage.threshold';
+ ```
+
+3. **鎵嬪姩瑙﹀彂瀹氭椂浠诲姟**
+ - 杩涘叆"绯荤粺鐩戞帶 > 瀹氭椂浠诲姟"
+ - 鎵惧埌"杞﹁締寮傚父杩愯鐩戞帶浠诲姟"
+ - 鐐瑰嚮"鎵ц涓�娆�"鎸夐挳
+
+4. **鏌ョ湅鎵ц缁撴灉**
+ - 鏌ョ湅"绯荤粺鐩戞帶 > 璋冨害鏃ュ織"
+ - 鏌ョ湅"杞﹁締绠$悊 > 杞﹁締寮傚父鍛婅"鍒楄〃
+ - 妫�鏌ユ槸鍚︽敹鍒颁紒涓氬井淇¢�氱煡
+
+5. **鎭㈠閰嶇疆**
+ ```sql
+ UPDATE sys_config SET config_value = '10'
+ WHERE config_key = 'vehicle.alert.mileage.threshold';
+ ```
+
+## 鎬ц兘浼樺寲寤鸿
+
+### 鏁版嵁搴撶储寮曪紙宸茶嚜鍔ㄥ垱寤猴級
+```sql
+-- 鍛婅璁板綍琛ㄧ储寮�
+CREATE INDEX idx_vehicle_id ON tb_vehicle_abnormal_alert(vehicle_id);
+CREATE INDEX idx_alert_date ON tb_vehicle_abnormal_alert(alert_date);
+CREATE INDEX idx_vehicle_date ON tb_vehicle_abnormal_alert(vehicle_id, alert_date);
+CREATE INDEX idx_status ON tb_vehicle_abnormal_alert(status);
+```
+
+### 鐩戞帶寤鸿
+- **杞﹁締鏁� < 100**锛氫娇鐢ㄩ粯璁ら厤缃�
+- **杞﹁締鏁� 100-500**锛氳�冭檻澧炲姞鏃堕棿绐楀彛涓�15鍒嗛挓
+- **杞﹁締鏁� > 500**锛氳�冭檻澧炲姞鎵ц闂撮殧涓�10鍒嗛挓
+
+### 鏃ュ織绾у埆
+鍦ㄧ敓浜х幆澧冿紝寤鸿璁剧疆鏃ュ織绾у埆涓� INFO锛�
+```yaml
+logging:
+ level:
+ com.ruoyi.quartz.task.VehicleAbnormalAlertTask: INFO
+```
+
+## 鏁呴殰鎺掓煡
+
+### 鏃ュ織浣嶇疆
+- **搴旂敤鏃ュ織**锛歚logs/sys-info.log`
+- **瀹氭椂浠诲姟鏃ュ織**锛氭暟鎹簱琛� `sys_job_log`
+
+### 鍏抽敭鏃ュ織鎼滅储
+```bash
+# 鐩戞帶浠诲姟鎵ц鏃ュ織
+grep "杞﹁締寮傚父杩愯鐩戞帶" logs/sys-info.log
+
+# 鍛婅鐢熸垚鏃ュ織
+grep "浜х敓寮傚父鍛婅" logs/sys-info.log
+
+# 閫氱煡鍙戦�佹棩蹇�
+grep "鍙戦�佸憡璀﹂�氱煡" logs/sys-info.log
+```
+
+### 鏁版嵁搴撴鏌�
+```sql
+-- 鏌ョ湅浠婃棩鍛婅缁熻
+SELECT status, COUNT(*) as count
+FROM tb_vehicle_abnormal_alert
+WHERE DATE(alert_date) = CURDATE()
+GROUP BY status;
+
+-- 鏌ョ湅鍛婅棰戠箒鐨勮溅杈�
+SELECT vehicle_no, COUNT(*) as alert_count
+FROM tb_vehicle_abnormal_alert
+WHERE DATE(alert_date) = CURDATE()
+GROUP BY vehicle_no
+ORDER BY alert_count DESC
+LIMIT 10;
+
+-- 鏌ョ湅瀹氭椂浠诲姟鎵ц鎯呭喌
+SELECT job_name, status, job_message, create_time
+FROM sys_job_log
+WHERE job_name = '杞﹁締寮傚父杩愯鐩戞帶浠诲姟'
+ORDER BY create_time DESC
+LIMIT 10;
+```
+
+## 鎶�鏈敮鎸�
+
+濡傞亣鍒伴棶棰橈細
+1. 鏌ョ湅鏈枃妗g殑"甯歌闂"閮ㄥ垎
+2. 妫�鏌ョ郴缁熸棩蹇楀拰瀹氭椂浠诲姟鏃ュ織
+3. 鏌ヨ鏁版嵁搴撹〃纭鏁版嵁鐘舵��
+4. 鑱旂郴鎶�鏈敮鎸佸洟闃�
+
+## 鏂囦欢娓呭崟
+
+| 鏂囦欢绫诲瀷 | 鏂囦欢璺緞 | 璇存槑 |
+|---------|---------|------|
+| SQL鑴氭湰 | sql/vehicle_abnormal_alert.sql | 鏁版嵁搴撳垵濮嬪寲鑴氭湰 |
+| 瀹炰綋绫� | ruoyi-system/.../domain/VehicleAbnormalAlert.java | 鍛婅瀹炰綋 |
+| Mapper | ruoyi-system/.../mapper/VehicleAbnormalAlertMapper.java | 鏁版嵁璁块棶 |
+| Service | ruoyi-system/.../service/impl/VehicleAbnormalAlertServiceImpl.java | 涓氬姟閫昏緫 |
+| Controller | ruoyi-admin/.../controller/system/VehicleAbnormalAlertController.java | 鎺ュ彛鎺у埗鍣� |
+| 瀹氭椂浠诲姟 | ruoyi-quartz/.../task/VehicleAbnormalAlertTask.java | 鐩戞帶浠诲姟 |
+| 閰嶇疆鏂囨。 | doc/杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘璇存槑.md | 瀹屾暣鏂囨。 |
+| 閮ㄧ讲鎸囧崡 | 鏈枃妗� | 蹇�熼儴缃� |
+
+---
+
+**閮ㄧ讲瀹屾垚鍚�**锛岃纭锛�
+- [ ] 鏁版嵁搴撹〃鍒涘缓鎴愬姛
+- [ ] 鑿滃崟鏄剧ず姝e父
+- [ ] 瀹氭椂浠诲姟宸插惎鐢�
+- [ ] 閰嶇疆鍙傛暟宸茶缃�
+- [ ] 娴嬭瘯鍛婅鐢熸垚鎴愬姛
+- [ ] 閫氱煡鍙戦�佹甯�
+
+**鐗堟湰**: V1.0.0
+**鏇存柊鏃ユ湡**: 2026-01-12
diff --git "a/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\350\217\234\345\215\225\351\205\215\347\275\256\350\257\264\346\230\216.md" "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\350\217\234\345\215\225\351\205\215\347\275\256\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..5845b52
--- /dev/null
+++ "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246-\350\217\234\345\215\225\351\205\215\347\275\256\350\257\264\346\230\216.md"
@@ -0,0 +1,350 @@
+# 杞﹁締寮傚父杩愯鐩戞帶鍛婅 - 鑿滃崟閰嶇疆璇存槑
+
+## 馃搵 姒傝堪
+
+鏈枃妗h缁嗚鏄庤溅杈嗗紓甯歌繍琛岀洃鎺у憡璀﹀姛鑳界殑鍚庡彴鑿滃崟缁撴瀯鍜屾潈闄愰厤缃�傛墽琛孲QL鑴氭湰鍚庝細鑷姩鍒涘缓瀹屾暣鐨勮彍鍗曠粨鏋勩��
+
+## 馃幆 鑿滃崟缁撴瀯
+
+### 涓�绾ц彍鍗曪細杞﹁締鐩戞帶
+
+```
+杞﹁締鐩戞帶 (vehicle-monitor) - 鐩綍鑿滃崟
+鈹溾攢鈹� 杞﹁締寮傚父鍛婅 (vehicleAlert) - 鑿滃崟
+鈹� 鈹溾攢鈹� 鍛婅鏌ヨ - 鎸夐挳鏉冮檺
+鈹� 鈹溾攢鈹� 澶勭悊鍛婅 - 鎸夐挳鏉冮檺
+鈹� 鈹溾攢鈹� 鍒犻櫎鍛婅 - 鎸夐挳鏉冮檺
+鈹� 鈹斺攢鈹� 瀵煎嚭鍛婅 - 鎸夐挳鏉冮檺
+鈹斺攢鈹� 鍛婅閰嶇疆绠$悊 (vehicleAlertConfig) - 鑿滃崟
+ 鈹溾攢鈹� 閰嶇疆鏌ヨ - 鎸夐挳鏉冮檺
+ 鈹溾攢鈹� 鏂板閰嶇疆 - 鎸夐挳鏉冮檺
+ 鈹溾攢鈹� 淇敼閰嶇疆 - 鎸夐挳鏉冮檺
+ 鈹溾攢鈹� 鍒犻櫎閰嶇疆 - 鎸夐挳鏉冮檺
+ 鈹斺攢鈹� 瀵煎嚭閰嶇疆 - 鎸夐挳鏉冮檺
+```
+
+## 馃搳 璇︾粏閰嶇疆
+
+### 1. 杞﹁締鐩戞帶锛堢埗鑿滃崟锛�
+
+| 瀛楁 | 鍊� |
+|------|-----|
+| 鑿滃崟鍚嶇О | 杞﹁締鐩戞帶 |
+| 鐖惰彍鍗� | 椤剁骇鑿滃崟 (0) |
+| 鏄剧ず鎺掑簭 | 5 |
+| 璺敱鍦板潃 | vehicle-monitor |
+| 鑿滃崟绫诲瀷 | 鐩綍 (M) |
+| 鑿滃崟鍥炬爣 | monitor |
+| 鏄惁鍙 | 鏄� |
+| 鑿滃崟鐘舵�� | 姝e父 |
+| 澶囨敞 | 杞﹁締鐩戞帶绠$悊鐩綍 |
+
+**鐗圭偣**锛�
+- 馃搧 鐩綍绫诲瀷锛屼笉瀵瑰簲鍏蜂綋椤甸潰
+- 馃帹 浣跨敤monitor鍥炬爣
+- 馃搷 椤剁骇鑿滃崟锛屾樉绀哄湪宸︿晶瀵艰埅鏍�
+
+### 2. 杞﹁締寮傚父鍛婅锛堝瓙鑿滃崟锛�
+
+| 瀛楁 | 鍊� |
+|------|-----|
+| 鑿滃崟鍚嶇О | 杞﹁締寮傚父鍛婅 |
+| 鐖惰彍鍗� | 杞﹁締鐩戞帶 |
+| 鏄剧ず鎺掑簭 | 1 |
+| 璺敱鍦板潃 | vehicleAlert |
+| 缁勪欢璺緞 | system/vehicleAlert/index |
+| 鑿滃崟绫诲瀷 | 鑿滃崟 (C) |
+| 鑿滃崟鍥炬爣 | warning |
+| 鏄惁缂撳瓨 | 鍚� |
+| 鏉冮檺鏍囪瘑 | system:vehicleAlert:list |
+| 澶囨敞 | 杞﹁締寮傚父杩愯鍛婅绠$悊 |
+
+**鍔熻兘鎸夐挳鏉冮檺**锛�
+
+#### 2.1 鍛婅鏌ヨ
+- **鏉冮檺鏍囪瘑**: `system:vehicleAlert:query`
+- **鐢ㄩ��**: 鏌ヨ鍛婅鍒楄〃鍜岃鎯�
+
+#### 2.2 澶勭悊鍛婅
+- **鏉冮檺鏍囪瘑**: `system:vehicleAlert:handle`
+- **鐢ㄩ��**: 澶勭悊鍗曟潯鎴栨壒閲忓鐞嗗憡璀�
+
+#### 2.3 鍒犻櫎鍛婅
+- **鏉冮檺鏍囪瘑**: `system:vehicleAlert:remove`
+- **鐢ㄩ��**: 鍒犻櫎鍛婅璁板綍
+
+#### 2.4 瀵煎嚭鍛婅
+- **鏉冮檺鏍囪瘑**: `system:vehicleAlert:export`
+- **鐢ㄩ��**: 瀵煎嚭鍛婅鏁版嵁涓篍xcel
+
+### 3. 鍛婅閰嶇疆绠$悊锛堝瓙鑿滃崟锛�
+
+| 瀛楁 | 鍊� |
+|------|-----|
+| 鑿滃崟鍚嶇О | 鍛婅閰嶇疆绠$悊 |
+| 鐖惰彍鍗� | 杞﹁締鐩戞帶 |
+| 鏄剧ず鎺掑簭 | 2 |
+| 璺敱鍦板潃 | vehicleAlertConfig |
+| 缁勪欢璺緞 | system/vehicleAlertConfig/index |
+| 鑿滃崟绫诲瀷 | 鑿滃崟 (C) |
+| 鑿滃崟鍥炬爣 | edit |
+| 鏄惁缂撳瓨 | 鍚� |
+| 鏉冮檺鏍囪瘑 | system:vehicleAlertConfig:list |
+| 澶囨敞 | 杞﹁締鍛婅閰嶇疆绠$悊 |
+
+**鍔熻兘鎸夐挳鏉冮檺**锛�
+
+#### 3.1 閰嶇疆鏌ヨ
+- **鏉冮檺鏍囪瘑**: `system:vehicleAlertConfig:query`
+- **鐢ㄩ��**: 鏌ヨ閰嶇疆鍒楄〃鍜岃鎯�
+
+#### 3.2 鏂板閰嶇疆
+- **鏉冮檺鏍囪瘑**: `system:vehicleAlertConfig:add`
+- **鐢ㄩ��**: 鏂板鍏ㄥ眬/閮ㄩ棬/杞﹁締閰嶇疆
+
+#### 3.3 淇敼閰嶇疆
+- **鏉冮檺鏍囪瘑**: `system:vehicleAlertConfig:edit`
+- **鐢ㄩ��**: 淇敼宸叉湁閰嶇疆
+
+#### 3.4 鍒犻櫎閰嶇疆
+- **鏉冮檺鏍囪瘑**: `system:vehicleAlertConfig:remove`
+- **鐢ㄩ��**: 鍒犻櫎閰嶇疆璁板綍
+
+#### 3.5 瀵煎嚭閰嶇疆
+- **鏉冮檺鏍囪瘑**: `system:vehicleAlertConfig:export`
+- **鐢ㄩ��**: 瀵煎嚭閰嶇疆鏁版嵁涓篍xcel
+
+## 馃攼 鏉冮檺鏍囪瘑鍒楄〃
+
+### 杞﹁締寮傚父鍛婅妯″潡
+
+| 鏉冮檺鏍囪瘑 | 璇存槑 | 绫诲瀷 |
+|---------|------|------|
+| `system:vehicleAlert:list` | 鍛婅鍒楄〃 | 鑿滃崟璁块棶 |
+| `system:vehicleAlert:query` | 鍛婅鏌ヨ | 鎸夐挳鏉冮檺 |
+| `system:vehicleAlert:handle` | 澶勭悊鍛婅 | 鎸夐挳鏉冮檺 |
+| `system:vehicleAlert:remove` | 鍒犻櫎鍛婅 | 鎸夐挳鏉冮檺 |
+| `system:vehicleAlert:export` | 瀵煎嚭鍛婅 | 鎸夐挳鏉冮檺 |
+
+### 鍛婅閰嶇疆绠$悊妯″潡
+
+| 鏉冮檺鏍囪瘑 | 璇存槑 | 绫诲瀷 |
+|---------|------|------|
+| `system:vehicleAlertConfig:list` | 閰嶇疆鍒楄〃 | 鑿滃崟璁块棶 |
+| `system:vehicleAlertConfig:query` | 閰嶇疆鏌ヨ | 鎸夐挳鏉冮檺 |
+| `system:vehicleAlertConfig:add` | 鏂板閰嶇疆 | 鎸夐挳鏉冮檺 |
+| `system:vehicleAlertConfig:edit` | 淇敼閰嶇疆 | 鎸夐挳鏉冮檺 |
+| `system:vehicleAlertConfig:remove` | 鍒犻櫎閰嶇疆 | 鎸夐挳鏉冮檺 |
+| `system:vehicleAlertConfig:export` | 瀵煎嚭閰嶇疆 | 鎸夐挳鏉冮檺 |
+
+## 馃殌 SQL鑴氭湰璇存槑
+
+### 鑴氭湰鐗圭偣
+
+1. **闃查噸澶嶆墽琛�**: 浣跨敤 `NOT EXISTS` 妫�鏌ワ紝閬垮厤閲嶅鎻掑叆
+2. **鍔ㄦ�佽幏鍙朓D**: 浣跨敤鍙橀噺瀛樺偍鐖惰彍鍗旾D
+3. **椤哄簭鎵ц**: 鍏堝垱寤虹埗鑿滃崟锛屽啀鍒涘缓瀛愯彍鍗曞拰鎸夐挳
+4. **瀹屾暣鎬�**: 涓�娆℃�у垱寤烘墍鏈夎彍鍗曞拰鏉冮檺
+
+### 鍏抽敭SQL璇彞
+
+#### 1. 鍒涘缓鐖惰彍鍗曪紙闃查噸澶嶏級
+```sql
+INSERT INTO sys_menu (...)
+SELECT '杞﹁締鐩戞帶', 0, 5, 'vehicle-monitor', ...
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_name = '杞﹁締鐩戞帶' AND menu_type = 'M');
+```
+
+#### 2. 鑾峰彇鐖惰彍鍗旾D
+```sql
+SET @vehicleMonitorMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = '杞﹁締鐩戞帶' AND menu_type = 'M' LIMIT 1);
+```
+
+#### 3. 鍒涘缓瀛愯彍鍗�
+```sql
+INSERT INTO sys_menu (...)
+SELECT '杞﹁締寮傚父鍛婅', @vehicleMonitorMenuId, 1, 'vehicleAlert', ...
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_name = '杞﹁締寮傚父鍛婅' AND perms = 'system:vehicleAlert:list');
+```
+
+## 馃帹 鍓嶇璺敱閰嶇疆
+
+濡傛灉浣跨敤鍔ㄦ�佽矾鐢憋紙浠庡悗绔姞杞借彍鍗曪級锛屾棤闇�棰濆閰嶇疆銆傚鏋滀娇鐢ㄩ潤鎬佽矾鐢憋紝闇�鍦� `router/index.js` 涓坊鍔狅細
+
+```javascript
+{
+ path: '/vehicle-monitor',
+ component: Layout,
+ redirect: '/vehicle-monitor/vehicleAlert',
+ name: 'VehicleMonitor',
+ meta: { title: '杞﹁締鐩戞帶', icon: 'monitor' },
+ children: [
+ {
+ path: 'vehicleAlert',
+ component: () => import('@/views/system/vehicleAlert/index'),
+ name: 'VehicleAlert',
+ meta: { title: '杞﹁締寮傚父鍛婅', icon: 'warning' }
+ },
+ {
+ path: 'vehicleAlertConfig',
+ component: () => import('@/views/system/vehicleAlertConfig/index'),
+ name: 'VehicleAlertConfig',
+ meta: { title: '鍛婅閰嶇疆绠$悊', icon: 'edit' }
+ }
+ ]
+}
+```
+
+## 馃懃 瑙掕壊鏉冮檺鍒嗛厤
+
+### 绠$悊鍛樿鑹诧紙admin锛�
+寤鸿鍒嗛厤鎵�鏈夋潈闄愶細
+- 鉁� 鍛婅鍒楄〃璁块棶
+- 鉁� 鍛婅鏌ヨ
+- 鉁� 澶勭悊鍛婅
+- 鉁� 鍒犻櫎鍛婅
+- 鉁� 瀵煎嚭鍛婅
+- 鉁� 閰嶇疆绠$悊鎵�鏈夋潈闄�
+
+### 鏅�氱敤鎴疯鑹诧紙user锛�
+寤鸿鍒嗛厤鍩虹鏉冮檺锛�
+- 鉁� 鍛婅鍒楄〃璁块棶
+- 鉁� 鍛婅鏌ヨ
+- 鉁� 澶勭悊鍛婅
+- 鉂� 鍒犻櫎鍛婅
+- 鉁� 瀵煎嚭鍛婅
+- 鉂� 閰嶇疆绠$悊鏉冮檺
+
+### 鏌ョ湅鑰呰鑹诧紙viewer锛�
+寤鸿鍒嗛厤鍙鏉冮檺锛�
+- 鉁� 鍛婅鍒楄〃璁块棶
+- 鉁� 鍛婅鏌ヨ
+- 鉂� 澶勭悊鍛婅
+- 鉂� 鍒犻櫎鍛婅
+- 鉁� 瀵煎嚭鍛婅
+- 鉂� 閰嶇疆绠$悊鏉冮檺
+
+## 馃敡 瑙掕壊鍒嗛厤姝ラ
+
+### 鏂瑰紡涓�锛氶�氳繃鍚庡彴绠$悊鐣岄潰
+
+1. 鐧诲綍鍚庡彴绯荤粺
+2. 杩涘叆 **绯荤粺绠$悊 > 瑙掕壊绠$悊**
+3. 閫夋嫨瑕佸垎閰嶆潈闄愮殑瑙掕壊锛岀偣鍑�"淇敼"
+4. 鍦�"鑿滃崟鏉冮檺"涓嬀閫夐渶瑕佺殑鑿滃崟鍜屾寜閽�
+5. 鐐瑰嚮"纭畾"淇濆瓨
+
+### 鏂瑰紡浜岋細閫氳繃SQL鐩存帴鍒嗛厤
+
+```sql
+-- 涓鸿鑹睮D=2锛堢ず渚嬶級鍒嗛厤鍛婅绠$悊鏉冮檺
+INSERT INTO sys_role_menu (role_id, menu_id)
+SELECT 2, menu_id FROM sys_menu
+WHERE perms LIKE 'system:vehicleAlert:%' OR perms LIKE 'system:vehicleAlertConfig:%';
+```
+
+## 馃搵 楠岃瘉娓呭崟
+
+鎵цSQL鑴氭湰鍚庯紝璇烽獙璇佷互涓嬪唴瀹癸細
+
+### 1. 鑿滃崟鍒涘缓楠岃瘉
+```sql
+-- 鏌ヨ杞﹁締鐩戞帶鑿滃崟缁撴瀯
+SELECT
+ m1.menu_name AS '涓�绾ц彍鍗�',
+ m2.menu_name AS '浜岀骇鑿滃崟',
+ m3.menu_name AS '鎸夐挳',
+ m3.perms AS '鏉冮檺鏍囪瘑'
+FROM sys_menu m1
+LEFT JOIN sys_menu m2 ON m2.parent_id = m1.menu_id
+LEFT JOIN sys_menu m3 ON m3.parent_id = m2.menu_id
+WHERE m1.menu_name = '杞﹁締鐩戞帶'
+ORDER BY m2.order_num, m3.order_num;
+```
+
+**棰勬湡缁撴灉**锛氬簲璇ョ湅鍒板畬鏁寸殑鑿滃崟鏍戠粨鏋�
+
+### 2. 鏉冮檺鏁伴噺楠岃瘉
+```sql
+-- 缁熻鍛婅鐩稿叧鏉冮檺鏁伴噺
+SELECT COUNT(*) AS '鏉冮檺鏁伴噺' FROM sys_menu
+WHERE perms LIKE 'system:vehicleAlert%';
+```
+
+**棰勬湡缁撴灉**锛氬簲璇ユ湁11涓潈闄愶紙2涓猯ist + 9涓寜閽級
+
+### 3. 鍓嶇璁块棶楠岃瘉
+- [ ] 鐧诲綍绯荤粺鍚庤兘鐪嬪埌"杞﹁締鐩戞帶"鑿滃崟
+- [ ] 鐐瑰嚮"杞﹁締寮傚父鍛婅"鑳芥甯歌闂〉闈�
+- [ ] 鐐瑰嚮"鍛婅閰嶇疆绠$悊"鑳芥甯歌闂〉闈�
+- [ ] 鍚勫姛鑳芥寜閽牴鎹潈闄愭纭樉绀�/闅愯棌
+
+## 鈿狅笍 甯歌闂
+
+### 1. 鑿滃崟涓嶆樉绀�
+
+**鍘熷洜**锛�
+- 鑿滃崟鏈垱寤烘垚鍔�
+- 鐢ㄦ埛瑙掕壊鏈垎閰嶈彍鍗曟潈闄�
+- 缂撳瓨鏈埛鏂�
+
+**瑙e喅鏂规硶**锛�
+```sql
+-- 妫�鏌ヨ彍鍗曟槸鍚﹀瓨鍦�
+SELECT * FROM sys_menu WHERE menu_name = '杞﹁締鐩戞帶';
+
+-- 妫�鏌ヨ鑹叉潈闄�
+SELECT rm.*, m.menu_name, m.perms
+FROM sys_role_menu rm
+JOIN sys_menu m ON rm.menu_id = m.menu_id
+WHERE rm.role_id = YOUR_ROLE_ID
+AND m.menu_name LIKE '%鍛婅%';
+
+-- 娓呴櫎鐢ㄦ埛缂撳瓨
+DELETE FROM sys_user_online WHERE user_name = 'YOUR_USERNAME';
+```
+
+### 2. 鎸夐挳涓嶆樉绀�
+
+**鍘熷洜**锛�
+- 鎸夐挳鏉冮檺鏈垎閰�
+- 鍓嶇v-hasPermi鎸囦护鏉冮檺鏍囪瘑涓嶅尮閰�
+
+**瑙e喅鏂规硶**锛�
+- 妫�鏌ヨ鑹叉槸鍚﹀垎閰嶄簡瀵瑰簲鐨勬寜閽潈闄�
+- 纭鍓嶇浠g爜涓殑鏉冮檺鏍囪瘑涓庢暟鎹簱涓�鑷�
+
+### 3. 閲嶅鎵цSQL鎶ラ敊
+
+**鍘熷洜**锛氳彍鍗曞凡瀛樺湪
+
+**瑙e喅鏂规硶**锛�
+- SQL鑴氭湰宸蹭娇鐢╜NOT EXISTS`闃查噸澶嶏紝姝e父鎯呭喌涓嶄細鎶ラ敊
+- 濡傛灉鎶ュ敮涓�閿啿绐侊紝璇存槑鏁版嵁宸插瓨鍦紝鍙互蹇界暐
+
+### 4. 鐖惰彍鍗旾D鑾峰彇澶辫触
+
+**鍘熷洜**锛氱埗鑿滃崟涓嶅瓨鍦ㄦ垨鍚嶇О涓嶅尮閰�
+
+**瑙e喅鏂规硶**锛�
+```sql
+-- 妫�鏌ョ埗鑿滃崟
+SELECT menu_id, menu_name FROM sys_menu WHERE menu_name = '杞﹁締鐩戞帶';
+
+-- 濡傛灉涓嶅瓨鍦紝鍏堟墽琛岀埗鑿滃崟鍒涘缓SQL
+```
+
+## 馃摓 鎶�鏈敮鎸�
+
+濡傛湁闂锛岃鍙傝�冿細
+- 馃搫 [瀹屾暣瀹炵幇鎬荤粨](./杞﹁締寮傚父杩愯鐩戞帶鍛婅-瀹屾暣瀹炵幇鎬荤粨.md)
+- 馃搫 [蹇�熼儴缃叉寚鍗梋(./杞﹁締寮傚父杩愯鐩戞帶鍛婅-蹇�熼儴缃叉寚鍗�.md)
+- 馃搫 [鍓嶇閮ㄧ讲鎸囧崡](./杞﹁締寮傚父杩愯鐩戞帶鍛婅-鍓嶇閮ㄧ讲鎸囧崡.md)
+
+---
+
+**鏂囨。鐗堟湰**: v1.0
+**鏇存柊鏃堕棿**: 2026-01-12
+**缁存姢浜哄憳**: AI寮�鍙戝姪鎵�
diff --git "a/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246\345\212\237\350\203\275\350\257\264\346\230\216.md" "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246\345\212\237\350\203\275\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..c7456fc
--- /dev/null
+++ "b/doc/\350\275\246\350\276\206\345\274\202\345\270\270\350\277\220\350\241\214\347\233\221\346\216\247\345\221\212\350\255\246\345\212\237\350\203\275\350\257\264\346\230\216.md"
@@ -0,0 +1,287 @@
+# 杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘瀹炵幇璇存槑
+
+## 鍔熻兘姒傝堪
+
+瀹炵幇杞﹁締鏃犱换鍔¤繍琛岃秴鍏噷鏁板憡璀﹀姛鑳斤紝褰撶洃鎺у埌杞﹁締鍦ㄦ棤缁戝畾浠诲姟鐘舵�佷笅杩愯瓒呰繃閰嶇疆鐨勫叕閲屾暟锛堥粯璁�10鍏噷锛夋椂锛岃嚜鍔ㄤ骇鐢熷憡璀﹀苟閫氳繃灏忕▼搴忓彂閫侀�氱煡缁欑浉鍏宠礋璐d汉銆�
+
+## 鍔熻兘鐗规��
+
+### 1. 鏅鸿兘鐩戞帶
+- **鏃堕棿绐楀彛鐩戞帶**锛氭瘡5鍒嗛挓鎵ц涓�娆★紝鐩戞帶鏈�杩�10鍒嗛挓锛堝彲閰嶇疆锛夊唴鐨勮溅杈嗚繍琛屾儏鍐�
+- **浠诲姟鐘舵�佹娴�**锛氳嚜鍔ㄨ瘑鍒溅杈嗘槸鍚︽湁姝e湪鎵ц鐨勪换鍔�
+- **閲岀▼鑷姩璁$畻**锛氬熀浜嶨PS鍒嗘閲岀▼璁板綍鑷姩绱杩愯鍏噷鏁�
+- **閮ㄩ棬鏅鸿兘璇嗗埆**锛氳嚜鍔ㄥ叧鑱旇溅杈嗗綊灞為儴闂ㄤ俊鎭�
+
+### 2. 鐏垫椿閰嶇疆
+- **鍏噷鏁伴槇鍊�**锛氶粯璁�10鍏噷锛屽彲閫氳繃绯荤粺閰嶇疆璋冩暣
+- **鍛婅棰戠巼闄愬埗**锛�
+ - 姣忓ぉ姣忚溅鏈�澶氬憡璀�5娆★紙鍙厤缃級
+ - 涓ゆ鍛婅鏈�灏忛棿闅�5鍒嗛挓锛堝彲閰嶇疆锛�
+- **鐩戞帶鏃堕棿绐楀彛**锛氶粯璁�10鍒嗛挓锛屽彲閰嶇疆
+- **閫氱煡鐢ㄦ埛閰嶇疆**锛氭敮鎸侀厤缃浐瀹氱敤鎴峰垪琛ㄦ垨鏍规嵁閮ㄩ棬鑷姩閫氱煡璐熻矗浜�
+
+### 3. 澶氭笭閬撻�氱煡
+- **浼佷笟寰俊閫氱煡**锛氶泦鎴愪紒涓氬井淇℃秷鎭帹閫�
+- **灏忕▼搴忛�氱煡**锛氶�氳繃灏忕▼搴忓彂閫佸憡璀︿俊鎭�
+- **閫氱煡鍐呭**锛氬寘鍚溅鐗屽彿銆佽繍琛屽叕閲屾暟銆佹椂闂存绛夊叧閿俊鎭�
+
+### 4. 瀹屾暣鐨勭鐞嗙晫闈�
+- **鍛婅鍒楄〃鏌ョ湅**锛氭敮鎸佹寜杞︾墝鍙枫�佹椂闂淬�侀儴闂ㄣ�佺姸鎬佺瓑澶氱淮搴︽煡璇�
+- **鍛婅璇︽儏鏌ョ湅**锛氭煡鐪嬪憡璀︾殑瀹屾暣淇℃伅
+- **鍛婅澶勭悊**锛氭敮鎸佸崟涓垨鎵归噺澶勭悊鍛婅锛岃褰曞鐞嗕汉鍜屽鐞嗘剰瑙�
+- **鏁版嵁瀵煎嚭**锛氭敮鎸佸鍑篍xcel杩涜鏁版嵁鍒嗘瀽
+- **缁熻鍔熻兘**锛氬睍绀烘湭澶勭悊鍛婅鏁伴噺銆佷粖鏃ュ憡璀︽暟閲忕瓑缁熻淇℃伅
+
+## 鎶�鏈疄鐜�
+
+### 鏁版嵁搴撹璁�
+
+#### 1. 杞﹁締寮傚父鍛婅璁板綍琛� (tb_vehicle_abnormal_alert)
+瀛樺偍姣忔鍛婅鐨勮缁嗕俊鎭細
+- 鍩烘湰淇℃伅锛氳溅杈咺D銆佽溅鐗屽彿銆佸憡璀︽椂闂淬�佺疮璁″叕閲屾暟
+- 鍛婅璇︽儏锛氬憡璀︾被鍨嬨�佸師鍥犳弿杩般�佸紑濮�/缁撴潫鏃堕棿
+- 澶勭悊淇℃伅锛氬鐞嗙姸鎬併�佸鐞嗕汉銆佸鐞嗘椂闂淬�佸鐞嗗娉�
+- 閫氱煡淇℃伅锛氶�氱煡鐘舵�併�侀�氱煡鏃堕棿銆侀�氱煡鐢ㄦ埛鍒楄〃
+- 閮ㄩ棬淇℃伅锛氬綊灞為儴闂↖D銆侀儴闂ㄥ悕绉�
+
+#### 2. 杞﹁締寮傚父鍛婅閰嶇疆琛� (tb_vehicle_alert_config)
+鏀寔鍏ㄥ眬銆侀儴闂ㄣ�佽溅杈嗕笁绾ч厤缃細
+- 閰嶇疆灞傜骇锛欸LOBAL(鍏ㄥ眬)/DEPT(閮ㄩ棬)/VEHICLE(杞﹁締)
+- 鍛婅鍙傛暟锛氬叕閲屾暟闃堝�笺�佹瘡鏃ュ憡璀︽鏁般�佸憡璀﹂棿闅�
+- 閫氱煡閰嶇疆锛氶�氱煡鐢ㄦ埛ID鍒楄〃銆侀�氱煡瑙掕壊鍒楄〃
+- 鍚敤鐘舵�侊細鍙伒娲诲紑鍚垨鍏抽棴鏌愪釜閰嶇疆
+
+### 鏍稿績缁勪欢
+
+#### 1. 瀹氭椂鐩戞帶浠诲姟 (VehicleAbnormalAlertTask)
+```java
+@Component("vehicleAbnormalAlertTask")
+public class VehicleAbnormalAlertTask {
+ // 姣�5鍒嗛挓鎵ц涓�娆�
+ public void monitorVehicleAbnormalRunning()
+}
+```
+
+**鐩戞帶娴佺▼**锛�
+1. 妫�鏌ュ姛鑳藉紑鍏虫槸鍚﹀惎鐢�
+2. 鍔犺浇閰嶇疆鍙傛暟锛堥槇鍊笺�侀鐜囬檺鍒剁瓑锛�
+3. 鏌ヨ鎵�鏈夋椿璺冭溅杈�
+4. 閫愯溅妫�鏌ワ細
+ - 鏄惁鏈夋鍦ㄦ墽琛岀殑浠诲姟
+ - 璁$畻鐩戞帶绐楀彛鍐呯殑杩愯閲岀▼
+ - 鍒ゆ柇鏄惁瓒呰繃闃堝��
+ - 妫�鏌ュ憡璀﹂鐜囬檺鍒�
+5. 鍒涘缓鍛婅璁板綍
+6. 鍙戦�侀�氱煡
+
+#### 2. 鍛婅鏈嶅姟 (VehicleAbnormalAlertService)
+鎻愪緵鍛婅鐨勫鍒犳敼鏌ャ�佸鐞嗙瓑鏍稿績涓氬姟閫昏緫锛�
+- `checkAndCreateAlert()`: 妫�鏌ュ苟鍒涘缓鍛婅
+- `handleAlert()`: 澶勭悊鍗曚釜鍛婅
+- `batchHandleAlert()`: 鎵归噺澶勭悊鍛婅
+
+#### 3. 鍛婅鎺у埗鍣� (VehicleAbnormalAlertController)
+鎻愪緵RESTful API鎺ュ彛锛�
+- `GET /system/vehicleAlert/list`: 鏌ヨ鍛婅鍒楄〃
+- `GET /system/vehicleAlert/{id}`: 鏌ヨ鍛婅璇︽儏
+- `PUT /system/vehicleAlert/handle/{id}`: 澶勭悊鍛婅
+- `GET /system/vehicleAlert/unhandledCount`: 鑾峰彇鏈鐞嗗憡璀︽暟
+- `POST /system/vehicleAlert/export`: 瀵煎嚭鍛婅鏁版嵁
+
+## 閰嶇疆璇存槑
+
+### 绯荤粺閰嶇疆鍙傛暟 (sys_config琛�)
+
+| 閰嶇疆閿� | 璇存槑 | 榛樿鍊� |
+|-------|------|--------|
+| vehicle.alert.enabled | 鍛婅鍔熻兘鎬诲紑鍏� | true |
+| vehicle.alert.mileage.threshold | 鍏噷鏁板憡璀﹂槇鍊硷紙鍏噷锛� | 10 |
+| vehicle.alert.daily.limit | 姣忔棩鏈�澶у憡璀︽鏁� | 5 |
+| vehicle.alert.interval.minutes | 鍛婅闂撮殧鏃堕棿锛堝垎閽燂級 | 5 |
+| vehicle.alert.time.window | 鐩戞帶鏃堕棿绐楀彛锛堝垎閽燂級 | 10 |
+| vehicle.alert.notify.users | 閫氱煡鐢ㄦ埛ID鍒楄〃 | 锛堢┖锛� |
+
+### 瀹氭椂浠诲姟閰嶇疆
+
+- **浠诲姟鍚嶇О**锛氳溅杈嗗紓甯歌繍琛岀洃鎺т换鍔�
+- **璋冪敤鐩爣**锛歚vehicleAbnormalAlertTask.monitorVehicleAbnormalRunning()`
+- **鎵ц棰戠巼**锛歚0 */5 * * * ?`锛堟瘡5鍒嗛挓鎵ц涓�娆★級
+- **骞跺彂鎺у埗**锛氱姝㈠苟鍙戞墽琛�
+- **榛樿鐘舵��**锛氭殏鍋滐紙闇�鎵嬪姩鍚敤锛�
+
+## 閮ㄧ讲姝ラ
+
+### 1. 鎵цSQL鑴氭湰
+```sql
+source sql/vehicle_abnormal_alert.sql;
+```
+
+璇ヨ剼鏈細鑷姩鍒涘缓锛�
+- 鍛婅璁板綍琛�
+- 鍛婅閰嶇疆琛�
+- 绯荤粺閰嶇疆鍙傛暟
+- 瀹氭椂浠诲姟
+- 鑿滃崟鏉冮檺
+
+### 2. 閰嶇疆閫氱煡鐢ㄦ埛
+鍦ㄧ郴缁熼厤缃腑璁剧疆 `vehicle.alert.notify.users`锛屽~鍏ユ帴鏀跺憡璀︾殑鐢ㄦ埛ID鍒楄〃锛堥�楀彿鍒嗛殧锛�
+
+### 3. 鍚敤瀹氭椂浠诲姟
+鍦�"绯荤粺鐩戞帶 > 瀹氭椂浠诲姟"涓壘鍒�"杞﹁締寮傚父杩愯鐩戞帶浠诲姟"锛岀偣鍑�"鎭㈠"鎸夐挳鍚敤
+
+### 4. 閰嶇疆浼佷笟寰俊锛堝彲閫夛級
+濡傞渶浣跨敤浼佷笟寰俊閫氱煡锛岀‘淇濅互涓嬮厤缃纭細
+- `qy_wechat.enable = true`
+- `qy_wechat.corp_id` = 浼佷笟ID
+- `qy_wechat.corp_secret` = 搴旂敤Secret
+- `qy_wechat.agent_id` = 搴旂敤ID
+
+## 浣跨敤鎸囧崡
+
+### 绠$悊鍛樻搷浣�
+
+#### 1. 鏌ョ湅鍛婅鍒楄〃
+- 杩涘叆"杞﹁締绠$悊 > 杞﹁締寮傚父鍛婅"
+- 鍙寜杞︾墝鍙枫�佹椂闂磋寖鍥淬�侀儴闂ㄣ�佺姸鎬佺瓫閫�
+- 榛樿鎸夊憡璀︽椂闂村�掑簭鎺掑垪
+
+#### 2. 澶勭悊鍛婅
+- 鐐瑰嚮"澶勭悊"鎸夐挳
+- 濉啓澶勭悊澶囨敞
+- 鎻愪氦鍚庡憡璀︾姸鎬佸彉鏇翠负"宸插鐞�"
+
+#### 3. 鎵归噺澶勭悊
+- 鍕鹃�夊涓憡璀�
+- 鐐瑰嚮"鎵归噺澶勭悊"
+- 濉啓缁熶竴鐨勫鐞嗗娉�
+
+#### 4. 瀵煎嚭鏁版嵁
+- 璁剧疆绛涢�夋潯浠�
+- 鐐瑰嚮"瀵煎嚭"鎸夐挳
+- 涓嬭浇Excel鏂囦欢杩涜鍒嗘瀽
+
+#### 5. 璋冩暣閰嶇疆
+- 杩涘叆"绯荤粺绠$悊 > 鍙傛暟璁剧疆"
+- 鎼滅储"vehicle.alert"
+- 淇敼鐩稿叧鍙傛暟
+- 淇敼鍚庣珛鍗崇敓鏁堬紝鏃犻渶閲嶅惎
+
+### 鐩戞帶鍘熺悊
+
+#### 閲岀▼璁$畻
+- 鍩轰簬`tb_vehicle_gps_segment_mileage`琛�
+- 姣�5鍒嗛挓鑷姩鍒嗘缁熻GPS閲岀▼
+- 鏌ヨ鐩戞帶绐楀彛鍐呯殑鎵�鏈夊垎娈甸噷绋嬭褰�
+- 绱姞璁$畻鎬昏繍琛屽叕閲屾暟
+
+#### 浠诲姟鍏宠仈鍒ゆ柇
+- 鏌ヨ`sys_task`鍜宍sys_task_vehicle`琛�
+- 妫�鏌ヨ溅杈嗗湪鐩戞帶鏃堕棿绐楀彛鍐呮槸鍚︽湁浠诲姟
+- 鎺掗櫎宸插畬鎴愬拰宸插彇娑堢殑浠诲姟
+- 鍙湁瀹屽叏鏃犱换鍔℃椂鎵嶈Е鍙戝憡璀�
+
+#### 棰戠巼鎺у埗
+- **姣忔棩娆℃暟闄愬埗**锛氭煡璇㈠綋澶╄杞﹀凡鍛婅娆℃暟锛岃揪鍒颁笂闄愬垯璺宠繃
+- **鏃堕棿闂撮殧闄愬埗**锛氭煡璇㈡渶鍚庝竴娆″憡璀︽椂闂达紝鏈揪鍒伴棿闅斿垯璺宠繃
+- 闃叉棰戠箒鍛婅楠氭壈
+
+## 鍛婅鐘舵�佹祦杞�
+
+```
+鏈鐞�(0) -> 宸插鐞�(1)
+ \-> 宸插拷鐣�(2)
+```
+
+- **鏈鐞�**锛氭柊鍒涘缓鐨勫憡璀︼紝寰呭鐞�
+- **宸插鐞�**锛氱鐞嗗憳宸插鐞嗗苟濉啓浜嗗鐞嗘剰瑙�
+- **宸插拷鐣�**锛氱鐞嗗憳纭涓鸿鎶ユ垨鏃犻渶澶勭悊
+
+## 娉ㄦ剰浜嬮」
+
+1. **GPS鏁版嵁渚濊禆**
+ - 闇�瑕丟PS鍒嗘閲岀▼璁$畻浠诲姟姝e父杩愯
+ - 纭繚`tb_vehicle_gps_segment_mileage`琛ㄦ湁鏁版嵁
+ - 寤鸿鍏堣繍琛孏PS閲岀▼璁$畻浠诲姟娴嬭瘯
+
+2. **鎬ц兘浼樺寲**
+ - 鐩戞帶浠诲姟姣�5鍒嗛挓鎵ц涓�娆�
+ - 姣忔鍙煡璇㈡渶杩�10鍒嗛挓鐨勬暟鎹�
+ - 鏁版嵁搴撳凡寤虹珛蹇呰鐨勭储寮�
+ - 澶ч噺杞﹁締鏃舵敞鎰忕洃鎺ф墽琛屾椂闂�
+
+3. **閫氱煡閰嶇疆**
+ - 浼樺厛浣跨敤閰嶇疆鐨勭敤鎴峰垪琛�
+ - 鍏舵鏍规嵁杞﹁締褰掑睘閮ㄩ棬鏌ユ壘璐熻矗浜�
+ - 鏈�鍚庝娇鐢ㄥ叏灞�榛樿鐢ㄦ埛鍒楄〃
+ - 纭繚鑷冲皯閰嶇疆涓�绉嶉�氱煡鏂瑰紡
+
+4. **娴嬭瘯寤鸿**
+ - 鍏堝湪娴嬭瘯鐜楠岃瘉
+ - 璋冩暣闃堝�间负杈冨皬鍊硷紙濡�1鍏噷锛夋柟渚胯Е鍙�
+ - 瑙傚療鍛婅璁板綍鍜岄�氱煡鏄惁姝e父
+ - 纭棰戠巼闄愬埗鏄惁鐢熸晥
+
+5. **鐩戞帶璋冧紭**
+ - 鏍规嵁瀹為檯鎯呭喌璋冩暣鍏噷鏁伴槇鍊�
+ - 鍚堢悊璁剧疆姣忔棩鍛婅娆℃暟
+ - 閫傚綋璋冩暣鐩戞帶鏃堕棿绐楀彛
+ - 閬垮厤鍛婅杩囦簬棰戠箒鎴栭仐婕�
+
+## 鎵╁睍鍔熻兘
+
+### 鏈潵鍙墿灞曠殑鍔熻兘鏂瑰悜
+
+1. **澶氱鍛婅绫诲瀷**
+ - 闀挎椂闂村仠杞﹀憡璀�
+ - 瓒呴�熷憡璀�
+ - 鍋忕璺嚎鍛婅
+
+2. **鍛婅瑙勫垯寮曟搸**
+ - 鑷畾涔夊憡璀﹁鍒�
+ - 瑙勫垯缁勫悎涓庝紭鍏堢骇
+ - 鍔ㄦ�佽皟鏁磋鍒�
+
+3. **鏁版嵁鍒嗘瀽**
+ - 鍛婅瓒嬪娍鍒嗘瀽
+ - 杞﹁締寮傚父琛屼负缁熻
+ - 閮ㄩ棬鍛婅瀵规瘮
+
+4. **鏅鸿兘鎺ㄨ崘**
+ - 鍩轰簬鍘嗗彶鏁版嵁棰勬祴寮傚父
+ - 鎺ㄨ崘鏈�浼橀厤缃弬鏁�
+ - 鑷姩璇嗗埆璇姤
+
+## 鎶�鏈敮鎸�
+
+濡傛湁闂锛岃鑱旂郴鎶�鏈敮鎸佸洟闃熸垨鏌ョ湅鐩稿叧鏂囨。锛�
+- 绯荤粺鏃ュ織锛歚/logs/sys-*.log`
+- 瀹氭椂浠诲姟鏃ュ織锛歚/monitor/job/log`
+- GPS閲岀▼璁$畻璇存槑锛歚/doc/GPS鍒嗘閲岀▼璁$畻.md`
+
+## 鏂囦欢娓呭崟
+
+### 鍚庣浠g爜
+- `ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleAbnormalAlert.java` - 鍛婅瀹炰綋绫�
+- `ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleAlertConfig.java` - 閰嶇疆瀹炰綋绫�
+- `ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleAbnormalAlertMapper.java` - 鏁版嵁璁块棶鎺ュ彛
+- `ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleAbnormalAlertService.java` - 涓氬姟鎺ュ彛
+- `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleAbnormalAlertServiceImpl.java` - 涓氬姟瀹炵幇
+- `ruoyi-system/src/main/resources/mapper/system/VehicleAbnormalAlertMapper.xml` - MyBatis鏄犲皠鏂囦欢
+- `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleAbnormalAlertController.java` - 鎺у埗鍣�
+- `ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleAbnormalAlertTask.java` - 瀹氭椂浠诲姟
+
+### 鏁版嵁搴撹剼鏈�
+- `sql/vehicle_abnormal_alert.sql` - 寤鸿〃鍙婂垵濮嬪寲鑴氭湰
+
+### 鏂囨。
+- 鏈枃妗� - 瀹屾暣鐨勫姛鑳借鏄庡拰浣跨敤鎸囧崡
+
+## 鏇存柊鏃ュ織
+
+### V1.0.0 (2026-01-12)
+- 鉁� 瀹炵幇鍩虹鐩戞帶鍔熻兘
+- 鉁� 鏀寔浼佷笟寰俊閫氱煡
+- 鉁� 瀹屾暣鐨勭鐞嗙晫闈�
+- 鉁� 鐏垫椿鐨勯厤缃郴缁�
+- 鉁� 鍛婅棰戠巼鎺у埗
+- 鉁� 鏁版嵁瀵煎嚭鍔熻兘
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/HospDataController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/HospDataController.java
index d4f92b1..6b3cba6 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/HospDataController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/HospDataController.java
@@ -2,16 +2,27 @@
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.HospitalTokenizerUtil;
+import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.HospData;
+import com.ruoyi.system.domain.HospitalTokenizerTask;
+import com.ruoyi.system.domain.TbHospData;
import com.ruoyi.system.mapper.HospDataMapper;
+import com.ruoyi.system.service.HospitalTokenizerAsyncService;
import com.ruoyi.system.service.ISQLHospDataService;
+import com.ruoyi.system.service.ITbHospDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import java.util.UUID;
/**
* 鍖婚櫌鏁版嵁Controller
@@ -30,6 +41,15 @@
@Autowired
private ISQLHospDataService sqlHospDataService;
+
+ @Autowired
+ private ITbHospDataService tbHospDataService;
+
+ @Autowired
+ private com.ruoyi.system.mapper.TbHospDataMapper tbHospDataMapper;
+
+ @Autowired
+ private HospitalTokenizerAsyncService asyncService;
/**
* 鎼滅储鍖婚櫌锛堜粠MySQL tb_hosp_data琛ㄦ煡璇級
@@ -181,4 +201,255 @@
return success(hospitals);
}
+
+ /**
+ * 鎵归噺鐢熸垚鎵�鏈夊尰闄㈢殑鍒嗚瘝锛堝紓姝ワ級
+ * 绠$悊鍛樻帴鍙o紝鐢ㄤ簬鍒濆鍖栨垨閲嶆柊鐢熸垚鍖婚櫌鍒嗚瘝
+ *
+ * @return 浠诲姟ID
+ */
+ @GetMapping("/generateKeywords")
+ public AjaxResult generateAllHospitalKeywords() {
+ logger.info("寮�濮嬫壒閲忕敓鎴愬尰闄㈠垎璇嶏紙寮傛锛�...");
+
+ try {
+ // 鐢熸垚浠诲姟ID
+ String taskId = UUID.randomUUID().toString().replace("-", "");
+
+ // 寮傛鎵ц浠诲姟
+ asyncService.executeTokenizerTask(taskId);
+
+ logger.info("鍖婚櫌鍒嗚瘝浠诲姟宸插惎鍔�: taskId={}", taskId);
+
+ // 绔嬪嵆杩斿洖浠诲姟ID
+ return success()
+ .put("taskId", taskId)
+ .put("message", "鍒嗚瘝浠诲姟宸插惎鍔紝璇锋煡璇换鍔¤繘搴�");
+
+ } catch (Exception e) {
+ logger.error("鍚姩鍖婚櫌鍒嗚瘝浠诲姟澶辫触", e);
+ return error("鍚姩澶辫触锛�" + e.getMessage());
+ }
+ }
+
+ /**
+ * 鏌ヨ鍖婚櫌鍒嗚瘝浠诲姟杩涘害
+ *
+ * @param taskId 浠诲姟ID
+ * @return 浠诲姟杩涘害淇℃伅
+ */
+ @GetMapping("/getTaskProgress")
+ public AjaxResult getTaskProgress(@RequestParam("taskId") String taskId) {
+ try {
+ HospitalTokenizerTask task = asyncService.getTaskStatus(taskId);
+
+ if (task == null) {
+ return error("浠诲姟涓嶅瓨鍦ㄦ垨宸茶繃鏈�");
+ }
+
+ return success(task);
+
+ } catch (Exception e) {
+ logger.error("鏌ヨ浠诲姟杩涘害澶辫触: taskId={}", taskId, e);
+ return error("鏌ヨ澶辫触锛�" + e.getMessage());
+ }
+ }
+
+ /**
+ * 鍩轰簬鍒嗚瘝鍖归厤鎼滅储鍖婚櫌
+ * 鍓嶇浼犲叆鍖婚櫌淇℃伅锛岃繘琛屽垎璇嶅悗涓庢暟鎹簱涓殑鍒嗚瘝鍖归厤
+ * 鏍规嵁鍖归厤鐨勫垎璇嶆暟閲忚繘琛屾潈閲嶆帓搴忥紝鍖归厤瓒婂鎺掑悕瓒婇潬鍓�
+ *
+ * @param searchText 鎼滅储鏂囨湰锛堝尰闄㈠悕绉般�佸湴鍧�绛夛級
+ * @param pageSize 杩斿洖缁撴灉鏁伴噺闄愬埗锛堥粯璁�50锛�
+ * @return 鍖归厤鐨勫尰闄㈠垪琛紙鎸夊尮閰嶅害鎺掑簭锛�
+ */
+ @GetMapping("/searchByKeywords")
+ public AjaxResult searchHospitalsByKeywords(
+ @RequestParam("searchText") String searchText,
+ @RequestParam(value = "pageSize", required = false, defaultValue = "50") Integer pageSize) {
+
+ logger.info("鍩轰簬鍒嗚瘝鍖归厤鎼滅储鍖婚櫌锛歴earchText={}, pageSize={}", searchText, pageSize);
+
+ if (searchText == null || searchText.trim().isEmpty()) {
+ return error("鎼滅储鏂囨湰涓嶈兘涓虹┖");
+ }
+
+ try {
+ long startTime = System.currentTimeMillis();
+
+ // 1. 瀵瑰墠绔紶鍏ョ殑鎼滅储鏂囨湰杩涜鍒嗚瘝
+ String searchKeywords = HospitalTokenizerUtil.tokenizeSearchText(searchText);
+ logger.info("鎼滅储鏂囨湰鍒嗚瘝缁撴灉锛歿}", searchKeywords);
+
+ if (searchKeywords.isEmpty()) {
+ return success(new ArrayList<>());
+ }
+
+ // 2. 灏嗗垎璇嶇粨鏋滄媶鍒嗕负鍏抽敭璇嶅垪琛紝鐢ㄤ簬鏁版嵁搴撻杩囨护
+ String[] keywordArray = searchKeywords.split(",");
+ List<String> keywordList = new ArrayList<>();
+ for (String keyword : keywordArray) {
+ String trimmed = keyword.trim();
+ if (!trimmed.isEmpty() && trimmed.length() >= 2) { // 鍙娇鐢�2涓瓧浠ヤ笂鐨勫叧閿瘝
+ keywordList.add(trimmed);
+ }
+ }
+
+ if (keywordList.isEmpty()) {
+ logger.warn("娌℃湁鏈夋晥鐨勫叧閿瘝鐢ㄤ簬鎼滅储");
+ return success(new ArrayList<>());
+ }
+
+ logger.info("浣跨敤鍏抽敭璇嶈繘琛屾暟鎹簱棰勮繃婊�: {}", keywordList);
+
+ // 3. 閫氳繃鏁版嵁搴撳眰闈㈤杩囨护锛屽彧鏌ヨ鍙兘鍖归厤鐨勫尰闄紙鑰屼笉鏄墍鏈夊尰闄級
+ List<TbHospData> candidateHospitals = tbHospDataMapper.selectTbHospDataByKeywords(keywordList, "0");
+
+ long queryTime = System.currentTimeMillis();
+ logger.info("鏁版嵁搴撻杩囨护瀹屾垚锛屽�欓�夊尰闄㈡暟閲�: {}, 鑰楁椂: {}ms", candidateHospitals.size(), queryTime - startTime);
+
+ // 4. 鎻愬彇鍊欓�夊尰闄㈢殑鍦板尯鍚嶇О锛堜粠 hopsArea 瀛楁锛�
+ Set<String> districtNames = new HashSet<>();
+ for (TbHospData hospital : candidateHospitals) {
+ if (StringUtils.isNotBlank(hospital.getHopsArea())) {
+ // 鎻愬彇鍦板尯鍚嶏紝绉婚櫎甯歌鍚庣紑
+ String area = hospital.getHopsArea()
+ .replace("鍖�", "")
+ .replace("甯�", "")
+ .replace("鍘�", "")
+ .trim();
+ if (area.length() > 0) {
+ districtNames.add(area);
+ }
+ }
+ }
+
+ logger.info("鎻愬彇鍒� {} 涓嫭鐗瑰湴鍖哄悕绉�", districtNames.size());
+
+ // 5. 瀵瑰�欓�夊尰闄㈣绠楀尮閰嶅垎鏁帮紝骞惰繃婊ゅ嚭鏈夊尮閰嶇殑鍖婚櫌
+ List<HospitalMatchResult> matchResults = new ArrayList<>();
+
+ long matchStartTime = System.currentTimeMillis();
+
+ for (TbHospData hospital : candidateHospitals) {
+ if (hospital.getHospKeywords() == null || hospital.getHospKeywords().isEmpty()) {
+ continue;
+ }
+
+ // 璁$畻鍖归厤鍒嗘暟锛堜紶鍏ュ尰闄㈠悕绉板拰鍦板尯鍚嶇О闆嗗悎锛�
+ int matchScore = HospitalTokenizerUtil.calculateMatchScore(
+ searchKeywords,
+ hospital.getHospKeywords(),
+ hospital.getHospName(),
+ districtNames
+ );
+
+ // 鍙繚鐣欐湁鍖归厤鐨勫尰闄�
+ if (matchScore > 0) {
+ matchResults.add(new HospitalMatchResult(hospital, matchScore));
+ }
+ }
+
+ long matchTime = System.currentTimeMillis();
+ logger.info("鍖归厤璁$畻瀹屾垚锛屾壘鍒� {} 涓尮閰嶇殑鍖婚櫌锛岃�楁椂: {}ms", matchResults.size(), matchTime - matchStartTime);
+
+ // 4. 鎸夊尮閰嶅垎鏁伴檷搴忔帓搴忥紙鍒嗘暟瓒婇珮鎺掑悕瓒婇潬鍓嶏級
+ matchResults.sort(Comparator.comparingInt(HospitalMatchResult::getMatchScore).reversed());
+
+ // 5. 闄愬埗杩斿洖鏁伴噺
+ if (pageSize != null && pageSize > 0 && matchResults.size() > pageSize) {
+ matchResults = matchResults.subList(0, pageSize);
+ }
+
+ // 6. 杞崲涓篐ospData瀵硅薄杩斿洖锛堝寘鍚尮閰嶅垎鏁帮級
+ List<HospDataWithScore> result = new ArrayList<>();
+ for (HospitalMatchResult matchResult : matchResults) {
+ TbHospData tbHospData = matchResult.getHospital();
+ HospData hospData = convertToHospData(tbHospData);
+ result.add(new HospDataWithScore(hospData, matchResult.getMatchScore()));
+ logger.debug("鍖婚櫌: {}, 鍖归厤鍒嗘暟: {}",
+ hospData.getHospName(), matchResult.getMatchScore());
+ }
+
+ logger.info("杩斿洖 {} 涓尰闄㈢粨鏋�", result.size());
+
+ long totalTime = System.currentTimeMillis() - startTime;
+ logger.info("鎼滅储瀹屾垚 - 鎬昏�楁椂: {}ms, 鏁版嵁搴撴煡璇�: {}ms, 鍖归厤璁$畻: {}ms",
+ totalTime, queryTime - startTime, matchTime - matchStartTime);
+
+ return success(result);
+
+ } catch (Exception e) {
+ logger.error("鍒嗚瘝鍖归厤鎼滅储澶辫触", e);
+ return error("鎼滅储澶辫触锛�" + e.getMessage());
+ }
+ }
+
+ /**
+ * 灏員bHospData杞崲涓篐ospData
+ */
+ private HospData convertToHospData(TbHospData tbHospData) {
+ HospData hospData = new HospData();
+ hospData.setHospId(tbHospData.getLegacyHospId());
+ hospData.setHospName(tbHospData.getHospName());
+ hospData.setHospCityId(tbHospData.getHospCityId());
+ hospData.setHospShort(tbHospData.getHospShort());
+ hospData.setHopsProvince(tbHospData.getHopsProvince());
+ hospData.setHopsCity(tbHospData.getHopsCity());
+ hospData.setHopsArea(tbHospData.getHopsArea());
+ hospData.setHospAddress(tbHospData.getHospAddress());
+ hospData.setHospTel(tbHospData.getHospTel());
+ hospData.setHospUnitId(tbHospData.getHospUnitId());
+ hospData.setHospState(tbHospData.getHospState());
+ hospData.setHospOaId(tbHospData.getHospOaId());
+ hospData.setHospIntroducerId(tbHospData.getHospIntroducerId());
+ if (tbHospData.getHospIntroducerDate() != null) {
+ hospData.setHospIntroducerDate(tbHospData.getHospIntroducerDate().toString());
+ }
+ hospData.setHospLevel(tbHospData.getHospLevel());
+ return hospData;
+ }
+
+ /**
+ * 鍖婚櫌鍖归厤缁撴灉鍐呴儴绫�
+ */
+ private static class HospitalMatchResult {
+ private TbHospData hospital;
+ private int matchScore;
+
+ public HospitalMatchResult(TbHospData hospital, int matchScore) {
+ this.hospital = hospital;
+ this.matchScore = matchScore;
+ }
+
+ public TbHospData getHospital() {
+ return hospital;
+ }
+
+ public int getMatchScore() {
+ return matchScore;
+ }
+ }
+
+ /**
+ * 鍖婚櫌鏁版嵁涓庡尮閰嶅垎鏁板寘瑁呯被
+ */
+ private static class HospDataWithScore {
+ private HospData hospital;
+ private int matchScore;
+
+ public HospDataWithScore(HospData hospital, int matchScore) {
+ this.hospital = hospital;
+ this.matchScore = matchScore;
+ }
+
+ public HospData getHospital() {
+ return hospital;
+ }
+
+ public int getMatchScore() {
+ return matchScore;
+ }
+ }
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleAbnormalAlertController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleAbnormalAlertController.java
new file mode 100644
index 0000000..bc3769f
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleAbnormalAlertController.java
@@ -0,0 +1,149 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.system.domain.VehicleAbnormalAlert;
+import com.ruoyi.system.service.IVehicleAbnormalAlertService;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+
+/**
+ * 杞﹁締寮傚父鍛婅Controller
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/vehicleAlert")
+public class VehicleAbnormalAlertController extends BaseController {
+
+ @Autowired
+ private IVehicleAbnormalAlertService vehicleAbnormalAlertService;
+
+ /**
+ * 鏌ヨ杞﹁締寮傚父鍛婅鍒楄〃
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlert:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(VehicleAbnormalAlert vehicleAbnormalAlert) {
+ startPage();
+ List<VehicleAbnormalAlert> list = vehicleAbnormalAlertService.selectVehicleAbnormalAlertList(vehicleAbnormalAlert);
+ return getDataTable(list);
+ }
+
+ /**
+ * 瀵煎嚭杞﹁締寮傚父鍛婅鍒楄〃
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlert:export')")
+ @Log(title = "杞﹁締寮傚父鍛婅", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, VehicleAbnormalAlert vehicleAbnormalAlert) {
+ List<VehicleAbnormalAlert> list = vehicleAbnormalAlertService.selectVehicleAbnormalAlertList(vehicleAbnormalAlert);
+ ExcelUtil<VehicleAbnormalAlert> util = new ExcelUtil<VehicleAbnormalAlert>(VehicleAbnormalAlert.class);
+ util.exportExcel(response, list, "杞﹁締寮傚父鍛婅鏁版嵁");
+ }
+
+ /**
+ * 鑾峰彇杞﹁締寮傚父鍛婅璇︾粏淇℃伅
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlert:query')")
+ @GetMapping(value = "/{alertId}")
+ public AjaxResult getInfo(@PathVariable("alertId") Long alertId) {
+ return success(vehicleAbnormalAlertService.selectVehicleAbnormalAlertByAlertId(alertId));
+ }
+
+ /**
+ * 鏂板杞﹁締寮傚父鍛婅
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlert:add')")
+ @Log(title = "杞﹁締寮傚父鍛婅", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody VehicleAbnormalAlert vehicleAbnormalAlert) {
+ return toAjax(vehicleAbnormalAlertService.insertVehicleAbnormalAlert(vehicleAbnormalAlert));
+ }
+
+ /**
+ * 淇敼杞﹁締寮傚父鍛婅
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlert:edit')")
+ @Log(title = "杞﹁締寮傚父鍛婅", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody VehicleAbnormalAlert vehicleAbnormalAlert) {
+ return toAjax(vehicleAbnormalAlertService.updateVehicleAbnormalAlert(vehicleAbnormalAlert));
+ }
+
+ /**
+ * 鍒犻櫎杞﹁締寮傚父鍛婅
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlert:remove')")
+ @Log(title = "杞﹁締寮傚父鍛婅", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{alertIds}")
+ public AjaxResult remove(@PathVariable Long[] alertIds) {
+ return toAjax(vehicleAbnormalAlertService.deleteVehicleAbnormalAlertByAlertIds(alertIds));
+ }
+
+ /**
+ * 澶勭悊鍛婅
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlert:handle')")
+ @Log(title = "澶勭悊杞﹁締鍛婅", businessType = BusinessType.UPDATE)
+ @PutMapping("/handle/{alertId}")
+ public AjaxResult handleAlert(@PathVariable Long alertId, @RequestBody VehicleAbnormalAlert vehicleAbnormalAlert) {
+ Long handlerId = getUserId();
+ String handlerName = getUsername();
+ String handleRemark = vehicleAbnormalAlert.getHandleRemark();
+
+ return toAjax(vehicleAbnormalAlertService.handleAlert(alertId, handlerId, handlerName, handleRemark));
+ }
+
+ /**
+ * 鎵归噺澶勭悊鍛婅
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlert:handle')")
+ @Log(title = "鎵归噺澶勭悊杞﹁締鍛婅", businessType = BusinessType.UPDATE)
+ @PutMapping("/batchHandle")
+ public AjaxResult batchHandleAlert(@RequestBody VehicleAbnormalAlert vehicleAbnormalAlert) {
+ Long[] alertIds = vehicleAbnormalAlert.getAlertId() != null ?
+ new Long[]{vehicleAbnormalAlert.getAlertId()} : new Long[0];
+ Long handlerId = getUserId();
+ String handlerName = getUsername();
+ String handleRemark = vehicleAbnormalAlert.getHandleRemark();
+
+ return toAjax(vehicleAbnormalAlertService.batchHandleAlert(alertIds, handlerId, handlerName, handleRemark));
+ }
+
+ /**
+ * 鑾峰彇鏈鐞嗗憡璀︽暟閲�
+ */
+ @GetMapping("/unhandledCount")
+ public AjaxResult getUnhandledCount() {
+ VehicleAbnormalAlert query = new VehicleAbnormalAlert();
+ query.setStatus("0"); // 鏈鐞�
+ List<VehicleAbnormalAlert> list = vehicleAbnormalAlertService.selectVehicleAbnormalAlertList(query);
+ return success(list.size());
+ }
+
+ /**
+ * 鑾峰彇浠婃棩鍛婅鏁伴噺
+ */
+ @GetMapping("/todayCount")
+ public AjaxResult getTodayCount() {
+ VehicleAbnormalAlert query = new VehicleAbnormalAlert();
+ query.setAlertDate(new java.util.Date());
+ List<VehicleAbnormalAlert> list = vehicleAbnormalAlertService.selectVehicleAbnormalAlertList(query);
+ return success(list.size());
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleAlertConfigController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleAlertConfigController.java
new file mode 100644
index 0000000..471c8da
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleAlertConfigController.java
@@ -0,0 +1,106 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.system.domain.VehicleAlertConfig;
+import com.ruoyi.system.service.IVehicleAlertConfigService;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+
+/**
+ * 杞﹁締鍛婅閰嶇疆Controller
+ *
+ * @author ruoyi
+ * @date 2026-01-12
+ */
+@RestController
+@RequestMapping("/system/vehicleAlertConfig")
+public class VehicleAlertConfigController extends BaseController
+{
+ @Autowired
+ private IVehicleAlertConfigService vehicleAlertConfigService;
+
+ /**
+ * 鏌ヨ杞﹁締鍛婅閰嶇疆鍒楄〃
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlertConfig:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(VehicleAlertConfig vehicleAlertConfig)
+ {
+ startPage();
+ List<VehicleAlertConfig> list = vehicleAlertConfigService.selectVehicleAlertConfigList(vehicleAlertConfig);
+ return getDataTable(list);
+ }
+
+ /**
+ * 瀵煎嚭杞﹁締鍛婅閰嶇疆鍒楄〃
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlertConfig:export')")
+ @Log(title = "杞﹁締鍛婅閰嶇疆", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, VehicleAlertConfig vehicleAlertConfig)
+ {
+ List<VehicleAlertConfig> list = vehicleAlertConfigService.selectVehicleAlertConfigList(vehicleAlertConfig);
+ ExcelUtil<VehicleAlertConfig> util = new ExcelUtil<VehicleAlertConfig>(VehicleAlertConfig.class);
+ util.exportExcel(response, list, "杞﹁締鍛婅閰嶇疆鏁版嵁");
+ }
+
+ /**
+ * 鑾峰彇杞﹁締鍛婅閰嶇疆璇︾粏淇℃伅
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlertConfig:query')")
+ @GetMapping(value = "/{configId}")
+ public AjaxResult getInfo(@PathVariable("configId") Long configId)
+ {
+ return success(vehicleAlertConfigService.selectVehicleAlertConfigByConfigId(configId));
+ }
+
+ /**
+ * 鏂板杞﹁締鍛婅閰嶇疆
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlertConfig:add')")
+ @Log(title = "杞﹁締鍛婅閰嶇疆", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody VehicleAlertConfig vehicleAlertConfig)
+ {
+ vehicleAlertConfig.setCreateBy(getUsername());
+ return toAjax(vehicleAlertConfigService.insertVehicleAlertConfig(vehicleAlertConfig));
+ }
+
+ /**
+ * 淇敼杞﹁締鍛婅閰嶇疆
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlertConfig:edit')")
+ @Log(title = "杞﹁締鍛婅閰嶇疆", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody VehicleAlertConfig vehicleAlertConfig)
+ {
+ vehicleAlertConfig.setUpdateBy(getUsername());
+ return toAjax(vehicleAlertConfigService.updateVehicleAlertConfig(vehicleAlertConfig));
+ }
+
+ /**
+ * 鍒犻櫎杞﹁締鍛婅閰嶇疆
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleAlertConfig:remove')")
+ @Log(title = "杞﹁締鍛婅閰嶇疆", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{configIds}")
+ public AjaxResult remove(@PathVariable Long[] configIds)
+ {
+ return toAjax(vehicleAlertConfigService.deleteVehicleAlertConfigByConfigIds(configIds));
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleSyncController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleSyncController.java
index fc61e22..6280257 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleSyncController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleSyncController.java
@@ -1,37 +1,165 @@
package com.ruoyi.web.controller.system;
+import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.system.domain.VehicleInfo;
+import com.ruoyi.system.domain.VehicleSyncDTO;
+import com.ruoyi.system.domain.vo.VehicleSyncVO;
+import com.ruoyi.system.mapper.VehicleInfoMapper;
+import com.ruoyi.system.service.IVehicleInfoService;
import com.ruoyi.system.service.IVehicleSyncDataService;
-import com.ruoyi.system.service.IVehicleSyncService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
/**
- * 杞﹁締鍚屾Controller
- *
+ * 杞﹁締鍚屾绠$悊Controller
+ *
* @author ruoyi
- * @date 2025-10-20
*/
@RestController
-@RequestMapping("/system/vehicle/sync")
-public class VehicleSyncController extends BaseController
-{
- @Autowired
- private IVehicleSyncService vehicleSyncService;
+@RequestMapping("/system/vehicleSync")
+public class VehicleSyncController extends BaseController {
+
+ private static final Logger log = LoggerFactory.getLogger(VehicleSyncController.class);
@Autowired
private IVehicleSyncDataService vehicleSyncDataService;
+
+ @Autowired
+ private VehicleInfoMapper vehicleInfoMapper;
+
+ @Autowired
+ private IVehicleInfoService vehicleInfoService;
+
/**
- * 鎵嬪姩鍚屾鏃х郴缁熻溅杈嗘暟鎹�
+ * 鏌ヨ杞﹁締鍚屾鍒楄〃锛堟樉绀烘棫绯荤粺杞﹁締鍙婂悓姝ョ姸鎬侊級
*/
- @PreAuthorize("@ss.hasPermi('system:vehicle:sync')")
- @PostMapping("/legacy")
- public AjaxResult syncLegacyVehicles()
- {
- return vehicleSyncService.syncVehicles(this.vehicleSyncDataService.getVehiclesFromSqlServer());
+ @PreAuthorize("@ss.hasPermi('system:vehicleSync:list')")
+ @GetMapping("/list")
+ public TableDataInfo list() {
+ try {
+ // 1. 浠庢棫绯荤粺鑾峰彇杞﹁締鍒楄〃
+ List<VehicleSyncDTO> oldVehicles = vehicleSyncDataService.getVehiclesFromSqlServer();
+
+ if (oldVehicles == null || oldVehicles.isEmpty()) {
+ return getDataTable(new ArrayList<>());
+ }
+
+ // 2. 杞崲涓篤O骞舵鏌ュ悓姝ョ姸鎬�
+ List<VehicleSyncVO> voList = new ArrayList<>();
+ for (VehicleSyncDTO dto : oldVehicles) {
+ VehicleSyncVO vo = convertToVO(dto);
+
+ // 3. 妫�鏌ヨ杞﹁締鏄惁宸插悓姝ュ埌鏂扮郴缁�
+ VehicleInfo existVehicle = vehicleInfoMapper.selectVehicleInfoByCarId(dto.getCarId());
+ if (existVehicle != null) {
+ vo.setSynced(true);
+ vo.setVehicleId(existVehicle.getVehicleId());
+ vo.setDeptId(existVehicle.getDeptId());
+ if (existVehicle.getDeptName() != null) {
+ vo.setDeptName(existVehicle.getDeptName());
+ }
+ } else {
+ vo.setSynced(false);
+ }
+
+ voList.add(vo);
+ }
+
+ return getDataTable(voList);
+
+ } catch (Exception e) {
+ log.error("鏌ヨ杞﹁締鍚屾鍒楄〃澶辫触", e);
+ return getDataTable(new ArrayList<>());
+ }
+ }
+
+ /**
+ * 鎵嬪姩鍚屾鍗曚釜杞﹁締鍒版柊绯荤粺
+ */
+ @PreAuthorize("@ss.hasPermi('system:vehicleSync:sync')")
+ @Log(title = "杞﹁締鍚屾", businessType = BusinessType.INSERT)
+ @PostMapping("/syncVehicle")
+ public AjaxResult syncVehicle(@RequestBody Map<String, Object> params) {
+ try {
+ Integer carId = (Integer) params.get("carId");
+ String vehicleNo = (String) params.get("vehicleNo");
+ Long deptId = params.get("deptId") != null ? Long.valueOf(params.get("deptId").toString()) : null;
+
+ if (carId == null || vehicleNo == null || deptId == null) {
+ return AjaxResult.error("鍙傛暟涓嶅畬鏁达細carId銆乿ehicleNo銆乨eptId 涓嶈兘涓虹┖");
+ }
+
+ // 1. 妫�鏌ユ槸鍚﹀凡瀛樺湪
+ VehicleInfo existVehicle = vehicleInfoMapper.selectVehicleInfoByCarId(carId);
+ if (existVehicle != null) {
+ return AjaxResult.error("璇ヨ溅杈嗗凡鍚屾锛岃溅杈咺D: " + existVehicle.getVehicleId());
+ }
+
+ // 2. 鍒涘缓鏂拌溅杈嗚褰�
+ VehicleInfo newVehicle = new VehicleInfo();
+ newVehicle.setCarId(carId);
+ newVehicle.setVehicleNo(vehicleNo);
+ newVehicle.setDeptId(deptId);
+ newVehicle.setStatus("0"); // 榛樿姝e父鐘舵��
+ newVehicle.setCreateBy(getUsername());
+
+ // 3. 鎻掑叆杞﹁締淇℃伅
+ int result = vehicleInfoMapper.insertVehicleInfo(newVehicle);
+
+ if (result > 0) {
+ log.info("鎵嬪姩鍚屾杞﹁締鎴愬姛锛歝arId={}, vehicleNo={}, vehicleId={}",
+ carId, vehicleNo, newVehicle.getVehicleId());
+ return AjaxResult.success("鍚屾鎴愬姛", newVehicle.getVehicleId());
+ } else {
+ return AjaxResult.error("鍚屾澶辫触");
+ }
+
+ } catch (Exception e) {
+ log.error("鎵嬪姩鍚屾杞﹁締澶辫触", e);
+ return AjaxResult.error("鍚屾澶辫触锛�" + e.getMessage());
+ }
+ }
+
+ /**
+ * 灏� DTO 杞崲涓� VO
+ */
+ private VehicleSyncVO convertToVO(VehicleSyncDTO dto) {
+ VehicleSyncVO vo = new VehicleSyncVO();
+ vo.setCarId(dto.getCarId());
+ vo.setVehicleNo(dto.getCarLicense());
+ vo.setCarOrdClass(dto.getCarOrdClass());
+
+ // 鏍规嵁carOrdClass鏄犲皠鍒嗗叕鍙稿悕绉�
+ String deptName = mapCarOrdClassToDeptName(dto.getCarOrdClass());
+ vo.setDeptName(deptName);
+
+ return vo;
+ }
+
+ /**
+ * 鏍规嵁鍗曟嵁绫诲瀷缂栫爜鏄犲皠鍒嗗叕鍙稿悕绉�
+ * 鍙互浠庨厤缃垨鏁版嵁搴撹鍙栵紝杩欓噷绠�鍖栧鐞�
+ */
+ private String mapCarOrdClassToDeptName(String carOrdClass) {
+ // TODO: 鏍规嵁瀹為檯涓氬姟瑙勫垯鏄犲皠
+ // 鍙互浠� sys_config 鎴栦笓闂ㄧ殑鏄犲皠琛ㄨ鍙�
+ Map<String, String> mapping = new HashMap<>();
+ mapping.put("01", "鎬诲叕鍙�");
+ mapping.put("02", "鍒嗗叕鍙窤");
+ mapping.put("03", "鍒嗗叕鍙窧");
+
+ return mapping.getOrDefault(carOrdClass, "鏈煡鍒嗗叕鍙�");
}
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
index fe67735..56ad818 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
@@ -184,10 +184,9 @@
@Log(title = "浠诲姟绠$悊", businessType = BusinessType.INSERT)
@PostMapping("/admin")
public AjaxResult adminAdd(@RequestBody TaskCreateVO createVO) {
- return toAjax(sysTaskService.insertSysTask(createVO));
+ Long taskId = sysTaskService.insertSysTask(createVO);
+ return taskId > 0 ? AjaxResult.success("鏂板鎴愬姛").put("taskId", taskId) : AjaxResult.error("鏂板澶辫触");
}
-
-
/**
* 鏂板浠诲姟锛圓PP绔級
@@ -195,7 +194,8 @@
@Log(title = "浠诲姟鍒涘缓", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult appAdd(@RequestBody TaskCreateVO createVO) {
- return toAjax(sysTaskService.insertSysTask(createVO));
+ Long taskId = sysTaskService.insertSysTask(createVO);
+ return taskId > 0 ? AjaxResult.success("鏂板鎴愬姛").put("taskId", taskId) : AjaxResult.error("鏂板澶辫触");
}
/**
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index 53c5a19..ffcd5be 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -178,12 +178,17 @@
tencent:
map:
key: 6YVBZ-ZJDLQ-JMY5F-BR7XG-H3TAV-C3FXC
-
+ ocr:
+ secretId: AKID52kDPMUP5WgbGfohKhOvFMGwObYEgtsP
+ secretKey: kLjtpkZDcpnWBcpnY5NQEoAHAoFRN4Wl
# 鐧惧害鍦板浘閰嶇疆
baidu:
map:
ak: GX7G1RmAbTEQHor9NKpzRiB2jerqaY1E # 璇锋浛鎹负鎮ㄧ殑鐧惧害鍦板浘API Key
-
+ ocr:
+ appId: 121882692
+ apiKey: zo03Zf3wIU7awOlRnwurDmlO
+ secretKey: tXqyZ5AQ9aT3n1ON4ERQA1aQ88L1sXFw
# 澶╁湴鍥鹃厤缃�
tianditu:
map:
@@ -244,7 +249,11 @@
qrcode:
size: 300
format: PNG
-
+ali:
+ ocr:
+ accessKeyId: LTAI5t7fbEzL7yctbNzA84Q2
+ accessKeySecret: llHxCvmnhS5YSfRCWeuhr5KxgBTnnz
+
# 瀵硅处閰嶇疆
reconciliation:
enabled: true
diff --git a/ruoyi-admin/src/main/resources/logback.xml b/ruoyi-admin/src/main/resources/logback.xml
index 8c3fc6f..3e0d23e 100644
--- a/ruoyi-admin/src/main/resources/logback.xml
+++ b/ruoyi-admin/src/main/resources/logback.xml
@@ -109,7 +109,7 @@
<logger name="org.springframework" level="warn" />
<!-- MyBatis SQL鏃ュ織 -->
<logger name="com.ruoyi.system.mapper" level="debug" />
-
+
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="file_info" />
@@ -148,8 +148,15 @@
</root>
</springProfile>
+ <!-- 榛樿鏍规棩蹇楀櫒閰嶇疆锛堝鏋滄病鏈夋寚瀹歱rofile锛� -->
+ <root level="info">
+ <appender-ref ref="console" />
+ <appender-ref ref="file_info" />
+ <appender-ref ref="file_error" />
+ </root>
+
<!--绯荤粺鐢ㄦ埛鎿嶄綔鏃ュ織-->
<logger name="sys-user" level="info">
- <appender-ref ref="sys-user"/>
+ <appender-ref ref="sys-user" />
</logger>
</configuration>
\ No newline at end of file
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
index dcb55fa..ced65f2 100644
--- a/ruoyi-common/pom.xml
+++ b/ruoyi-common/pom.xml
@@ -139,6 +139,13 @@
<scope>provided</scope>
</dependency>
+ <!-- HanLP 涓枃鍒嗚瘝搴� -->
+ <dependency>
+ <groupId>com.hankcs</groupId>
+ <artifactId>hanlp</artifactId>
+ <version>portable-1.8.4</version>
+ </dependency>
+
</dependencies>
</project>
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/HospitalTokenizerUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/HospitalTokenizerUtil.java
new file mode 100644
index 0000000..839d971
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/HospitalTokenizerUtil.java
@@ -0,0 +1,698 @@
+package com.ruoyi.common.utils;
+
+import com.hankcs.hanlp.HanLP;
+import com.hankcs.hanlp.seg.common.Term;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.hankcs.hanlp.utility.TextUtility.isChinese;
+
+/**
+ * 鍖婚櫌淇℃伅鍒嗚瘝宸ュ叿绫�
+ * 浣跨敤 HanLP 涓撲笟涓枃鍒嗚瘝搴撹繘琛屽垎璇嶅鐞�
+ *
+ * @author ruoyi
+ * @date 2026-01-20
+ */
+public class HospitalTokenizerUtil {
+
+ /**
+ * 鍋滅敤璇嶉泦鍚堬紙闇�瑕佽繃婊ょ殑甯歌璇嶆眹锛�
+ * 娉ㄦ剰锛氣�滃尯鈥濄�佲�滀腑鈥濈瓑鍦ㄥ尰闄㈠悕绉颁腑鏈夋剰涔夛紝涓嶅簲杩囨护
+ */
+ private static final Set<String> STOP_WORDS = new HashSet<>(Arrays.asList(
+ "鍖婚櫌", "璇婃墍", "鍗敓", "闀�", "涔�",
+ "琛楅亾", "璺�", "鍙�", "鏍�", "鍗曞厓", "瀹�", "灞�", "妤�", "鐨�", "浜�",
+ "鍦�", "涓�", "鍜�", "鍙�", "绛�", "涔�", "浜�", "涓�", "鏈�", "鏃�"
+ ));
+
+ /**
+ * 楂樻潈閲嶈瘝璇紙鍖荤枟鏈烘瀯鐗瑰緛璇嶏級
+ * 娉ㄦ剰锛氬湴鍖哄悕涓嶅啀鏀惧湪楂樻潈閲嶈瘝涓紝閬垮厤鍒嗛櫌鍥犲寘鍚叾浠栧湴鍖哄悕鑰岃幏寰楅澶栧姞鍒�
+ */
+ private static final Set<String> HIGH_WEIGHT_WORDS = new HashSet<>(Arrays.asList(
+ "浜烘皯", "涓尰", "涓タ鍖�", "涓タ鍖荤粨鍚�", "鍖荤枟", "濡囧辜", "鍎跨", "鑲ょ",
+ "鍙h厰", "鐪肩", "楠ㄧ", "鏁村舰", "绮剧", "搴峰", "鎬ユ晳", "鍖诲闄�",
+ "鍖荤澶у", "涓撶", "绗竴", "绗簩", "绗笁", "绗洓", "绗簲",
+ "鍐涘尯", "鍐涘尰", "涓績", "闄勫睘", "鐪佺珛", "甯傜珛", "鍖虹珛"
+ ));
+
+ /**
+ * 鍖婚櫌鍚嶇О鍒嗚瘝鐨勯珮棰戝叧閿瘝瀛楀吀锛堢敤浜庡己鍒舵彁鍙栧畬鏁村尰鐤楃浉鍏崇煭璇級
+ * 浠呭寘鍚尰鐤楁満鏋勭浉鍏宠瘝锛屼笉鍖呭惈鍏蜂綋琛屾斂鍦板悕锛岄伩鍏嶅湴鍖虹‖缂栫爜
+ */
+ private static final Set<String> HOSPITAL_KEYWORD_DICT = new HashSet<>(Arrays.asList(
+ "涓尰闄�", "涓尰鍖婚櫌", "甯傚尰闄�", "鐪佸尰闄�", "浜烘皯鍖婚櫌", "涓績鍖婚櫌", "鍙h厰鍖婚櫌",
+ "鍗庝鲸鍖婚櫌", "鍎跨鍖婚櫌", "鐪肩涓績", "绂忓埄闄�", "闂ㄨ瘖閮�", "涓北澶у", "闄勫睘鍖婚櫌",
+ "瀛欓�镐粰"
+ ));
+
+ /** 缁勫悎璇嶇敓鎴愮殑鏈�灏忓瓧绗﹂暱搴� */
+ private static final int MIN_COMBINED_LEN = 4;
+ /** 缁勫悎璇嶇敓鎴愮殑鏈�澶у瓧绗﹂暱搴� */
+ private static final int MAX_COMBINED_LEN = 30;
+ /** 缁勫悎璇嶇敓鎴愭椂鍖呭惈鐨勬渶澶у垎璇嶆暟閲忥紙娣卞害锛� */
+ private static final int MAX_COMBINED_WORDS = 10;
+
+ /**
+ * 瀵瑰尰闄俊鎭繘琛屽垎璇嶏紙浣跨敤 HanLP锛�
+ *
+ * @param hospName 鍖婚櫌鍚嶇О
+ * @param hospShort 鍖婚櫌绠�绉�
+ * @param province 鐪佷唤
+ * @param city 鍩庡競
+ * @param area 鍖哄煙
+ * @param address 璇︾粏鍦板潃
+ * @return 鍒嗚瘝缁撴灉锛堥�楀彿鍒嗛殧鐨勫叧閿瘝瀛楃涓诧級
+ */
+ public static String tokenize(String hospName, String hospShort, String province,
+ String city, String area, String address) {
+ Set<String> keywords = new LinkedHashSet<>();
+
+ // 1. 琛屾斂鍖哄垝锛氬彧浣滀负鐙珛鍏抽敭璇嶏紝涓嶅弬涓庣粍鍚�
+ if (StringUtils.isNotBlank(province)) {
+ keywords.add(province.trim());
+ }
+ if (StringUtils.isNotBlank(city)) {
+ keywords.add(city.trim());
+ }
+ if (StringUtils.isNotBlank(area)) {
+ keywords.add(area.trim());
+ }
+
+ // 2. 鍖婚櫌鍚嶇О锛氬幓鎺夌渷銆佸競鍓嶇紑锛屽彧瀵光�滃尯+鍖婚櫌涓讳綋鈥濆仛鍒嗚瘝鍜岀粍鍚�
+ if (StringUtils.isNotBlank(hospName)) {
+ String nameForSeg = hospName.trim();
+
+ // 鍘绘帀鍓嶉潰鐨勭渷浠�
+ if (StringUtils.isNotBlank(province) && nameForSeg.startsWith(province)) {
+ nameForSeg = nameForSeg.substring(province.length());
+ }
+ // 鍐嶅幓鎺夊煄甯�
+ if (StringUtils.isNotBlank(city) && nameForSeg.startsWith(city)) {
+ nameForSeg = nameForSeg.substring(city.length());
+ }
+ // 鍖轰繚鐣欙細渚嬪 "瓒婄鍖轰腑鍖诲尰闄�"锛岃繖鏍峰彲浠ョ敓鎴� "瓒婄鍖轰腑鍖婚櫌"銆�"涓尰闄�" 绛夌粍鍚堣瘝
+ keywords.addAll(extractKeywordsByHanLP(nameForSeg));
+ // 鍩轰簬鍖婚櫌鍏ㄧО锛屽己鍒舵彁鍙栭珮棰戝尰鐤楀叧閿瘝锛堝鈥滀腑鍖婚櫌鈥濃�滃効绔ュ尰闄⑩�濈瓑锛�
+ addDictPhrases(hospName, keywords);
+ }
+
+ // 3. 鍖婚櫌绠�绉帮細閫氬父涓嶅甫鐪佸競鍖猴紝鐩存帴鍒嗚瘝
+ if (StringUtils.isNotBlank(hospShort)) {
+ keywords.addAll(extractKeywordsByHanLP(hospShort));
+ addDictPhrases(hospShort, keywords);
+ }
+
+ // 4. 杩囨护鍋滅敤璇嶅拰鏃犳晥璇�
+ keywords = keywords.stream()
+ .filter(keyword -> !STOP_WORDS.contains(keyword))
+ .filter(keyword -> keyword.length() > 0)
+ .filter(HospitalTokenizerUtil::isValidKeyword)
+ .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
+
+ return String.join(",", keywords);
+ }
+
+ /**
+ * 浣跨敤 HanLP 浠庢枃鏈腑鎻愬彇鍏抽敭璇�
+ *
+ * @param text 鏂囨湰
+ * @return 鍏抽敭璇嶉泦鍚�
+ */
+ private static Set<String> extractKeywordsByHanLP(String text) {
+ Set<String> keywords = new LinkedHashSet<>();
+
+ if (StringUtils.isBlank(text)) {
+ return keywords;
+ }
+
+ try {
+ // 浣跨敤 HanLP 杩涜鍒嗚瘝
+ List<Term> terms = HanLP.segment(text.trim());
+
+ // 娣诲姞瀹屾暣鏂囨湰锛堝鏋滀笉澶暱锛�
+ if (text.length() <= 20) {
+ keywords.add(text.trim());
+ }
+
+ // 鎻愬彇鍒嗚瘝缁撴灉
+ List<String> validWords = new ArrayList<>();
+ for (Term term : terms) {
+ String word = term.word;
+
+ // 杩囨护鍗曞瓧绗︼紙闄ら潪鏄噸瑕佺殑涓枃瀛楃锛�
+ if (word.length() == 1 && !isChinese(word.charAt(0))) {
+ continue;
+ }
+
+ // 娣诲姞鏈夋晥鐨勫垎璇�
+ if (isValidKeyword(word)) {
+ keywords.add(word);
+ validWords.add(word);
+ }
+ }
+
+ // 銆愬叧閿紭鍖栥�戠敓鎴愯繛缁粍鍚堣瘝
+ // 浣嗚杩囨护鎺夋嫭鍙峰唴瀹癸紝閬垮厤鐢熸垚鏃犳剰涔夌殑鍒嗛櫌缁勫悎璇�
+ // 渚嬪锛歔"瓒婄鍖�", "涓尰", "闄�"] 鈫� 鐢熸垚 "瓒婄鍖轰腑鍖�", "涓尰闄�", "瓒婄鍖轰腑鍖婚櫌"
+
+ // 绉婚櫎鎷彿鍐呭鐢ㄤ簬鐢熸垚缁勫悎璇�
+ String textWithoutBrackets = text
+ .replaceAll("锛圼^锛塢*锛�", "") // 绉婚櫎涓枃鎷彿
+ .replaceAll("\\([^\\)]*\\)", "") // 绉婚櫎鑻辨枃鎷彿
+ .replaceAll("銆怺^銆慮*銆�", "") // 绉婚櫎鏂规嫭鍙�
+ .trim();
+
+ // 瀵圭Щ闄ゆ嫭鍙峰悗鐨勬枃鏈噸鏂板垎璇�
+ List<Term> cleanTerms = HanLP.segment(textWithoutBrackets);
+ List<String> cleanValidWords = new ArrayList<>();
+ for (Term term : cleanTerms) {
+ String word = term.word;
+ if (word.length() == 1 && !isChinese(word.charAt(0))) {
+ continue;
+ }
+ if (isValidKeyword(word)) {
+ cleanValidWords.add(word);
+ }
+ }
+
+ // 鍩轰簬骞插噣鐨勫垎璇嶇敓鎴愮粍鍚堣瘝
+ for (int len = 2; len <= Math.min(MAX_COMBINED_WORDS, cleanValidWords.size()); len++) {
+ for (int i = 0; i <= cleanValidWords.size() - len; i++) {
+ StringBuilder combined = new StringBuilder();
+ for (int j = i; j < i + len; j++) {
+ combined.append(cleanValidWords.get(j));
+ }
+ String combinedWord = combined.toString();
+
+ // 鍙坊鍔犻暱搴﹀悎鐞嗙殑缁勫悎璇�
+ if (combinedWord.length() >= MIN_COMBINED_LEN && combinedWord.length() <= MAX_COMBINED_LEN) {
+ keywords.add(combinedWord);
+ // 閽堝鈥滆秺绉�鍖轰腑鍖婚櫌鈥濊繖绫绘ā寮忥紝棰濆鐢熸垚鍘绘帀鈥滃尯鈥濈殑绠�鍖栧叧閿瘝锛屽鈥滆秺绉�涓尰闄⑩��
+ String simplified = simplifyDistrictInKeyword(combinedWord);
+ if (simplified != null && simplified.length() >= MIN_COMBINED_LEN && simplified.length() <= MAX_COMBINED_LEN) {
+ keywords.add(simplified);
+ }
+ }
+ }
+ }
+
+ } catch (Exception e) {
+ // HanLP 鍒嗚瘝澶辫触鏃讹紝闄嶇骇浣跨敤绠�鍗曞垎璇�
+ keywords.addAll(extractKeywordsByNGram(text));
+ }
+
+ return keywords;
+ }
+
+ /**
+ * 闄嶇骇鏂规锛氫娇鐢ㄧ畝鍗曠殑 N-Gram 鍒嗚瘝
+ *
+ * @param text 鏂囨湰
+ * @return 鍏抽敭璇嶉泦鍚�
+ */
+ private static Set<String> extractKeywordsByNGram(String text) {
+ Set<String> keywords = new LinkedHashSet<>();
+
+ if (StringUtils.isBlank(text)) {
+ return keywords;
+ }
+
+ text = text.trim();
+ int length = text.length();
+
+ // 鐢熸垚2-4瀛楃鐨凬-Gram
+ for (int n = 2; n <= 4 && n <= length; n++) {
+ for (int i = 0; i <= length - n; i++) {
+ String ngram = text.substring(i, i + n);
+ if (isValidKeyword(ngram)) {
+ keywords.add(ngram);
+ }
+ }
+ }
+
+ return keywords;
+ }
+
+ /**
+ * 鍒ゆ柇鍏抽敭璇嶆槸鍚︽湁鏁�
+ *
+ * @param keyword 鍏抽敭璇�
+ * @return 鏄惁鏈夋晥
+ */
+ private static boolean isValidKeyword(String keyword) {
+ if (StringUtils.isBlank(keyword)) {
+ return false;
+ }
+
+ // 杩囨护绾暟瀛�
+ if (keyword.matches("^\\d+$")) {
+ return false;
+ }
+
+ // 杩囨护绾鍙�
+ if (keyword.matches("^[\\p{P}\\p{S}]+$")) {
+ return false;
+ }
+
+ // 鑷冲皯鍖呭惈涓�涓腑鏂囨垨瀛楁瘝
+ return keyword.matches(".*[\\u4e00-\\u9fa5a-zA-Z].*");
+ }
+
+ /**
+ * 閽堝鍚湁鈥滃尯涓尰闄�/鍖轰腑鍖烩�濈殑缁勫悎璇嶏紝鐢熸垚鍘绘帀鈥滃尯鈥濈殑绠�鍖栧舰寮�
+ * 渚嬪锛�"瓒婄鍖轰腑鍖婚櫌" 鈫� "瓒婄涓尰闄�"锛�"瓒婄鍖轰腑鍖�" 鈫� "瓒婄涓尰"
+ */
+ private static String simplifyDistrictInKeyword(String keyword) {
+ if (StringUtils.isBlank(keyword)) {
+ return null;
+ }
+ // 閫氱敤瑙勫垯锛氬幓鎺夆�滃尯鈥濊繖涓鏀垮眰绾ф爣璇嗭紝浣嗕粎闄愪簬鈥滃尯涓尰闄�/鍖轰腑鍖烩�濊繖绉嶅尰鐤楀満鏅�
+ if (keyword.contains("鍖轰腑鍖婚櫌")) {
+ return keyword.replaceFirst("鍖轰腑鍖婚櫌", "涓尰闄�");
+ }
+ if (keyword.contains("鍖轰腑鍖�")) {
+ return keyword.replaceFirst("鍖轰腑鍖�", "涓尰");
+ }
+ return null;
+ }
+
+ /**
+ * 鍩轰簬鍖婚櫌鍚嶇О/绠�绉帮紝寮哄埗鎻愬彇鍖婚櫌鍏抽敭璇嶅瓧鍏镐腑鐨勭煭璇�
+ */
+ private static void addDictPhrases(String text, Set<String> keywords) {
+ if (StringUtils.isBlank(text) || keywords == null) {
+ return;
+ }
+ for (String phrase : HOSPITAL_KEYWORD_DICT) {
+ if (text.contains(phrase)) {
+ keywords.add(phrase);
+ }
+ }
+ }
+
+ /* 绉婚櫎鍖婚櫌鍚嶇О涓殑鍦板煙鍓嶇紑锛堢渷/甯�/鑷不鍖虹瓑锛�
+ * 閫氱敤澶勭悊锛屼笉纭紪鐮佸叿浣撳湴鍚�
+ *
+ * @param hospName 鍖婚櫌鍚嶇О
+ * @return 绉婚櫎鍦板煙鍓嶇紑鍚庣殑鍚嶇О
+ */
+ private static String removeLocationPrefixes(String hospName) {
+ if (StringUtils.isBlank(hospName)) {
+ return hospName;
+ }
+
+ String result = hospName;
+
+ // 绉婚櫎甯歌鐨勮鏀垮尯鍒掑悗缂�
+ // 鐪佺骇锛� XX鐪併�乆X甯傦紙鐩磋緰甯傦級銆乆X鑷不鍖�
+ result = result.replaceFirst("^[\\u4e00-\\u9fa5]{2,10}鐪�", "");
+ result = result.replaceFirst("^[\\u4e00-\\u9fa5]{2,10}鑷不鍖�", "");
+
+ // 鍦扮骇甯傦細XX甯�
+ result = result.replaceFirst("^[\\u4e00-\\u9fa5]{2,10}甯�", "");
+
+ // 鍘跨骇锛歑X鍖恒�乆X鍘裤�乆X甯傦紙鍘跨骇甯傦級
+ result = result.replaceFirst("^[\\u4e00-\\u9fa5]{2,10}鍖�", "");
+ result = result.replaceFirst("^[\\u4e00-\\u9fa5]{2,10}鍘�", "");
+
+ return result.trim();
+ }
+
+ /**
+ * 璁$畻涓や釜鍒嗚瘝闆嗗悎鐨勫尮閰嶅害锛堜紭鍖栫増锛�
+ * 鑰冭檻鍥犵礌锛�
+ * 0. 銆愭牳蹇冦�戝畬鏁存悳绱㈡枃鏈湪keywords涓瓨鍦� 鈫� 楂樺垎锛�+100鍒嗭級
+ * 1. 瀹屾暣鍖归厤鍔犲垎锛堝崟涓瘝鍖归厤锛�
+ * 1.5 瓒呯骇鍔犲垎锛氬畬鏁存悳绱㈡枃鏈寘鍚湪鍖婚櫌鍚嶄腑锛�+80鍒嗭級锛屾湭鍖归厤鍐呭娓愯繘鎯╃綒
+ * 2. 璇嶈鏉冮噸锛堥噸瑕佽瘝姹囧姞鍒嗭級
+ * 3. 杩炵画鍖归厤鍔犲垎
+ * 4. 瀛楃鐩镐技搴�
+ * 5. 璐熷悜鍖归厤鎯╃綒锛堝尰闄㈠悕涓嚭鐜版悳绱㈣瘝涔嬪鐨勫湴鍖哄悕 -30鍒嗭級
+ * 6. 鍒嗛櫌杞诲井闄嶆潈锛�-10鍒嗭級
+ * 7. 鎷彿鍐呭杞诲井鎯╃綒锛�-5鍒嗭級
+ *
+ * @param searchKeywords 鎼滅储鍒嗚瘝锛堥�楀彿鍒嗛殧锛�
+ * @param hospitalKeywords 鍖婚櫌鍒嗚瘝锛堥�楀彿鍒嗛殧锛�
+ * @param hospName 鍖婚櫌鍚嶇О锛堢敤浜庡畬鏁村尮閰嶅垽鏂級
+ * @param districtNames 鍦板尯鍚嶇О闆嗗悎锛堢敤浜庤礋鍚戝尮閰嶆鏌ワ紝鍙负null锛�
+ * @return 鍖归厤鍒嗘暟
+ */
+ public static int calculateMatchScore(String searchKeywords, String hospitalKeywords, String hospName, Set<String> districtNames) {
+ if (StringUtils.isBlank(searchKeywords) || StringUtils.isBlank(hospitalKeywords)) {
+ return 0;
+ }
+
+ List<String> searchWords = Arrays.asList(searchKeywords.split(","));
+ List<String> hospWords = Arrays.asList(hospitalKeywords.split(","));
+ Set<String> searchWordsSet = new HashSet<>(searchWords);
+ Set<String> hospWordsSet = new HashSet<>(hospWords);
+
+ int totalScore = 0;
+
+ // 0. 銆愭牳蹇冧紭鍖栥�戦鍏堝垽鏂槸鍚﹀瓨鍦ㄢ�滃畬鏁村尮閰嶁��
+ // 绾﹀畾锛歴earchKeywords 鐨勭涓�涓垎璇嶄负鍘熷鎼滅储鏂囨湰
+ String fullSearchText = searchWords.get(0);
+ boolean keywordFullMatch = hospWordsSet.contains(fullSearchText);
+ boolean nameFullMatch = (hospName != null && hospName.contains(fullSearchText));
+
+ if (keywordFullMatch || nameFullMatch) {
+ // 瀹屾暣鍖归厤浼樺厛锛氱洿鎺ョ粰鍥哄畾鏋侀珮鍒嗭紝纭繚鎺掑湪鏈�鍓嶉潰
+ totalScore = 1000; // 鎻愬崌鍩虹鍒嗕负1000锛屼綔涓哄垎鏁板ぉ鑺辨澘
+
+ // 瀵瑰畬鏁村尮閰嶇粨鏋滐紝浠嶇劧鍙互搴旂敤鍦板尯鎯╃綒鍜屽垎闄�/鎷彿杞诲井闄嶆潈锛屼繚璇佽涔夋纭�
+ if (districtNames != null && !districtNames.isEmpty()) {
+ totalScore -= calculateNegativeMatchPenalty(searchWordsSet, districtNames, hospName);
+ }
+
+// if (isBranchHospital(hospName)) {
+// totalScore -= 10; // 鍒嗛櫌鎵�10鍒�
+// }
+
+// if (hospName != null && (hospName.contains("锛�") || hospName.contains("(") || hospName.contains("銆�"))) {
+// totalScore -= 5; // 鎷彿鍐呭杞诲井鎵e垎
+// }
+
+ return Math.max(0, totalScore);
+ }
+
+ // 1. 瀹屾暣鍖归厤鍔犲垎锛堝崟涓瘝鍖归厤锛�
+ for (String searchWord : searchWords) {
+ if (searchWord.length() >= 4 && hospName != null && hospName.contains(searchWord)) {
+ totalScore += 50; // 瀹屾暣璇嶅尮閰嶅姞鍒�
+ }
+ }
+
+ // 1.5 瓒呯骇鍔犲垎锛氭悳绱㈡枃鏈笌鍖婚櫌鍚嶇殑瀹屾暣鐩镐技搴�
+ if (hospName != null) {
+ // 瀹屽叏鍖呭惈鍔犲垎
+ if (hospName.contains(fullSearchText)) {
+ totalScore += 500; // 鎻愬崌鍖呭惈鍏崇郴鐨勫垎鏁帮紝纭繚鍖呭惈鎼滅储鍏ㄧО鐨勭粨鏋滄帓鍚嶉潬鍓�
+ } else {
+ // 璁$畻鏁翠綋鐩镐技搴�
+ int similarity = calculateStringSimilarity(fullSearchText, hospName);
+ if (similarity > 80) {
+ totalScore += similarity / 2; // 楂樺害鐩镐技涔熷姞鍒嗭紝浣嗘潈閲嶉檷浣�
+ }
+ }
+
+ // 鏈尮閰嶅唴瀹规笎杩涙儵缃氾細鍖婚櫌鍚嶄腑鏈夋悳绱㈣瘝涔嬪鐨勫唴瀹�
+ String cleanedHospName = removeLocationPrefixes(hospName);
+ int unmatchedLength = cleanedHospName.length() - fullSearchText.length();
+ if (unmatchedLength > 0) {
+ // 娓愯繘鎯╃綒锛�1-5瀛楁墸1鍒�/瀛楋紝6-10瀛楁墸2鍒�/瀛楋紝11+瀛楁墸3鍒�/瀛�
+ if (unmatchedLength <= 5) {
+ totalScore -= unmatchedLength * 1;
+ } else if (unmatchedLength <= 10) {
+ totalScore -= 5 + (unmatchedLength - 5) * 2;
+ } else {
+ totalScore -= 5 + 10 + (unmatchedLength - 10) * 3;
+ }
+ }
+ }
+
+ // 2. 鍒嗚瘝鍖归厤璁″垎锛堜紭鍏堝尮閰嶈緝闀跨殑鎼滅储璇嶏紝鍛戒腑鍗虫锛�
+ List<String> sortedSearchWords = new ArrayList<>(searchWords);
+ sortedSearchWords.sort((a, b) -> Integer.compare(b.length(), a.length())); // 鎸夐暱搴︿粠闀垮埌鐭�
+ boolean anyMatch = false;
+
+ for (String searchWord : sortedSearchWords) {
+ boolean isLong = searchWord.length() >= 4;
+ if (hospWords.contains(searchWord)) {
+ int wordScore;
+ if (isLong) {
+ // 闀胯瘝瀹屾暣鍖归厤锛氶珮鍒�
+ wordScore = 40 + searchWord.length() * 4;
+ } else {
+ // 鐭瘝瀹屾暣鍖归厤锛氫綆鍒�
+ wordScore = 10 + searchWord.length() * 2;
+ }
+
+ // 楂樻潈閲嶈瘝棰濆鍔犲垎
+ if (HIGH_WEIGHT_WORDS.contains(searchWord)) {
+ wordScore += 15;
+ }
+
+ totalScore += wordScore;
+ anyMatch = true;
+
+ // 銆愭牳蹇冧慨鏀广�戝彧瑕佸尮閰嶅埌涓�涓垎璇嶏紙鏃犺闀跨煭锛夛紝灏变腑鏂悗缁尮閰嶏紝閬靛惊闀胯瘝浼樺厛鍘熷垯
+ break;
+ } else {
+ // 2.3 閮ㄥ垎鍖归厤锛堝寘鍚叧绯伙級锛屽彧瀵硅緝闀挎悳绱㈣瘝鑰冭檻
+ if (isLong) {
+ for (String hospWord : hospWords) {
+ if (hospWord.contains(searchWord) || searchWord.contains(hospWord)) {
+ int partialScore = Math.min(searchWord.length(), hospWord.length()) * 2;
+ totalScore += partialScore;
+ anyMatch = true;
+ break;
+ }
+ }
+ if (anyMatch) {
+ break; // 鍛戒腑鍗虫
+ }
+ }
+ }
+ }
+
+ // 濡傛灉宸茬粡鏈夊尮閰嶏紝鍒欏簲鐢ㄨ礋鍚戞儵缃氥�佸垎闄�/鎷彿璋冩暣骞惰繑鍥�
+ if (anyMatch) {
+ if (districtNames != null && !districtNames.isEmpty()) {
+ totalScore -= calculateNegativeMatchPenalty(searchWordsSet, districtNames, hospName);
+ }
+ if (isBranchHospital(hospName)) {
+ totalScore -= 10;
+ }
+ if (hospName != null && (hospName.contains("锛�") || hospName.contains("(") || hospName.contains("銆�"))) {
+ totalScore -= 5;
+ }
+ return Math.max(0, totalScore);
+ }
+
+ // 3. 杩炵画鍖归厤鍔犲垎
+ totalScore += calculateContinuousMatchBonus(searchWords, hospWords);
+
+ // 4. 瀛楃鐩镐技搴﹀姞鍒嗭紙瀵逛簬闀胯瘝锛�
+ for (String searchWord : searchWords) {
+ if (searchWord.length() >= 4) {
+ for (String hospWord : hospWords) {
+ if (hospWord.length() >= 4) {
+ int similarity = calculateStringSimilarity(searchWord, hospWord);
+ if (similarity > 70) { // 鐩镐技搴﹁秴杩�70%
+ totalScore += similarity / 10;
+ }
+ }
+ }
+ }
+ }
+
+ // 5. 璐熷悜鍖归厤鎯╃綒锛氬尰闄㈠悕涓寘鍚悳绱㈣瘝涔嬪鐨勯珮鏉冮噸鍦板尯鍚�
+ if (districtNames != null && !districtNames.isEmpty()) {
+ totalScore -= calculateNegativeMatchPenalty(searchWordsSet, districtNames, hospName);
+ }
+
+ // 6. 鍒嗛櫌杞诲井闄嶆潈锛氫富闄紭鍏堬紝浣嗕笉瑕佽繃搴︽儵缃�
+ if (isBranchHospital(hospName)) {
+ totalScore -= 10; // 鍒嗛櫌鎵�10鍒嗭紙鏀逛负鍥哄畾鎵e垎锛岃�岄潪鎵撴姌锛�
+ }
+
+ // 7. 鎷彿鍐呭杞诲井鎯╃綒锛氭嫭鍙峰唴閫氬父鏄瑕佷俊鎭�
+ if (hospName != null && (hospName.contains("锛�") || hospName.contains("(") || hospName.contains("銆�"))) {
+ totalScore -= 5; // 鍖呭惈鎷彿鎵�5鍒嗭紙鏀逛负鍥哄畾鎵e垎锛�
+ }
+
+ return Math.max(0, totalScore); // 纭繚鍒嗘暟涓嶄负璐�
+ }
+
+ /**
+ * 璁$畻璐熷悜鍖归厤鎯╃綒
+ * 濡傛灉鍖婚櫌鍚嶄腑鍖呭惈鎼滅储璇嶄箣澶栫殑鍦板尯鍚嶇О锛屽簲璇ラ檷浣庢帓鍚�
+ *
+ * @param searchWords 鎼滅储璇嶉泦鍚�
+ * @param districtNames 鎵�鏈夊尰闄㈢殑鍦板尯鍚嶇О闆嗗悎锛堜粠鍖婚櫌琛ㄧ殑 hopsArea 瀛楁鎻愬彇锛�
+ * @param hospName 褰撳墠鍖婚櫌鍚嶇О
+ * @return 鎯╃綒鍒嗘暟
+ */
+ private static int calculateNegativeMatchPenalty(Set<String> searchWords, Set<String> districtNames, String hospName) {
+ if (hospName == null || districtNames == null || districtNames.isEmpty()) {
+ return 0;
+ }
+
+ int penalty = 0;
+
+ // 妫�鏌ュ尰闄㈠悕涓殑鍦板尯鍚�
+ for (String district : districtNames) {
+ if (StringUtils.isBlank(district)) {
+ continue;
+ }
+
+ // 濡傛灉鍖婚櫌鍚嶅寘鍚鍦板尯鍚�
+ if (hospName.contains(district)) {
+ // 妫�鏌ユ槸鍚﹀湪鎼滅储璇嶄腑鍑虹幇
+ boolean inSearchWords = false;
+
+ // 1. 鐩存帴鍖归厤锛氭悳绱㈣瘝闆嗗悎涓寘鍚鍦板尯鍚�
+ if (searchWords.contains(district)) {
+ inSearchWords = true;
+ } else {
+ // 2. 閮ㄥ垎鍖归厤锛氭悳绱㈣瘝鐨勪换浣曚竴涓瘝鍖呭惈璇ュ湴鍖哄悕
+ for (String searchWord : searchWords) {
+ if (searchWord.contains(district)) {
+ inSearchWords = true;
+ break;
+ }
+ }
+ }
+
+ // 濡傛灉鍖婚櫌鍚嶅寘鍚鍦板尯鍚嶏紝浣嗘悳绱㈣瘝涓病鏈夛紝鍒欐墸鍒�
+ if (!inSearchWords) {
+ penalty += 30; // 鍖呭惈涓嶇浉鍏冲湴鍖哄悕锛屾墸30鍒�
+ }
+ }
+ }
+
+ return penalty;
+ }
+
+ /**
+ * 鍒ゆ柇鏄惁涓哄垎闄�
+ */
+ private static boolean isBranchHospital(String hospName) {
+ if (hospName == null) {
+ return false;
+ }
+
+ // 鍒嗛櫌鐗瑰緛鍏抽敭璇�
+ String[] branchKeywords = {
+ "鍒嗛櫌", "鍒嗛儴", "闂ㄨ瘖閮�", "绀惧尯鍗敓", "鍗敓绔�", "鍗敓鏈嶅姟涓績",
+ "涓滈櫌", "瑗块櫌", "鍗楅櫌", "鍖楅櫌", "鏂伴櫌", "鑰侀櫌"
+ };
+
+ for (String keyword : branchKeywords) {
+ if (hospName.contains(keyword)) {
+ return true;
+ }
+ }
+
+ // 鍖呭惈鍏蜂綋璺悕/琛楅亾鍚嶄篃鍙兘鏄垎闄�
+ String[] roadKeywords = {
+ "璺垎闄�", "琛楀垎闄�", "閬撳垎闄�", "澶ч亾鍒嗛櫌"
+ };
+
+ for (String keyword : roadKeywords) {
+ if (hospName.contains(keyword)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 璁$畻杩炵画鍖归厤鍔犲垎
+ */
+ private static int calculateContinuousMatchBonus(List<String> searchWords, List<String> hospWords) {
+ int bonus = 0;
+ int consecutiveCount = 0;
+
+ for (int i = 0; i < searchWords.size() - 1; i++) {
+ String word1 = searchWords.get(i);
+ String word2 = searchWords.get(i + 1);
+
+ // 鍒ゆ柇鏄惁鍦ㄥ尰闄㈠垎璇嶄腑杩炵画鍑虹幇
+ boolean found = false;
+ for (int j = 0; j < hospWords.size() - 1; j++) {
+ if (hospWords.get(j).equals(word1) && hospWords.get(j + 1).equals(word2)) {
+ consecutiveCount++;
+ found = true;
+ break;
+ }
+ }
+
+ if (found) {
+ bonus += consecutiveCount * 5; // 杩炵画瓒婇暱鍔犲垎瓒婂
+ } else {
+ consecutiveCount = 0;
+ }
+ }
+
+ return bonus;
+ }
+
+ /**
+ * 璁$畻瀛楃涓茬浉浼煎害锛堜娇鐢↙evenshtein璺濈锛�
+ *
+ * @param s1 瀛楃涓�1
+ * @param s2 瀛楃涓�2
+ * @return 鐩镐技搴︾櫨鍒嗘瘮 (0-100)
+ */
+ private static int calculateStringSimilarity(String s1, String s2) {
+ if (s1.equals(s2)) {
+ return 100;
+ }
+
+ int maxLen = Math.max(s1.length(), s2.length());
+ if (maxLen == 0) {
+ return 100;
+ }
+
+ int distance = levenshteinDistance(s1, s2);
+ return (int) ((1 - (double) distance / maxLen) * 100);
+ }
+
+ /**
+ * 璁$畻Levenshtein璺濈锛堢紪杈戣窛绂伙級
+ */
+ private static int levenshteinDistance(String s1, String s2) {
+ int len1 = s1.length();
+ int len2 = s2.length();
+
+ int[][] dp = new int[len1 + 1][len2 + 1];
+
+ for (int i = 0; i <= len1; i++) {
+ dp[i][0] = i;
+ }
+
+ for (int j = 0; j <= len2; j++) {
+ dp[0][j] = j;
+ }
+
+ for (int i = 1; i <= len1; i++) {
+ for (int j = 1; j <= len2; j++) {
+ int cost = s1.charAt(i - 1) == s2.charAt(j - 1) ? 0 : 1;
+ dp[i][j] = Math.min(
+ Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1),
+ dp[i - 1][j - 1] + cost
+ );
+ }
+ }
+
+ return dp[len1][len2];
+ }
+
+ /**
+ * 瀵规枃鏈繘琛屽垎璇嶏紙鍓嶇浼犲叆鐨勬悳绱㈠叧閿瘝锛�
+ *
+ * @param text 鎼滅储鏂囨湰
+ * @return 鍒嗚瘝缁撴灉锛堥�楀彿鍒嗛殧锛�
+ */
+ public static String tokenizeSearchText(String text) {
+ if (StringUtils.isBlank(text)) {
+ return "";
+ }
+
+ Set<String> keywords = extractKeywordsByHanLP(text.trim());
+
+ // 杩囨护鍋滅敤璇�
+ keywords = keywords.stream()
+ .filter(keyword -> !STOP_WORDS.contains(keyword))
+ .filter(keyword -> keyword.length() > 0)
+ .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
+
+ return String.join(",", keywords);
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java
index ba6caaf..72c8e29 100644
--- a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java
@@ -13,7 +13,7 @@
import com.ruoyi.system.service.ITaskStatusPushService;
/**
- * 鏃х郴缁熷悓姝ュ畾鏃朵换鍔�
+ * 鏃х郴缁熷悓姝ュ畾鏃朵换鍔� 鏂扮郴缁熶腑鐨勪换鍔″悓姝ュ埌鏃х郴缁熶腑
*
* @author ruoyi
* @date 2024-01-20
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacyTransferSyncTask.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacyTransferSyncTask.java
index 9e27a30..71c1435 100644
--- a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacyTransferSyncTask.java
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacyTransferSyncTask.java
@@ -8,7 +8,7 @@
/**
* 鏃х郴缁熻浆杩愬崟鍚屾瀹氭椂浠诲姟
- *
+ * (鏃х郴缁熻縼绉诲埌鏂扮郴缁�)
* @author ruoyi
* @date 2025-11-19
*/
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleAbnormalAlertTask.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleAbnormalAlertTask.java
new file mode 100644
index 0000000..8576d4d
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleAbnormalAlertTask.java
@@ -0,0 +1,547 @@
+package com.ruoyi.quartz.task;
+
+import com.ruoyi.common.core.domain.entity.SysDept;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.domain.*;
+import com.ruoyi.system.mapper.*;
+import com.ruoyi.system.service.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 杞﹁締寮傚父杩愯鐩戞帶瀹氭椂浠诲姟
+ *
+ * @author ruoyi
+ */
+@Component("vehicleAbnormalAlertTask")
+public class VehicleAbnormalAlertTask {
+
+ private static final Logger log = LoggerFactory.getLogger(VehicleAbnormalAlertTask.class);
+
+ @Autowired
+ private ISysConfigService configService;
+
+ @Autowired
+ private VehicleGpsSegmentMileageMapper segmentMileageMapper;
+
+ @Autowired
+ private VehicleInfoMapper vehicleInfoMapper;
+
+ @Autowired
+ private SysTaskMapper sysTaskMapper;
+
+ @Autowired
+ private VehicleAbnormalAlertMapper alertMapper;
+
+ @Autowired
+ private IQyWechatService qyWechatService;
+
+ @Autowired
+ private ISysDeptService deptService;
+
+ @Autowired
+ private IVehicleAbnormalAlertService alertService;
+
+ @Autowired
+ private IVehicleAlertConfigService alertConfigService;
+
+ @Autowired
+ private ISysUserService userService;
+
+ /**
+ * 鐩戞帶杞﹁締寮傚父杩愯鎯呭喌
+ */
+ public void monitorVehicleAbnormalRunning() {
+ try {
+ // 妫�鏌ュ姛鑳藉紑鍏�
+ if (!isAlertEnabled()) {
+ log.debug("杞﹁締寮傚父鍛婅鍔熻兘鏈惎鐢紝璺宠繃鐩戞帶");
+ return;
+ }
+
+ log.info("寮�濮嬫墽琛岃溅杈嗗紓甯歌繍琛岀洃鎺т换鍔�");
+
+ // 鍔犺浇閰嶇疆鍙傛暟
+ AlertConfig config = loadAlertConfig();
+
+ // 鑾峰彇鐩戞帶鏃堕棿绐楀彛
+ Date endTime = new Date();
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(endTime);
+ cal.add(Calendar.MINUTE, -config.timeWindow);
+ Date startTime = cal.getTime();
+
+ // 鏌ヨ鎵�鏈夋椿璺冭溅杈�
+ List<VehicleInfo> vehicles = vehicleInfoMapper.selectVehicleInfoList(new VehicleInfo());
+ if (vehicles == null || vehicles.isEmpty()) {
+ log.debug("娌℃湁鎵惧埌闇�瑕佺洃鎺х殑杞﹁締");
+ return;
+ }
+
+ log.info("寮�濮嬬洃鎺� {} 杈嗚溅杈嗭紝鏃堕棿绐楀彛: {} 鍒� {}", vehicles.size(),
+ DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, startTime),
+ DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, endTime));
+
+ int alertCount = 0;
+ for (VehicleInfo vehicle : vehicles) {
+ try {
+ if (checkVehicleAbnormalRunning(vehicle, startTime, endTime, config)) {
+ alertCount++;
+ }
+ } catch (Exception e) {
+ log.error("妫�鏌ヨ溅杈� {} 寮傚父杩愯澶辫触", vehicle.getVehicleNo(), e);
+ }
+ }
+
+ log.info("杞﹁締寮傚父杩愯鐩戞帶浠诲姟瀹屾垚锛屽叡浜х敓 {} 涓憡璀�", alertCount);
+
+ } catch (Exception e) {
+ log.error("杞﹁締寮傚父杩愯鐩戞帶浠诲姟鎵ц澶辫触", e);
+ }
+ }
+
+ /**
+ * 妫�鏌ュ崟涓溅杈嗘槸鍚﹀紓甯歌繍琛�
+ */
+ private boolean checkVehicleAbnormalRunning(VehicleInfo vehicle, Date startTime, Date endTime, AlertConfig globalConfig) {
+ Long vehicleId = vehicle.getVehicleId();
+ String vehicleNo = vehicle.getVehicleNo();
+ Long deptId = vehicle.getDeptId();
+
+ // 鑾峰彇璇ヨ溅杈嗙殑閰嶇疆锛堜紭鍏堢骇锛氳溅杈� > 閮ㄩ棬 > 鍏ㄥ眬锛�
+ AlertConfig config = getVehicleAlertConfig(vehicleId, deptId, globalConfig);
+ if (config == null) {
+ log.debug("杞﹁締 {} 鏈壘鍒版湁鏁堥厤缃紝璺宠繃鐩戞帶", vehicleNo);
+ return false;
+ }
+
+ // 1. 鏌ヨ杞﹁締鍦ㄦ椂闂寸獥鍙e唴鐨勬�昏繍琛岄噷绋�
+ BigDecimal totalMileage = calculateVehicleMileage(vehicleId, startTime, endTime);
+ if (totalMileage == null || totalMileage.compareTo(BigDecimal.ZERO) == 0) {
+ log.debug("杞﹁締 {} 鍦ㄧ洃鎺х獥鍙e唴鏃犺繍琛岄噷绋�", vehicleNo);
+ return false;
+ }
+
+ // 2. 鏌ヨ杞﹁締鍦ㄦ椂闂寸獥鍙e唴鏈変换鍔℃椂鐨勯噷绋�
+ BigDecimal taskMileage = calculateTaskMileage(vehicleId, startTime, endTime);
+ if (taskMileage == null) {
+ taskMileage = BigDecimal.ZERO;
+ }
+
+ // 3. 璁$畻闈炰换鍔$姸鎬佷笅鐨勮繍琛岄噷绋�
+ BigDecimal nonTaskMileage = totalMileage.subtract(taskMileage);
+ if (nonTaskMileage.compareTo(BigDecimal.ZERO) <= 0) {
+ log.debug("杞﹁締 {} 鍦ㄧ洃鎺х獥鍙e唴鏃犻潪浠诲姟閲岀▼", vehicleNo);
+ return false;
+ }
+
+ log.debug("杞﹁締 {} 鎬婚噷绋�: {}km, 浠诲姟閲岀▼: {}km, 闈炰换鍔¢噷绋�: {}km",
+ vehicleNo, totalMileage, taskMileage, nonTaskMileage);
+
+ // 4. 妫�鏌ラ潪浠诲姟閲岀▼鏄惁瓒呰繃鍏噷鏁伴槇鍊�
+ if (nonTaskMileage.compareTo(config.mileageThreshold) <= 0) {
+ log.debug("杞﹁締 {} 闈炰换鍔¤繍琛岄噷绋� {}km 鏈秴杩囬槇鍊� {}km",
+ vehicleNo, nonTaskMileage, config.mileageThreshold);
+ return false;
+ }
+
+ // 5. 妫�鏌ュ憡璀﹂鐜囬檺鍒�
+ if (!checkAlertFrequency(vehicleId, config)) {
+ log.debug("杞﹁締 {} 宸茶揪鍒板憡璀﹂鐜囬檺鍒�", vehicleNo);
+ return false;
+ }
+
+ // 6. 鍒涘缓鍛婅璁板綍
+ return createAlertAndNotify(vehicle, nonTaskMileage, startTime, endTime, config);
+ }
+
+ /**
+ * 璁$畻杞﹁締杩愯閲岀▼
+ */
+ private BigDecimal calculateVehicleMileage(Long vehicleId, Date startTime, Date endTime) {
+ try {
+ // 鏌ヨ杞﹁締鍦ㄦ椂闂寸獥鍙e唴鐨勫垎娈甸噷绋嬭褰�
+ VehicleGpsSegmentMileage query = new VehicleGpsSegmentMileage();
+ query.setVehicleId(vehicleId);
+
+ Map<String, Object> params = new HashMap<>();
+ params.put("vehicleId", vehicleId);
+ params.put("startTime", startTime);
+ params.put("endTime", endTime);
+
+ List<VehicleGpsSegmentMileage> segments = segmentMileageMapper.selectSegmentsByTimeRange(params);
+
+ if (segments == null || segments.isEmpty()) {
+ return BigDecimal.ZERO;
+ }
+
+ // 绱姞鎵�鏈夊垎娈甸噷绋�
+ BigDecimal totalMileage = segments.stream()
+ .map(VehicleGpsSegmentMileage::getSegmentDistance)
+ .filter(Objects::nonNull)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+ return totalMileage;
+
+ } catch (Exception e) {
+ log.error("璁$畻杞﹁締閲岀▼澶辫触锛寁ehicleId={}", vehicleId, e);
+ return BigDecimal.ZERO;
+ }
+ }
+
+ /**
+ * 璁$畻杞﹁締鍦ㄦ湁浠诲姟鏃剁殑杩愯閲岀▼
+ */
+ private BigDecimal calculateTaskMileage(Long vehicleId, Date startTime, Date endTime) {
+ try {
+ // 1. 鏌ヨ杞﹁締鍦ㄦ椂闂寸獥鍙e唴鐨勪换鍔�
+ Map<String, Object> taskParams = new HashMap<>();
+ taskParams.put("vehicleId", vehicleId);
+ taskParams.put("startTime", startTime);
+ taskParams.put("endTime", endTime);
+
+ List<SysTask> tasks = sysTaskMapper.selectVehicleTasksInTimeRange(taskParams);
+
+ if (tasks == null || tasks.isEmpty()) {
+ return BigDecimal.ZERO;
+ }
+
+ // 鎺掗櫎宸插彇娑堢殑浠诲姟
+ List<SysTask> activeTasks = tasks.stream()
+ .filter(t -> !"CANCELLED".equals(t.getTaskStatus()))
+ .collect(Collectors.toList());
+
+ if (activeTasks.isEmpty()) {
+ return BigDecimal.ZERO;
+ }
+
+ // 2. 鏌ヨ杞﹁締鐨勬墍鏈塆PS鍒嗘閲岀▼
+ Map<String, Object> segmentParams = new HashMap<>();
+ segmentParams.put("vehicleId", vehicleId);
+ segmentParams.put("startTime", startTime);
+ segmentParams.put("endTime", endTime);
+
+ List<VehicleGpsSegmentMileage> segments = segmentMileageMapper.selectSegmentsByTimeRange(segmentParams);
+
+ if (segments == null || segments.isEmpty()) {
+ return BigDecimal.ZERO;
+ }
+
+ // 3. 绛涢�夊嚭鍦ㄤ换鍔℃椂闂磋寖鍥村唴鐨勫垎娈甸噷绋�
+ BigDecimal taskMileage = BigDecimal.ZERO;
+
+ for (VehicleGpsSegmentMileage segment : segments) {
+ Date segmentStart = segment.getSegmentStartTime();
+ Date segmentEnd = segment.getSegmentEndTime();
+
+ if (segmentStart == null || segmentEnd == null) {
+ continue;
+ }
+
+ // 妫�鏌ヨ鍒嗘鏄惁鍦ㄤ换鎰忎换鍔$殑鏃堕棿鑼冨洿鍐�
+ for (SysTask task : activeTasks) {
+ Date taskStart = task.getPlannedStartTime();
+ Date taskEnd = task.getActualEndTime();
+
+ // 濡傛灉浠诲姟杩樻病瀹屾垚锛屼娇鐢ㄥ綋鍓嶆椂闂翠綔涓虹粨鏉熸椂闂�
+ if (taskEnd == null) {
+ taskEnd = endTime;
+ }
+
+ if (taskStart == null) {
+ continue;
+ }
+
+ // 鍒ゆ柇鍒嗘鏃堕棿鏄惁涓庝换鍔℃椂闂存湁閲嶅彔
+ if (isTimeOverlap(segmentStart, segmentEnd, taskStart, taskEnd)) {
+ if (segment.getSegmentDistance() != null) {
+ taskMileage = taskMileage.add(segment.getSegmentDistance());
+ }
+ break; // 璇ュ垎娈靛凡璁″叆浠诲姟閲岀▼锛屼笉閲嶅璁$畻
+ }
+ }
+ }
+
+ return taskMileage;
+
+ } catch (Exception e) {
+ log.error("璁$畻浠诲姟閲岀▼澶辫触锛寁ehicleId={}", vehicleId, e);
+ return BigDecimal.ZERO;
+ }
+ }
+
+ /**
+ * 鍒ゆ柇涓や釜鏃堕棿娈垫槸鍚︽湁閲嶅彔
+ */
+ private boolean isTimeOverlap(Date start1, Date end1, Date start2, Date end2) {
+ // 鏃堕棿娈�1: [start1, end1]
+ // 鏃堕棿娈�2: [start2, end2]
+ // 鏈夐噸鍙犵殑鏉′欢锛歴tart1 < end2 && start2 < end1
+ return start1.before(end2) && start2.before(end1);
+ }
+
+ /**
+ * 妫�鏌ュ憡璀﹂鐜囬檺鍒�
+ */
+ private boolean checkAlertFrequency(Long vehicleId, AlertConfig config) {
+ try {
+ // 1. 妫�鏌ュ綋鏃ュ憡璀︽鏁�
+ Date today = DateUtils.parseDate(DateUtils.getDate());
+ int dailyCount = alertMapper.selectDailyAlertCount(vehicleId, today);
+ if (dailyCount >= config.dailyLimit) {
+ log.debug("杞﹁締 {} 浠婃棩鍛婅娆℃暟 {} 宸茶揪涓婇檺 {}", vehicleId, dailyCount, config.dailyLimit);
+ return false;
+ }
+
+ // 2. 妫�鏌ュ憡璀﹂棿闅�
+ Date lastAlertTime = alertMapper.selectLastAlertTime(vehicleId);
+ if (lastAlertTime != null) {
+ long minutesDiff = (new Date().getTime() - lastAlertTime.getTime()) / (1000 * 60);
+ if (minutesDiff < config.alertInterval) {
+ log.debug("杞﹁締 {} 璺濈涓婃鍛婅浠� {} 鍒嗛挓锛屾湭杈惧埌闂撮殧 {} 鍒嗛挓",
+ vehicleId, minutesDiff, config.alertInterval);
+ return false;
+ }
+ }
+
+ return true;
+
+ } catch (Exception e) {
+ log.error("妫�鏌ュ憡璀﹂鐜囧け璐ワ紝vehicleId={}", vehicleId, e);
+ // 鍑洪敊鏃惰皑鎱庡鐞嗭紝鍏佽鍛婅
+ return true;
+ }
+ }
+
+ /**
+ * 鍒涘缓鍛婅骞跺彂閫侀�氱煡
+ */
+ private boolean createAlertAndNotify(VehicleInfo vehicle, BigDecimal mileage,
+ Date startTime, Date endTime, AlertConfig config) {
+ try {
+ Long vehicleId = vehicle.getVehicleId();
+ String vehicleNo = vehicle.getVehicleNo();
+
+ // 鑾峰彇杞﹁締褰掑睘閮ㄩ棬淇℃伅
+ Long deptId = vehicle.getDeptId();
+ String deptName = vehicle.getDeptName();
+
+ // 鍒涘缓鍛婅璁板綍
+ boolean created = alertService.checkAndCreateAlert(
+ vehicleId, vehicleNo, mileage, startTime, endTime, deptId, deptName);
+
+ if (!created) {
+ return false;
+ }
+
+ log.info("杞﹁締 {} 浜х敓寮傚父鍛婅锛氭棤浠诲姟杩愯 {}km锛屾椂闂� {} 鑷� {}",
+ vehicleNo, mileage,
+ DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, startTime),
+ DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, endTime));
+
+ // 鍙戦�侀�氱煡
+ sendAlertNotification(vehicle, mileage, deptId, config);
+
+ return true;
+
+ } catch (Exception e) {
+ log.error("鍒涘缓鍛婅澶辫触锛寁ehicleNo={}", vehicle.getVehicleNo(), e);
+ return false;
+ }
+ }
+
+ /**
+ * 鍙戦�佸憡璀﹂�氱煡
+ */
+ private void sendAlertNotification(VehicleInfo vehicle, BigDecimal mileage,
+ Long deptId, AlertConfig config) {
+ try {
+ // 鑾峰彇閫氱煡鐢ㄦ埛鍒楄〃
+ List<Long> notifyUserIds = getNotifyUsers(deptId, config);
+ if (notifyUserIds.isEmpty()) {
+ log.warn("杞﹁締 {} 鍛婅鏃犻�氱煡鐢ㄦ埛锛岃烦杩囧彂閫�", vehicle.getVehicleNo());
+ return;
+ }
+
+ // 鏋勯�犻�氱煡鍐呭
+ String title = "杞﹁締寮傚父杩愯鍛婅";
+ String content = String.format("杞﹁締 %s 鍦ㄦ棤浠诲姟鐘舵�佷笅杩愯浜� %.2f 鍏噷锛岃鍙婃椂褰曞叆浠诲姟鍗曪紝鐐瑰嚮鍘诲綍鍗曘��",
+ vehicle.getVehicleNo(), mileage);
+
+ // 閫氳繃浼佷笟寰俊鍙戦�侀�氱煡
+ if (qyWechatService != null && qyWechatService.isEnabled()) {
+ for (Long userId : notifyUserIds) {
+ try {
+ // 杩欓噷鍙互鏍规嵁瀹為檯闇�姹傛寚瀹氳烦杞摼鎺�
+ String notifyUrl = "/pagesTask/create-emergency"; // 瀹為檯閾炬帴闇�鏍规嵁涓氬姟璋冩暣
+ qyWechatService.sendNotifyMessageWithDefaultAppId(userId, title, content, notifyUrl);
+ log.info("宸插悜鐢ㄦ埛 {} 鍙戦�佽溅杈� {} 寮傚父鍛婅閫氱煡", userId, vehicle.getVehicleNo());
+ } catch (Exception e) {
+ log.error("鍚戠敤鎴� {} 鍙戦�佸憡璀﹂�氱煡澶辫触", userId, e);
+ }
+ }
+ } else {
+ log.warn("浼佷笟寰俊鏈嶅姟鏈惎鐢紝鏃犳硶鍙戦�佸憡璀﹂�氱煡");
+ }
+
+ } catch (Exception e) {
+ log.error("鍙戦�佸憡璀﹂�氱煡澶辫触锛寁ehicleNo={}", vehicle.getVehicleNo(), e);
+ }
+ }
+
+ /**
+ * 鑾峰彇閫氱煡鐢ㄦ埛鍒楄〃
+ */
+ private List<Long> getNotifyUsers(Long deptId, AlertConfig config) {
+ List<Long> userIds = new ArrayList<>();
+
+ try {
+ // 1. 浼樺厛浣跨敤閰嶇疆鐨勭敤鎴峰垪琛�
+ if (StringUtils.isNotEmpty(config.notifyUsers)) {
+ String[] userIdStrs = config.notifyUsers.split(",");
+ for (String userIdStr : userIdStrs) {
+ try {
+ userIds.add(Long.parseLong(userIdStr.trim()));
+ } catch (NumberFormatException e) {
+ log.warn("鏃犳晥鐨勭敤鎴稩D: {}", userIdStr);
+ }
+ }
+ }
+
+ // 2. 濡傛灉娌℃湁閰嶇疆鐢ㄦ埛锛屾煡璇㈣溅杈嗘墍灞為儴闂ㄧ殑璐熻矗浜�
+ if ( deptId != null) {
+ SysDept dept = deptService.selectDeptById(deptId);
+ if (dept != null && StringUtils.isNotEmpty(dept.getLeader())) {
+ // leader鏄敤鎴峰悕锛岄�氳繃鐢ㄦ埛鍚嶆煡璇㈢敤鎴稩D
+ com.ruoyi.common.core.domain.entity.SysUser leaderUser =
+ userService.selectUserByUserName(dept.getLeader());
+ if (leaderUser != null) {
+ userIds.add(leaderUser.getUserId());
+ log.info("浣跨敤閮ㄩ棬 {} 璐熻矗浜�: {} (ID: {})",
+ dept.getDeptName(), dept.getLeader(), leaderUser.getUserId());
+ } else {
+ log.warn("閮ㄩ棬 {} 璐熻矗浜� {} 鏈壘鍒板搴旂敤鎴�",
+ dept.getDeptName(), dept.getLeader());
+ }
+ }
+ }
+
+ // 3. 濡傛灉杩樻槸娌℃湁鐢ㄦ埛锛屼娇鐢ㄧ郴缁熼粯璁ら厤缃殑鐢ㄦ埛鍒楄〃锛堟�诲叕鍙歌礋璐d汉锛�
+ if (userIds.isEmpty()) {
+ String defaultUsers = configService.selectConfigByKey("vehicle.alert.default.users");
+ if (StringUtils.isNotEmpty(defaultUsers)) {
+ String[] defaultUserIds = defaultUsers.split(",");
+ for (String userId : defaultUserIds) {
+ try {
+ userIds.add(Long.parseLong(userId.trim()));
+ log.info("浣跨敤绯荤粺榛樿閫氱煡鐢ㄦ埛: {}", userId);
+ } catch (NumberFormatException e) {
+ log.warn("鏃犳晥鐨勯粯璁ょ敤鎴稩D: {}", userId);
+ }
+ }
+ }
+ }
+
+ } catch (Exception e) {
+ log.error("鑾峰彇閫氱煡鐢ㄦ埛鍒楄〃澶辫触", e);
+ }
+
+ return userIds;
+ }
+
+ /**
+ * 妫�鏌ュ憡璀﹀姛鑳芥槸鍚﹀惎鐢�
+ */
+ private boolean isAlertEnabled() {
+ try {
+ String enabled = configService.selectConfigByKey("vehicle.alert.enabled");
+ return "true".equalsIgnoreCase(enabled);
+ } catch (Exception e) {
+ log.warn("鑾峰彇鍛婅寮�鍏抽厤缃け璐ワ紝浣跨敤榛樿鍊�(false)", e);
+ return false;
+ }
+ }
+
+ /**
+ * 鑾峰彇杞﹁締鐨勫憡璀﹂厤缃紙浼樺厛绾э細杞﹁締 > 閮ㄩ棬 > 鍏ㄥ眬锛�
+ */
+ private AlertConfig getVehicleAlertConfig(Long vehicleId, Long deptId, AlertConfig globalConfig) {
+ try {
+ // 浠庢暟鎹簱鏌ヨ閰嶇疆
+ VehicleAlertConfig dbConfig = alertConfigService.getConfigByVehicle(vehicleId, deptId);
+
+ if (dbConfig != null) {
+ // 灏嗘暟鎹簱閰嶇疆杞崲涓篈lertConfig
+ AlertConfig config = new AlertConfig();
+ config.mileageThreshold = dbConfig.getMileageThreshold();
+ config.dailyLimit = dbConfig.getDailyAlertLimit();
+ config.alertInterval = dbConfig.getAlertInterval();
+ config.timeWindow = globalConfig.timeWindow; // 鏃堕棿绐楀彛浣跨敤鍏ㄥ眬閰嶇疆
+ config.notifyUsers = dbConfig.getNotifyUserIds();
+ return config;
+ }
+
+ // 濡傛灉娌℃湁鏁版嵁搴撻厤缃紝浣跨敤鍏ㄥ眬閰嶇疆
+ return globalConfig;
+
+ } catch (Exception e) {
+ log.error("鑾峰彇杞﹁締閰嶇疆澶辫触锛寁ehicleId={}", vehicleId, e);
+ return globalConfig;
+ }
+ }
+
+ /**
+ * 鍔犺浇鍛婅閰嶇疆鍙傛暟
+ */
+ private AlertConfig loadAlertConfig() {
+ AlertConfig config = new AlertConfig();
+
+ try {
+ // 鍏噷鏁伴槇鍊�
+ String thresholdStr = configService.selectConfigByKey("vehicle.alert.mileage.threshold");
+ config.mileageThreshold = StringUtils.isNotEmpty(thresholdStr)
+ ? new BigDecimal(thresholdStr) : new BigDecimal("10");
+
+ // 姣忔棩鍛婅娆℃暟闄愬埗
+ String limitStr = configService.selectConfigByKey("vehicle.alert.daily.limit");
+ config.dailyLimit = StringUtils.isNotEmpty(limitStr) ? Integer.parseInt(limitStr) : 5;
+
+ // 鍛婅闂撮殧鏃堕棿
+ String intervalStr = configService.selectConfigByKey("vehicle.alert.interval.minutes");
+ config.alertInterval = StringUtils.isNotEmpty(intervalStr) ? Integer.parseInt(intervalStr) : 5;
+
+ // 鐩戞帶鏃堕棿绐楀彛
+ String windowStr = configService.selectConfigByKey("vehicle.alert.time.window");
+ config.timeWindow = StringUtils.isNotEmpty(windowStr) ? Integer.parseInt(windowStr) : 10;
+
+ // 閫氱煡鐢ㄦ埛鍒楄〃
+ config.notifyUsers = configService.selectConfigByKey("vehicle.alert.notify.users");
+
+ log.debug("鍛婅閰嶇疆: 闃堝��={}km, 姣忔棩闄愬埗={}娆�, 闂撮殧={}鍒嗛挓, 鏃堕棿绐楀彛={}鍒嗛挓",
+ config.mileageThreshold, config.dailyLimit, config.alertInterval, config.timeWindow);
+
+ } catch (Exception e) {
+ log.error("鍔犺浇鍛婅閰嶇疆澶辫触锛屼娇鐢ㄩ粯璁ゅ��", e);
+ }
+
+ return config;
+ }
+
+ /**
+ * 鍛婅閰嶇疆鍐呴儴绫�
+ */
+ private static class AlertConfig {
+ BigDecimal mileageThreshold = new BigDecimal("10"); // 鍏噷鏁伴槇鍊�
+ int dailyLimit = 5; // 姣忔棩鍛婅娆℃暟闄愬埗
+ int alertInterval = 5; // 鍛婅闂撮殧锛堝垎閽燂級
+ int timeWindow = 10; // 鐩戞帶鏃堕棿绐楀彛锛堝垎閽燂級
+ String notifyUsers; // 閫氱煡鐢ㄦ埛鍒楄〃
+ }
+}
diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml
index c18868b..8a7b2d1 100644
--- a/ruoyi-system/pom.xml
+++ b/ruoyi-system/pom.xml
@@ -51,9 +51,45 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
-
+ <dependency>
+ <groupId>com.baidu.aip</groupId>
+ <artifactId>java-sdk</artifactId>
+ <version>4.16.19</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.aliyun</groupId>
+ <artifactId>aliyun-java-sdk-core</artifactId>
+ <version>4.6.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.tencentcloudapi</groupId>
+ <artifactId>tencentcloud-sdk-java-ocr</artifactId>
+ <version>3.1.1399</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>1.15</version>
+ </dependency>
+ <dependency>
+ <groupId>com.aliyun</groupId>
+ <artifactId>ocr_api20210707</artifactId>
+ <version>3.1.2</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
<!-- Spring Test -->
<dependency>
<groupId>org.springframework</groupId>
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/config/BaiduOCRConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/config/BaiduOCRConfig.java
new file mode 100644
index 0000000..4a9247c
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/config/BaiduOCRConfig.java
@@ -0,0 +1,72 @@
+package com.ruoyi.system.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 鐧惧害OCR鏈嶅姟閰嶇疆绫�
+ * 鐢ㄤ簬绠$悊鐧惧害AI寮�鏀惧钩鍙癘CR鏈嶅姟鐨勭浉鍏抽厤缃�
+ */
+@Component
+@ConfigurationProperties(prefix = "baidu.ocr")
+public class BaiduOCRConfig {
+
+ /**
+ * App ID
+ */
+ private String appId;
+
+ /**
+ * API Key
+ */
+ private String apiKey;
+
+ /**
+ * Secret Key
+ */
+ private String secretKey;
+
+ // Getter 鍜� Setter 鏂规硶
+ public String getAppId() {
+ return appId;
+ }
+
+ public void setAppId(String appId) {
+ this.appId = appId;
+ }
+
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getSecretKey() {
+ return secretKey;
+ }
+
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ /**
+ * 楠岃瘉閰嶇疆鏄惁瀹屾暣
+ * @return 閰嶇疆鏄惁瀹屾暣
+ */
+ public boolean isValid() {
+ return appId != null && !appId.trim().isEmpty() &&
+ apiKey != null && !apiKey.trim().isEmpty() &&
+ secretKey != null && !secretKey.trim().isEmpty();
+ }
+
+ @Override
+ public String toString() {
+ return "BaiduOCRConfig{" +
+ "appId='" + (appId != null ? "***" : "null") + '\'' +
+ ", apiKey='" + (apiKey != null ? "***" : "null") + '\'' +
+ ", secretKey='" + (secretKey != null ? "***" : "null") + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/config/OCRConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/config/OCRConfig.java
new file mode 100644
index 0000000..f1a792c
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/config/OCRConfig.java
@@ -0,0 +1,98 @@
+package com.ruoyi.system.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * OCR鏈嶅姟閰嶇疆绫�
+ * 鐢ㄤ簬绠$悊闃块噷浜慜CR鏈嶅姟鐨勭浉鍏抽厤缃�
+ */
+@Component
+@ConfigurationProperties(prefix = "ali.ocr")
+public class OCRConfig {
+
+ /**
+ * AccessKey ID
+ */
+ private String accessKeyId;
+
+ /**
+ * AccessKey Secret
+ */
+ private String accessKeySecret;
+
+ /**
+ * OCR鏈嶅姟绔偣
+ */
+ private String endpoint = "ocr-api.cn-hangzhou.aliyuncs.com";
+
+ /**
+ * 杩炴帴瓒呮椂鏃堕棿锛堟绉掞級
+ */
+ private Integer connectTimeout = 10000;
+
+ /**
+ * 璇诲彇瓒呮椂鏃堕棿锛堟绉掞級
+ */
+ private Integer readTimeout = 30000;
+
+ // Getter 鍜� Setter 鏂规硶
+ public String getAccessKeyId() {
+ return accessKeyId;
+ }
+
+ public void setAccessKeyId(String accessKeyId) {
+ this.accessKeyId = accessKeyId;
+ }
+
+ public String getAccessKeySecret() {
+ return accessKeySecret;
+ }
+
+ public void setAccessKeySecret(String accessKeySecret) {
+ this.accessKeySecret = accessKeySecret;
+ }
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public Integer getConnectTimeout() {
+ return connectTimeout;
+ }
+
+ public void setConnectTimeout(Integer connectTimeout) {
+ this.connectTimeout = connectTimeout;
+ }
+
+ public Integer getReadTimeout() {
+ return readTimeout;
+ }
+
+ public void setReadTimeout(Integer readTimeout) {
+ this.readTimeout = readTimeout;
+ }
+
+ /**
+ * 楠岃瘉閰嶇疆鏄惁瀹屾暣
+ * @return 閰嶇疆鏄惁瀹屾暣
+ */
+ public boolean isValid() {
+ return accessKeyId != null && !accessKeyId.trim().isEmpty() &&
+ accessKeySecret != null && !accessKeySecret.trim().isEmpty();
+ }
+
+ @Override
+ public String toString() {
+ return "OCRConfig{" +
+ "accessKeyId='" + (accessKeyId != null ? "***" : "null") + '\'' +
+ ", endpoint='" + endpoint + '\'' +
+ ", connectTimeout=" + connectTimeout +
+ ", readTimeout=" + readTimeout +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/config/TencentOCRConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/config/TencentOCRConfig.java
new file mode 100644
index 0000000..609dba1
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/config/TencentOCRConfig.java
@@ -0,0 +1,52 @@
+package com.ruoyi.system.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 鑵捐浜慜CR閰嶇疆绫�
+ * 鐢ㄤ簬閰嶇疆鑵捐浜慜CR鏈嶅姟鐨勭浉鍏冲弬鏁�
+ */
+@Component
+@ConfigurationProperties(prefix = "tencent.ocr")
+public class TencentOCRConfig {
+
+ /**
+ * 鑵捐浜慡ecretId
+ */
+ private String secretId;
+
+ /**
+ * 鑵捐浜慡ecretKey
+ */
+ private String secretKey;
+
+ /**
+ * 鑵捐浜慜CR鏈嶅姟绔偣
+ */
+ private String endpoint = "ocr.tencentcloudapi.com";
+
+ public String getSecretId() {
+ return secretId;
+ }
+
+ public void setSecretId(String secretId) {
+ this.secretId = secretId;
+ }
+
+ public String getSecretKey() {
+ return secretKey;
+ }
+
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/NetworkDiagController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/NetworkDiagController.java
new file mode 100644
index 0000000..97a78c8
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/NetworkDiagController.java
@@ -0,0 +1,160 @@
+package com.ruoyi.system.controller;
+
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.controller.BaseController;
+import org.springframework.web.bind.annotation.*;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.*;
+import java.net.Socket;
+import java.io.IOException;
+
+/**
+ * 缃戠粶璇婃柇Controller
+ * 鎻愪緵OCR鏈嶅姟杩炴帴璇婃柇銆丏NS瑙f瀽娴嬭瘯銆佺綉缁滆繛閫氭�ф祴璇曠瓑鍔熻兘
+ */
+@RestController
+@RequestMapping("/system/diag")
+public class NetworkDiagController extends BaseController {
+
+ /**
+ * 璇婃柇OCR鏈嶅姟杩炴帴
+ * @return 璇婃柇缁撴灉
+ */
+ @GetMapping("/ocrConnection")
+ public AjaxResult diagOcrConnection() {
+ Map<String, Object> result = new HashMap<>();
+
+ String ocrEndpoint = "ocr-api.cn-hangzhou.aliyuncs.com";
+ int port = 443; // HTTPS绔彛
+
+ try {
+ result.put("dns", testDns(ocrEndpoint));
+ result.put("connection", testConnection(ocrEndpoint, port));
+ result.put("network", getNetworkConfig());
+
+ return success(result);
+
+ } catch (Exception e) {
+ logger.error("缃戠粶璇婃柇澶辫触", e);
+ return error("缃戠粶璇婃柇澶辫触: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 娴嬭瘯DNS瑙f瀽
+ * @param params 鍖呭惈hostname鐨勫弬鏁�
+ * @return DNS瑙f瀽缁撴灉
+ */
+ @PostMapping("/testDns")
+ public AjaxResult testDns(@RequestBody Map<String, String> params) {
+ String hostname = params.get("hostname");
+ if (hostname == null || hostname.trim().isEmpty()) {
+ return error("涓绘満鍚嶄笉鑳戒负绌�");
+ }
+ return success(testDns(hostname));
+ }
+
+ /**
+ * 娴嬭瘯缃戠粶杩為�氭��
+ * @param params 鍖呭惈host鍜宲ort鐨勫弬鏁�
+ * @return 杩為�氭�ф祴璇曠粨鏋�
+ */
+ @PostMapping("/testConnectivity")
+ public AjaxResult testConnectivity(@RequestBody Map<String, Object> params) {
+ String host = (String) params.get("host");
+ Integer port = (Integer) params.get("port");
+
+ if (host == null || host.trim().isEmpty()) {
+ return error("涓绘満涓嶈兘涓虹┖");
+ }
+ if (port == null) {
+ return error("绔彛涓嶈兘涓虹┖");
+ }
+
+ return success(testConnection(host, port));
+ }
+
+ /**
+ * 娴嬭瘯DNS瑙f瀽
+ * @param hostname 涓绘満鍚�
+ * @return DNS瑙f瀽缁撴灉
+ */
+ private Map<String, Object> testDns(String hostname) {
+ Map<String, Object> dnsResult = new HashMap<>();
+
+ try {
+ InetAddress[] addresses = InetAddress.getAllByName(hostname);
+ dnsResult.put("success", true);
+ dnsResult.put("hostname", hostname);
+ dnsResult.put("ipCount", addresses.length);
+
+ String[] ips = new String[addresses.length];
+ for (int i = 0; i < addresses.length; i++) {
+ ips[i] = addresses[i].getHostAddress();
+ }
+ dnsResult.put("ips", ips);
+
+ } catch (UnknownHostException e) {
+ dnsResult.put("success", false);
+ dnsResult.put("error", "DNS瑙f瀽澶辫触: " + e.getMessage());
+ dnsResult.put("suggestion", "璇锋鏌NS鏈嶅姟鍣ㄩ厤缃紝鍙互灏濊瘯浣跨敤鍏叡DNS锛堝8.8.8.8锛�");
+ }
+
+ return dnsResult;
+ }
+
+ /**
+ * 娴嬭瘯缃戠粶杩炴帴
+ * @param hostname 涓绘満鍚�
+ * @param port 绔彛鍙�
+ * @return 杩炴帴娴嬭瘯缁撴灉
+ */
+ private Map<String, Object> testConnection(String hostname, int port) {
+ Map<String, Object> connResult = new HashMap<>();
+ long startTime = System.currentTimeMillis();
+
+ try (Socket socket = new Socket()) {
+ socket.connect(new java.net.InetSocketAddress(hostname, port), 10000); // 10绉掕秴鏃�
+ long endTime = System.currentTimeMillis();
+ long responseTime = endTime - startTime;
+
+ connResult.put("success", true);
+ connResult.put("hostname", hostname);
+ connResult.put("port", port);
+ connResult.put("responseTime", responseTime + "ms");
+ connResult.put("connected", true);
+
+ } catch (IOException e) {
+ long endTime = System.currentTimeMillis();
+ long responseTime = endTime - startTime;
+
+ connResult.put("success", false);
+ connResult.put("hostname", hostname);
+ connResult.put("port", port);
+ connResult.put("responseTime", responseTime + "ms");
+ connResult.put("error", "杩炴帴澶辫触: " + e.getMessage());
+ connResult.put("suggestion", "璇锋鏌ラ槻鐏璁剧疆锛岀‘璁ょ鍙�" + port + "鏄惁寮�鏀�");
+ }
+
+ return connResult;
+ }
+
+ /**
+ * 鑾峰彇缃戠粶閰嶇疆淇℃伅
+ * @return 缃戠粶閰嶇疆淇℃伅
+ */
+ private Map<String, Object> getNetworkConfig() {
+ Map<String, Object> networkConfig = new HashMap<>();
+ try {
+ networkConfig.put("httpProxy", System.getProperty("http.proxyHost"));
+ networkConfig.put("httpsProxy", System.getProperty("https.proxyHost"));
+ networkConfig.put("localHostAddress", InetAddress.getLocalHost().getHostAddress());
+ networkConfig.put("localHostName", InetAddress.getLocalHost().getHostName());
+ } catch (Exception e) {
+ networkConfig.put("error", "鑾峰彇缃戠粶閰嶇疆澶辫触: " + e.getMessage());
+ }
+ return networkConfig;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/OCRController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/OCRController.java
new file mode 100644
index 0000000..d370b89
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/OCRController.java
@@ -0,0 +1,433 @@
+package com.ruoyi.system.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.file.FileUploadUtils;
+import com.ruoyi.system.utils.AliOCRUtil;
+import com.ruoyi.system.utils.BaiduOCRUtil;
+import com.ruoyi.system.utils.TencentOCRUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * OCR璇嗗埆Controller
+ * 鏀寔闃块噷浜慜CR鍜岀櫨搴CR鏈嶅姟
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/ocr")
+public class OCRController extends BaseController {
+
+ @Autowired
+ private AliOCRUtil aliOCRUtil;
+
+ // 鏀寔鐨凮CR璇嗗埆绫诲瀷
+ private static final List<String> SUPPORTED_TYPES = Arrays.asList("General", "Invoice", "IdCard", "HandWriting");
+
+ /**
+ * 涓婁紶鍥剧墖骞惰繘琛孫CR璇嗗埆
+ * @param file 涓婁紶鐨勫浘鐗囨枃浠�
+ * @param type 璇嗗埆绫诲瀷锛圙eneral-閫氱敤, Invoice-鍙戠エ, IdCard-韬唤璇�, HandWriting-鎵嬪啓浣擄級
+ * @param provider OCR鏈嶅姟鎻愪緵鍟嗭紙ali-闃块噷浜�, baidu-鐧惧害锛�
+ * @return OCR璇嗗埆缁撴灉
+ */
+ @PostMapping(value = "/recognize", consumes = "multipart/form-data")
+ public AjaxResult recognizeImage(@RequestParam("file") MultipartFile file,
+ @RequestParam(value = "type", defaultValue = "General") String type,
+ @RequestParam(value = "provider", defaultValue = "ali") String provider,
+ @RequestParam(value = "itemNames", required = false) String[] itemNames) {
+ try {
+ if (file.isEmpty()) {
+ return error("涓婁紶鍥剧墖涓嶈兘涓虹┖");
+ }
+
+ // 楠岃瘉璇嗗埆绫诲瀷
+ if (!SUPPORTED_TYPES.contains(type)) {
+ return error("涓嶆敮鎸佺殑璇嗗埆绫诲瀷: " + type + ", 鏀寔鐨勭被鍨�: " + String.join(",", SUPPORTED_TYPES));
+ }
+
+ // 淇濆瓨涓存椂鏂囦欢
+ String tempDir = System.getProperty("java.io.tmpdir");
+ String originalFilename = file.getOriginalFilename();
+ File tempFile = new File(tempDir, System.currentTimeMillis() + "_" + originalFilename);
+ file.transferTo(tempFile);
+
+ // 鏍规嵁鎻愪緵鍟嗚皟鐢ㄤ笉鍚岀殑OCR鏈嶅姟
+ JSONObject ocrResult;
+ if ("baidu".equalsIgnoreCase(provider)) {
+ // 鐧惧害OCR鍙敮鎸侀儴鍒嗙被鍨�
+ if ("General".equals(type)) {
+ ocrResult = BaiduOCRUtil.generalRecognize(tempFile);
+ } else if ("HandWriting".equals(type)) {
+ ocrResult = BaiduOCRUtil.handwritingRecognize(tempFile);
+ } else {
+ ocrResult = BaiduOCRUtil.generalRecognize(tempFile); // 榛樿浣跨敤閫氱敤璇嗗埆
+ }
+ } else if ("tencent".equalsIgnoreCase(provider)) {
+ // 鑵捐浜慜CR鍙敮鎸侀儴鍒嗙被鍨�
+ if ("General".equals(type)) {
+ ocrResult = TencentOCRUtil.generalRecognize(tempFile);
+ } else if ("HandWriting".equals(type)) {
+ ocrResult = TencentOCRUtil.handwritingRecognize(tempFile.getAbsolutePath(), itemNames);
+ } else {
+ ocrResult = TencentOCRUtil.generalRecognize(tempFile); // 榛樿浣跨敤閫氱敤璇嗗埆
+ }
+ } else {
+ // 闃块噷浜慜CR
+ ocrResult = AliOCRUtil.recognizeTextByFile(tempFile, type);
+ }
+
+ // 鍒犻櫎涓存椂鏂囦欢
+ tempFile.delete();
+
+ // 鏋勫缓杩斿洖缁撴灉
+ Map<String, Object> result = new HashMap<>();
+ result.put("ocrResult", ocrResult);
+ result.put("fileName", originalFilename);
+ result.put("type", type);
+ result.put("provider", provider);
+
+ if (ocrResult.getBooleanValue("success")) {
+ return success(result);
+ } else {
+ return error("OCR璇嗗埆澶辫触: " + ocrResult.getString("error"));
+ }
+
+ } catch (Exception e) {
+ logger.error("OCR璇嗗埆寮傚父", e);
+ return error("OCR璇嗗埆寮傚父: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 閫氳繃鍥剧墖URL杩涜OCR璇嗗埆
+ * @param imageUrl 鍥剧墖URL鍦板潃
+ * @param type 璇嗗埆绫诲瀷锛圙eneral-閫氱敤, Invoice-鍙戠エ, IdCard-韬唤璇�, HandWriting-鎵嬪啓浣擄級
+ * @param provider OCR鏈嶅姟鎻愪緵鍟嗭紙ali-闃块噷浜�, baidu-鐧惧害锛�
+ * @return OCR璇嗗埆缁撴灉
+ */
+ @GetMapping("/recognizeByUrl")
+ public AjaxResult recognizeByUrl(@RequestParam("imageUrl") String imageUrl,
+ @RequestParam(value = "type", defaultValue = "General") String type,
+ @RequestParam(value = "provider", defaultValue = "ali") String provider,
+ @RequestParam(value = "itemNames", required = false) String[] itemNames) {
+ try {
+ // 楠岃瘉璇嗗埆绫诲瀷
+ if (!SUPPORTED_TYPES.contains(type)) {
+ return error("涓嶆敮鎸佺殑璇嗗埆绫诲瀷: " + type + ", 鏀寔鐨勭被鍨�: " + String.join(",", SUPPORTED_TYPES));
+ }
+
+ // 鏍规嵁鎻愪緵鍟嗚皟鐢ㄤ笉鍚岀殑OCR鏈嶅姟
+ JSONObject ocrResult;
+ if ("baidu".equalsIgnoreCase(provider)) {
+ // 鐧惧害OCR鍙敮鎸侀儴鍒嗙被鍨�
+ if ("General".equals(type)) {
+ ocrResult = BaiduOCRUtil.generalRecognize(imageUrl);
+ } else if ("HandWriting".equals(type)) {
+ ocrResult = BaiduOCRUtil.handwritingRecognize(imageUrl);
+ } else {
+ ocrResult = BaiduOCRUtil.generalRecognize(imageUrl); // 榛樿浣跨敤閫氱敤璇嗗埆
+ }
+ } else if ("tencent".equalsIgnoreCase(provider)) {
+ // 鑵捐浜慜CR鍙敮鎸侀儴鍒嗙被鍨�
+ if ("General".equals(type)) {
+ ocrResult = TencentOCRUtil.generalRecognize(imageUrl);
+ } else if ("HandWriting".equals(type)) {
+ ocrResult = TencentOCRUtil.handwritingRecognize(imageUrl, itemNames);
+ } else {
+ ocrResult = TencentOCRUtil.generalRecognize(imageUrl); // 榛樿浣跨敤閫氱敤璇嗗埆
+ }
+ } else {
+ // 闃块噷浜慜CR
+ ocrResult = AliOCRUtil.recognizeTextByUrl(imageUrl, type);
+ }
+
+ // 鏋勫缓杩斿洖缁撴灉
+ Map<String, Object> result = new HashMap<>();
+ result.put("ocrResult", ocrResult);
+ result.put("imageUrl", imageUrl);
+ result.put("type", type);
+ result.put("provider", provider);
+
+ if (ocrResult.getBooleanValue("success")) {
+ return success(result);
+ } else {
+ return error("OCR璇嗗埆澶辫触: " + ocrResult.getString("error"));
+ }
+
+ } catch (Exception e) {
+ logger.error("OCR璇嗗埆寮傚父", e);
+ return error("OCR璇嗗埆寮傚父: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 鑾峰彇鏀寔鐨凮CR璇嗗埆绫诲瀷鍒楄〃
+ * @return 璇嗗埆绫诲瀷鍒楄〃
+ */
+ @GetMapping("/types")
+ public AjaxResult getSupportedTypes() {
+ Map<String, Object> result = new HashMap<>();
+ result.put("types", SUPPORTED_TYPES);
+
+ List<Map<String, String>> typeList = SUPPORTED_TYPES.stream().map(type -> {
+ Map<String, String> typeInfo = new HashMap<>();
+ typeInfo.put("value", type);
+
+ // 鏍规嵁绫诲瀷璁剧疆鏄剧ず鍚嶇О
+ switch (type) {
+ case "General":
+ typeInfo.put("label", "閫氱敤鏂囧瓧璇嗗埆");
+ break;
+ case "Invoice":
+ typeInfo.put("label", "鍙戠エ璇嗗埆");
+ break;
+ case "IdCard":
+ typeInfo.put("label", "韬唤璇佽瘑鍒�");
+ break;
+ case "HandWriting":
+ typeInfo.put("label", "鎵嬪啓浣撹瘑鍒�");
+ break;
+ default:
+ typeInfo.put("label", type);
+ break;
+ }
+ return typeInfo;
+ }).collect(Collectors.toList());
+
+ result.put("typeList", typeList);
+ return success(result);
+ }
+
+ /**
+ * 鑾峰彇鏀寔鐨凮CR鏈嶅姟鎻愪緵鍟嗗垪琛�
+ * @return OCR鏈嶅姟鎻愪緵鍟嗗垪琛�
+ */
+ @GetMapping("/providers")
+ public AjaxResult getSupportedProviders() {
+ Map<String, Object> result = new HashMap<>();
+ List<Map<String, String>> providerList = Arrays.asList(
+ createProviderInfo("ali", "闃块噷浜慜CR", true),
+ createProviderInfo("baidu", "鐧惧害OCR", true),
+ createProviderInfo("tencent", "鑵捐浜慜CR", true)
+ );
+ result.put("providers", providerList);
+ return success(result);
+ }
+
+ /**
+ * 鎻愬彇OCR缁撴灉涓殑鐩爣瀛楁
+ * @param ocrResult OCR鍘熷缁撴灉
+ * @return 鎻愬彇鐨勫瓧娈典俊鎭�
+ */
+ @PostMapping("/extractFields")
+ public AjaxResult extractFields(@RequestBody JSONObject ocrResult) {
+ try {
+ // 妫�鏌ユ槸鍚︿负鐧惧害OCR缁撴灉
+ String provider = ocrResult.getString("provider");
+ Map<String, String> extracted;
+ if ("baidu".equalsIgnoreCase(provider)) {
+ extracted = BaiduOCRUtil.extractTargetFields(ocrResult);
+ } else if ("tencent".equalsIgnoreCase(provider)) {
+ extracted = TencentOCRUtil.extractTargetFields(ocrResult);
+ } else {
+ extracted = AliOCRUtil.extractTargetFields(ocrResult);
+ }
+ return success(extracted);
+ } catch (Exception e) {
+ logger.error("瀛楁鎻愬彇寮傚父", e);
+ return error("瀛楁鎻愬彇寮傚父: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 鑵捐浜戞墜鍐欎綋璇嗗埆锛堟敮鎸佽嚜瀹氫箟瀛楁鎻愬彇锛�
+ * @param file 涓婁紶鐨勫浘鐗囨枃浠�
+ * @param itemNames 闇�瑕佹彁鍙栫殑瀛楁鍚嶇О鏁扮粍
+ * @return 璇嗗埆缁撴灉 Map锛宬ey涓哄瓧娈靛悕锛寁alue涓鸿瘑鍒唴瀹�
+ */
+ @PostMapping(value = "/tencent/handwriting", consumes = "multipart/form-data")
+ public AjaxResult tencentHandwritingRecognize(@RequestParam("file") MultipartFile file,
+ @RequestParam(value = "itemNames", required = false) String[] itemNames) {
+ try {
+ if (file.isEmpty()) {
+ return error("涓婁紶鍥剧墖涓嶈兘涓虹┖");
+ }
+
+ // 淇濆瓨涓存椂鏂囦欢
+ String tempDir = System.getProperty("java.io.tmpdir");
+ String originalFilename = file.getOriginalFilename();
+ File tempFile = new File(tempDir, System.currentTimeMillis() + "_" + originalFilename);
+ file.transferTo(tempFile);
+
+ // 璋冪敤鑵捐浜戞墜鍐欎綋璇嗗埆
+ Map<String, String> resultMap = TencentOCRUtil.handwritingRecognizeWith(tempFile.getAbsolutePath(), itemNames);
+
+ // 鍒犻櫎涓存椂鏂囦欢
+ tempFile.delete();
+
+ // 妫�鏌ユ槸鍚︽湁閿欒
+ if (resultMap.containsKey("error")) {
+ return error("鑵捐浜慜CR鎵嬪啓浣撹瘑鍒け璐�: " + resultMap.get("error"));
+ }
+
+ // 鏋勫缓杩斿洖缁撴灉
+ Map<String, Object> result = new HashMap<>();
+ result.put("fileName", originalFilename);
+ result.put("type", "HandWriting");
+ result.put("provider", "tencent");
+ result.put("fields", resultMap);
+ result.put("fieldCount", resultMap.size());
+
+ return success(result);
+
+ } catch (Exception e) {
+ logger.error("鑵捐浜慜CR鎵嬪啓浣撹瘑鍒紓甯�", e);
+ return error("鑵捐浜慜CR鎵嬪啓浣撹瘑鍒紓甯�: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 鑵捐浜戞墜鍐欎綋璇嗗埆閫氳繃URL锛堟敮鎸佽嚜瀹氫箟瀛楁鎻愬彇锛�
+ * @param imageUrl 鍥剧墖URL鍦板潃
+ * @param itemNames 闇�瑕佹彁鍙栫殑瀛楁鍚嶇О鏁扮粍
+ * @return 璇嗗埆缁撴灉 Map锛宬ey涓哄瓧娈靛悕锛寁alue涓鸿瘑鍒唴瀹�
+ */
+ @GetMapping("/tencent/handwritingByUrl")
+ public AjaxResult tencentHandwritingRecognizeByUrl(@RequestParam("imageUrl") String imageUrl,
+ @RequestParam(value = "itemNames", required = false) String[] itemNames) {
+ try {
+ // 璋冪敤鑵捐浜戞墜鍐欎綋璇嗗埆
+ Map<String, String> resultMap = TencentOCRUtil.handwritingRecognizeWith(imageUrl, itemNames);
+
+ // 妫�鏌ユ槸鍚︽湁閿欒
+ if (resultMap.containsKey("error")) {
+ return error("鑵捐浜慜CR鎵嬪啓浣撹瘑鍒け璐�: " + resultMap.get("error"));
+ }
+
+ // 鏋勫缓杩斿洖缁撴灉
+ Map<String, Object> result = new HashMap<>();
+ result.put("imageUrl", imageUrl);
+ result.put("type", "HandWriting");
+ result.put("provider", "tencent");
+ result.put("fields", resultMap);
+ result.put("fieldCount", resultMap.size());
+
+ return success(result);
+
+ } catch (Exception e) {
+ logger.error("鑵捐浜慜CR鎵嬪啓浣撹瘑鍒紓甯�", e);
+ return error("鑵捐浜慜CR鎵嬪啓浣撹瘑鍒紓甯�: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 鑵捐浜戞墜鍐欎綋璇嗗埆锛堟敮鎸佸鍥剧墖鎵归噺璇嗗埆锛�
+ * @param files 涓婁紶鐨勫浘鐗囨枃浠舵暟缁�
+ * @param itemNames 闇�瑕佹彁鍙栫殑瀛楁鍚嶇О鏁扮粍
+ * @return 璇嗗埆缁撴灉锛屽悎骞舵墍鏈夊浘鐗囩殑璇嗗埆瀛楁
+ */
+ @PostMapping(value = "/tencent/handwriting/batch", consumes = "multipart/form-data")
+ public AjaxResult tencentHandwritingRecognizeBatch(@RequestParam("files") MultipartFile[] files,
+ @RequestParam(value = "itemNames", required = false) String[] itemNames) {
+ try {
+ if (files == null || files.length == 0) {
+ return error("涓婁紶鍥剧墖涓嶈兘涓虹┖");
+ }
+
+ // 鍚堝苟鎵�鏈夊浘鐗囩殑璇嗗埆缁撴灉
+ Map<String, String> mergedResultMap = new HashMap<>();
+ int successCount = 0;
+ int failCount = 0;
+ StringBuilder errorMessages = new StringBuilder();
+
+ for (MultipartFile file : files) {
+ if (file.isEmpty()) {
+ continue;
+ }
+
+ try {
+ // 淇濆瓨涓存椂鏂囦欢
+ String tempDir = System.getProperty("java.io.tmpdir");
+ String originalFilename = file.getOriginalFilename();
+ File tempFile = new File(tempDir, System.currentTimeMillis() + "_" + originalFilename);
+ file.transferTo(tempFile);
+
+ // 璋冪敤鑵捐浜戞墜鍐欎綋璇嗗埆
+ Map<String, String> resultMap = TencentOCRUtil.handwritingRecognizeWith(tempFile.getAbsolutePath(), itemNames);
+
+ // 鍒犻櫎涓存椂鏂囦欢
+ tempFile.delete();
+
+ // 妫�鏌ユ槸鍚︽湁閿欒
+ if (resultMap.containsKey("error")) {
+ failCount++;
+ errorMessages.append(originalFilename).append(":").append(resultMap.get("error")).append("; ");
+ logger.warn("鍥剧墖 {} 璇嗗埆澶辫触: {}", originalFilename, resultMap.get("error"));
+ } else {
+ // 鍚堝苟璇嗗埆缁撴灉锛堝鏋渒ey宸插瓨鍦紝涓嶈鐩栵級
+ for (Map.Entry<String, String> entry : resultMap.entrySet()) {
+ if (!mergedResultMap.containsKey(entry.getKey()) || mergedResultMap.get(entry.getKey()).isEmpty()) {
+ mergedResultMap.put(entry.getKey(), entry.getValue());
+ }
+ }
+ successCount++;
+ logger.info("鍥剧墖 {} 璇嗗埆鎴愬姛锛屾彁鍙� {} 涓瓧娈�", originalFilename, resultMap.size());
+ }
+ } catch (Exception e) {
+ failCount++;
+ errorMessages.append(file.getOriginalFilename()).append(":").append(e.getMessage()).append("; ");
+ logger.error("澶勭悊鍥剧墖 {} 鏃跺彂鐢熷紓甯�", file.getOriginalFilename(), e);
+ }
+ }
+
+ // 鏋勫缓杩斿洖缁撴灉
+ Map<String, Object> result = new HashMap<>();
+ result.put("type", "HandWriting");
+ result.put("provider", "tencent");
+ result.put("fields", mergedResultMap);
+ result.put("fieldCount", mergedResultMap.size());
+ result.put("totalImages", files.length);
+ result.put("successCount", successCount);
+ result.put("failCount", failCount);
+
+ if (failCount > 0) {
+ result.put("errors", errorMessages.toString());
+ }
+
+ if (successCount == 0) {
+ return error("鎵�鏈夊浘鐗囪瘑鍒け璐�: " + errorMessages.toString());
+ }
+
+ return success(result);
+
+ } catch (Exception e) {
+ logger.error("鑵捐浜慜CR鎵嬪啓浣撴壒閲忚瘑鍒紓甯�", e);
+ return error("鑵捐浜慜CR鎵嬪啓浣撴壒閲忚瘑鍒紓甯�: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 鍒涘缓鏈嶅姟鎻愪緵鍟嗕俊鎭�
+ * @param value 鏈嶅姟鎻愪緵鍟嗘爣璇�
+ * @param label 鏈嶅姟鎻愪緵鍟嗘樉绀哄悕绉�
+ * @param available 鏄惁鍙敤
+ * @return 鏈嶅姟鎻愪緵鍟嗕俊鎭�
+ */
+ private Map<String, String> createProviderInfo(String value, String label, boolean available) {
+ Map<String, String> providerInfo = new HashMap<>();
+ providerInfo.put("value", value);
+ providerInfo.put("label", label);
+ providerInfo.put("available", String.valueOf(available));
+ return providerInfo;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/HospitalTokenizerTask.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/HospitalTokenizerTask.java
new file mode 100644
index 0000000..95d6b4d
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/HospitalTokenizerTask.java
@@ -0,0 +1,143 @@
+package com.ruoyi.system.domain;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 鍖婚櫌鍒嗚瘝浠诲姟鐘舵��
+ *
+ * @author ruoyi
+ * @date 2026-01-20
+ */
+public class HospitalTokenizerTask implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 浠诲姟ID */
+ private String taskId;
+
+ /** 浠诲姟鐘舵��: RUNNING-杩愯涓�, SUCCESS-鎴愬姛, FAILED-澶辫触 */
+ private String status;
+
+ /** 鎬诲尰闄㈡暟閲� */
+ private Integer totalCount;
+
+ /** 宸插鐞嗘暟閲� */
+ private Integer processedCount;
+
+ /** 鎴愬姛鏁伴噺 */
+ private Integer successCount;
+
+ /** 澶辫触鏁伴噺 */
+ private Integer failedCount;
+
+ /** 杩涘害鐧惧垎姣� */
+ private Integer progress;
+
+ /** 寮�濮嬫椂闂� */
+ private Date startTime;
+
+ /** 缁撴潫鏃堕棿 */
+ private Date endTime;
+
+ /** 閿欒淇℃伅 */
+ private String errorMessage;
+
+ public HospitalTokenizerTask() {
+ }
+
+ public HospitalTokenizerTask(String taskId) {
+ this.taskId = taskId;
+ this.status = "RUNNING";
+ this.totalCount = 0;
+ this.processedCount = 0;
+ this.successCount = 0;
+ this.failedCount = 0;
+ this.progress = 0;
+ this.startTime = new Date();
+ }
+
+ public String getTaskId() {
+ return taskId;
+ }
+
+ public void setTaskId(String taskId) {
+ this.taskId = taskId;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public Integer getTotalCount() {
+ return totalCount;
+ }
+
+ public void setTotalCount(Integer totalCount) {
+ this.totalCount = totalCount;
+ }
+
+ public Integer getProcessedCount() {
+ return processedCount;
+ }
+
+ public void setProcessedCount(Integer processedCount) {
+ this.processedCount = processedCount;
+ // 鑷姩璁$畻杩涘害
+ if (totalCount != null && totalCount > 0) {
+ this.progress = (int) ((processedCount * 100.0) / totalCount);
+ }
+ }
+
+ public Integer getSuccessCount() {
+ return successCount;
+ }
+
+ public void setSuccessCount(Integer successCount) {
+ this.successCount = successCount;
+ }
+
+ public Integer getFailedCount() {
+ return failedCount;
+ }
+
+ public void setFailedCount(Integer failedCount) {
+ this.failedCount = failedCount;
+ }
+
+ public Integer getProgress() {
+ return progress;
+ }
+
+ public void setProgress(Integer progress) {
+ this.progress = progress;
+ }
+
+ public Date getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(Date startTime) {
+ this.startTime = startTime;
+ }
+
+ public Date getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(Date endTime) {
+ this.endTime = endTime;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TbHospData.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TbHospData.java
index 4c11f45..9c692ff 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TbHospData.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TbHospData.java
@@ -65,6 +65,9 @@
/** 鍖婚櫌绾у埆 */
private Integer hospLevel;
+ /** 鍖婚櫌淇℃伅鍒嗚瘝锛堥�楀彿鍒嗛殧锛� */
+ private String hospKeywords;
+
/** 鏁版嵁鐘舵�侊紙0姝e父 1鍋滅敤锛� */
private String status;
@@ -196,6 +199,14 @@
this.hospLevel = hospLevel;
}
+ public String getHospKeywords() {
+ return hospKeywords;
+ }
+
+ public void setHospKeywords(String hospKeywords) {
+ this.hospKeywords = hospKeywords;
+ }
+
public String getStatus() {
return status;
}
@@ -223,6 +234,7 @@
.append("hospIntroducerId", getHospIntroducerId())
.append("hospIntroducerDate", getHospIntroducerDate())
.append("hospLevel", getHospLevel())
+ .append("hospKeywords", getHospKeywords())
.append("status", getStatus())
.append("remark", getRemark())
.append("createBy", getCreateBy())
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleAbnormalAlert.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleAbnormalAlert.java
new file mode 100644
index 0000000..a9a65f0
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleAbnormalAlert.java
@@ -0,0 +1,302 @@
+package com.ruoyi.system.domain;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 杞﹁締寮傚父杩愯鍛婅璁板綍瀵硅薄 tb_vehicle_abnormal_alert
+ *
+ * @author ruoyi
+ */
+public class VehicleAbnormalAlert extends BaseEntity implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /** 鍛婅ID */
+ private Long alertId;
+
+ /** 杞﹁締ID */
+ @Excel(name = "杞﹁締ID")
+ private Long vehicleId;
+
+ /** 杞︾墝鍙� */
+ @Excel(name = "杞︾墝鍙�")
+ private String vehicleNo;
+
+ /** 鍛婅鏃ユ湡 */
+ @JsonFormat(pattern = "yyyy-MM-dd")
+ @Excel(name = "鍛婅鏃ユ湡", width = 30, dateFormat = "yyyy-MM-dd")
+ private Date alertDate;
+
+ /** 鍛婅鏃堕棿 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "鍛婅鏃堕棿", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+ private Date alertTime;
+
+ /** 绱杩愯鍏噷鏁�(鍏噷) */
+ @Excel(name = "绱鍏噷鏁�")
+ private BigDecimal mileage;
+
+ /** 鍛婅绫诲瀷(NO_TASK_MILEAGE-鏃犱换鍔¤秴鍏噷) */
+ @Excel(name = "鍛婅绫诲瀷", readConverterExp = "NO_TASK_MILEAGE=鏃犱换鍔¤秴鍏噷")
+ private String alertType;
+
+ /** 鍛婅鍘熷洜鎻忚堪 */
+ @Excel(name = "鍛婅鍘熷洜")
+ private String alertReason;
+
+ /** 寮�濮嬭繍琛屾椂闂� */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "寮�濮嬫椂闂�", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+ private Date startTime;
+
+ /** 缁撴潫杩愯鏃堕棿 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "缁撴潫鏃堕棿", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+ private Date endTime;
+
+ /** 褰撴棩鍛婅娆℃暟 */
+ @Excel(name = "褰撴棩鍛婅娆℃暟")
+ private Integer alertCount;
+
+ /** 鐘舵�侊紙0-鏈鐞� 1-宸插鐞� 2-宸插拷鐣ワ級 */
+ @Excel(name = "鐘舵��", readConverterExp = "0=鏈鐞�,1=宸插鐞�,2=宸插拷鐣�")
+ private String status;
+
+ /** 澶勭悊浜篒D */
+ private Long handlerId;
+
+ /** 澶勭悊浜哄鍚� */
+ @Excel(name = "澶勭悊浜�")
+ private String handlerName;
+
+ /** 澶勭悊鏃堕棿 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "澶勭悊鏃堕棿", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+ private Date handleTime;
+
+ /** 澶勭悊澶囨敞 */
+ @Excel(name = "澶勭悊澶囨敞")
+ private String handleRemark;
+
+ /** 閫氱煡鐘舵�侊紙0-鏈彂閫� 1-宸插彂閫� 2-鍙戦�佸け璐ワ級 */
+ @Excel(name = "閫氱煡鐘舵��", readConverterExp = "0=鏈彂閫�,1=宸插彂閫�,2=鍙戦�佸け璐�")
+ private String notifyStatus;
+
+ /** 閫氱煡鏃堕棿 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date notifyTime;
+
+ /** 閫氱煡鐢ㄦ埛ID鍒楄〃锛堥�楀彿鍒嗛殧锛� */
+ private String notifyUsers;
+
+ /** 褰掑睘閮ㄩ棬ID */
+ @Excel(name = "褰掑睘閮ㄩ棬ID")
+ private Long deptId;
+
+ /** 褰掑睘閮ㄩ棬鍚嶇О */
+ @Excel(name = "褰掑睘閮ㄩ棬")
+ private String deptName;
+
+ public void setAlertId(Long alertId) {
+ this.alertId = alertId;
+ }
+
+ public Long getAlertId() {
+ return alertId;
+ }
+
+ public void setVehicleId(Long vehicleId) {
+ this.vehicleId = vehicleId;
+ }
+
+ public Long getVehicleId() {
+ return vehicleId;
+ }
+
+ public void setVehicleNo(String vehicleNo) {
+ this.vehicleNo = vehicleNo;
+ }
+
+ public String getVehicleNo() {
+ return vehicleNo;
+ }
+
+ public void setAlertDate(Date alertDate) {
+ this.alertDate = alertDate;
+ }
+
+ public Date getAlertDate() {
+ return alertDate;
+ }
+
+ public void setAlertTime(Date alertTime) {
+ this.alertTime = alertTime;
+ }
+
+ public Date getAlertTime() {
+ return alertTime;
+ }
+
+ public void setMileage(BigDecimal mileage) {
+ this.mileage = mileage;
+ }
+
+ public BigDecimal getMileage() {
+ return mileage;
+ }
+
+ public void setAlertType(String alertType) {
+ this.alertType = alertType;
+ }
+
+ public String getAlertType() {
+ return alertType;
+ }
+
+ public void setAlertReason(String alertReason) {
+ this.alertReason = alertReason;
+ }
+
+ public String getAlertReason() {
+ return alertReason;
+ }
+
+ public void setStartTime(Date startTime) {
+ this.startTime = startTime;
+ }
+
+ public Date getStartTime() {
+ return startTime;
+ }
+
+ public void setEndTime(Date endTime) {
+ this.endTime = endTime;
+ }
+
+ public Date getEndTime() {
+ return endTime;
+ }
+
+ public void setAlertCount(Integer alertCount) {
+ this.alertCount = alertCount;
+ }
+
+ public Integer getAlertCount() {
+ return alertCount;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setHandlerId(Long handlerId) {
+ this.handlerId = handlerId;
+ }
+
+ public Long getHandlerId() {
+ return handlerId;
+ }
+
+ public void setHandlerName(String handlerName) {
+ this.handlerName = handlerName;
+ }
+
+ public String getHandlerName() {
+ return handlerName;
+ }
+
+ public void setHandleTime(Date handleTime) {
+ this.handleTime = handleTime;
+ }
+
+ public Date getHandleTime() {
+ return handleTime;
+ }
+
+ public void setHandleRemark(String handleRemark) {
+ this.handleRemark = handleRemark;
+ }
+
+ public String getHandleRemark() {
+ return handleRemark;
+ }
+
+ public void setNotifyStatus(String notifyStatus) {
+ this.notifyStatus = notifyStatus;
+ }
+
+ public String getNotifyStatus() {
+ return notifyStatus;
+ }
+
+ public void setNotifyTime(Date notifyTime) {
+ this.notifyTime = notifyTime;
+ }
+
+ public Date getNotifyTime() {
+ return notifyTime;
+ }
+
+ public void setNotifyUsers(String notifyUsers) {
+ this.notifyUsers = notifyUsers;
+ }
+
+ public String getNotifyUsers() {
+ return notifyUsers;
+ }
+
+ public void setDeptId(Long deptId) {
+ this.deptId = deptId;
+ }
+
+ public Long getDeptId() {
+ return deptId;
+ }
+
+ public void setDeptName(String deptName) {
+ this.deptName = deptName;
+ }
+
+ public String getDeptName() {
+ return deptName;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("alertId", getAlertId())
+ .append("vehicleId", getVehicleId())
+ .append("vehicleNo", getVehicleNo())
+ .append("alertDate", getAlertDate())
+ .append("alertTime", getAlertTime())
+ .append("mileage", getMileage())
+ .append("alertType", getAlertType())
+ .append("alertReason", getAlertReason())
+ .append("startTime", getStartTime())
+ .append("endTime", getEndTime())
+ .append("alertCount", getAlertCount())
+ .append("status", getStatus())
+ .append("handlerId", getHandlerId())
+ .append("handlerName", getHandlerName())
+ .append("handleTime", getHandleTime())
+ .append("handleRemark", getHandleRemark())
+ .append("notifyStatus", getNotifyStatus())
+ .append("notifyTime", getNotifyTime())
+ .append("notifyUsers", getNotifyUsers())
+ .append("deptId", getDeptId())
+ .append("deptName", getDeptName())
+ .append("createTime", getCreateTime())
+ .append("updateTime", getUpdateTime())
+ .toString();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleAlertConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleAlertConfig.java
new file mode 100644
index 0000000..01e2070
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleAlertConfig.java
@@ -0,0 +1,156 @@
+package com.ruoyi.system.domain;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 杞﹁締寮傚父鍛婅閰嶇疆瀵硅薄 tb_vehicle_alert_config
+ *
+ * @author ruoyi
+ */
+public class VehicleAlertConfig extends BaseEntity implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /** 閰嶇疆ID */
+ private Long configId;
+
+ /** 閰嶇疆绫诲瀷(GLOBAL-鍏ㄥ眬/DEPT-閮ㄩ棬/VEHICLE-杞﹁締) */
+ @Excel(name = "閰嶇疆绫诲瀷", readConverterExp = "GLOBAL=鍏ㄥ眬,DEPT=閮ㄩ棬,VEHICLE=杞﹁締")
+ private String configType;
+
+ /** 閮ㄩ棬ID锛堥儴闂ㄩ厤缃椂浣跨敤锛� */
+ @Excel(name = "閮ㄩ棬ID")
+ private Long deptId;
+
+ /** 杞﹁締ID锛堣溅杈嗛厤缃椂浣跨敤锛� */
+ @Excel(name = "杞﹁締ID")
+ private Long vehicleId;
+
+ /** 鐩爣鍚嶇О锛堥儴闂ㄥ悕绉版垨杞︾墝鍙凤級 */
+ private String targetName;
+
+ /** 鍏噷鏁板憡璀﹂槇鍊�(鍏噷) */
+ @Excel(name = "鍏噷鏁伴槇鍊�")
+ private BigDecimal mileageThreshold;
+
+ /** 姣忔棩鏈�澶у憡璀︽鏁� */
+ @Excel(name = "姣忔棩鍛婅娆℃暟")
+ private Integer dailyAlertLimit;
+
+ /** 鍛婅闂撮殧鏃堕棿(鍒嗛挓) */
+ @Excel(name = "鍛婅闂撮殧(鍒嗛挓)")
+ private Integer alertInterval;
+
+ /** 閫氱煡鐢ㄦ埛ID鍒楄〃锛堥�楀彿鍒嗛殧锛� */
+ @Excel(name = "閫氱煡鐢ㄦ埛")
+ private String notifyUserIds;
+
+ /** 鐘舵�侊紙0-鍚敤 1-鍋滅敤锛� */
+ @Excel(name = "鐘舵��", readConverterExp = "0=鍚敤,1=鍋滅敤")
+ private String status;
+
+ public void setConfigId(Long configId) {
+ this.configId = configId;
+ }
+
+ public Long getConfigId() {
+ return configId;
+ }
+
+ public void setConfigType(String configType) {
+ this.configType = configType;
+ }
+
+ public String getConfigType() {
+ return configType;
+ }
+
+ public void setDeptId(Long deptId) {
+ this.deptId = deptId;
+ }
+
+ public Long getDeptId() {
+ return deptId;
+ }
+
+ public void setVehicleId(Long vehicleId) {
+ this.vehicleId = vehicleId;
+ }
+
+ public Long getVehicleId() {
+ return vehicleId;
+ }
+
+ public void setTargetName(String targetName) {
+ this.targetName = targetName;
+ }
+
+ public String getTargetName() {
+ return targetName;
+ }
+
+ public void setMileageThreshold(BigDecimal mileageThreshold) {
+ this.mileageThreshold = mileageThreshold;
+ }
+
+ public BigDecimal getMileageThreshold() {
+ return mileageThreshold;
+ }
+
+ public void setDailyAlertLimit(Integer dailyAlertLimit) {
+ this.dailyAlertLimit = dailyAlertLimit;
+ }
+
+ public Integer getDailyAlertLimit() {
+ return dailyAlertLimit;
+ }
+
+ public void setAlertInterval(Integer alertInterval) {
+ this.alertInterval = alertInterval;
+ }
+
+ public Integer getAlertInterval() {
+ return alertInterval;
+ }
+
+ public void setNotifyUserIds(String notifyUserIds) {
+ this.notifyUserIds = notifyUserIds;
+ }
+
+ public String getNotifyUserIds() {
+ return notifyUserIds;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("configId", getConfigId())
+ .append("configType", getConfigType())
+ .append("deptId", getDeptId())
+ .append("vehicleId", getVehicleId())
+ .append("targetName", getTargetName())
+ .append("mileageThreshold", getMileageThreshold())
+ .append("dailyAlertLimit", getDailyAlertLimit())
+ .append("alertInterval", getAlertInterval())
+ .append("notifyUserIds", getNotifyUserIds())
+ .append("status", getStatus())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/VehicleSyncVO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/VehicleSyncVO.java
new file mode 100644
index 0000000..637c1fb
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/VehicleSyncVO.java
@@ -0,0 +1,100 @@
+package com.ruoyi.system.domain.vo;
+
+/**
+ * 杞﹁締鍚屾瑙嗗浘瀵硅薄
+ * 鐢ㄤ簬鍓嶇鏄剧ず鏃х郴缁熻溅杈嗘暟鎹強鍚屾鐘舵��
+ *
+ * @author ruoyi
+ */
+public class VehicleSyncVO {
+
+ /** 鏃х郴缁熻溅杈咺D */
+ private Integer carId;
+
+ /** 杞︾墝鍙� */
+ private String vehicleNo;
+
+ /** 杞﹁締鍗曟嵁绫诲瀷缂栫爜 */
+ private String carOrdClass;
+
+ /** 褰掑睘鍒嗗叕鍙革紙浠庢棫绯荤粺鑾峰彇鐨勫崟鎹被鍨嬫槧灏勶級 */
+ private String deptName;
+
+ /** 鏄惁宸插悓姝ュ埌鏂扮郴缁� */
+ private Boolean synced;
+
+ /** 鏂扮郴缁熶腑鐨勮溅杈咺D锛堝凡鍚屾鏃舵湁鍊硷級 */
+ private Long vehicleId;
+
+ /** 鏂扮郴缁熶腑鐨勯儴闂↖D锛堝凡鍚屾鏃舵湁鍊硷級 */
+ private Long deptId;
+
+ public Integer getCarId() {
+ return carId;
+ }
+
+ public void setCarId(Integer carId) {
+ this.carId = carId;
+ }
+
+ public String getVehicleNo() {
+ return vehicleNo;
+ }
+
+ public void setVehicleNo(String vehicleNo) {
+ this.vehicleNo = vehicleNo;
+ }
+
+ public String getCarOrdClass() {
+ return carOrdClass;
+ }
+
+ public void setCarOrdClass(String carOrdClass) {
+ this.carOrdClass = carOrdClass;
+ }
+
+ public String getDeptName() {
+ return deptName;
+ }
+
+ public void setDeptName(String deptName) {
+ this.deptName = deptName;
+ }
+
+ public Boolean getSynced() {
+ return synced;
+ }
+
+ public void setSynced(Boolean synced) {
+ this.synced = synced;
+ }
+
+ public Long getVehicleId() {
+ return vehicleId;
+ }
+
+ public void setVehicleId(Long vehicleId) {
+ this.vehicleId = vehicleId;
+ }
+
+ public Long getDeptId() {
+ return deptId;
+ }
+
+ public void setDeptId(Long deptId) {
+ this.deptId = deptId;
+ }
+
+ @Override
+ public String toString() {
+ return "VehicleSyncVO{" +
+ "carId=" + carId +
+ ", vehicleNo='" + vehicleNo + '\'' +
+ ", carOrdClass='" + carOrdClass + '\'' +
+ ", deptName='" + deptName + '\'' +
+ ", synced=" + synced +
+ ", vehicleId=" + vehicleId +
+ ", deptId=" + deptId +
+ '}';
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java b/ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java
index d343005..88bfce9 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java
@@ -83,7 +83,7 @@
sendDispatchNotify(assigneeIds, task.getCreatorId(), event.getTaskId(),task.getShowTaskCode(), buildNotifyContent(task, emergency));
}
}
-
+ syncDispatchActualStartTime(emergency, task);
Long taskId= event.getTaskId();
Long dispatchOrdId= event.getDispatchOrderId();
Long serviceOrdId= event.getServiceOrderId();
@@ -94,6 +94,19 @@
log.error("澶勭悊浠诲姟娲惧彂鍚屾浜嬩欢澶辫触", ex);
}
}
+
+ private void syncDispatchActualStartTime(SysTaskEmergency emergency, SysTask task) {
+ try {
+ //杩欓噷涔熷悓姝ヤ竴涓嬪疄闄呮椂闂�
+ Long disatpchOrdId = emergency.getLegacyDispatchOrdId();
+ Date actualTime = task.getActualStartTime();
+ legacySystemSyncService.updateDispatchActualTime(disatpchOrdId, actualTime);
+ }catch (Exception ex){
+ log.error("鍚屾瀹為檯鏃堕棿澶辫触", ex);
+ }
+ }
+
+
/**
* 鐩戝惉浠诲姟鍒涘缓浜嬩欢
*
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/DispatchOrdMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/DispatchOrdMapper.java
index bf5be21..5db1a6e 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/DispatchOrdMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/DispatchOrdMapper.java
@@ -100,4 +100,14 @@
public int updateDispatchOrdCancelReason(@Param("dispatchOrdID") Long dispatchOrdID,
@Param("cancelReasonId") Integer cancelReasonId,
@Param("cancelReasonText") String cancelReasonText);
+
+ /**
+ * 鏇存柊璋冨害鍗曞疄闄呭紑濮嬫椂闂�
+ *
+ * @param dispatchOrdID 璋冨害鍗旾D
+ * @param actualDate 瀹為檯寮�濮嬫椂闂�
+ * @return 褰卞搷琛屾暟
+ */
+ public int updateDispatchOrdActualDate(@Param("dispatchOrdID") Long dispatchOrdID,
+ @Param("actualDate") java.util.Date actualDate);
}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java
index fbd9c61..cd4b187 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java
@@ -149,4 +149,12 @@
* @return 浠诲姟鏁伴噺
*/
public int countTaskByPhoneAndDate(@Param("phone") String phone, @Param("createDate") String createDate);
+
+ /**
+ * 鏌ヨ杞﹁締鍦ㄦ寚瀹氭椂闂磋寖鍥村唴鐨勪换鍔″垪琛�
+ *
+ * @param params 鍖呭惈vehicleId銆乻tartTime銆乪ndTime鐨勫弬鏁癕ap
+ * @return 浠诲姟鍒楄〃
+ */
+ public List<SysTask> selectVehicleTasksInTimeRange(java.util.Map<String, Object> params);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TbHospDataMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TbHospDataMapper.java
index 91f52c7..0a08f0d 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TbHospDataMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TbHospDataMapper.java
@@ -67,4 +67,14 @@
* @return 缁撴灉
*/
int deleteTbHospDataByIds(@Param("hospIds") Long[] hospIds);
+
+ /**
+ * 鏍规嵁鍒嗚瘝鍏抽敭璇嶉杩囨护鍖婚櫌鏁版嵁
+ * 閫氳繃鏁版嵁搴撳眰闈㈢殑LIKE鍖归厤蹇�熺瓫閫夊嚭鍙兘鍖归厤鐨勫尰闄�
+ *
+ * @param keywords 鍒嗚瘝鍏抽敭璇嶅垪琛�
+ * @param status 鍖婚櫌鐘舵�侊紙0-姝e父锛�
+ * @return 鍖婚櫌鏁版嵁闆嗗悎
+ */
+ List<TbHospData> selectTbHospDataByKeywords(@Param("keywords") List<String> keywords, @Param("status") String status);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleAbnormalAlertMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleAbnormalAlertMapper.java
new file mode 100644
index 0000000..fb5d76c
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleAbnormalAlertMapper.java
@@ -0,0 +1,92 @@
+package com.ruoyi.system.mapper;
+
+import java.util.Date;
+import java.util.List;
+import com.ruoyi.system.domain.VehicleAbnormalAlert;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 杞﹁締寮傚父鍛婅Mapper鎺ュ彛
+ *
+ * @author ruoyi
+ */
+public interface VehicleAbnormalAlertMapper {
+ /**
+ * 鏌ヨ杞﹁締寮傚父鍛婅
+ *
+ * @param alertId 杞﹁締寮傚父鍛婅涓婚敭
+ * @return 杞﹁締寮傚父鍛婅
+ */
+ public VehicleAbnormalAlert selectVehicleAbnormalAlertByAlertId(Long alertId);
+
+ /**
+ * 鏌ヨ杞﹁締寮傚父鍛婅鍒楄〃
+ *
+ * @param vehicleAbnormalAlert 杞﹁締寮傚父鍛婅
+ * @return 杞﹁締寮傚父鍛婅闆嗗悎
+ */
+ public List<VehicleAbnormalAlert> selectVehicleAbnormalAlertList(VehicleAbnormalAlert vehicleAbnormalAlert);
+
+ /**
+ * 鏂板杞﹁締寮傚父鍛婅
+ *
+ * @param vehicleAbnormalAlert 杞﹁締寮傚父鍛婅
+ * @return 缁撴灉
+ */
+ public int insertVehicleAbnormalAlert(VehicleAbnormalAlert vehicleAbnormalAlert);
+
+ /**
+ * 淇敼杞﹁締寮傚父鍛婅
+ *
+ * @param vehicleAbnormalAlert 杞﹁締寮傚父鍛婅
+ * @return 缁撴灉
+ */
+ public int updateVehicleAbnormalAlert(VehicleAbnormalAlert vehicleAbnormalAlert);
+
+ /**
+ * 鍒犻櫎杞﹁締寮傚父鍛婅
+ *
+ * @param alertId 杞﹁締寮傚父鍛婅涓婚敭
+ * @return 缁撴灉
+ */
+ public int deleteVehicleAbnormalAlertByAlertId(Long alertId);
+
+ /**
+ * 鎵归噺鍒犻櫎杞﹁締寮傚父鍛婅
+ *
+ * @param alertIds 闇�瑕佸垹闄ょ殑鏁版嵁涓婚敭闆嗗悎
+ * @return 缁撴灉
+ */
+ public int deleteVehicleAbnormalAlertByAlertIds(Long[] alertIds);
+
+ /**
+ * 鏌ヨ杞﹁締褰撴棩鍛婅娆℃暟
+ *
+ * @param vehicleId 杞﹁締ID
+ * @param alertDate 鍛婅鏃ユ湡
+ * @return 鍛婅娆℃暟
+ */
+ public int selectDailyAlertCount(@Param("vehicleId") Long vehicleId, @Param("alertDate") Date alertDate);
+
+ /**
+ * 鏌ヨ杞﹁締鏈�鍚庝竴娆″憡璀︽椂闂�
+ *
+ * @param vehicleId 杞﹁締ID
+ * @return 鏈�鍚庡憡璀︽椂闂�
+ */
+ public Date selectLastAlertTime(@Param("vehicleId") Long vehicleId);
+
+ /**
+ * 鎵归噺澶勭悊鍛婅
+ *
+ * @param alertIds 鍛婅ID鍒楄〃
+ * @param handlerId 澶勭悊浜篒D
+ * @param handlerName 澶勭悊浜哄鍚�
+ * @param handleRemark 澶勭悊澶囨敞
+ * @return 缁撴灉
+ */
+ public int batchHandleAlert(@Param("alertIds") Long[] alertIds,
+ @Param("handlerId") Long handlerId,
+ @Param("handlerName") String handlerName,
+ @Param("handleRemark") String handleRemark);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleAlertConfigMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleAlertConfigMapper.java
new file mode 100644
index 0000000..7faeb5b
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleAlertConfigMapper.java
@@ -0,0 +1,71 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.system.domain.VehicleAlertConfig;
+
+/**
+ * 杞﹁締鍛婅閰嶇疆Mapper鎺ュ彛
+ *
+ * @author ruoyi
+ * @date 2026-01-12
+ */
+public interface VehicleAlertConfigMapper
+{
+ /**
+ * 鏌ヨ杞﹁締鍛婅閰嶇疆
+ *
+ * @param configId 杞﹁締鍛婅閰嶇疆涓婚敭
+ * @return 杞﹁締鍛婅閰嶇疆
+ */
+ public VehicleAlertConfig selectVehicleAlertConfigByConfigId(Long configId);
+
+ /**
+ * 鏌ヨ杞﹁締鍛婅閰嶇疆鍒楄〃
+ *
+ * @param vehicleAlertConfig 杞﹁締鍛婅閰嶇疆
+ * @return 杞﹁締鍛婅閰嶇疆闆嗗悎
+ */
+ public List<VehicleAlertConfig> selectVehicleAlertConfigList(VehicleAlertConfig vehicleAlertConfig);
+
+ /**
+ * 鏌ヨ杞﹁締鐨勯厤缃紙浼樺厛绾э細杞﹁締 > 閮ㄩ棬 > 鍏ㄥ眬锛�
+ *
+ * @param vehicleId 杞﹁締ID
+ * @param deptId 閮ㄩ棬ID
+ * @return 杞﹁締鍛婅閰嶇疆
+ */
+ public VehicleAlertConfig selectConfigByVehicle(@Param("vehicleId") Long vehicleId, @Param("deptId") Long deptId);
+
+ /**
+ * 鏂板杞﹁締鍛婅閰嶇疆
+ *
+ * @param vehicleAlertConfig 杞﹁締鍛婅閰嶇疆
+ * @return 缁撴灉
+ */
+ public int insertVehicleAlertConfig(VehicleAlertConfig vehicleAlertConfig);
+
+ /**
+ * 淇敼杞﹁締鍛婅閰嶇疆
+ *
+ * @param vehicleAlertConfig 杞﹁締鍛婅閰嶇疆
+ * @return 缁撴灉
+ */
+ public int updateVehicleAlertConfig(VehicleAlertConfig vehicleAlertConfig);
+
+ /**
+ * 鍒犻櫎杞﹁締鍛婅閰嶇疆
+ *
+ * @param configId 杞﹁締鍛婅閰嶇疆涓婚敭
+ * @return 缁撴灉
+ */
+ public int deleteVehicleAlertConfigByConfigId(Long configId);
+
+ /**
+ * 鎵归噺鍒犻櫎杞﹁締鍛婅閰嶇疆
+ *
+ * @param configIds 闇�瑕佸垹闄ょ殑鏁版嵁涓婚敭闆嗗悎
+ * @return 缁撴灉
+ */
+ public int deleteVehicleAlertConfigByConfigIds(Long[] configIds);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsSegmentMileageMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsSegmentMileageMapper.java
index 3314f93..8a8d46b 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsSegmentMileageMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsSegmentMileageMapper.java
@@ -84,4 +84,11 @@
*/
public Long selectLastCalculatedGpsId(@Param("vehicleId") Long vehicleId,
@Param("beforeTime") Date beforeTime);
+
+ /**
+ * 鏌ヨ杞﹁締鍦ㄦ寚瀹氭椂闂磋寖鍥村唴鐨勫垎娈甸噷绋嬭褰�
+ * @param params 鍖呭惈vehicleId銆乻tartTime銆乪ndTime鐨勫弬鏁癕ap
+ * @return 鍒嗘閲岀▼璁板綍鍒楄〃
+ */
+ public List<VehicleGpsSegmentMileage> selectSegmentsByTimeRange(java.util.Map<String, Object> params);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/HospitalTokenizerAsyncService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/HospitalTokenizerAsyncService.java
new file mode 100644
index 0000000..6a8dbef
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/HospitalTokenizerAsyncService.java
@@ -0,0 +1,149 @@
+package com.ruoyi.system.service;
+
+import com.ruoyi.system.domain.HospitalTokenizerTask;
+import com.ruoyi.system.domain.TbHospData;
+import com.ruoyi.system.mapper.TbHospDataMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 鍖婚櫌鍒嗚瘝寮傛浠诲姟鏈嶅姟
+ *
+ * @author ruoyi
+ * @date 2026-01-20
+ */
+@Service
+public class HospitalTokenizerAsyncService {
+
+ private static final Logger logger = LoggerFactory.getLogger(HospitalTokenizerAsyncService.class);
+
+ @Autowired
+ private ITbHospDataService tbHospDataService;
+
+ @Autowired
+ private TbHospDataMapper tbHospDataMapper;
+
+ /**
+ * 浠诲姟鐘舵�佺紦瀛� (鐢熶骇鐜寤鸿浣跨敤 Redis)
+ */
+ private static final Map<String, HospitalTokenizerTask> taskCache = new ConcurrentHashMap<>();
+
+ /**
+ * 寮傛鎵ц鍖婚櫌鍒嗚瘝浠诲姟
+ *
+ * @param taskId 浠诲姟ID
+ */
+ @Async("taskExecutor")
+ public void executeTokenizerTask(String taskId) {
+ HospitalTokenizerTask task = new HospitalTokenizerTask(taskId);
+ taskCache.put(taskId, task);
+
+ logger.info("寮�濮嬫墽琛屽尰闄㈠垎璇嶅紓姝ヤ换鍔�: taskId={}", taskId);
+
+ try {
+ // 鏌ヨ鎵�鏈夋甯哥姸鎬佺殑鍖婚櫌
+ TbHospData query = new TbHospData();
+ query.setStatus("0");
+ List<TbHospData> hospitalList = tbHospDataMapper.selectTbHospDataList(query);
+
+ task.setTotalCount(hospitalList.size());
+ logger.info("鏌ヨ鍒� {} 涓尰闄㈤渶瑕佺敓鎴愬垎璇�", hospitalList.size());
+
+ int successCount = 0;
+ int failedCount = 0;
+
+ // 閬嶅巻澶勭悊姣忎釜鍖婚櫌
+ for (int i = 0; i < hospitalList.size(); i++) {
+ TbHospData hospital = hospitalList.get(i);
+
+ try {
+ // 鐢熸垚鍒嗚瘝
+ String keywords = tbHospDataService.generateKeywordsForHospital(hospital);
+ hospital.setHospKeywords(keywords);
+
+ // 鏇存柊鏁版嵁搴�
+ int result = tbHospDataMapper.updateTbHospData(hospital);
+ if (result > 0) {
+ successCount++;
+ } else {
+ failedCount++;
+ }
+
+ } catch (Exception e) {
+ failedCount++;
+ logger.error("鐢熸垚鍖婚櫌鍒嗚瘝澶辫触: hospId={}, hospName={}",
+ hospital.getHospId(), hospital.getHospName(), e);
+ }
+
+ // 鏇存柊浠诲姟杩涘害
+ task.setProcessedCount(i + 1);
+ task.setSuccessCount(successCount);
+ task.setFailedCount(failedCount);
+
+ // 姣忓鐞�100鏉¤緭鍑轰竴娆℃棩蹇�
+ if ((i + 1) % 100 == 0) {
+ logger.info("鍖婚櫌鍒嗚瘝杩涘害: {}/{}, 鎴愬姛: {}, 澶辫触: {}",
+ i + 1, hospitalList.size(), successCount, failedCount);
+ }
+ }
+
+ // 浠诲姟瀹屾垚
+ task.setStatus("SUCCESS");
+ task.setEndTime(new Date());
+
+ logger.info("鍖婚櫌鍒嗚瘝浠诲姟瀹屾垚: taskId={}, 鎬绘暟: {}, 鎴愬姛: {}, 澶辫触: {}",
+ taskId, hospitalList.size(), successCount, failedCount);
+
+ } catch (Exception e) {
+ // 浠诲姟澶辫触
+ task.setStatus("FAILED");
+ task.setEndTime(new Date());
+ task.setErrorMessage(e.getMessage());
+
+ logger.error("鍖婚櫌鍒嗚瘝浠诲姟鎵ц澶辫触: taskId={}", taskId, e);
+ }
+ }
+
+ /**
+ * 鑾峰彇浠诲姟鐘舵��
+ *
+ * @param taskId 浠诲姟ID
+ * @return 浠诲姟鐘舵��
+ */
+ public HospitalTokenizerTask getTaskStatus(String taskId) {
+ return taskCache.get(taskId);
+ }
+
+ /**
+ * 娓呯悊浠诲姟缂撳瓨
+ *
+ * @param taskId 浠诲姟ID
+ */
+ public void clearTask(String taskId) {
+ taskCache.remove(taskId);
+ }
+
+ /**
+ * 娓呯悊鎵�鏈夊凡瀹屾垚鐨勪换鍔� (瓒呰繃1灏忔椂)
+ */
+ public void clearExpiredTasks() {
+ long oneHourAgo = System.currentTimeMillis() - 3600000;
+
+ taskCache.entrySet().removeIf(entry -> {
+ HospitalTokenizerTask task = entry.getValue();
+ if (task.getEndTime() != null && task.getEndTime().getTime() < oneHourAgo) {
+ logger.info("娓呯悊杩囨湡浠诲姟: taskId={}", entry.getKey());
+ return true;
+ }
+ return false;
+ });
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IDispatchOrdService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IDispatchOrdService.java
index 3e3efa0..a954fc3 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IDispatchOrdService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IDispatchOrdService.java
@@ -88,4 +88,13 @@
public int updateDispatchOrdState(Long dispatchOrdID, Integer dispatchOrdState);
public void cancelDispatchOrd(Long dispatchOrdID,Integer cancelReason,String cancelCReasonTxt);
+
+ /**
+ * 鏇存柊璋冨害鍗曞疄闄呭紑濮嬫椂闂�
+ *
+ * @param dispatchOrdID 璋冨害鍗旾D
+ * @param actualDate 瀹為檯寮�濮嬫椂闂�
+ * @return 褰卞搷琛屾暟
+ */
+ public int updateDispatchOrdActualDate(Long dispatchOrdID, java.util.Date actualDate);
}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ILegacySystemSyncService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ILegacySystemSyncService.java
index 7187a02..7ad1705 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ILegacySystemSyncService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ILegacySystemSyncService.java
@@ -2,6 +2,8 @@
import com.ruoyi.system.domain.SysTask;
+import java.util.Date;
+
/**
* 鏃х郴缁熷悓姝ervice鎺ュ彛
*
@@ -10,7 +12,13 @@
*/
public interface ILegacySystemSyncService {
-
+ /**
+ * 鏇存柊璋冨害鍗曞疄闄呭畬鎴愭椂闂�
+ * @param dispatchOrdId
+ * @param actualTime
+ * @return
+ */
+ Integer updateDispatchActualTime(Long dispatchOrdId, Date actualTime);
/**
* 鍚屾鎬ユ晳杞繍浠诲姟鍒版棫绯荤粺
*
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IQyWechatService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IQyWechatService.java
index a3e6c13..22a7059 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IQyWechatService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IQyWechatService.java
@@ -19,6 +19,16 @@
boolean sendNotifyMessage(Long userId, String title, String content, String notifyUrl);
/**
+ * 鍙戦�佷紒涓氬井淇℃秷鎭紝甯﹂粯璁ゅ簲鐢↖D
+ * @param userId 鐢ㄦ埛ID
+ * @param title 娑堟伅鏍囬
+ * @param content 娑堟伅鍐呭
+ * @param businessUrl 灏忕▼搴忚闂矾寰�
+ * @return
+ */
+ boolean sendNotifyMessageWithDefaultAppId(Long userId, String title, String content, String businessUrl);
+
+ /**
* 鍙戦�佷紒涓氬井淇℃秷鎭紝甯﹀皬绋嬪簭璺緞閾炬帴
* @param userId
* @param title
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
index ce9ae98..3def5e1 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
@@ -59,19 +59,29 @@
* @param createVO 浠诲姟鍒涘缓瀵硅薄
* @return 缁撴灉
*/
- public int insertSysTask(TaskCreateVO createVO);
+ /**
+ * 鏂板浠诲姟
+ *
+ * @param createVO 浠诲姟鍒涘缓淇℃伅
+ * @return 浠诲姟ID
+ */
+ public Long insertSysTask(TaskCreateVO createVO);
/**
* 鏂板浠诲姟绠$悊锛堝厑璁镐粠澶栭儴浼犲叆鐢ㄦ埛淇℃伅銆侀儴闂ㄤ俊鎭拰鏃堕棿淇℃伅锛�
*
* @param createVO 浠诲姟鍒涘缓瀵硅薄
+ * @param serviceOrderId 鏈嶅姟鍗旾D
+ * @param dispatchOrderId 璋冨害鍗旾D
+ * @param serviceOrdNo 鏈嶅姟鍗曠紪鍙�
* @param userId 鐢ㄦ埛ID
+ * @param userName 鐢ㄦ埛鍚嶇О
* @param deptId 閮ㄩ棬ID
* @param createTime 鍒涘缓鏃堕棿
* @param updateTime 鏇存柊鏃堕棿
- * @return 缁撴灉
+ * @return 浠诲姟ID
*/
- public int insertTask(TaskCreateVO createVO,Long serviceOrderId,Long dispatchOrderId, String serviceOrdNo, Long userId,String userName, Long deptId, Date createTime, Date updateTime);
+ public Long insertTask(TaskCreateVO createVO,Long serviceOrderId,Long dispatchOrderId, String serviceOrdNo, Long userId,String userName, Long deptId, Date createTime, Date updateTime);
/**
* 淇敼浠诲姟绠$悊
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITbHospDataService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITbHospDataService.java
index 79817f9..da508bb 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITbHospDataService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITbHospDataService.java
@@ -66,4 +66,20 @@
* @return 缁撴灉
*/
int deleteTbHospDataById(Long hospId);
+
+ /**
+ * 鎵归噺鐢熸垚骞舵洿鏂版墍鏈夊尰闄㈢殑鍒嗚瘝
+ * 渚涘尰闄㈠悓姝ユ椂璋冪敤
+ *
+ * @return 鏇存柊鐨勫尰闄㈡暟閲�
+ */
+ int generateAllHospitalKeywords();
+
+ /**
+ * 涓哄崟涓尰闄㈢敓鎴愬垎璇�
+ *
+ * @param tbHospData 鍖婚櫌鏁版嵁
+ * @return 鐢熸垚鐨勫垎璇�
+ */
+ String generateKeywordsForHospital(TbHospData tbHospData);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleAbnormalAlertService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleAbnormalAlertService.java
new file mode 100644
index 0000000..66f5e81
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleAbnormalAlertService.java
@@ -0,0 +1,97 @@
+package com.ruoyi.system.service;
+
+import java.util.Date;
+import java.util.List;
+import com.ruoyi.system.domain.VehicleAbnormalAlert;
+
+/**
+ * 杞﹁締寮傚父鍛婅Service鎺ュ彛
+ *
+ * @author ruoyi
+ */
+public interface IVehicleAbnormalAlertService {
+ /**
+ * 鏌ヨ杞﹁締寮傚父鍛婅
+ *
+ * @param alertId 杞﹁締寮傚父鍛婅涓婚敭
+ * @return 杞﹁締寮傚父鍛婅
+ */
+ public VehicleAbnormalAlert selectVehicleAbnormalAlertByAlertId(Long alertId);
+
+ /**
+ * 鏌ヨ杞﹁締寮傚父鍛婅鍒楄〃
+ *
+ * @param vehicleAbnormalAlert 杞﹁締寮傚父鍛婅
+ * @return 杞﹁締寮傚父鍛婅闆嗗悎
+ */
+ public List<VehicleAbnormalAlert> selectVehicleAbnormalAlertList(VehicleAbnormalAlert vehicleAbnormalAlert);
+
+ /**
+ * 鏂板杞﹁締寮傚父鍛婅
+ *
+ * @param vehicleAbnormalAlert 杞﹁締寮傚父鍛婅
+ * @return 缁撴灉
+ */
+ public int insertVehicleAbnormalAlert(VehicleAbnormalAlert vehicleAbnormalAlert);
+
+ /**
+ * 淇敼杞﹁締寮傚父鍛婅
+ *
+ * @param vehicleAbnormalAlert 杞﹁締寮傚父鍛婅
+ * @return 缁撴灉
+ */
+ public int updateVehicleAbnormalAlert(VehicleAbnormalAlert vehicleAbnormalAlert);
+
+ /**
+ * 鎵归噺鍒犻櫎杞﹁締寮傚父鍛婅
+ *
+ * @param alertIds 闇�瑕佸垹闄ょ殑杞﹁締寮傚父鍛婅涓婚敭闆嗗悎
+ * @return 缁撴灉
+ */
+ public int deleteVehicleAbnormalAlertByAlertIds(Long[] alertIds);
+
+ /**
+ * 鍒犻櫎杞﹁締寮傚父鍛婅淇℃伅
+ *
+ * @param alertId 杞﹁締寮傚父鍛婅涓婚敭
+ * @return 缁撴灉
+ */
+ public int deleteVehicleAbnormalAlertByAlertId(Long alertId);
+
+ /**
+ * 澶勭悊鍛婅
+ *
+ * @param alertId 鍛婅ID
+ * @param handlerId 澶勭悊浜篒D
+ * @param handlerName 澶勭悊浜哄鍚�
+ * @param handleRemark 澶勭悊澶囨敞
+ * @return 缁撴灉
+ */
+ public int handleAlert(Long alertId, Long handlerId, String handlerName, String handleRemark);
+
+ /**
+ * 鎵归噺澶勭悊鍛婅
+ *
+ * @param alertIds 鍛婅ID鍒楄〃
+ * @param handlerId 澶勭悊浜篒D
+ * @param handlerName 澶勭悊浜哄鍚�
+ * @param handleRemark 澶勭悊澶囨敞
+ * @return 缁撴灉
+ */
+ public int batchHandleAlert(Long[] alertIds, Long handlerId, String handlerName, String handleRemark);
+
+ /**
+ * 妫�鏌ュ苟鍒涘缓杞﹁締寮傚父鍛婅
+ *
+ * @param vehicleId 杞﹁締ID
+ * @param vehicleNo 杞︾墝鍙�
+ * @param mileage 杩愯鍏噷鏁�
+ * @param startTime 寮�濮嬫椂闂�
+ * @param endTime 缁撴潫鏃堕棿
+ * @param deptId 閮ㄩ棬ID
+ * @param deptName 閮ㄩ棬鍚嶇О
+ * @return 鏄惁鍒涘缓鎴愬姛
+ */
+ public boolean checkAndCreateAlert(Long vehicleId, String vehicleNo, java.math.BigDecimal mileage,
+ Date startTime, Date endTime, Long deptId, String deptName);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleAlertConfigService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleAlertConfigService.java
new file mode 100644
index 0000000..8952322
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleAlertConfigService.java
@@ -0,0 +1,70 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.ruoyi.system.domain.VehicleAlertConfig;
+
+/**
+ * 杞﹁締鍛婅閰嶇疆Service鎺ュ彛
+ *
+ * @author ruoyi
+ * @date 2026-01-12
+ */
+public interface IVehicleAlertConfigService
+{
+ /**
+ * 鏌ヨ杞﹁締鍛婅閰嶇疆
+ *
+ * @param configId 杞﹁締鍛婅閰嶇疆涓婚敭
+ * @return 杞﹁締鍛婅閰嶇疆
+ */
+ public VehicleAlertConfig selectVehicleAlertConfigByConfigId(Long configId);
+
+ /**
+ * 鏌ヨ杞﹁締鍛婅閰嶇疆鍒楄〃
+ *
+ * @param vehicleAlertConfig 杞﹁締鍛婅閰嶇疆
+ * @return 杞﹁締鍛婅閰嶇疆闆嗗悎
+ */
+ public List<VehicleAlertConfig> selectVehicleAlertConfigList(VehicleAlertConfig vehicleAlertConfig);
+
+ /**
+ * 鑾峰彇杞﹁締鐨勫憡璀﹂厤缃紙浼樺厛绾э細杞﹁締 > 閮ㄩ棬 > 鍏ㄥ眬锛�
+ *
+ * @param vehicleId 杞﹁締ID
+ * @param deptId 閮ㄩ棬ID
+ * @return 杞﹁締鍛婅閰嶇疆
+ */
+ public VehicleAlertConfig getConfigByVehicle(Long vehicleId, Long deptId);
+
+ /**
+ * 鏂板杞﹁締鍛婅閰嶇疆
+ *
+ * @param vehicleAlertConfig 杞﹁締鍛婅閰嶇疆
+ * @return 缁撴灉
+ */
+ public int insertVehicleAlertConfig(VehicleAlertConfig vehicleAlertConfig);
+
+ /**
+ * 淇敼杞﹁締鍛婅閰嶇疆
+ *
+ * @param vehicleAlertConfig 杞﹁締鍛婅閰嶇疆
+ * @return 缁撴灉
+ */
+ public int updateVehicleAlertConfig(VehicleAlertConfig vehicleAlertConfig);
+
+ /**
+ * 鎵归噺鍒犻櫎杞﹁締鍛婅閰嶇疆
+ *
+ * @param configIds 闇�瑕佸垹闄ょ殑杞﹁締鍛婅閰嶇疆涓婚敭闆嗗悎
+ * @return 缁撴灉
+ */
+ public int deleteVehicleAlertConfigByConfigIds(Long[] configIds);
+
+ /**
+ * 鍒犻櫎杞﹁締鍛婅閰嶇疆淇℃伅
+ *
+ * @param configId 杞﹁締鍛婅閰嶇疆涓婚敭
+ * @return 缁撴灉
+ */
+ public int deleteVehicleAlertConfigByConfigId(Long configId);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DispatchOrdServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DispatchOrdServiceImpl.java
index 9a61dbb..471cea1 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DispatchOrdServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DispatchOrdServiceImpl.java
@@ -126,6 +126,18 @@
public void cancelDispatchOrd(Long dispatchOrdID, Integer cancelReason, String cancelCReasonTxt) {
dispatchOrdMapper.updateDispatchOrdCancelReason(dispatchOrdID, cancelReason, cancelCReasonTxt);
}
+
+ /**
+ * 鏇存柊璋冨害鍗曞疄闄呭紑濮嬫椂闂�
+ *
+ * @param dispatchOrdID 璋冨害鍗旾D
+ * @param actualDate 瀹為檯寮�濮嬫椂闂�
+ * @return 褰卞搷琛屾暟
+ */
+ @Override
+ public int updateDispatchOrdActualDate(Long dispatchOrdID, java.util.Date actualDate) {
+ return dispatchOrdMapper.updateDispatchOrdActualDate(dispatchOrdID, actualDate);
+ }
}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/HospDataSyncServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/HospDataSyncServiceImpl.java
index 4c0eb86..8b5e2a0 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/HospDataSyncServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/HospDataSyncServiceImpl.java
@@ -84,17 +84,23 @@
{
// 鏇存柊宸叉湁鏁版嵁
updateHospData(existingHosp, dto);
+ // 鐢熸垚鍒嗚瘝
+ String keywords = tbHospDataService.generateKeywordsForHospital(existingHosp);
+ existingHosp.setHospKeywords(keywords);
tbHospDataService.updateTbHospData(existingHosp);
updateCount++;
- log.debug("鏇存柊鍖婚櫌: {} (HospID={})", dto.getHospName(), dto.getHospId());
+ log.debug("鏇存柊鍖婚櫌: {} (HospID={}), 鍒嗚瘝: {}", dto.getHospName(), dto.getHospId(), keywords);
}
else
{
// 鎻掑叆鏂版暟鎹�
TbHospData newHosp = convertToTbHospData(dto);
+ // 鐢熸垚鍒嗚瘝
+ String keywords = tbHospDataService.generateKeywordsForHospital(newHosp);
+ newHosp.setHospKeywords(keywords);
tbHospDataService.insertTbHospData(newHosp);
insertCount++;
- log.debug("鏂板鍖婚櫌: {} (HospID={})", dto.getHospName(), dto.getHospId());
+ log.debug("鏂板鍖婚櫌: {} (HospID={}), 鍒嗚瘝: {}", dto.getHospName(), dto.getHospId(), keywords);
}
}
catch (Exception e)
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacySystemSyncServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacySystemSyncServiceImpl.java
index f8a37d7..dc90f20 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacySystemSyncServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacySystemSyncServiceImpl.java
@@ -82,7 +82,10 @@
private ITaskDispatchSyncService taskDispatchSyncService;
-
+ @Override
+ public Integer updateDispatchActualTime(Long dispatchOrdId, Date actualTime) {
+ return dispatchOrdService.updateDispatchOrdActualDate(dispatchOrdId, actualTime);
+ }
@Override
public Long syncEmergencyTaskToLegacy(Long taskId) {
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
index 77bca76..c709ddc 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
@@ -346,13 +346,13 @@
createTaskVo.setDeptId(deptId);
- int result = sysTaskService.insertTask(createTaskVo,serviceOrdID,dispatchOrdID, serviceOrdNo, taskCreatorId,createUserName, deptId, ServiceOrd_CC_Time, ServiceOrd_CC_Time);
+ Long taskId = sysTaskService.insertTask(createTaskVo,serviceOrdID,dispatchOrdID, serviceOrdNo, taskCreatorId,createUserName, deptId, ServiceOrd_CC_Time, ServiceOrd_CC_Time);
- if (result > 0) {
-// log.info("杞繍鍗曞悓姝ユ垚鍔�: ServiceOrdID={}, DispatchOrdID={}, 鍒涘缓鐨勪换鍔D={}", serviceOrdID, dispatchOrdID, result);
+ if (taskId != null && taskId > 0) {
+// log.info("杞繍鍗曞悓姝ユ垚鍔�: ServiceOrdID={}, DispatchOrdID={}, 鍒涘缓鐨勪换鍔D={}", serviceOrdID, dispatchOrdID, taskId);
try {
- notifyTransferOrderByWechat((long) result, serviceOrdID, dispatchOrdID, serviceOrdNo, ServiceOrd_CC_Time, dept, order);
+ notifyTransferOrderByWechat(taskId, serviceOrdID, dispatchOrdID, serviceOrdNo, ServiceOrd_CC_Time, dept, order);
} catch (Exception e) {
log.error("杞繍鍗曞悓姝ユ垚鍔熷悗鍙戦�佸井淇¢�氱煡澶辫触: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID, e);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/QyWechatServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/QyWechatServiceImpl.java
index 6d511f2..942496e 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/QyWechatServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/QyWechatServiceImpl.java
@@ -1,5 +1,6 @@
package com.ruoyi.system.service.impl;
+import com.ruoyi.common.config.WechatConfig;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.QyWechatArticle;
@@ -38,6 +39,8 @@
@Autowired
private SysUserMapper userMapper;
+ @Autowired
+ private WechatConfig wechatConfig;
/**
* 鍙戦�佷紒涓氬井淇℃秷鎭�
*/
@@ -66,6 +69,11 @@
}
@Override
+ public boolean sendNotifyMessageWithDefaultAppId(Long userId, String title, String content, String businessUrl){
+ String appId=wechatConfig.getAppId();
+ return sendNotifyMessage(userId,title,content,appId,businessUrl);
+ }
+ @Override
public boolean sendNotifyMessage(Long userId, String title, String content, String appId, String businessUrl) {
try {
// 妫�鏌ユ湇鍔℃槸鍚﹀惎鐢�
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
index 60aae88..b0141f1 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -252,14 +252,14 @@
* @return 缁撴灉
*/
@Override
- public int insertSysTask(TaskCreateVO createVO) {
+ public Long insertSysTask(TaskCreateVO createVO) {
// 鑾峰彇褰撳墠鐢ㄦ埛鍚嶅拰鐢ㄦ埛ID
String username = SecurityUtils.getUsername();
Long userId = SecurityUtils.getUserId();
// 鏍¢獙鐢ㄦ埛ID鏄惁涓虹┖鎴栦负0
if(userId==null || userId==0){
log.error("insertSysTask 鐢ㄦ埛ID涓虹┖ userName:{}",username);
- return 0;
+ return 0L;
}
SysTask task = new SysTask();
// 鍒涘缓鏂扮殑浠诲姟瀵硅薄
@@ -347,7 +347,7 @@
}).start();
}
- return result;
+ return result > 0 ? task.getTaskId() : 0L;
}
/**
@@ -361,7 +361,7 @@
* @return 缁撴灉
*/
@Override
- public int insertTask(TaskCreateVO createVO,Long serviceOrderId,Long dispatchOrderId, String serviceOrdNo, Long userId,String userName, Long deptId, Date createTime, Date updateTime) {
+ public Long insertTask(TaskCreateVO createVO,Long serviceOrderId,Long dispatchOrderId, String serviceOrdNo, Long userId,String userName, Long deptId, Date createTime, Date updateTime) {
SysTask task = new SysTask();
if(createVO.getTaskCode()!=null){
task.setTaskCode(createVO.getTaskCode());
@@ -455,7 +455,7 @@
this.sendEmeryTaskProcess(task, dispatchOrderId);
}
- return result;
+ return result > 0 ? task.getTaskId() : 0L;
}
private void sendTaskAssigneeEvent(TaskCreateVO createVO,SysTask task,Long userId,String userName){
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusPushServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusPushServiceImpl.java
index 9d495d9..af792e6 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusPushServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusPushServiceImpl.java
@@ -134,6 +134,27 @@
cancelDispatch(emergency.getLegacyDispatchOrdId(), emergency.getCancelReason(), emergency.getCancelBy());
}
}
+
+ // 鍒ゆ柇鏄惁闇�瑕佹洿鏂板疄闄呭紑濮嬫椂闂达細浠庡緟澶勭悊杞埌鍏朵粬鐘舵�侊紙闄ゅ彇娑堝锛�
+ if ( targetStatusCode != 10 && task.getActualStartTime() != null) {
+ try {
+ int rows = dispatchOrdService.updateDispatchOrdActualDate(
+ emergency.getLegacyDispatchOrdId(),
+ task.getActualStartTime());
+ if (rows > 0) {
+ log.info("銆愭柊鎺ㄦ棫銆戞洿鏂板疄闄呭紑濮嬫椂闂存垚鍔燂紝浠诲姟ID: {}, DispatchOrdID: {}, 瀹為檯寮�濮嬫椂闂�: {}",
+ taskId, emergency.getLegacyDispatchOrdId(), task.getActualStartTime());
+ } else {
+ log.warn("銆愭柊鎺ㄦ棫銆戞洿鏂板疄闄呭紑濮嬫椂闂村け璐ワ紝鏈壘鍒板搴旇皟搴﹀崟锛孌ispatchOrdID: {}",
+ emergency.getLegacyDispatchOrdId());
+ }
+ } catch (Exception e) {
+ log.error("銆愭柊鎺ㄦ棫銆戞洿鏂板疄闄呭紑濮嬫椂闂村紓甯革紝DispatchOrdID: {}",
+ emergency.getLegacyDispatchOrdId(), e);
+ // 涓嶆姏鍑哄紓甯革紝缁х画鎵ц鐘舵�佹帹閫�
+ }
+ }
+
// 鎺ㄩ�佺姸鎬佸埌鏃х郴缁�
boolean result = updateLegacyTaskStatus(emergency.getLegacyDispatchOrdId(), targetStatusCode);
@@ -174,7 +195,7 @@
try {
int totalSuccessCount = 0;
- int pageSize = 10; // 姣忛〉10鏉�
+ int pageSize = 5; // 姣忛〉10鏉�
int offset = 0;
while (true) {
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusSyncServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusSyncServiceImpl.java
index 80db40c..e30517b 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusSyncServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusSyncServiceImpl.java
@@ -231,7 +231,7 @@
// 妫�鏌ョ姸鎬佹槸鍚﹀彉鍖�
if (newStatus.getCode().equals(task.getTaskStatus())) {
- log.debug("浠诲姟鐘舵�佹湭鍙樺寲锛屼换鍔D: {}, 褰撳墠鐘舵��: {}", taskId, newStatus.getInfo());
+ log.debug("鍙樺寲锛屼换鍔D: {}, 褰撳墠鐘舵��: {}", taskId, newStatus.getInfo());
return true;
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TbHospDataServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TbHospDataServiceImpl.java
index 3f19ae7..5fa0363 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TbHospDataServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TbHospDataServiceImpl.java
@@ -1,8 +1,11 @@
package com.ruoyi.system.service.impl;
+import com.ruoyi.common.utils.HospitalTokenizerUtil;
import com.ruoyi.system.domain.TbHospData;
import com.ruoyi.system.mapper.TbHospDataMapper;
import com.ruoyi.system.service.ITbHospDataService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -16,6 +19,8 @@
@Service
public class TbHospDataServiceImpl implements ITbHospDataService
{
+ private static final Logger logger = LoggerFactory.getLogger(TbHospDataServiceImpl.class);
+
@Autowired
private TbHospDataMapper tbHospDataMapper;
@@ -102,4 +107,63 @@
{
return tbHospDataMapper.deleteTbHospDataById(hospId);
}
+
+ /**
+ * 鎵归噺鐢熸垚骞舵洿鏂版墍鏈夊尰闄㈢殑鍒嗚瘝
+ * 渚涘尰闄㈠悓姝ユ椂璋冪敤
+ *
+ * @return 鏇存柊鐨勫尰闄㈡暟閲�
+ */
+ @Override
+ public int generateAllHospitalKeywords()
+ {
+ logger.info("寮�濮嬫壒閲忕敓鎴愬尰闄㈠垎璇�...");
+
+ // 鏌ヨ鎵�鏈夋甯哥姸鎬佺殑鍖婚櫌
+ TbHospData query = new TbHospData();
+ query.setStatus("0");
+ List<TbHospData> hospitalList = tbHospDataMapper.selectTbHospDataList(query);
+
+ logger.info("鏌ヨ鍒� {} 涓尰闄㈤渶瑕佺敓鎴愬垎璇�", hospitalList.size());
+
+ int updateCount = 0;
+ for (TbHospData hospital : hospitalList) {
+ try {
+ // 鐢熸垚鍒嗚瘝
+ String keywords = generateKeywordsForHospital(hospital);
+ hospital.setHospKeywords(keywords);
+
+ // 鏇存柊鏁版嵁搴�
+ int result = tbHospDataMapper.updateTbHospData(hospital);
+ if (result > 0) {
+ updateCount++;
+ }
+ } catch (Exception e) {
+ logger.error("鐢熸垚鍖婚櫌鍒嗚瘝澶辫触: hospId={}, hospName={}",
+ hospital.getHospId(), hospital.getHospName(), e);
+ }
+ }
+
+ logger.info("鍖婚櫌鍒嗚瘝鐢熸垚瀹屾垚锛屾洿鏂颁簡 {} 涓尰闄�", updateCount);
+ return updateCount;
+ }
+
+ /**
+ * 涓哄崟涓尰闄㈢敓鎴愬垎璇�
+ *
+ * @param tbHospData 鍖婚櫌鏁版嵁
+ * @return 鐢熸垚鐨勫垎璇�
+ */
+ @Override
+ public String generateKeywordsForHospital(TbHospData tbHospData)
+ {
+ return HospitalTokenizerUtil.tokenize(
+ tbHospData.getHospName(),
+ tbHospData.getHospShort(),
+ tbHospData.getHopsProvince(),
+ tbHospData.getHopsCity(),
+ tbHospData.getHopsArea(),
+ tbHospData.getHospAddress()
+ );
+ }
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleAbnormalAlertServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleAbnormalAlertServiceImpl.java
new file mode 100644
index 0000000..2eb389b
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleAbnormalAlertServiceImpl.java
@@ -0,0 +1,173 @@
+package com.ruoyi.system.service.impl;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.system.mapper.VehicleAbnormalAlertMapper;
+import com.ruoyi.system.domain.VehicleAbnormalAlert;
+import com.ruoyi.system.service.IVehicleAbnormalAlertService;
+
+/**
+ * 杞﹁締寮傚父鍛婅Service涓氬姟灞傚鐞�
+ *
+ * @author ruoyi
+ */
+@Service
+public class VehicleAbnormalAlertServiceImpl implements IVehicleAbnormalAlertService {
+
+ @Autowired
+ private VehicleAbnormalAlertMapper vehicleAbnormalAlertMapper;
+
+ /**
+ * 鏌ヨ杞﹁締寮傚父鍛婅
+ *
+ * @param alertId 杞﹁締寮傚父鍛婅涓婚敭
+ * @return 杞﹁締寮傚父鍛婅
+ */
+ @Override
+ public VehicleAbnormalAlert selectVehicleAbnormalAlertByAlertId(Long alertId) {
+ return vehicleAbnormalAlertMapper.selectVehicleAbnormalAlertByAlertId(alertId);
+ }
+
+ /**
+ * 鏌ヨ杞﹁締寮傚父鍛婅鍒楄〃
+ *
+ * @param vehicleAbnormalAlert 杞﹁締寮傚父鍛婅
+ * @return 杞﹁締寮傚父鍛婅
+ */
+ @Override
+ public List<VehicleAbnormalAlert> selectVehicleAbnormalAlertList(VehicleAbnormalAlert vehicleAbnormalAlert) {
+ return vehicleAbnormalAlertMapper.selectVehicleAbnormalAlertList(vehicleAbnormalAlert);
+ }
+
+ /**
+ * 鏂板杞﹁締寮傚父鍛婅
+ *
+ * @param vehicleAbnormalAlert 杞﹁締寮傚父鍛婅
+ * @return 缁撴灉
+ */
+ @Override
+ public int insertVehicleAbnormalAlert(VehicleAbnormalAlert vehicleAbnormalAlert) {
+ vehicleAbnormalAlert.setCreateTime(DateUtils.getNowDate());
+ return vehicleAbnormalAlertMapper.insertVehicleAbnormalAlert(vehicleAbnormalAlert);
+ }
+
+ /**
+ * 淇敼杞﹁締寮傚父鍛婅
+ *
+ * @param vehicleAbnormalAlert 杞﹁締寮傚父鍛婅
+ * @return 缁撴灉
+ */
+ @Override
+ public int updateVehicleAbnormalAlert(VehicleAbnormalAlert vehicleAbnormalAlert) {
+ vehicleAbnormalAlert.setUpdateTime(DateUtils.getNowDate());
+ return vehicleAbnormalAlertMapper.updateVehicleAbnormalAlert(vehicleAbnormalAlert);
+ }
+
+ /**
+ * 鎵归噺鍒犻櫎杞﹁締寮傚父鍛婅
+ *
+ * @param alertIds 闇�瑕佸垹闄ょ殑杞﹁締寮傚父鍛婅涓婚敭
+ * @return 缁撴灉
+ */
+ @Override
+ public int deleteVehicleAbnormalAlertByAlertIds(Long[] alertIds) {
+ return vehicleAbnormalAlertMapper.deleteVehicleAbnormalAlertByAlertIds(alertIds);
+ }
+
+ /**
+ * 鍒犻櫎杞﹁締寮傚父鍛婅淇℃伅
+ *
+ * @param alertId 杞﹁締寮傚父鍛婅涓婚敭
+ * @return 缁撴灉
+ */
+ @Override
+ public int deleteVehicleAbnormalAlertByAlertId(Long alertId) {
+ return vehicleAbnormalAlertMapper.deleteVehicleAbnormalAlertByAlertId(alertId);
+ }
+
+ /**
+ * 澶勭悊鍛婅
+ *
+ * @param alertId 鍛婅ID
+ * @param handlerId 澶勭悊浜篒D
+ * @param handlerName 澶勭悊浜哄鍚�
+ * @param handleRemark 澶勭悊澶囨敞
+ * @return 缁撴灉
+ */
+ @Override
+ public int handleAlert(Long alertId, Long handlerId, String handlerName, String handleRemark) {
+ VehicleAbnormalAlert alert = new VehicleAbnormalAlert();
+ alert.setAlertId(alertId);
+ alert.setStatus("1"); // 宸插鐞�
+ alert.setHandlerId(handlerId);
+ alert.setHandlerName(handlerName);
+ alert.setHandleTime(new Date());
+ alert.setHandleRemark(handleRemark);
+
+ return updateVehicleAbnormalAlert(alert);
+ }
+
+ /**
+ * 鎵归噺澶勭悊鍛婅
+ *
+ * @param alertIds 鍛婅ID鍒楄〃
+ * @param handlerId 澶勭悊浜篒D
+ * @param handlerName 澶勭悊浜哄鍚�
+ * @param handleRemark 澶勭悊澶囨敞
+ * @return 缁撴灉
+ */
+ @Override
+ public int batchHandleAlert(Long[] alertIds, Long handlerId, String handlerName, String handleRemark) {
+ return vehicleAbnormalAlertMapper.batchHandleAlert(alertIds, handlerId, handlerName, handleRemark);
+ }
+
+ /**
+ * 妫�鏌ュ苟鍒涘缓杞﹁締寮傚父鍛婅
+ *
+ * @param vehicleId 杞﹁締ID
+ * @param vehicleNo 杞︾墝鍙�
+ * @param mileage 杩愯鍏噷鏁�
+ * @param startTime 寮�濮嬫椂闂�
+ * @param endTime 缁撴潫鏃堕棿
+ * @param deptId 閮ㄩ棬ID
+ * @param deptName 閮ㄩ棬鍚嶇О
+ * @return 鏄惁鍒涘缓鎴愬姛
+ */
+ @Override
+ public boolean checkAndCreateAlert(Long vehicleId, String vehicleNo, BigDecimal mileage,
+ Date startTime, Date endTime, Long deptId, String deptName) {
+ try {
+ // 鑾峰彇褰撴棩鍛婅娆℃暟
+ Date today = DateUtils.parseDate(DateUtils.getDate());
+ int todayCount = vehicleAbnormalAlertMapper.selectDailyAlertCount(vehicleId, today);
+
+ // 鍒涘缓鍛婅璁板綍
+ VehicleAbnormalAlert alert = new VehicleAbnormalAlert();
+ alert.setVehicleId(vehicleId);
+ alert.setVehicleNo(vehicleNo);
+ alert.setAlertDate(today);
+ alert.setAlertTime(new Date());
+ alert.setMileage(mileage);
+ alert.setAlertType("NO_TASK_MILEAGE");
+ alert.setAlertReason(String.format("杞﹁締鍦ㄦ棤浠诲姟鐘舵�佷笅杩愯浜� %.2f 鍏噷", mileage));
+ alert.setStartTime(startTime);
+ alert.setEndTime(endTime);
+ alert.setAlertCount(todayCount + 1);
+ alert.setStatus("0"); // 鏈鐞�
+ alert.setNotifyStatus("0"); // 鏈彂閫�
+ alert.setDeptId(deptId);
+ alert.setDeptName(deptName);
+
+ int result = insertVehicleAbnormalAlert(alert);
+ return result > 0;
+
+ } catch (Exception e) {
+ throw new RuntimeException("鍒涘缓鍛婅璁板綍澶辫触: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleAlertConfigServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleAlertConfigServiceImpl.java
new file mode 100644
index 0000000..efbe423
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleAlertConfigServiceImpl.java
@@ -0,0 +1,109 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.List;
+import com.ruoyi.common.utils.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.system.mapper.VehicleAlertConfigMapper;
+import com.ruoyi.system.domain.VehicleAlertConfig;
+import com.ruoyi.system.service.IVehicleAlertConfigService;
+
+/**
+ * 杞﹁締鍛婅閰嶇疆Service涓氬姟灞傚鐞�
+ *
+ * @author ruoyi
+ * @date 2026-01-12
+ */
+@Service
+public class VehicleAlertConfigServiceImpl implements IVehicleAlertConfigService
+{
+ @Autowired
+ private VehicleAlertConfigMapper vehicleAlertConfigMapper;
+
+ /**
+ * 鏌ヨ杞﹁締鍛婅閰嶇疆
+ *
+ * @param configId 杞﹁締鍛婅閰嶇疆涓婚敭
+ * @return 杞﹁締鍛婅閰嶇疆
+ */
+ @Override
+ public VehicleAlertConfig selectVehicleAlertConfigByConfigId(Long configId)
+ {
+ return vehicleAlertConfigMapper.selectVehicleAlertConfigByConfigId(configId);
+ }
+
+ /**
+ * 鏌ヨ杞﹁締鍛婅閰嶇疆鍒楄〃
+ *
+ * @param vehicleAlertConfig 杞﹁締鍛婅閰嶇疆
+ * @return 杞﹁締鍛婅閰嶇疆
+ */
+ @Override
+ public List<VehicleAlertConfig> selectVehicleAlertConfigList(VehicleAlertConfig vehicleAlertConfig)
+ {
+ return vehicleAlertConfigMapper.selectVehicleAlertConfigList(vehicleAlertConfig);
+ }
+
+ /**
+ * 鑾峰彇杞﹁締鐨勫憡璀﹂厤缃紙浼樺厛绾э細杞﹁締 > 閮ㄩ棬 > 鍏ㄥ眬锛�
+ *
+ * @param vehicleId 杞﹁締ID
+ * @param deptId 閮ㄩ棬ID
+ * @return 杞﹁締鍛婅閰嶇疆
+ */
+ @Override
+ public VehicleAlertConfig getConfigByVehicle(Long vehicleId, Long deptId)
+ {
+ return vehicleAlertConfigMapper.selectConfigByVehicle(vehicleId, deptId);
+ }
+
+ /**
+ * 鏂板杞﹁締鍛婅閰嶇疆
+ *
+ * @param vehicleAlertConfig 杞﹁締鍛婅閰嶇疆
+ * @return 缁撴灉
+ */
+ @Override
+ public int insertVehicleAlertConfig(VehicleAlertConfig vehicleAlertConfig)
+ {
+ vehicleAlertConfig.setCreateTime(DateUtils.getNowDate());
+ return vehicleAlertConfigMapper.insertVehicleAlertConfig(vehicleAlertConfig);
+ }
+
+ /**
+ * 淇敼杞﹁締鍛婅閰嶇疆
+ *
+ * @param vehicleAlertConfig 杞﹁締鍛婅閰嶇疆
+ * @return 缁撴灉
+ */
+ @Override
+ public int updateVehicleAlertConfig(VehicleAlertConfig vehicleAlertConfig)
+ {
+ vehicleAlertConfig.setUpdateTime(DateUtils.getNowDate());
+ return vehicleAlertConfigMapper.updateVehicleAlertConfig(vehicleAlertConfig);
+ }
+
+ /**
+ * 鎵归噺鍒犻櫎杞﹁締鍛婅閰嶇疆
+ *
+ * @param configIds 闇�瑕佸垹闄ょ殑杞﹁締鍛婅閰嶇疆涓婚敭
+ * @return 缁撴灉
+ */
+ @Override
+ public int deleteVehicleAlertConfigByConfigIds(Long[] configIds)
+ {
+ return vehicleAlertConfigMapper.deleteVehicleAlertConfigByConfigIds(configIds);
+ }
+
+ /**
+ * 鍒犻櫎杞﹁締鍛婅閰嶇疆淇℃伅
+ *
+ * @param configId 杞﹁締鍛婅閰嶇疆涓婚敭
+ * @return 缁撴灉
+ */
+ @Override
+ public int deleteVehicleAlertConfigByConfigId(Long configId)
+ {
+ return vehicleAlertConfigMapper.deleteVehicleAlertConfigByConfigId(configId);
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleGpsSegmentMileageServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleGpsSegmentMileageServiceImpl.java
index a534c6e..8c20c0a 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleGpsSegmentMileageServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleGpsSegmentMileageServiceImpl.java
@@ -254,9 +254,8 @@
}
}
- // 姣忔壒娆$粨鏉熷悗锛屼富鍔ㄥ缓璁瓽C
+ // 姣忔壒娆$粨鏉熷悗锛屼富鍔ㄥ缓璁瓽C锛堜笉闇�瑕佹樉寮忔竻绌哄紩鐢紝灞�閮ㄥ彉閲忎細鑷姩閲婃斁锛�
if (batchEnd < vehicleIds.size()) {
- uncalculatedGps = null; // 鏄惧紡娓呯┖寮曠敤
System.gc();
logger.debug("琛ュ伩璁$畻鎵规 {}-{} 瀹屾垚锛屽凡寤鸿JVM鍥炴敹鍐呭瓨", batchStart + 1, batchEnd);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/utils/AliOCRUtil.java b/ruoyi-system/src/main/java/com/ruoyi/system/utils/AliOCRUtil.java
new file mode 100644
index 0000000..c5c05d2
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/utils/AliOCRUtil.java
@@ -0,0 +1,457 @@
+package com.ruoyi.system.utils;
+
+import com.aliyun.ocr_api20210707.Client;
+import com.aliyun.ocr_api20210707.models.RecognizeAllTextRequest;
+import com.aliyun.ocr_api20210707.models.RecognizeAllTextResponse;
+import com.aliyun.ocr_api20210707.models.RecognizeHandwritingRequest;
+import com.aliyun.ocr_api20210707.models.RecognizeHandwritingResponse;
+import com.aliyun.tea.TeaException;
+import com.aliyun.teaopenapi.models.Config;
+import com.aliyun.teautil.models.RuntimeOptions;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.ruoyi.system.config.OCRConfig;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 闃块噷浜慜CR宸ュ叿绫� - 閫氱敤鏂囧瓧璇嗗埆
+ * 鏀寔澶氱璇嗗埆绫诲瀷锛氶�氱敤鏂囧瓧銆佸彂绁ㄣ�佽韩浠借瘉銆佹墜鍐欎綋绛�
+ * 浣跨敤 ocr-api20210707 SDK
+ *
+ * 浣跨敤绀轰緥锛�
+ * // 閫氱敤鏂囧瓧璇嗗埆
+ * JSONObject result = AliOCRUtil.recognizeGeneral("https://example.com/image.jpg");
+ *
+ * // 鏈湴鍥剧墖璇嗗埆
+ * File imageFile = new File("path/to/image.jpg");
+ * JSONObject result = AliOCRUtil.recognizeGeneral(imageFile);
+ *
+ * // 鎵嬪啓浣撹瘑鍒�
+ * JSONObject result = AliOCRUtil.recognizeHandwriting(imageFile);
+ *
+ * // 鎻愬彇鍏抽敭瀛楁
+ * Map<String, String> fields = AliOCRUtil.extractTargetFields(result);
+ */
+@Component
+public class AliOCRUtil {
+
+ private static final Logger log = LoggerFactory.getLogger(AliOCRUtil.class);
+
+ private static OCRConfig staticOcrConfig;
+
+ @Autowired
+ public void setOcrConfig(OCRConfig ocrConfig) {
+ AliOCRUtil.staticOcrConfig = ocrConfig;
+ }
+
+
+ // OCR API 绔偣
+ private static final String ENDPOINT = "ocr-api.cn-hangzhou.aliyuncs.com";
+
+ /**
+ * 鍒涘缓闃块噷浜慜CR瀹㈡埛绔�
+ * @return OCR瀹㈡埛绔疄渚�
+ * @throws Exception 鍒涘缓澶辫触鏃舵姏鍑哄紓甯�
+ */
+ private static Client createClient() throws Exception {
+ Config config = new Config()
+ .setAccessKeyId(staticOcrConfig.getAccessKeyId())
+ .setAccessKeySecret(staticOcrConfig.getAccessKeySecret());
+ config.endpoint = ENDPOINT;
+ // 璁剧疆杩炴帴瓒呮椂鏃堕棿
+ config.connectTimeout = 10000; // 10绉�
+ config.readTimeout = 30000; // 30绉�
+ return new Client(config);
+ }
+
+ /**
+ * 鍒涘缓闃块噷浜慜CR瀹㈡埛绔紙鏀寔澶栭儴浼犲叆AK/SK锛�
+ * @param accessKeyId 闃块噷浜慉ccessKey ID
+ * @param accessKeySecret 闃块噷浜慉ccessKey Secret
+ * @return OCR瀹㈡埛绔疄渚�
+ * @throws Exception 鍒涘缓澶辫触鏃舵姏鍑哄紓甯�
+ */
+ public static Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
+ Config config = new Config()
+ .setAccessKeyId(accessKeyId)
+ .setAccessKeySecret(accessKeySecret);
+ config.endpoint = ENDPOINT;
+ // 璁剧疆杩炴帴瓒呮椂鏃堕棿
+ config.connectTimeout = 10000; // 10绉�
+ config.readTimeout = 30000; // 30绉�
+ return new Client(config);
+ }
+
+ /**
+ * 灏嗗浘鐗囨枃浠惰浆涓築ase64缂栫爜
+ * @param imageFile 鍥剧墖鏂囦欢
+ * @return Base64缂栫爜瀛楃涓�
+ * @throws IOException 璇诲彇鏂囦欢澶辫触
+ */
+ public static String imageToBase64(File imageFile) throws IOException {
+ try (FileInputStream fis = new FileInputStream(imageFile)) {
+ byte[] buffer = new byte[(int) imageFile.length()];
+ fis.read(buffer);
+ return Base64.encodeBase64String(buffer);
+ }
+ }
+
+ /**
+ * 璋冪敤闃块噷浜戦�氱敤鏂囧瓧璇嗗埆OCR鎺ュ彛锛堜娇鐢ㄥ浘鐗嘦RL锛�
+ * @param imageUrl 鍥剧墖URL鍦板潃
+ * @param type 璇嗗埆绫诲瀷锛堝锛�"Invoice"-鍙戠エ, "IdCard"-韬唤璇�, "General"-閫氱敤, "HandWriting"-鎵嬪啓浣擄級
+ * @return OCR璇嗗埆缁撴灉锛圝SON鏍煎紡锛�
+ */
+ public static JSONObject recognizeTextByUrl(String imageUrl, String type) {
+ try {
+ Client client = createClient();
+ RecognizeAllTextRequest request = new RecognizeAllTextRequest()
+ .setUrl(imageUrl)
+ .setType(type);
+
+ RuntimeOptions runtime = new RuntimeOptions();
+ RecognizeAllTextResponse response = client.recognizeAllTextWithOptions(request, runtime);
+
+ // 灏嗗搷搴旇浆涓篔SONObject
+ String responseJson = com.aliyun.teautil.Common.toJSONString(response.body.data);
+ JSONObject result = JSON.parseObject(responseJson);
+ result.put("success", true);
+
+ log.info("OCR璇嗗埆鎴愬姛锛岀被鍨�: {}, URL: {}", type, imageUrl);
+ return result;
+
+ } catch (TeaException error) {
+ log.error("OCR璇嗗埆澶辫触锛圱eaException锛�: {}", error.getMessage(), error);
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", error.getMessage());
+ if (error.getData() != null) {
+ errorResult.put("recommend", error.getData().get("Recommend"));
+ }
+ return errorResult;
+
+ } catch (Exception error) {
+ log.error("OCR璇嗗埆澶辫触锛圗xception锛�: {}", error.getMessage(), error);
+
+ // 鏋勫缓璇︾粏鐨勯敊璇俊鎭�
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+
+ // 鍒ゆ柇閿欒绫诲瀷
+ String errorMsg = error.getMessage();
+ if (errorMsg != null && errorMsg.contains("ocr-api.cn-hangzhou.aliyuncs.com")) {
+ errorResult.put("error", "缃戠粶杩炴帴澶辫触锛氭棤娉曡闂樋閲屼簯OCR鏈嶅姟");
+ errorResult.put("detail", "璇锋鏌ワ細1) 缃戠粶杩炴帴鏄惁姝e父 2) DNS瑙f瀽鏄惁鍙敤 3) 闃茬伀澧欒缃� 4) 浠g悊閰嶇疆");
+ errorResult.put("endpoint", ENDPOINT);
+ } else if (error instanceof java.io.FileNotFoundException) {
+ errorResult.put("error", "鏂囦欢涓嶅瓨鍦�: " + imageUrl);
+ } else if (error instanceof java.io.IOException) {
+ errorResult.put("error", "鏂囦欢璇诲彇澶辫触: " + errorMsg);
+ } else {
+ errorResult.put("error", errorMsg != null ? errorMsg : "鏈煡閿欒");
+ }
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 璋冪敤闃块噷浜戦�氱敤鏂囧瓧璇嗗埆OCR鎺ュ彛锛堜娇鐢ㄦ湰鍦板浘鐗囨枃浠讹級
+ * @param imageFile 鍥剧墖鏂囦欢
+ * @param type 璇嗗埆绫诲瀷锛堝锛�"Invoice"-鍙戠エ, "IdCard"-韬唤璇�, "General"-閫氱敤, "HandWriting"-鎵嬪啓浣擄級
+ * @return OCR璇嗗埆缁撴灉锛圝SON鏍煎紡锛�
+ */
+ public static JSONObject recognizeTextByFile(File imageFile, String type) {
+ FileInputStream fis = null;
+ try {
+ // 鐩存帴璇诲彇鍥剧墖鏂囦欢涓哄瓧鑺傛祦
+ fis = new FileInputStream(imageFile);
+
+ Client client = createClient();
+ RecognizeAllTextRequest request = new RecognizeAllTextRequest()
+ .setBody(fis) // 鐩存帴浼犲叆鏂囦欢娴�
+ .setType(type);
+ JSONObject result;
+ RuntimeOptions runtime = new RuntimeOptions();
+ if(type.equals("HandWriting")){//澶勭悊鎵嬪啓{
+ RecognizeHandwritingRequest handwritingRequest=new RecognizeHandwritingRequest();
+ handwritingRequest.setBody(fis);
+ handwritingRequest.setNeedSortPage(true);//浠庝笂鍒颁笅锛屼粠宸﹀埌鍙�
+ RecognizeHandwritingResponse response = client.recognizeHandwriting(handwritingRequest);
+ String responseJson = com.aliyun.teautil.Common.toJSONString(response.body.data);
+ result = JSON.parseObject(responseJson);
+ result.put("success", true);
+ }else {
+ RecognizeAllTextResponse response = client.recognizeAllTextWithOptions(request, runtime);
+
+ // 灏嗗搷搴旇浆涓篔SONObject
+ String responseJson = com.aliyun.teautil.Common.toJSONString(response.body.data);
+ result = JSON.parseObject(responseJson);
+ result.put("success", true);
+ }
+ log.info("OCR璇嗗埆鎴愬姛锛岀被鍨�: {}, 鏂囦欢: {} 缁撴灉:{}", type, imageFile.getName(),result);
+ return result;
+
+ } catch (TeaException error) {
+ log.error("OCR璇嗗埆澶辫触锛圱eaException锛�: {}", error.getMessage(), error);
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", error.getMessage());
+ if (error.getData() != null) {
+ errorResult.put("recommend", error.getData().get("Recommend"));
+ }
+ return errorResult;
+
+ } catch (Exception error) {
+ log.error("OCR璇嗗埆澶辫触锛圗xception锛�: {}", error.getMessage(), error);
+
+ // 鏋勫缓璇︾粏鐨勯敊璇俊鎭�
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+
+ // 鍒ゆ柇閿欒绫诲瀷
+ String errorMsg = error.getMessage();
+ if (errorMsg != null && errorMsg.contains("ocr-api.cn-hangzhou.aliyuncs.com")) {
+ errorResult.put("error", "缃戠粶杩炴帴澶辫触锛氭棤娉曡闂樋閲屼簯OCR鏈嶅姟");
+ errorResult.put("detail", "璇锋鏌ワ細1) 缃戠粶杩炴帴鏄惁姝e父 2) DNS瑙f瀽鏄惁鍙敤 3) 闃茬伀澧欒缃� 4) 浠g悊閰嶇疆");
+ errorResult.put("endpoint", ENDPOINT);
+ } else if (error instanceof java.io.FileNotFoundException) {
+ errorResult.put("error", "鏂囦欢涓嶅瓨鍦�: " + imageFile.getAbsolutePath());
+ } else if (error instanceof java.io.IOException) {
+ errorResult.put("error", "鏂囦欢璇诲彇澶辫触: " + errorMsg);
+ } else {
+ errorResult.put("error", errorMsg != null ? errorMsg : "鏈煡閿欒");
+ }
+
+ return errorResult;
+ } finally {
+ // 鍏抽棴鏂囦欢娴�
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException e) {
+ log.error("鍏抽棴鏂囦欢娴佸け璐�", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * 璇嗗埆绫诲瀷鏋氫妇
+ */
+ public enum OcrType {
+ GENERAL("General", "閫氱敤鏂囧瓧璇嗗埆"),
+ INVOICE("Invoice", "鍙戠エ璇嗗埆"),
+ IDCARD("IdCard", "韬唤璇佽瘑鍒�"),
+ HANDWRITING("HandWriting", "鎵嬪啓浣撹瘑鍒�");
+
+ private final String code;
+ private final String desc;
+
+ OcrType(String code, String desc) {
+ this.code = code;
+ this.desc = desc;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getDesc() {
+ return desc;
+ }
+ }
+
+ /**
+ * 閫氱敤鏂囧瓧璇嗗埆 - 鎵嬪啓浣撹瘑鍒�
+ * @param imageUrl 鍥剧墖URL
+ * @return OCR璇嗗埆缁撴灉
+ */
+ public static JSONObject recognizeHandwriting(String imageUrl) {
+ return recognizeTextByUrl(imageUrl, "HandWriting");
+ }
+
+ /**
+ * 閫氱敤鏂囧瓧璇嗗埆 - 鎵嬪啓浣撹瘑鍒� - 鏈湴鏂囦欢
+ * @param imageFile 鍥剧墖鏂囦欢
+ * @return OCR璇嗗埆缁撴灉
+ */
+ public static JSONObject recognizeHandwriting(File imageFile) {
+ return recognizeTextByFile(imageFile, "HandWriting");
+ }
+
+ /**
+ * 閫氱敤鏂囧瓧璇嗗埆
+ * @param imageUrl 鍥剧墖URL
+ * @return OCR璇嗗埆缁撴灉
+ */
+ public static JSONObject recognizeGeneral(String imageUrl) {
+ return recognizeTextByUrl(imageUrl, "General");
+ }
+
+ /**
+ * 閫氱敤鏂囧瓧璇嗗埆 - 鏈湴鏂囦欢
+ * @param imageFile 鍥剧墖鏂囦欢
+ * @return OCR璇嗗埆缁撴灉
+ */
+ public static JSONObject recognizeGeneral(File imageFile) {
+ return recognizeTextByFile(imageFile, "General");
+ }
+
+ /**
+ * 璇嗗埆閫氱敤绁ㄦ嵁锛堝彂绁ㄣ�佹敹鎹瓑锛�
+ * @param imageUrl 鍥剧墖URL
+ * @return OCR璇嗗埆缁撴灉
+ */
+ public static JSONObject recognizeInvoice(String imageUrl) {
+ return recognizeTextByUrl(imageUrl, "Invoice");
+ }
+
+ /**
+ * 璇嗗埆閫氱敤绁ㄦ嵁锛堝彂绁ㄣ�佹敹鎹瓑锛�- 鏈湴鏂囦欢
+ * @param imageFile 鍥剧墖鏂囦欢
+ * @return OCR璇嗗埆缁撴灉
+ */
+ public static JSONObject recognizeInvoice(File imageFile) {
+ return recognizeTextByFile(imageFile, "Invoice");
+ }
+
+ /**
+ * 璇嗗埆韬唤璇�
+ * @param imageUrl 鍥剧墖URL
+ * @return OCR璇嗗埆缁撴灉
+ */
+ public static JSONObject recognizeIdCard(String imageUrl) {
+ return recognizeTextByUrl(imageUrl, "IdCard");
+ }
+
+ /**
+ * 璇嗗埆韬唤璇� - 鏈湴鏂囦欢
+ * @param imageFile 鍥剧墖鏂囦欢
+ * @return OCR璇嗗埆缁撴灉
+ */
+ public static JSONObject recognizeIdCard(File imageFile) {
+ return recognizeTextByFile(imageFile, "IdCard");
+ }
+
+ /**
+ * 浠嶰CR缁撴灉涓彁鍙栫洰鏍囧瓧娈碉紙閲戦銆佹棩鏈熴�佸娉ㄧ瓑锛�
+ * @param ocrResult OCR璇嗗埆鐨勫師濮嬬粨鏋�
+ * @return 鎻愬彇鍚庣殑鐩爣瀛楁
+ */
+ public static Map<String, String> extractTargetFields(JSONObject ocrResult) {
+ Map<String, String> extracted = new HashMap<>();
+
+ // 鏍¢獙OCR缁撴灉鏄惁鏈夋晥
+ if (!ocrResult.containsKey("success") || !ocrResult.getBooleanValue("success")) {
+ extracted.put("error", ocrResult.getString("error"));
+ return extracted;
+ }
+
+ // 鑾峰彇璇嗗埆鐨勬枃瀛楀唴瀹�
+ if (!ocrResult.containsKey("content")) {
+ extracted.put("error", "OCR璇嗗埆缁撴灉涓虹┖");
+ return extracted;
+ }
+
+ String content = ocrResult.getString("content");
+
+ // 濡傛灉鏈夌粨鏋勫寲鐨刾rism_wordsInfo瀛楁锛屼紭鍏堜娇鐢�
+ if (ocrResult.containsKey("prism_wordsInfo")) {
+ JSONArray wordsInfo = ocrResult.getJSONArray("prism_wordsInfo");
+
+ // 鎻愬彇閲戦锛堝尮閰嶅寘鍚�"閲戦""鍚堣""楼"鐨勫瓧娈碉級
+ for (int i = 0; i < wordsInfo.size(); i++) {
+ JSONObject word = wordsInfo.getJSONObject(i);
+ String text = word.getString("word");
+ if (text.contains("閲戦") || text.contains("鍚堣") || text.contains("楼")) {
+ extracted.put("totalAmount", text);
+ break;
+ }
+ }
+
+ // 鎻愬彇鏃ユ湡
+ for (int i = 0; i < wordsInfo.size(); i++) {
+ JSONObject word = wordsInfo.getJSONObject(i);
+ String text = word.getString("word");
+ if (text.contains("鏃ユ湡") || text.matches(".*\\d{4}[-/骞碷\\d{1,2}[-/鏈圿\\d{1,2}.*")) {
+ extracted.put("date", text);
+ break;
+ }
+ }
+
+ // 鎻愬彇澶囨敞
+ for (int i = 0; i < wordsInfo.size(); i++) {
+ JSONObject word = wordsInfo.getJSONObject(i);
+ String text = word.getString("word");
+ if (text.contains("澶囨敞")) {
+ extracted.put("remark", text);
+ break;
+ }
+ }
+ } else {
+ // 浣跨敤鏁翠綋鏂囨湰鍐呭杩涜绠�鍗曟彁鍙�
+ extracted.put("fullText", content);
+ }
+
+ return extracted;
+ }
+
+ /**
+ * 浣跨敤鑷畾涔堿ccessKey杩涜OCR璇嗗埆
+ * @param imageUrl 鍥剧墖URL
+ * @param type 璇嗗埆绫诲瀷
+ * @param accessKeyId AccessKey ID
+ * @param accessKeySecret AccessKey Secret
+ * @return OCR璇嗗埆缁撴灉
+ */
+ public static JSONObject recognizeTextWithCredentials(String imageUrl, String type,
+ String accessKeyId, String accessKeySecret) {
+ try {
+ Client client = createClient(accessKeyId, accessKeySecret);
+ RecognizeAllTextRequest request = new RecognizeAllTextRequest()
+ .setUrl(imageUrl)
+ .setType(type);
+
+ RuntimeOptions runtime = new RuntimeOptions();
+ RecognizeAllTextResponse response = client.recognizeAllTextWithOptions(request, runtime);
+
+ String responseJson = com.aliyun.teautil.Common.toJSONString(response.body.data);
+ JSONObject result = JSON.parseObject(responseJson);
+ result.put("success", true);
+
+ log.info("OCR璇嗗埆鎴愬姛锛堣嚜瀹氫箟AK锛夛紝绫诲瀷: {}", type);
+ return result;
+
+ } catch (TeaException error) {
+ log.error("OCR璇嗗埆澶辫触锛圱eaException锛�: {}", error.getMessage(), error);
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", error.getMessage());
+ if (error.getData() != null) {
+ errorResult.put("recommend", error.getData().get("Recommend"));
+ }
+ return errorResult;
+
+ } catch (Exception error) {
+ log.error("OCR璇嗗埆澶辫触锛圗xception锛�: {}", error.getMessage(), error);
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", error.getMessage());
+ return errorResult;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/utils/BaiduOCRUtil.java b/ruoyi-system/src/main/java/com/ruoyi/system/utils/BaiduOCRUtil.java
new file mode 100644
index 0000000..7513752
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/utils/BaiduOCRUtil.java
@@ -0,0 +1,445 @@
+package com.ruoyi.system.utils;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baidu.aip.ocr.AipOcr;
+import com.ruoyi.system.config.BaiduOCRConfig;
+
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 鐧惧害OCR宸ュ叿绫�
+ * 浣跨敤鐧惧害AI寮�鏀惧钩鍙扮殑OCR鏈嶅姟杩涜鏂囧瓧璇嗗埆
+ * 鏀寔閫氱敤鏂囧瓧璇嗗埆銆佹墜鍐欎綋璇嗗埆绛夊绉嶈瘑鍒被鍨�
+ *
+ * 浣跨敤绀轰緥锛�
+ * // 閫氱敤鏂囧瓧璇嗗埆
+ * JSONObject result = BaiduOCRUtil.generalRecognize("path/to/image.jpg");
+ *
+ * // 鎵嬪啓浣撹瘑鍒�
+ * JSONObject result = BaiduOCRUtil.handwritingRecognize("path/to/image.jpg");
+ */
+@Component
+@Slf4j
+public class BaiduOCRUtil {
+
+
+
+ private static BaiduOCRConfig staticBaiduOcrConfig;
+
+ @Autowired
+ public void setBaiduOcrConfig(BaiduOCRConfig baiduOcrConfig) {
+ BaiduOCRUtil.staticBaiduOcrConfig = baiduOcrConfig;
+ }
+
+ /**
+ * 鑾峰彇鐧惧害OCR瀹㈡埛绔疄渚�
+ * @return AipOcr瀹㈡埛绔疄渚�
+ */
+ private static AipOcr getClient() {
+ AipOcr client = new AipOcr(staticBaiduOcrConfig.getAppId(),
+ staticBaiduOcrConfig.getApiKey(),
+ staticBaiduOcrConfig.getSecretKey());
+
+ // 璁剧疆杩炴帴瓒呮椂鏃堕棿鍜宻ocket瓒呮椂鏃堕棿
+ client.setConnectionTimeoutInMillis(2000);
+ client.setSocketTimeoutInMillis(60000);
+
+ return client;
+ }
+
+ /**
+ * 閫氱敤鏂囧瓧璇嗗埆锛堝浘鐗囪矾寰勶級
+ * @param imagePath 鍥剧墖璺緞
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject generalRecognize(String imagePath) {
+ try {
+ AipOcr client = getClient();
+
+ // 鍙傛暟涓哄浘鐗囪矾寰�
+ HashMap<String, String> options = new HashMap<String, String>();
+ options.put("language_type", "CHN_ENG"); // 璇嗗埆璇█绫诲瀷
+ options.put("detect_direction", "true"); // 鏄惁妫�娴嬪浘鍍忔湞鍚�
+ options.put("detect_language", "true"); // 鏄惁妫�娴嬭瑷�
+ options.put("probability", "true"); // 鏄惁杩斿洖璇嗗埆缁撴灉涓瘡涓�琛岀殑缃俊搴�
+
+ org.json.JSONObject res = client.basicGeneral(imagePath, options);
+ log.info("鐧惧害OCR閫氱敤鏂囧瓧璇嗗埆鎴愬姛锛屽浘鐗囪矾寰�: {}", imagePath);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(res.toString()));
+ result.put("content", extractContentFromBaiduResult(JSON.parseObject(res.toString())));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鐧惧害OCR閫氱敤鏂囧瓧璇嗗埆澶辫触: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 閫氱敤鏂囧瓧璇嗗埆锛堟枃浠跺璞★級
+ * @param imageFile 鍥剧墖鏂囦欢瀵硅薄
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject generalRecognize(File imageFile) {
+ try {
+ AipOcr client = getClient();
+
+ // 鍙傛暟涓哄浘鐗囨枃浠�
+ HashMap<String, String> options = new HashMap<String, String>();
+ options.put("language_type", "CHN_ENG");
+ options.put("detect_direction", "true");
+ options.put("detect_language", "true");
+ options.put("probability", "true");
+
+ // 璇诲彇鏂囦欢瀛楄妭鏁扮粍鎴栦娇鐢ㄦ枃浠惰矾寰�
+ org.json.JSONObject res = client.basicGeneral(imageFile.getAbsolutePath(), options);
+ log.info("鐧惧害OCR閫氱敤鏂囧瓧璇嗗埆鎴愬姛锛屾枃浠跺悕: {}", imageFile.getName());
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(res.toString()));
+ result.put("content", extractContentFromBaiduResult(JSON.parseObject(res.toString())));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鐧惧害OCR閫氱敤鏂囧瓧璇嗗埆澶辫触: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 閫氱敤鏂囧瓧璇嗗埆锛堝浘鐗囧瓧鑺傛暟缁勶級
+ * @param imageBytes 鍥剧墖瀛楄妭鏁扮粍
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject generalRecognize(byte[] imageBytes) {
+ try {
+ AipOcr client = getClient();
+
+ HashMap<String, String> options = new HashMap<String, String>();
+ options.put("language_type", "CHN_ENG");
+ options.put("detect_direction", "true");
+ options.put("detect_language", "true");
+ options.put("probability", "true");
+
+ org.json.JSONObject res = client.basicGeneral(imageBytes, options);
+ log.info("鐧惧害OCR閫氱敤鏂囧瓧璇嗗埆鎴愬姛锛屽瓧鑺傛暟缁勯暱搴�: {}", imageBytes.length);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(res.toString()));
+ result.put("content", extractContentFromBaiduResult(JSON.parseObject(res.toString())));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鐧惧害OCR閫氱敤鏂囧瓧璇嗗埆澶辫触: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 楂樼簿搴︽枃瀛楄瘑鍒�
+ * @param imagePath 鍥剧墖璺緞
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject accurateRecognize(String imagePath) {
+ try {
+ AipOcr client = getClient();
+
+ HashMap<String, String> options = new HashMap<String, String>();
+ options.put("recognize_granularity", "big"); // 鏄惁瀹氫綅鍗曞瓧绗︿綅缃�
+ options.put("language_type", "CHN_ENG");
+ options.put("detect_direction", "true");
+ options.put("detect_language", "true");
+ options.put("vertexes_location", "true"); // 鏄惁杩斿洖鏂囧瓧澶栨帴澶氳竟褰㈤《鐐逛綅缃�
+ options.put("probability", "true");
+
+ org.json.JSONObject res = client.accurateGeneral(imagePath, options);
+ log.info("鐧惧害OCR楂樼簿搴︽枃瀛楄瘑鍒垚鍔燂紝鍥剧墖璺緞: {}", imagePath);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(res.toString()));
+ result.put("content", extractContentFromBaiduResult(JSON.parseObject(res.toString())));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鐧惧害OCR楂樼簿搴︽枃瀛楄瘑鍒け璐�: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 鎵嬪啓浣撹瘑鍒�
+ * @param imagePath 鍥剧墖璺緞
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject handwritingRecognize(String imagePath) {
+ try {
+ AipOcr client = getClient();
+
+ // 鎵嬪啓浣撹瘑鍒弬鏁�
+ HashMap<String, String> options = new HashMap<String, String>();
+ options.put("language_type", "CHN_ENG");
+
+ org.json.JSONObject res = client.handwriting(imagePath, options);
+ log.info("鐧惧害OCR鎵嬪啓浣撹瘑鍒垚鍔燂紝鍥剧墖璺緞: {}", imagePath);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(res.toString()));
+ result.put("content", extractContentFromBaiduResult(JSON.parseObject(res.toString())));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鐧惧害OCR鎵嬪啓浣撹瘑鍒け璐�: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 鎵嬪啓浣撹瘑鍒紙鏂囦欢瀵硅薄锛�
+ * @param imageFile 鍥剧墖鏂囦欢瀵硅薄
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject handwritingRecognize(File imageFile) {
+ try {
+ AipOcr client = getClient();
+
+ HashMap<String, String> options = new HashMap<String, String>();
+ options.put("language_type", "CHN_ENG");
+
+ org.json.JSONObject res = client.handwriting(imageFile.getAbsolutePath(), options);
+ log.info("鐧惧害OCR鎵嬪啓浣撹瘑鍒垚鍔燂紝鏂囦欢鍚�: {}", imageFile.getName());
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(res.toString()));
+ result.put("content", extractContentFromBaiduResult(JSON.parseObject(res.toString())));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鐧惧害OCR鎵嬪啓浣撹瘑鍒け璐�: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 韬唤璇佽瘑鍒�
+ * @param imagePath 鍥剧墖璺緞
+ * @param isFront true涓烘闈紝false涓哄弽闈�
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject idCardRecognize(String imagePath, boolean isFront) {
+ try {
+ AipOcr client = getClient();
+
+ HashMap<String, String> options = new HashMap<String, String>();
+ String idCardSide = isFront ? "front" : "back";
+
+ org.json.JSONObject res = client.idcard(imagePath, idCardSide, options);
+ log.info("鐧惧害OCR韬唤璇佽瘑鍒垚鍔燂紝鍥剧墖璺緞: {}锛屾柟鍚�: {}", imagePath, idCardSide);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(res.toString()));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鐧惧害OCR韬唤璇佽瘑鍒け璐�: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 閾惰鍗¤瘑鍒�
+ * @param imagePath 鍥剧墖璺緞
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject bankCardRecognize(String imagePath) {
+ try {
+ AipOcr client = getClient();
+
+ org.json.JSONObject res = client.bankcard(imagePath, new HashMap<String, String>());
+ log.info("鐧惧害OCR閾惰鍗¤瘑鍒垚鍔燂紝鍥剧墖璺緞: {}", imagePath);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(res.toString()));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鐧惧害OCR閾惰鍗¤瘑鍒け璐�: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 钀ヤ笟鎵х収璇嗗埆
+ * @param imagePath 鍥剧墖璺緞
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject businessLicenseRecognize(String imagePath) {
+ try {
+ AipOcr client = getClient();
+
+ HashMap<String, String> options = new HashMap<String, String>();
+
+ org.json.JSONObject res = client.businessLicense(imagePath, options);
+ log.info("鐧惧害OCR钀ヤ笟鎵х収璇嗗埆鎴愬姛锛屽浘鐗囪矾寰�: {}", imagePath);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(res.toString()));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鐧惧害OCR钀ヤ笟鎵х収璇嗗埆澶辫触: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 浠庣櫨搴CR缁撴灉涓彁鍙栨枃鏈唴瀹�
+ * @param result 鐧惧害OCR杩斿洖鐨勭粨鏋滐紙fastjson鏍煎紡锛�
+ * @return 鎻愬彇鐨勬枃鏈唴瀹�
+ */
+ private static String extractContentFromBaiduResult(JSONObject result) {
+ StringBuilder content = new StringBuilder();
+
+ if (result.containsKey("words_result") && result.getJSONArray("words_result") != null) {
+ JSONArray wordsResult = result.getJSONArray("words_result");
+ for (int i = 0; i < wordsResult.size(); i++) {
+ JSONObject wordResult = wordsResult.getJSONObject(i);
+ if (wordResult.containsKey("words")) {
+ content.append(wordResult.getString("words")).append("\n");
+ }
+ }
+ }
+
+ return content.toString().trim();
+ }
+
+ /**
+ * 浠庤瘑鍒粨鏋滀腑鎻愬彇鐩爣瀛楁锛堥噾棰濄�佹棩鏈熴�佸娉ㄧ瓑锛�
+ * @param ocrResult OCR璇嗗埆鐨勫師濮嬬粨鏋�
+ * @return 鎻愬彇鍚庣殑鐩爣瀛楁
+ */
+ public static java.util.Map<String, String> extractTargetFields(JSONObject ocrResult) {
+ java.util.Map<String, String> extracted = new java.util.HashMap<>();
+
+ // 鏍¢獙OCR缁撴灉鏄惁鏈夋晥
+ if (!ocrResult.containsKey("success") || !ocrResult.getBooleanValue("success")) {
+ extracted.put("error", ocrResult.getString("error"));
+ return extracted;
+ }
+
+ // 鑾峰彇璇嗗埆鐨勬枃瀛楀唴瀹�
+ String content = ocrResult.getString("content");
+ if (content == null || content.isEmpty()) {
+ extracted.put("error", "OCR璇嗗埆缁撴灉涓虹┖");
+ return extracted;
+ }
+
+ // 鍦ㄥ唴瀹逛腑鏌ユ壘鐗瑰畾鍏抽敭璇�
+ String[] lines = content.split("\n");
+ for (String line : lines) {
+ line = line.trim();
+
+ // 鏌ユ壘閲戦鐩稿叧淇℃伅
+ if (line.contains("閲戦") || line.contains("鍚堣") || line.contains("鎬昏") || line.matches(".*\\d+\\.\\d{2}.*")) {
+ if (!extracted.containsKey("totalAmount")) {
+ extracted.put("totalAmount", line);
+ }
+ }
+
+ // 鏌ユ壘鏃ユ湡鐩稿叧淇℃伅
+ if (line.contains("鏃ユ湡") || line.matches(".*\\d{4}[-/骞碷\\d{1,2}[-/鏈圿\\d{1,2}.*")) {
+ if (!extracted.containsKey("date")) {
+ extracted.put("date", line);
+ }
+ }
+
+ // 鏌ユ壘澶囨敞鐩稿叧淇℃伅
+ if (line.contains("澶囨敞") || line.contains("璇存槑")) {
+ if (!extracted.containsKey("remark")) {
+ extracted.put("remark", line);
+ }
+ }
+ }
+
+ // 濡傛灉娌℃湁鎵惧埌鐗瑰畾瀛楁锛岃繑鍥炲叏鏂�
+ if (extracted.isEmpty()) {
+ extracted.put("fullText", content);
+ }
+
+ return extracted;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/utils/TencentOCRUtil.java b/ruoyi-system/src/main/java/com/ruoyi/system/utils/TencentOCRUtil.java
new file mode 100644
index 0000000..6597b02
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/utils/TencentOCRUtil.java
@@ -0,0 +1,506 @@
+package com.ruoyi.system.utils;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.tencentcloudapi.common.AbstractModel;
+import com.tencentcloudapi.common.Credential;
+import com.tencentcloudapi.common.profile.ClientProfile;
+import com.tencentcloudapi.common.profile.HttpProfile;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.tencentcloudapi.ocr.v20181119.OcrClient;
+import com.tencentcloudapi.ocr.v20181119.models.*;
+
+import com.ruoyi.system.config.TencentOCRConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.util.Base64;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 鑵捐浜慜CR宸ュ叿绫�
+ * 浣跨敤鑵捐浜慜CR鏈嶅姟杩涜鏂囧瓧璇嗗埆
+ * 鏀寔閫氱敤鏂囧瓧璇嗗埆銆佹墜鍐欎綋璇嗗埆绛夊绉嶈瘑鍒被鍨�
+ *
+ * 浣跨敤绀轰緥锛�
+ * // 閫氱敤鏂囧瓧璇嗗埆
+ * JSONObject result = TencentOCRUtil.generalRecognize("path/to/image.jpg");
+ *
+ * // 鎵嬪啓浣撹瘑鍒�
+ * JSONObject result = TencentOCRUtil.handwritingRecognize("path/to/image.jpg");
+ */
+@Component
+public class TencentOCRUtil {
+
+ private static final Logger log = LoggerFactory.getLogger(TencentOCRUtil.class);
+
+ private static TencentOCRConfig staticTencentOcrConfig;
+
+ @Autowired
+ public void setTencentOcrConfig(TencentOCRConfig tencentOcrConfig) {
+ TencentOCRUtil.staticTencentOcrConfig = tencentOcrConfig;
+ }
+
+ /**
+ * 鑾峰彇鑵捐浜慜CR瀹㈡埛绔疄渚�
+ * @return OcrClient瀹㈡埛绔疄渚�
+ * @throws TencentCloudSDKException SDK寮傚父
+ */
+ private static OcrClient getClient() throws TencentCloudSDKException {
+ Credential cred = new Credential(
+ staticTencentOcrConfig.getSecretId(),
+ staticTencentOcrConfig.getSecretKey()
+ );
+
+ HttpProfile httpProfile = new HttpProfile();
+ httpProfile.setEndpoint(staticTencentOcrConfig.getEndpoint());
+
+ ClientProfile clientProfile = new ClientProfile();
+ clientProfile.setHttpProfile(httpProfile);
+
+ return new OcrClient(cred, "", clientProfile);
+ }
+
+ /**
+ * 閫氱敤鏂囧瓧璇嗗埆锛堝浘鐗囪矾寰勶級
+ * @param imagePath 鍥剧墖璺緞
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject generalRecognize(String imagePath) {
+ try {
+ byte[] imageBytes = Files.readAllBytes(new File(imagePath).toPath());
+ String base64Image = Base64.getEncoder().encodeToString(imageBytes);
+
+ OcrClient client = getClient();
+ GeneralBasicOCRRequest req = new GeneralBasicOCRRequest();
+ req.setImageBase64(base64Image);
+
+ GeneralBasicOCRResponse resp = client.GeneralBasicOCR(req);
+
+ log.info("鑵捐浜慜CR閫氱敤鏂囧瓧璇嗗埆鎴愬姛锛屽浘鐗囪矾寰�: {}", imagePath);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(AbstractModel.toJsonString(resp)));
+ result.put("content", extractContentFromTencentResult(JSON.parseObject(AbstractModel.toJsonString(resp))));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鑵捐浜慜CR閫氱敤鏂囧瓧璇嗗埆澶辫触: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 閫氱敤鏂囧瓧璇嗗埆锛堟枃浠跺璞★級
+ * @param imageFile 鍥剧墖鏂囦欢瀵硅薄
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject generalRecognize(File imageFile) {
+ try {
+ byte[] imageBytes = Files.readAllBytes(imageFile.toPath());
+ String base64Image = Base64.getEncoder().encodeToString(imageBytes);
+
+ OcrClient client = getClient();
+ GeneralBasicOCRRequest req = new GeneralBasicOCRRequest();
+ req.setImageBase64(base64Image);
+
+ GeneralBasicOCRResponse resp = client.GeneralBasicOCR(req);
+
+ log.info("鑵捐浜慜CR閫氱敤鏂囧瓧璇嗗埆鎴愬姛锛屾枃浠跺悕: {}", imageFile.getName());
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(AbstractModel.toJsonString(resp)));
+ result.put("content", extractContentFromTencentResult(JSON.parseObject(AbstractModel.toJsonString(resp))));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鑵捐浜慜CR閫氱敤鏂囧瓧璇嗗埆澶辫触: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 閫氱敤鏂囧瓧璇嗗埆锛堝浘鐗囧瓧鑺傛暟缁勶級
+ * @param imageBytes 鍥剧墖瀛楄妭鏁扮粍
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject generalRecognize(byte[] imageBytes) {
+ try {
+ String base64Image = Base64.getEncoder().encodeToString(imageBytes);
+
+ OcrClient client = getClient();
+ GeneralBasicOCRRequest req = new GeneralBasicOCRRequest();
+ req.setImageBase64(base64Image);
+
+ GeneralBasicOCRResponse resp = client.GeneralBasicOCR(req);
+
+ log.info("鑵捐浜慜CR閫氱敤鏂囧瓧璇嗗埆鎴愬姛锛屽瓧鑺傛暟缁勯暱搴�: {}", imageBytes.length);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(AbstractModel.toJsonString(resp)));
+ result.put("content", extractContentFromTencentResult(JSON.parseObject(AbstractModel.toJsonString(resp))));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鑵捐浜慜CR閫氱敤鏂囧瓧璇嗗埆澶辫触: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 楂樼簿搴︽枃瀛楄瘑鍒�
+ * @param imagePath 鍥剧墖璺緞
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject accurateRecognize(String imagePath) {
+ try {
+ byte[] imageBytes = Files.readAllBytes(new File(imagePath).toPath());
+ String base64Image = Base64.getEncoder().encodeToString(imageBytes);
+
+ OcrClient client = getClient();
+ GeneralAccurateOCRRequest req = new GeneralAccurateOCRRequest();
+ req.setImageBase64(base64Image);
+
+ GeneralAccurateOCRResponse resp = client.GeneralAccurateOCR(req);
+
+ log.info("鑵捐浜慜CR楂樼簿搴︽枃瀛楄瘑鍒垚鍔燂紝鍥剧墖璺緞: {}", imagePath);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(AbstractModel.toJsonString(resp)));
+ result.put("content", extractContentFromTencentResult(JSON.parseObject(AbstractModel.toJsonString(resp))));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鑵捐浜慜CR楂樼簿搴︽枃瀛楄瘑鍒け璐�: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ public static JSONObject handwritingRecognize(String imagePath, String[] itemNames) {
+ try {
+ byte[] imageBytes = Files.readAllBytes(new File(imagePath).toPath());
+ String base64Image = Base64.getEncoder().encodeToString(imageBytes);
+
+ OcrClient client = getClient();
+
+ ExtractDocMultiRequest req = new ExtractDocMultiRequest();
+ req.setImageBase64(base64Image);
+ // {"鎮h�呯鍚嶏紙鎵嬪嵃锛�", "绛惧瓧浜鸿韩浠借瘉鍙风爜", "鏃ユ湡", "鑱旂郴鐢佃瘽", "鏈汉", "绛惧瓧浜轰笌鎮h�呭叧绯�"}
+ req.setItemNames(itemNames != null ? itemNames : new String[]{"鎮h�呭鍚�", "鎬у埆", "骞撮緞", "韬唤璇佸彿", "璇婃柇", "闇�鏀粯杞繍璐圭敤", "琛岀▼", "寮�濮嬫椂闂�", "缁撴潫鏃堕棿", "瀹跺睘绛惧悕"});
+ req.setOutputLanguage("cn");
+ req.setReturnFullText(false);
+ req.setItemNamesShowMode(false);
+ ExtractDocMultiResponse resp = client.ExtractDocMulti(req);
+
+ log.info("鑵捐浜慜CR鎵嬪啓浣撹瘑鍒垚鍔燂紝鍥剧墖璺緞: {}", imagePath);
+
+ // 瑙f瀽鍝嶅簲鏁版嵁
+ JSONObject responseData = JSON.parseObject(AbstractModel.toJsonString(resp));
+
+
+ log.info("鎵嬪啓浣撹瘑鍒彁鍙栧埌 {} 涓瓧娈�", responseData.size());
+ return responseData;
+
+ } catch (Exception e) {
+ log.error("鑵捐浜慜CR鎵嬪啓浣撹瘑鍒け璐�: {}", e.getMessage(), e);
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("error", e.getMessage());
+ return errorResult;
+ }
+ }
+ /**
+ * 鎵嬪啓浣撹瘑鍒�
+ * @param imagePath 鍥剧墖璺緞
+ * @param itemNames 闇�瑕佹彁鍙栫殑瀛楁鍚嶇О鏁扮粍
+ * @return 璇嗗埆缁撴灉 Map锛宬ey涓篈utoName鐨勫�硷紝value涓篈utoContent鐨勫��
+ */
+ public static Map<String, String> handwritingRecognizeWith(String imagePath, String[] itemNames) {
+ Map<String, String> resultMap = new HashMap<>();
+ try {
+ JSONObject responseData = handwritingRecognize(imagePath, itemNames);
+
+ // 浠嶴tructuralList涓彁鍙栨暟鎹�
+ if (responseData.containsKey("StructuralList") && responseData.getJSONArray("StructuralList") != null) {
+ JSONArray structuralList = responseData.getJSONArray("StructuralList");
+ for (int i = 0; i < structuralList.size(); i++) {
+ JSONObject structural = structuralList.getJSONObject(i);
+ if (structural.containsKey("Groups") && structural.getJSONArray("Groups") != null) {
+ JSONArray groups = structural.getJSONArray("Groups");
+ for (int j = 0; j < groups.size(); j++) {
+ JSONObject group = groups.getJSONObject(j);
+ if (group.containsKey("Lines") && group.getJSONArray("Lines") != null) {
+ JSONArray lines = group.getJSONArray("Lines");
+ for (int k = 0; k < lines.size(); k++) {
+ JSONObject line = lines.getJSONObject(k);
+ String autoName = null;
+ String autoContent = null;
+
+ // 鎻愬彇AutoName
+ if (line.containsKey("Key") && line.getJSONObject("Key") != null) {
+ JSONObject key = line.getJSONObject("Key");
+ if (key.containsKey("AutoName")) {
+ autoName = key.getString("AutoName");
+ }
+ }
+
+ // 鎻愬彇AutoContent
+ if (line.containsKey("Value") && line.getJSONObject("Value") != null) {
+ JSONObject value = line.getJSONObject("Value");
+ if (value.containsKey("AutoContent")) {
+ autoContent = value.getString("AutoContent");
+ }
+ }
+
+ // 灏嗛敭鍊煎鏀惧叆缁撴灉Map
+ if (autoName != null && autoContent != null) {
+ resultMap.put(autoName, autoContent);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ log.info("鎵嬪啓浣撹瘑鍒彁鍙栧埌 {} 涓瓧娈�", resultMap.size());
+ return resultMap;
+
+ } catch (Exception e) {
+ log.error("鑵捐浜慜CR鎵嬪啓浣撹瘑鍒け璐�: {}", e.getMessage(), e);
+ resultMap.put("error", e.getMessage());
+ return resultMap;
+ }
+ }
+
+ /**
+ * 韬唤璇佽瘑鍒�
+ * @param imagePath 鍥剧墖璺緞
+ * @param cardSide 韬唤璇佹鍙嶉潰锛�"FRONT"琛ㄧず姝i潰锛�"BACK"琛ㄧず鍙嶉潰
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject idCardRecognize(String imagePath, String cardSide) {
+ try {
+ byte[] imageBytes = Files.readAllBytes(new File(imagePath).toPath());
+ String base64Image = Base64.getEncoder().encodeToString(imageBytes);
+
+ OcrClient client = getClient();
+ IDCardOCRRequest req = new IDCardOCRRequest();
+ req.setImageBase64(base64Image);
+ req.setCardSide(cardSide);
+
+ IDCardOCRResponse resp = client.IDCardOCR(req);
+
+ log.info("鑵捐浜慜CR韬唤璇佽瘑鍒垚鍔燂紝鍥剧墖璺緞: {}锛屾柟鍚�: {}", imagePath, cardSide);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(AbstractModel.toJsonString(resp)));
+ result.put("content", extractContentFromTencentResult(JSON.parseObject(AbstractModel.toJsonString(resp))));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鑵捐浜慜CR韬唤璇佽瘑鍒け璐�: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 閾惰鍗¤瘑鍒�
+ * @param imagePath 鍥剧墖璺緞
+ * @return 璇嗗埆缁撴灉
+ */
+ public static JSONObject bankCardRecognize(String imagePath) {
+ try {
+ byte[] imageBytes = Files.readAllBytes(new File(imagePath).toPath());
+ String base64Image = Base64.getEncoder().encodeToString(imageBytes);
+
+ OcrClient client = getClient();
+ BankCardOCRRequest req = new BankCardOCRRequest();
+ req.setImageBase64(base64Image);
+
+ BankCardOCRResponse resp = client.BankCardOCR(req);
+
+ log.info("鑵捐浜慜CR閾惰鍗¤瘑鍒垚鍔燂紝鍥剧墖璺緞: {}", imagePath);
+
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ result.put("data", JSON.parseObject(AbstractModel.toJsonString(resp)));
+ result.put("content", extractContentFromTencentResult(JSON.parseObject(AbstractModel.toJsonString(resp))));
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("鑵捐浜慜CR閾惰鍗¤瘑鍒け璐�: {}", e.getMessage(), e);
+
+ JSONObject errorResult = new JSONObject();
+ errorResult.put("success", false);
+ errorResult.put("error", e.getMessage());
+
+ return errorResult;
+ }
+ }
+
+ /**
+ * 浠庤吘璁簯OCR缁撴灉涓彁鍙栫函鏂囨湰鍐呭
+ * @param result OCR璇嗗埆缁撴灉
+ * @return 鎻愬彇鐨勬枃鏈唴瀹�
+ */
+ private static String extractContentFromTencentResult(JSONObject result) {
+ StringBuilder content = new StringBuilder();
+
+ // 澶勭悊閫氱敤OCR缁撴灉
+ if (result.containsKey("TextDetections") && result.getJSONArray("TextDetections") != null) {
+ JSONArray textDetections = result.getJSONArray("TextDetections");
+ for (int i = 0; i < textDetections.size(); i++) {
+ JSONObject detection = textDetections.getJSONObject(i);
+ if (detection.containsKey("DetectedText")) {
+ content.append(detection.getString("DetectedText")).append("\n");
+ }
+ }
+ }
+ // 澶勭悊鎵嬪啓浣揙CR缁撴灉
+ else if (result.containsKey("Items") && result.getJSONArray("Items") != null) {
+ JSONArray items = result.getJSONArray("Items");
+ for (int i = 0; i < items.size(); i++) {
+ JSONObject item = items.getJSONObject(i);
+ if (item.containsKey("Itemstring")) {
+ content.append(item.getString("Itemstring")).append("\n");
+ }
+ }
+ }
+ // 澶勭悊韬唤璇丱CR缁撴灉
+ else if (result.containsKey("Name") || result.containsKey("Sex") || result.containsKey("Nation") ||
+ result.containsKey("Birth") || result.containsKey("Address") || result.containsKey("IdNum")) {
+ if (result.containsKey("Name") && result.getString("Name") != null) {
+ content.append("濮撳悕: ").append(result.getString("Name")).append("\n");
+ }
+ if (result.containsKey("Sex") && result.getString("Sex") != null) {
+ content.append("鎬у埆: ").append(result.getString("Sex")).append("\n");
+ }
+ if (result.containsKey("Nation") && result.getString("Nation") != null) {
+ content.append("姘戞棌: ").append(result.getString("Nation")).append("\n");
+ }
+ if (result.containsKey("Birth") && result.getString("Birth") != null) {
+ content.append("鍑虹敓: ").append(result.getString("Birth")).append("\n");
+ }
+ if (result.containsKey("Address") && result.getString("Address") != null) {
+ content.append("鍦板潃: ").append(result.getString("Address")).append("\n");
+ }
+ if (result.containsKey("IdNum") && result.getString("IdNum") != null) {
+ content.append("韬唤璇佸彿: ").append(result.getString("IdNum")).append("\n");
+ }
+ }
+ // 澶勭悊閾惰鍗CR缁撴灉
+ else if (result.containsKey("CardNo") && result.getString("CardNo") != null) {
+ content.append("閾惰鍗″彿: ").append(result.getString("CardNo")).append("\n");
+ }
+
+ return content.toString().trim();
+ }
+
+ /**
+ * 浠庤瘑鍒粨鏋滀腑鎻愬彇鐩爣瀛楁锛堥噾棰濄�佹棩鏈熴�佸娉ㄧ瓑锛�
+ * @param ocrResult OCR璇嗗埆鐨勫師濮嬬粨鏋�
+ * @return 鎻愬彇鍚庣殑鐩爣瀛楁
+ */
+ public static Map<String, String> extractTargetFields(JSONObject ocrResult) {
+ Map<String, String> extracted = new HashMap<>();
+
+ // 鏍¢獙OCR缁撴灉鏄惁鏈夋晥
+ if (!ocrResult.containsKey("success") || !ocrResult.getBooleanValue("success")) {
+ extracted.put("error", ocrResult.getString("error"));
+ return extracted;
+ }
+
+ // 鑾峰彇璇嗗埆鐨勬枃瀛楀唴瀹�
+ String content = ocrResult.getString("content");
+ if (content == null || content.isEmpty()) {
+ extracted.put("error", "OCR璇嗗埆缁撴灉涓虹┖");
+ return extracted;
+ }
+
+ // 鍦ㄥ唴瀹逛腑鏌ユ壘鐗瑰畾鍏抽敭璇�
+ String[] lines = content.split("\n");
+ for (String line : lines) {
+ line = line.trim();
+
+ // 鏌ユ壘閲戦鐩稿叧淇℃伅
+ if (line.contains("閲戦") || line.contains("鍚堣") || line.contains("鎬昏") || line.matches(".*\\d+\\.\\d{2}.*")) {
+ if (!extracted.containsKey("totalAmount")) {
+ extracted.put("totalAmount", line);
+ }
+ }
+
+ // 鏌ユ壘鏃ユ湡鐩稿叧淇℃伅
+ if (line.contains("鏃ユ湡") || line.matches(".*\\d{4}[-/骞碷\\d{1,2}[-/鏈圿\\d{1,2}.*")) {
+ if (!extracted.containsKey("date")) {
+ extracted.put("date", line);
+ }
+ }
+
+ // 鏌ユ壘澶囨敞鐩稿叧淇℃伅
+ if (line.contains("澶囨敞") || line.contains("璇存槑")) {
+ if (!extracted.containsKey("remark")) {
+ extracted.put("remark", line);
+ }
+ }
+ }
+
+ // 濡傛灉娌℃湁鎵惧埌鐗瑰畾瀛楁锛岃繑鍥炲叏鏂�
+ if (extracted.isEmpty()) {
+ extracted.put("fullText", content);
+ }
+
+ return extracted;
+ }
+
+ /**
+ * 鎵嬪啓浣撹瘑鍒紙浣跨敤榛樿瀛楁锛�
+ * @param imagePath 鍥剧墖璺緞
+ * @return 璇嗗埆缁撴灉 Map锛宬ey涓篈utoName鐨勫�硷紝value涓篈utoContent鐨勫��
+ */
+ public static Map<String, String> handwritingRecognize(String imagePath) {
+ String[] defaultItemNames = {"鎮h�呭鍚�", "鎬у埆", "骞撮緞", "韬唤璇佸彿", "璇婃柇", "闇�鏀粯杞繍璐圭敤", "琛岀▼", "寮�濮嬫椂闂�", "缁撴潫鏃堕棿", "瀹跺睘绛惧悕"};
+ return handwritingRecognizeWith(imagePath, defaultItemNames);
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/DispatchOrdMapper.xml b/ruoyi-system/src/main/resources/mapper/system/DispatchOrdMapper.xml
index 565843e..3abe0f2 100644
--- a/ruoyi-system/src/main/resources/mapper/system/DispatchOrdMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/DispatchOrdMapper.xml
@@ -115,5 +115,12 @@
DispatchOrdCancelReasonTXT = #{cancelReasonText}
where DispatchOrdID = #{dispatchOrdID}
</update>
+
+ <!-- 鏇存柊璋冨害鍗曞疄闄呭紑濮嬫椂闂� -->
+ <update id="updateDispatchOrdActualDate">
+ update DispatchOrd
+ set DispatchOrdActualDate = #{actualDate}
+ where DispatchOrdID = #{dispatchOrdID}
+ </update>
</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/TbHospDataMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TbHospDataMapper.xml
index 050df08..f5ceab2 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TbHospDataMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/TbHospDataMapper.xml
@@ -21,6 +21,7 @@
<result property="hospIntroducerId" column="hosp_introducer_id" />
<result property="hospIntroducerDate" column="hosp_introducer_date" />
<result property="hospLevel" column="hosp_level" />
+ <result property="hospKeywords" column="hosp_keywords" />
<result property="status" column="status" />
<result property="remark" column="remark" />
<result property="createBy" column="create_by" />
@@ -33,7 +34,7 @@
select hosp_id, legacy_hosp_id, hosp_name, hosp_city_id, hosp_short,
hops_province, hops_city, hops_area, hosp_address, hosp_tel,
hosp_unit_id, hosp_state, hosp_oa_id, hosp_introducer_id,
- hosp_introducer_date, hosp_level, status, remark,
+ hosp_introducer_date, hosp_level, hosp_keywords, status, remark,
create_by, create_time, update_by, update_time
from tb_hosp_data
</sql>
@@ -90,6 +91,7 @@
<if test="hospIntroducerId != null">hosp_introducer_id,</if>
<if test="hospIntroducerDate != null">hosp_introducer_date,</if>
<if test="hospLevel != null">hosp_level,</if>
+ <if test="hospKeywords != null">hosp_keywords,</if>
<if test="status != null">status,</if>
<if test="remark != null">remark,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
@@ -110,6 +112,7 @@
<if test="hospIntroducerId != null">#{hospIntroducerId},</if>
<if test="hospIntroducerDate != null">#{hospIntroducerDate},</if>
<if test="hospLevel != null">#{hospLevel},</if>
+ <if test="hospKeywords != null">#{hospKeywords},</if>
<if test="status != null">#{status},</if>
<if test="remark != null">#{remark},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
@@ -135,6 +138,7 @@
<if test="hospIntroducerId != null">hosp_introducer_id = #{hospIntroducerId},</if>
<if test="hospIntroducerDate != null">hosp_introducer_date = #{hospIntroducerDate},</if>
<if test="hospLevel != null">hosp_level = #{hospLevel},</if>
+ <if test="hospKeywords != null">hosp_keywords = #{hospKeywords},</if>
<if test="status != null">status = #{status},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
@@ -153,5 +157,23 @@
#{hospId}
</foreach>
</delete>
+
+ <!-- 鏍规嵁鍒嗚瘝鍏抽敭璇嶉杩囨护鍖婚櫌鏁版嵁 -->
+ <select id="selectTbHospDataByKeywords" resultMap="TbHospDataResult">
+ <include refid="selectTbHospDataVo"/>
+ <where>
+ <if test="status != null and status != ''">
+ and status = #{status}
+ </if>
+ <if test="keywords != null and keywords.size() > 0">
+ and (
+ <foreach collection="keywords" item="keyword" separator="OR">
+ hosp_keywords like concat('%', #{keyword}, '%')
+ </foreach>
+ )
+ </if>
+ </where>
+ order by hosp_id desc
+ </select>
</mapper>
diff --git a/ruoyi-system/src/main/resources/mapper/system/VehicleAbnormalAlertMapper.xml b/ruoyi-system/src/main/resources/mapper/system/VehicleAbnormalAlertMapper.xml
new file mode 100644
index 0000000..7e98d1d
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/VehicleAbnormalAlertMapper.xml
@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.VehicleAbnormalAlertMapper">
+
+ <resultMap type="VehicleAbnormalAlert" id="VehicleAbnormalAlertResult">
+ <result property="alertId" column="alert_id" />
+ <result property="vehicleId" column="vehicle_id" />
+ <result property="vehicleNo" column="vehicle_no" />
+ <result property="alertDate" column="alert_date" />
+ <result property="alertTime" column="alert_time" />
+ <result property="mileage" column="mileage" />
+ <result property="alertType" column="alert_type" />
+ <result property="alertReason" column="alert_reason" />
+ <result property="startTime" column="start_time" />
+ <result property="endTime" column="end_time" />
+ <result property="alertCount" column="alert_count" />
+ <result property="status" column="status" />
+ <result property="handlerId" column="handler_id" />
+ <result property="handlerName" column="handler_name" />
+ <result property="handleTime" column="handle_time" />
+ <result property="handleRemark" column="handle_remark" />
+ <result property="notifyStatus" column="notify_status" />
+ <result property="notifyTime" column="notify_time" />
+ <result property="notifyUsers" column="notify_users" />
+ <result property="deptId" column="dept_id" />
+ <result property="deptName" column="dept_name" />
+ <result property="createTime" column="create_time" />
+ <result property="updateTime" column="update_time" />
+ </resultMap>
+
+ <sql id="selectVehicleAbnormalAlertVo">
+ select alert_id, vehicle_id, vehicle_no, alert_date, alert_time, mileage, alert_type, alert_reason,
+ start_time, end_time, alert_count, status, handler_id, handler_name, handle_time, handle_remark,
+ notify_status, notify_time, notify_users, dept_id, dept_name, create_time, update_time
+ from tb_vehicle_abnormal_alert
+ </sql>
+
+ <select id="selectVehicleAbnormalAlertList" parameterType="VehicleAbnormalAlert" resultMap="VehicleAbnormalAlertResult">
+ <include refid="selectVehicleAbnormalAlertVo"/>
+ <where>
+ <if test="vehicleId != null "> and vehicle_id = #{vehicleId}</if>
+ <if test="vehicleNo != null and vehicleNo != ''"> and vehicle_no like concat('%', #{vehicleNo}, '%')</if>
+ <if test="alertDate != null "> and date(alert_date) = date(#{alertDate})</if>
+ <if test="params.beginAlertTime != null and params.beginAlertTime != ''"><!-- 寮�濮嬫椂闂存绱� -->
+ AND date_format(alert_time,'%y%m%d') >= date_format(#{params.beginAlertTime},'%y%m%d')
+ </if>
+ <if test="params.endAlertTime != null and params.endAlertTime != ''"><!-- 缁撴潫鏃堕棿妫�绱� -->
+ AND date_format(alert_time,'%y%m%d') <= date_format(#{params.endAlertTime},'%y%m%d')
+ </if>
+ <if test="alertType != null and alertType != ''"> and alert_type = #{alertType}</if>
+ <if test="status != null and status != ''"> and status = #{status}</if>
+ <if test="deptId != null "> and dept_id = #{deptId}</if>
+ <if test="deptName != null and deptName != ''"> and dept_name like concat('%', #{deptName}, '%')</if>
+ </where>
+ order by alert_time desc
+ </select>
+
+ <select id="selectVehicleAbnormalAlertByAlertId" parameterType="Long" resultMap="VehicleAbnormalAlertResult">
+ <include refid="selectVehicleAbnormalAlertVo"/>
+ where alert_id = #{alertId}
+ </select>
+
+ <insert id="insertVehicleAbnormalAlert" parameterType="VehicleAbnormalAlert" useGeneratedKeys="true" keyProperty="alertId">
+ insert into tb_vehicle_abnormal_alert
+ <trim prefix="(" suffix=")" suffixOverrides=",">
+ <if test="vehicleId != null">vehicle_id,</if>
+ <if test="vehicleNo != null and vehicleNo != ''">vehicle_no,</if>
+ <if test="alertDate != null">alert_date,</if>
+ <if test="alertTime != null">alert_time,</if>
+ <if test="mileage != null">mileage,</if>
+ <if test="alertType != null">alert_type,</if>
+ <if test="alertReason != null">alert_reason,</if>
+ <if test="startTime != null">start_time,</if>
+ <if test="endTime != null">end_time,</if>
+ <if test="alertCount != null">alert_count,</if>
+ <if test="status != null">status,</if>
+ <if test="handlerId != null">handler_id,</if>
+ <if test="handlerName != null">handler_name,</if>
+ <if test="handleTime != null">handle_time,</if>
+ <if test="handleRemark != null">handle_remark,</if>
+ <if test="notifyStatus != null">notify_status,</if>
+ <if test="notifyTime != null">notify_time,</if>
+ <if test="notifyUsers != null">notify_users,</if>
+ <if test="deptId != null">dept_id,</if>
+ <if test="deptName != null">dept_name,</if>
+ <if test="createTime != null">create_time,</if>
+ <if test="updateTime != null">update_time,</if>
+ </trim>
+ <trim prefix="values (" suffix=")" suffixOverrides=",">
+ <if test="vehicleId != null">#{vehicleId},</if>
+ <if test="vehicleNo != null and vehicleNo != ''">#{vehicleNo},</if>
+ <if test="alertDate != null">#{alertDate},</if>
+ <if test="alertTime != null">#{alertTime},</if>
+ <if test="mileage != null">#{mileage},</if>
+ <if test="alertType != null">#{alertType},</if>
+ <if test="alertReason != null">#{alertReason},</if>
+ <if test="startTime != null">#{startTime},</if>
+ <if test="endTime != null">#{endTime},</if>
+ <if test="alertCount != null">#{alertCount},</if>
+ <if test="status != null">#{status},</if>
+ <if test="handlerId != null">#{handlerId},</if>
+ <if test="handlerName != null">#{handlerName},</if>
+ <if test="handleTime != null">#{handleTime},</if>
+ <if test="handleRemark != null">#{handleRemark},</if>
+ <if test="notifyStatus != null">#{notifyStatus},</if>
+ <if test="notifyTime != null">#{notifyTime},</if>
+ <if test="notifyUsers != null">#{notifyUsers},</if>
+ <if test="deptId != null">#{deptId},</if>
+ <if test="deptName != null">#{deptName},</if>
+ <if test="createTime != null">#{createTime},</if>
+ <if test="updateTime != null">#{updateTime},</if>
+ </trim>
+ </insert>
+
+ <update id="updateVehicleAbnormalAlert" parameterType="VehicleAbnormalAlert">
+ update tb_vehicle_abnormal_alert
+ <trim prefix="SET" suffixOverrides=",">
+ <if test="vehicleId != null">vehicle_id = #{vehicleId},</if>
+ <if test="vehicleNo != null and vehicleNo != ''">vehicle_no = #{vehicleNo},</if>
+ <if test="alertDate != null">alert_date = #{alertDate},</if>
+ <if test="alertTime != null">alert_time = #{alertTime},</if>
+ <if test="mileage != null">mileage = #{mileage},</if>
+ <if test="alertType != null">alert_type = #{alertType},</if>
+ <if test="alertReason != null">alert_reason = #{alertReason},</if>
+ <if test="startTime != null">start_time = #{startTime},</if>
+ <if test="endTime != null">end_time = #{endTime},</if>
+ <if test="alertCount != null">alert_count = #{alertCount},</if>
+ <if test="status != null">status = #{status},</if>
+ <if test="handlerId != null">handler_id = #{handlerId},</if>
+ <if test="handlerName != null">handler_name = #{handlerName},</if>
+ <if test="handleTime != null">handle_time = #{handleTime},</if>
+ <if test="handleRemark != null">handle_remark = #{handleRemark},</if>
+ <if test="notifyStatus != null">notify_status = #{notifyStatus},</if>
+ <if test="notifyTime != null">notify_time = #{notifyTime},</if>
+ <if test="notifyUsers != null">notify_users = #{notifyUsers},</if>
+ <if test="deptId != null">dept_id = #{deptId},</if>
+ <if test="deptName != null">dept_name = #{deptName},</if>
+ <if test="updateTime != null">update_time = #{updateTime},</if>
+ </trim>
+ where alert_id = #{alertId}
+ </update>
+
+ <delete id="deleteVehicleAbnormalAlertByAlertId" parameterType="Long">
+ delete from tb_vehicle_abnormal_alert where alert_id = #{alertId}
+ </delete>
+
+ <delete id="deleteVehicleAbnormalAlertByAlertIds" parameterType="String">
+ delete from tb_vehicle_abnormal_alert where alert_id in
+ <foreach item="alertId" collection="array" open="(" separator="," close=")">
+ #{alertId}
+ </foreach>
+ </delete>
+
+ <select id="selectDailyAlertCount" resultType="int">
+ select count(*) from tb_vehicle_abnormal_alert
+ where vehicle_id = #{vehicleId} and date(alert_date) = date(#{alertDate})
+ </select>
+
+ <select id="selectLastAlertTime" resultType="java.util.Date">
+ select max(alert_time) from tb_vehicle_abnormal_alert
+ where vehicle_id = #{vehicleId}
+ </select>
+
+ <update id="batchHandleAlert">
+ update tb_vehicle_abnormal_alert
+ set status = '1',
+ handler_id = #{handlerId},
+ handler_name = #{handlerName},
+ handle_time = now(),
+ handle_remark = #{handleRemark}
+ where alert_id in
+ <foreach item="alertId" collection="alertIds" open="(" separator="," close=")">
+ #{alertId}
+ </foreach>
+ </update>
+</mapper>
diff --git a/ruoyi-system/src/main/resources/mapper/system/VehicleAlertConfigMapper.xml b/ruoyi-system/src/main/resources/mapper/system/VehicleAlertConfigMapper.xml
new file mode 100644
index 0000000..27daa3f
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/VehicleAlertConfigMapper.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.VehicleAlertConfigMapper">
+
+ <resultMap type="VehicleAlertConfig" id="VehicleAlertConfigResult">
+ <result property="configId" column="config_id" />
+ <result property="configType" column="config_type" />
+ <result property="deptId" column="dept_id" />
+ <result property="vehicleId" column="vehicle_id" />
+ <result property="mileageThreshold" column="mileage_threshold" />
+ <result property="dailyAlertLimit" column="daily_alert_limit" />
+ <result property="alertInterval" column="alert_interval" />
+ <result property="notifyUserIds" column="notify_user_ids" />
+ <result property="status" column="status" />
+ <result property="remark" column="remark" />
+ <result property="createBy" column="create_by" />
+ <result property="createTime" column="create_time" />
+ <result property="updateBy" column="update_by" />
+ <result property="updateTime" column="update_time" />
+ </resultMap>
+
+ <sql id="selectVehicleAlertConfigVo">
+ select c.config_id, c.config_type, c.dept_id, c.vehicle_id, c.mileage_threshold,
+ c.daily_alert_limit, c.alert_interval, c.notify_user_ids, c.status, c.remark,
+ c.create_by, c.create_time, c.update_by, c.update_time,
+ CASE
+ WHEN c.config_type = 'DEPT' THEN d.dept_name
+ WHEN c.config_type = 'VEHICLE' THEN v.vehicle_no
+ ELSE NULL
+ END as target_name
+ from tb_vehicle_alert_config c
+ left join sys_dept d on c.dept_id = d.dept_id
+ left join tb_vehicle_info v on c.vehicle_id = v.vehicle_id
+ </sql>
+
+ <select id="selectVehicleAlertConfigList" parameterType="VehicleAlertConfig" resultMap="VehicleAlertConfigResult">
+ <include refid="selectVehicleAlertConfigVo"/>
+ <where>
+ <if test="configType != null and configType != ''"> and c.config_type = #{configType}</if>
+ <if test="deptId != null "> and c.dept_id = #{deptId}</if>
+ <if test="vehicleId != null "> and c.vehicle_id = #{vehicleId}</if>
+ <if test="status != null and status != ''"> and c.status = #{status}</if>
+ </where>
+ order by
+ CASE c.config_type
+ WHEN 'GLOBAL' THEN 3
+ WHEN 'DEPT' THEN 2
+ WHEN 'VEHICLE' THEN 1
+ END,
+ c.create_time desc
+ </select>
+
+ <select id="selectVehicleAlertConfigByConfigId" parameterType="Long" resultMap="VehicleAlertConfigResult">
+ <include refid="selectVehicleAlertConfigVo"/>
+ where c.config_id = #{configId}
+ </select>
+
+ <select id="selectConfigByVehicle" resultMap="VehicleAlertConfigResult">
+ <include refid="selectVehicleAlertConfigVo"/>
+ where c.status = '0'
+ and (
+ (c.config_type = 'VEHICLE' and c.vehicle_id = #{vehicleId})
+ or (c.config_type = 'DEPT' and c.dept_id = #{deptId})
+ or c.config_type = 'GLOBAL'
+ )
+ order by
+ CASE c.config_type
+ WHEN 'VEHICLE' THEN 1
+ WHEN 'DEPT' THEN 2
+ WHEN 'GLOBAL' THEN 3
+ END
+ limit 1
+ </select>
+
+ <insert id="insertVehicleAlertConfig" parameterType="VehicleAlertConfig" useGeneratedKeys="true" keyProperty="configId">
+ insert into tb_vehicle_alert_config
+ <trim prefix="(" suffix=")" suffixOverrides=",">
+ <if test="configType != null and configType != ''">config_type,</if>
+ <if test="deptId != null">dept_id,</if>
+ <if test="vehicleId != null">vehicle_id,</if>
+ <if test="mileageThreshold != null">mileage_threshold,</if>
+ <if test="dailyAlertLimit != null">daily_alert_limit,</if>
+ <if test="alertInterval != null">alert_interval,</if>
+ <if test="notifyUserIds != null">notify_user_ids,</if>
+ <if test="status != null">status,</if>
+ <if test="remark != null">remark,</if>
+ <if test="createBy != null">create_by,</if>
+ create_time
+ </trim>
+ <trim prefix="values (" suffix=")" suffixOverrides=",">
+ <if test="configType != null and configType != ''">#{configType},</if>
+ <if test="deptId != null">#{deptId},</if>
+ <if test="vehicleId != null">#{vehicleId},</if>
+ <if test="mileageThreshold != null">#{mileageThreshold},</if>
+ <if test="dailyAlertLimit != null">#{dailyAlertLimit},</if>
+ <if test="alertInterval != null">#{alertInterval},</if>
+ <if test="notifyUserIds != null">#{notifyUserIds},</if>
+ <if test="status != null">#{status},</if>
+ <if test="remark != null">#{remark},</if>
+ <if test="createBy != null">#{createBy},</if>
+ now()
+ </trim>
+ </insert>
+
+ <update id="updateVehicleAlertConfig" parameterType="VehicleAlertConfig">
+ update tb_vehicle_alert_config
+ <trim prefix="SET" suffixOverrides=",">
+ <if test="configType != null and configType != ''">config_type = #{configType},</if>
+ <if test="deptId != null">dept_id = #{deptId},</if>
+ <if test="vehicleId != null">vehicle_id = #{vehicleId},</if>
+ <if test="mileageThreshold != null">mileage_threshold = #{mileageThreshold},</if>
+ <if test="dailyAlertLimit != null">daily_alert_limit = #{dailyAlertLimit},</if>
+ <if test="alertInterval != null">alert_interval = #{alertInterval},</if>
+ <if test="notifyUserIds != null">notify_user_ids = #{notifyUserIds},</if>
+ <if test="status != null">status = #{status},</if>
+ <if test="remark != null">remark = #{remark},</if>
+ <if test="updateBy != null">update_by = #{updateBy},</if>
+ update_time = now()
+ </trim>
+ where config_id = #{configId}
+ </update>
+
+ <delete id="deleteVehicleAlertConfigByConfigId" parameterType="Long">
+ delete from tb_vehicle_alert_config where config_id = #{configId}
+ </delete>
+
+ <delete id="deleteVehicleAlertConfigByConfigIds" parameterType="String">
+ delete from tb_vehicle_alert_config where config_id in
+ <foreach item="configId" collection="array" open="(" separator="," close=")">
+ #{configId}
+ </foreach>
+ </delete>
+</mapper>
diff --git a/ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml b/ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml
index 6282f5d..73671e3 100644
--- a/ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml
@@ -72,6 +72,19 @@
AND segment_distance >0
</select>
+ <!-- 鏌ヨ杞﹁締鍦ㄦ寚瀹氭椂闂磋寖鍥村唴鐨勫垎娈甸噷绋� -->
+ <select id="selectSegmentsByTimeRange" resultMap="VehicleGpsSegmentMileageResult">
+ SELECT segment_id, vehicle_id, vehicle_no, segment_start_time, segment_end_time,
+ start_longitude, start_latitude, end_longitude, end_latitude,
+ segment_distance, gps_point_count, gps_ids, task_id, task_code, calculate_method
+ FROM tb_vehicle_gps_segment_mileage
+ WHERE vehicle_id = #{vehicleId}
+ AND segment_start_time <= #{endTime}
+ AND segment_end_time >= #{startTime}
+ AND segment_distance > 0
+ ORDER BY segment_start_time
+ </select>
+
<!-- 鎸変换鍔D鏌ヨ鍒嗘閲岀▼鍒楄〃 -->
<select id="selectSegmentsByTaskId" resultMap="VehicleGpsSegmentMileageResult">
<include refid="selectVehicleGpsSegmentMileageVo"/>
diff --git a/ruoyi-ui/src/api/system/networkDiag.js b/ruoyi-ui/src/api/system/networkDiag.js
new file mode 100644
index 0000000..f89b4b7
--- /dev/null
+++ b/ruoyi-ui/src/api/system/networkDiag.js
@@ -0,0 +1,41 @@
+import request from '@/utils/request'
+
+/**
+ * OCR鏈嶅姟杩炴帴璇婃柇
+ */
+export function diagOcrConnection() {
+ return request({
+ url: '/system/diag/ocrConnection',
+ method: 'get'
+ })
+}
+
+/**
+ * 閫氱敤缃戠粶杩為�氭�ф祴璇�
+ * @param {String} host - 鐩爣涓绘満
+ * @param {Number} port - 鐩爣绔彛
+ */
+export function testConnectivity(host, port) {
+ return request({
+ url: '/system/diag/testConnectivity',
+ method: 'post',
+ data: {
+ host,
+ port
+ }
+ })
+}
+
+/**
+ * DNS瑙f瀽娴嬭瘯
+ * @param {String} hostname - 涓绘満鍚�
+ */
+export function testDnsResolution(hostname) {
+ return request({
+ url: '/system/diag/testDns',
+ method: 'post',
+ data: {
+ hostname
+ }
+ })
+}
\ No newline at end of file
diff --git a/ruoyi-ui/src/api/system/ocr.js b/ruoyi-ui/src/api/system/ocr.js
new file mode 100644
index 0000000..98fba9c
--- /dev/null
+++ b/ruoyi-ui/src/api/system/ocr.js
@@ -0,0 +1,69 @@
+import request from '@/utils/request'
+
+/**
+ * 涓婁紶鍥剧墖杩涜OCR璇嗗埆
+ * @param {FormData} data - 鍖呭惈file銆乼ype鍜宲rovider鐨勮〃鍗曟暟鎹�
+ */
+export function recognizeImage(data) {
+ return request({
+ url: '/system/ocr/recognize',
+ method: 'post',
+ data: data,
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ 'repeatSubmit': false // 绂佺敤闃查噸澶嶆彁浜ゆ鏌�
+ }
+ })
+}
+
+/**
+ * 鑾峰彇鏀寔鐨凮CR璇嗗埆绫诲瀷
+ */
+export function getOcrTypes() {
+ return request({
+ url: '/system/ocr/types',
+ method: 'get'
+ })
+}
+
+/**
+ * 鑾峰彇鏀寔鐨凮CR鏈嶅姟鎻愪緵鍟�
+ */
+export function getOcrProviders() {
+ return request({
+ url: '/system/ocr/providers',
+ method: 'get'
+ })
+}
+
+/**
+ * 閫氳繃URL杩涜OCR璇嗗埆
+ * @param {String} imageUrl - 鍥剧墖URL鍦板潃
+ * @param {String} type - 璇嗗埆绫诲瀷
+ * @param {String} provider - OCR鏈嶅姟鎻愪緵鍟�
+ * @param {Array} itemNames - 闇�瑕佹彁鍙栫殑瀛楁鍚嶇О鏁扮粍
+ */
+export function recognizeByUrl(imageUrl, type, provider, itemNames) {
+ return request({
+ url: '/system/ocr/recognizeByUrl',
+ method: 'get',
+ params: {
+ imageUrl: imageUrl,
+ type: type || 'General',
+ provider: provider || 'ali',
+ itemNames: itemNames
+ }
+ })
+}
+
+/**
+ * 鎻愬彇OCR缁撴灉涓殑鐩爣瀛楁
+ * @param {Object} ocrResult - OCR鍘熷缁撴灉
+ */
+export function extractFields(ocrResult) {
+ return request({
+ url: '/system/ocr/extractFields',
+ method: 'post',
+ data: ocrResult
+ })
+}
\ No newline at end of file
diff --git a/ruoyi-ui/src/api/system/vehicle.js b/ruoyi-ui/src/api/system/vehicle.js
index 56f0627..edae927 100644
--- a/ruoyi-ui/src/api/system/vehicle.js
+++ b/ruoyi-ui/src/api/system/vehicle.js
@@ -50,4 +50,4 @@
method: 'get',
params: query
})
-}
\ No newline at end of file
+}
diff --git a/ruoyi-ui/src/api/system/vehicleAlert.js b/ruoyi-ui/src/api/system/vehicleAlert.js
new file mode 100644
index 0000000..4e985f6
--- /dev/null
+++ b/ruoyi-ui/src/api/system/vehicleAlert.js
@@ -0,0 +1,80 @@
+import request from '@/utils/request'
+
+// 鏌ヨ杞﹁締寮傚父鍛婅鍒楄〃
+export function listVehicleAlert(query) {
+ return request({
+ url: '/system/vehicleAlert/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ杞﹁締寮傚父鍛婅璇︾粏
+export function getVehicleAlert(alertId) {
+ return request({
+ url: '/system/vehicleAlert/' + alertId,
+ method: 'get'
+ })
+}
+
+// 鏂板杞﹁締寮傚父鍛婅
+export function addVehicleAlert(data) {
+ return request({
+ url: '/system/vehicleAlert',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼杞﹁締寮傚父鍛婅
+export function updateVehicleAlert(data) {
+ return request({
+ url: '/system/vehicleAlert',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎杞﹁締寮傚父鍛婅
+export function delVehicleAlert(alertId) {
+ return request({
+ url: '/system/vehicleAlert/' + alertId,
+ method: 'delete'
+ })
+}
+
+// 澶勭悊鍛婅
+export function handleAlert(alertId, data) {
+ return request({
+ url: '/system/vehicleAlert/handle/' + alertId,
+ method: 'put',
+ data: data
+ })
+}
+
+// 鎵归噺澶勭悊鍛婅
+export function batchHandleAlert(alertIds, data) {
+ return request({
+ url: '/system/vehicleAlert/batchHandle',
+ method: 'put',
+ params: { alertIds: alertIds.join(',') },
+ data: data
+ })
+}
+
+// 鑾峰彇鏈鐞嗗憡璀︽暟閲�
+export function getUnhandledCount() {
+ return request({
+ url: '/system/vehicleAlert/unhandledCount',
+ method: 'get'
+ })
+}
+
+// 瀵煎嚭杞﹁締寮傚父鍛婅
+export function exportVehicleAlert(query) {
+ return request({
+ url: '/system/vehicleAlert/export',
+ method: 'get',
+ params: query
+ })
+}
diff --git a/ruoyi-ui/src/api/system/vehicleAlertConfig.js b/ruoyi-ui/src/api/system/vehicleAlertConfig.js
new file mode 100644
index 0000000..a92d123
--- /dev/null
+++ b/ruoyi-ui/src/api/system/vehicleAlertConfig.js
@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 鏌ヨ杞﹁締鍛婅閰嶇疆鍒楄〃
+export function listVehicleAlertConfig(query) {
+ return request({
+ url: '/system/vehicleAlertConfig/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ杞﹁締鍛婅閰嶇疆璇︾粏
+export function getVehicleAlertConfig(configId) {
+ return request({
+ url: '/system/vehicleAlertConfig/' + configId,
+ method: 'get'
+ })
+}
+
+// 鏂板杞﹁締鍛婅閰嶇疆
+export function addVehicleAlertConfig(data) {
+ return request({
+ url: '/system/vehicleAlertConfig',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼杞﹁締鍛婅閰嶇疆
+export function updateVehicleAlertConfig(data) {
+ return request({
+ url: '/system/vehicleAlertConfig',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎杞﹁締鍛婅閰嶇疆
+export function delVehicleAlertConfig(configId) {
+ return request({
+ url: '/system/vehicleAlertConfig/' + configId,
+ method: 'delete'
+ })
+}
+
+// 瀵煎嚭杞﹁締鍛婅閰嶇疆
+export function exportVehicleAlertConfig(query) {
+ return request({
+ url: '/system/vehicleAlertConfig/export',
+ method: 'get',
+ params: query
+ })
+}
diff --git a/ruoyi-ui/src/api/system/vehicleSync.js b/ruoyi-ui/src/api/system/vehicleSync.js
new file mode 100644
index 0000000..44dd199
--- /dev/null
+++ b/ruoyi-ui/src/api/system/vehicleSync.js
@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 鏌ヨ杞﹁締鍚屾鍒楄〃
+export function listVehicleSync() {
+ return request({
+ url: '/system/vehicleSync/list',
+ method: 'get'
+ })
+}
+
+// 鎵嬪姩鍚屾杞﹁締
+export function syncVehicle(data) {
+ return request({
+ url: '/system/vehicleSync/syncVehicle',
+ method: 'post',
+ data: data
+ })
+}
diff --git a/ruoyi-ui/src/router/index.js b/ruoyi-ui/src/router/index.js
index 81cbe25..6266b3e 100644
--- a/ruoyi-ui/src/router/index.js
+++ b/ruoyi-ui/src/router/index.js
@@ -151,6 +151,57 @@
// 鍔ㄦ�佽矾鐢憋紝鍩轰簬鐢ㄦ埛鏉冮檺鍔ㄦ�佸幓鍔犺浇
export const dynamicRoutes = [
qywechatRouter,
+
+ // 缃戠粶璇婃柇璺敱
+ {
+ path: "/system/diag",
+ component: Layout,
+ redirect: "/system/diag/ocrConnection",
+ name: "Diag",
+ meta: {
+ title: "缃戠粶璇婃柇",
+ icon: "link",
+ permissions: ["system:diag:view"]
+ },
+ children: [
+ {
+ path: "ocrConnection",
+ component: () => import("@/views/system/diag/ocrConnection"),
+ name: "OCRConnectionDiag",
+ meta: {
+ title: "OCR杩炴帴璇婃柇",
+ icon: "monitor",
+ permissions: ["system:diag:ocr"]
+ }
+ }
+ ]
+ },
+
+ // 鍖婚櫌鍒嗚瘝娴嬭瘯璺敱
+ {
+ path: "/system/hospital",
+ component: Layout,
+ redirect: "/system/hospital/tokenizer",
+ name: "Hospital",
+ meta: {
+ title: "鍖婚櫌绠$悊",
+ icon: "hospital",
+ permissions: ["system:hospital:view"]
+ },
+ children: [
+ {
+ path: "tokenizer",
+ component: () => import("@/views/system/hospital/tokenizer"),
+ name: "HospitalTokenizer",
+ meta: {
+ title: "鍖婚櫌鍒嗚瘝娴嬭瘯",
+ icon: "search",
+ permissions: ["system:hospital:tokenizer"]
+ }
+ }
+ ]
+ },
+
{
path: '/system/user-auth',
component: Layout,
diff --git a/ruoyi-ui/src/views/system/diag/ocrConnection.vue b/ruoyi-ui/src/views/system/diag/ocrConnection.vue
new file mode 100644
index 0000000..f081e10
--- /dev/null
+++ b/ruoyi-ui/src/views/system/diag/ocrConnection.vue
@@ -0,0 +1,261 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <div slot="header" class="clearfix">
+ <span>OCR鏈嶅姟杩炴帴璇婃柇</span>
+ </div>
+
+ <el-alert
+ title="璇婃柇璇存槑"
+ type="info"
+ :closable="false"
+ style="margin-bottom: 20px"
+ >
+ <div>
+ 鏈〉闈㈢敤浜庤瘖鏂璒CR鏈嶅姟杩炴帴鐘舵�侊紝鍖呮嫭DNS瑙f瀽銆佺綉缁滆繛閫氭�х瓑銆�<br/>
+ <strong>璇婃柇鍐呭锛�</strong>DNS瑙f瀽娴嬭瘯銆佽繛鎺ユ祴璇曘�佺綉缁滈厤缃鏌�
+ </div>
+ </el-alert>
+
+ <div style="text-align: center; margin-bottom: 30px;">
+ <el-button
+ type="primary"
+ :loading="diagnosing"
+ @click="handleDiagnosis"
+ >
+ <i class="el-icon-search"></i> 寮�濮嬭瘖鏂�
+ </el-button>
+ </div>
+
+ <!-- 璇婃柇缁撴灉 -->
+ <div v-if="diagnosisResult">
+ <!-- DNS瑙f瀽缁撴灉 -->
+ <el-card shadow="never" class="result-card">
+ <div slot="header">
+ <span>DNS瑙f瀽娴嬭瘯</span>
+ <el-tag
+ :type="diagnosisResult.dns.success ? 'success' : 'danger'"
+ style="float: right; margin-top: 5px;"
+ >
+ {{ diagnosisResult.dns.success ? '鉁� 鎴愬姛' : '鉂� 澶辫触' }}
+ </el-tag>
+ </div>
+
+ <div class="result-content">
+ <p><strong>鍩熷悕锛�</strong>{{ ocrEndpoint }}</p>
+ <p v-if="diagnosisResult.dns.ips"><strong>IP鍦板潃锛�</strong>{{ diagnosisResult.dns.ips.join(', ') }}</p>
+ <p v-if="diagnosisResult.dns.ipCount"><strong>瑙f瀽鏁伴噺锛�</strong>{{ diagnosisResult.dns.ipCount }}涓�</p>
+ <p v-if="diagnosisResult.dns.error"><strong>閿欒淇℃伅锛�</strong>{{ diagnosisResult.dns.error }}</p>
+
+ <div v-if="!diagnosisResult.dns.success" style="margin-top: 15px;">
+ <el-alert
+ title="DNS瑙f瀽寤鸿"
+ type="warning"
+ :closable="false"
+ show-icon
+ >
+ <ul style="margin: 0; padding-left: 20px;">
+ <li>妫�鏌NS鏈嶅姟鍣ㄩ厤缃�</li>
+ <li>灏濊瘯浣跨敤鍏叡DNS锛堝8.8.8.8鎴�114.114.114.114锛�</li>
+ <li>纭鍩熷悕鏄惁姝g‘锛歿{ ocrEndpoint }}</li>
+ </ul>
+ </el-alert>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 杩炴帴娴嬭瘯缁撴灉 -->
+ <el-card shadow="never" class="result-card">
+ <div slot="header">
+ <span>杩炴帴娴嬭瘯</span>
+ <el-tag
+ :type="diagnosisResult.connection.success ? 'success' : 'danger'"
+ style="float: right; margin-top: 5px;"
+ >
+ {{ diagnosisResult.connection.success ? '鉁� 鎴愬姛' : '鉂� 澶辫触' }}
+ </el-tag>
+ </div>
+
+ <div class="result-content">
+ <p><strong>娴嬭瘯鍦板潃锛�</strong>{{ ocrEndpoint }}:{{ ocrPort }}</p>
+ <p v-if="diagnosisResult.connection.responseTime"><strong>鍝嶅簲鏃堕棿锛�</strong>{{ diagnosisResult.connection.responseTime }}</p>
+ <p v-if="diagnosisResult.connection.error"><strong>閿欒淇℃伅锛�</strong>{{ diagnosisResult.connection.error }}</p>
+
+ <div v-if="!diagnosisResult.connection.success" style="margin-top: 15px;">
+ <el-alert
+ title="杩炴帴娴嬭瘯寤鸿"
+ type="warning"
+ :closable="false"
+ show-icon
+ >
+ <ul style="margin: 0; padding-left: 20px;">
+ <li>妫�鏌ラ槻鐏鏄惁寮�鏀緖{ ocrPort }}绔彛</li>
+ <li>纭鏈嶅姟鍣ㄥ厑璁稿嚭绔橦TTPS璇锋眰</li>
+ <li>妫�鏌ョ綉缁滅瓥鐣ユ槸鍚﹀厑璁歌闂閮ㄦ湇鍔�</li>
+ <li>楠岃瘉浠g悊閰嶇疆锛堝閫傜敤锛�</li>
+ </ul>
+ </el-alert>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 缃戠粶閰嶇疆 -->
+ <el-card shadow="never" class="result-card">
+ <div slot="header">
+ <span>缃戠粶閰嶇疆</span>
+ </div>
+
+ <div class="result-content">
+ <p><strong>HTTP浠g悊锛�</strong>{{ diagnosisResult.network.httpProxy || '鏃�' }}</p>
+ <p><strong>HTTPS浠g悊锛�</strong>{{ diagnosisResult.network.httpsProxy || '鏃�' }}</p>
+ <p><strong>鏈湴鍦板潃锛�</strong>{{ diagnosisResult.network.localHostAddress || '-' }}</p>
+ <p><strong>鏈湴涓绘満鍚嶏細</strong>{{ diagnosisResult.network.localHostName || '-' }}</p>
+ </div>
+ </el-card>
+
+ <!-- 璇婃柇寤鸿 -->
+ <el-card shadow="never" class="result-card">
+ <div slot="header">
+ <span>璇婃柇寤鸿</span>
+ </div>
+
+ <div class="result-content">
+ <div v-if="overallStatus === 'success'">
+ <el-alert
+ title="璇婃柇缁撴灉锛氫竴鍒囨甯�"
+ type="success"
+ :closable="false"
+ show-icon
+ >
+ <p>OCR鏈嶅姟杩炴帴姝e父锛屾偍鍙互姝e父浣跨敤OCR鍔熻兘銆�</p>
+ </el-alert>
+ </div>
+ <div v-else-if="overallStatus === 'warning'">
+ <el-alert
+ title="璇婃柇缁撴灉锛氶儴鍒嗛棶棰�"
+ type="warning"
+ :closable="false"
+ show-icon
+ >
+ <p>瀛樺湪閮ㄥ垎缃戠粶闂锛屽彲鑳藉奖鍝峅CR鏈嶅姟浣跨敤銆�</p>
+ </el-alert>
+ </div>
+ <div v-else>
+ <el-alert
+ title="璇婃柇缁撴灉锛氬瓨鍦ㄩ棶棰�"
+ type="error"
+ :closable="false"
+ show-icon
+ >
+ <p>缃戠粶杩炴帴瀛樺湪闂锛岄渶瑕佷慨澶嶅悗鎵嶈兘浣跨敤OCR鏈嶅姟銆�</p>
+ </el-alert>
+ </div>
+
+ <h4 style="margin: 15px 0 10px 0;">瑙e喅鏂规锛�</h4>
+ <ol>
+ <li><strong>妫�鏌ョ綉缁滆繛鎺�</strong> - 纭繚鏈嶅姟鍣ㄥ彲浠ヨ闂缃�</li>
+ <li><strong>楠岃瘉DNS瑙f瀽</strong> - 纭鍩熷悕 {{ ocrEndpoint }} 鑳藉姝g‘瑙f瀽</li>
+ <li><strong>妫�鏌ラ槻鐏璁剧疆</strong> - 纭繚{{ ocrPort }}绔彛寮�鏀�</li>
+ <li><strong>閰嶇疆浠g悊锛堝闇�瑕侊級</strong> - 濡傛灉閫氳繃浠g悊璁块棶锛屾鏌ヤ唬鐞嗛厤缃�</li>
+ <li><strong>楠岃瘉闃块噷浜戞湇鍔�</strong> - 纭OCR鏈嶅姟宸插紑閫氫笖AccessKey鏈夋晥</li>
+ </ol>
+ </div>
+ </el-card>
+ </div>
+
+ <!-- 璇婃柇涓� -->
+ <div v-if="diagnosing" style="text-align: center; padding: 50px 0">
+ <i class="el-icon-loading" style="font-size: 40px; color: #409EFF"></i>
+ <p style="margin-top: 20px; color: #909399">姝e湪璇婃柇缃戠粶杩炴帴锛岃绋嶅��...</p>
+ <p style="color: #c0c4cc; font-size: 12px;">杩欏彲鑳介渶瑕佸嚑绉掗挓鏃堕棿</p>
+ </div>
+
+ <!-- 鏈紑濮嬭瘖鏂� -->
+ <div v-else-if="!diagnosisResult" style="text-align: center; padding: 50px 0; color: #909399">
+ <i class="el-icon-monitor" style="font-size: 60px"></i>
+ <p style="margin-top: 20px">鐐瑰嚮"寮�濮嬭瘖鏂�"鎸夐挳妫�娴婳CR鏈嶅姟杩炴帴鐘舵��</p>
+ </div>
+ </el-card>
+ </div>
+</template>
+
+<script>
+import { diagOcrConnection } from "@/api/system/networkDiag";
+
+export default {
+ name: "OCRConnectionDiag",
+ data() {
+ return {
+ diagnosing: false,
+ diagnosisResult: null,
+ ocrEndpoint: 'ocr-api.cn-hangzhou.aliyuncs.com',
+ ocrPort: 443
+ };
+ },
+ computed: {
+ overallStatus() {
+ if (!this.diagnosisResult) return null;
+
+ const { dns, connection } = this.diagnosisResult;
+
+ if (dns.success && connection.success) {
+ return 'success';
+ } else if (!dns.success || !connection.success) {
+ return 'error';
+ }
+
+ return 'warning';
+ }
+ },
+ methods: {
+ /** 寮�濮嬭瘖鏂� */
+ handleDiagnosis() {
+ this.diagnosing = true;
+ this.diagnosisResult = null;
+
+ diagOcrConnection().then(response => {
+ this.diagnosisResult = response.data;
+ this.diagnosing = false;
+ }).catch(error => {
+ this.$modal.msgError('缃戠粶璇婃柇澶辫触: ' + error.message);
+ this.diagnosing = false;
+ });
+ }
+ }
+};
+</script>
+
+<style scoped>
+.result-card {
+ margin-bottom: 20px;
+}
+
+.result-content p {
+ margin: 8px 0;
+ line-height: 1.5;
+}
+
+.result-content ul {
+ margin: 10px 0;
+ padding-left: 20px;
+}
+
+.result-content ol {
+ margin: 10px 0;
+ padding-left: 20px;
+}
+
+.result-content li {
+ margin: 5px 0;
+ line-height: 1.5;
+}
+
+.box-card {
+ margin-bottom: 20px;
+}
+
+h4 {
+ margin: 15px 0 10px 0;
+ color: #303133;
+}
+</style>
\ No newline at end of file
diff --git a/ruoyi-ui/src/views/system/hospital/tokenizer.vue b/ruoyi-ui/src/views/system/hospital/tokenizer.vue
new file mode 100644
index 0000000..dddcbe1
--- /dev/null
+++ b/ruoyi-ui/src/views/system/hospital/tokenizer.vue
@@ -0,0 +1,418 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card" shadow="hover">
+ <div slot="header" class="clearfix">
+ <span style="font-weight: bold; font-size: 16px;">鍖婚櫌鍒嗚瘝绠$悊涓庢祴璇曞伐鍏�</span>
+ <el-tag type="info" size="small" style="margin-left: 10px;">浣跨敤 HanLP 涓撲笟涓枃鍒嗚瘝</el-tag>
+ </div>
+
+ <!-- 鎵归噺鍒嗚瘝鍖哄煙 -->
+ <el-divider content-position="left">
+ <i class="el-icon-setting"></i> 鎵归噺鍒嗚瘝绠$悊
+ </el-divider>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-alert
+ title="鎻愮ず"
+ type="info"
+ :closable="false"
+ style="margin-bottom: 15px;">
+ <template slot>
+ 鎵归噺涓烘墍鏈夊尰闄㈢敓鎴愬垎璇嶆暟鎹�傞娆¢儴缃叉椂蹇呴』鎵ц涓�娆★紝鎴栧湪鍒嗚瘝绠楁硶鏇存柊鍚庨噸鏂扮敓鎴愩��
+ </template>
+ </el-alert>
+
+ <el-button
+ type="primary"
+ icon="el-icon-cpu"
+ :loading="generating"
+ @click="handleGenerateKeywords"
+ size="medium">
+ {{ generating ? '姝e湪鐢熸垚鍒嗚瘝...' : '鎵归噺鐢熸垚鎵�鏈夊尰闄㈠垎璇�' }}
+ </el-button>
+
+ <el-button
+ type="success"
+ icon="el-icon-refresh"
+ @click="resetStatus"
+ size="medium"
+ v-if="generateResult">
+ 閲嶇疆
+ </el-button>
+
+ <!-- 杩涘害鏉� -->
+ <div v-if="generating && taskProgress" style="margin-top: 20px;">
+ <el-progress
+ :percentage="taskProgress.progress || 0"
+ :status="taskProgress.status === 'FAILED' ? 'exception' : null">
+ </el-progress>
+ <div style="margin-top: 10px; color: #606266; font-size: 14px;">
+ <span>杩涘害: {{ taskProgress.processedCount || 0 }} / {{ taskProgress.totalCount || 0 }}</span>
+ <span style="margin-left: 20px;">鎴愬姛: {{ taskProgress.successCount || 0 }}</span>
+ <span style="margin-left: 20px;">澶辫触: {{ taskProgress.failedCount || 0 }}</span>
+ </div>
+ </div>
+
+ <div v-if="generateResult" style="margin-top: 15px;">
+ <el-result
+ :icon="generateResult.success ? 'success' : 'error'"
+ :title="generateResult.title"
+ :subTitle="generateResult.message">
+ </el-result>
+ </div>
+ </el-col>
+ </el-row>
+
+ <!-- 鍒嗚瘝娴嬭瘯鍖哄煙 -->
+ <el-divider content-position="left">
+ <i class="el-icon-search"></i> 鍖婚櫌鍖归厤娴嬭瘯
+ </el-divider>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-alert
+ title="娴嬭瘯璇存槑"
+ type="success"
+ :closable="false"
+ style="margin-bottom: 15px;">
+ <template slot>
+ 杈撳叆鍖婚櫌鍚嶇О銆佸湴鍧�鎴栧叧閿瘝锛岀郴缁熷皢鑷姩杩涜鍒嗚瘝骞跺尮閰嶇浉浼肩殑鍖婚櫌銆傚尮閰嶇粨鏋滄寜鐩稿叧搴︽帓搴忋��
+ </template>
+ </el-alert>
+
+ <el-form :model="searchForm" label-width="100px">
+ <el-form-item label="鎼滅储鏂囨湰">
+ <el-input
+ v-model="searchForm.searchText"
+ placeholder="璇疯緭鍏ュ尰闄㈠悕绉般�佸湴鍧�鎴栧叧閿瘝锛屼緥濡傦細鍖椾含鍗忓拰鍖婚櫌銆佷笂娴风憺閲�"
+ clearable
+ @keyup.enter.native="handleSearch"
+ style="width: 60%;">
+ <el-button
+ slot="append"
+ icon="el-icon-search"
+ @click="handleSearch"
+ :loading="searching">
+ 鎼滅储
+ </el-button>
+ </el-input>
+
+ <el-input-number
+ v-model="searchForm.pageSize"
+ :min="5"
+ :max="100"
+ :step="5"
+ controls-position="right"
+ style="width: 150px; margin-left: 10px;">
+ </el-input-number>
+ <span style="margin-left: 5px; color: #909399;">鏉$粨鏋�</span>
+ </el-form-item>
+
+ <el-form-item label="鍒嗚瘝缁撴灉" v-if="tokenizedKeywords">
+ <el-tag
+ v-for="(keyword, index) in tokenizedKeywords.split(',')"
+ :key="index"
+ type="success"
+ size="small"
+ style="margin-right: 5px; margin-bottom: 5px;">
+ {{ keyword }}
+ </el-tag>
+ </el-form-item>
+ </el-form>
+
+ <!-- 鎼滅储缁撴灉琛ㄦ牸 -->
+ <el-table
+ v-loading="searching"
+ :data="searchResults"
+ border
+ stripe
+ style="width: 100%; margin-top: 10px;"
+ :height="400"
+ v-if="searchResults.length > 0">
+ <el-table-column type="index" label="鎺掑悕" width="60" align="center" />
+ <el-table-column prop="matchScore" label="鍖归厤鍒嗘暟" width="100" align="center" sortable>
+ <template slot-scope="scope">
+ <el-tag :type="getScoreType(scope.row.matchScore)" size="medium">
+ {{ scope.row.matchScore }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="hospital.hospId" label="鍖婚櫌ID" width="80" align="center" />
+ <el-table-column prop="hospital.hospName" label="鍖婚櫌鍚嶇О" min-width="180" show-overflow-tooltip />
+ <el-table-column prop="hospital.hospShort" label="绠�绉�" width="120" show-overflow-tooltip />
+ <el-table-column label="鍦板潃" min-width="220" show-overflow-tooltip>
+ <template slot-scope="scope">
+ {{ formatAddress(scope.row.hospital) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="hospital.hospTel" label="鐢佃瘽" width="130" />
+ <el-table-column label="鐘舵��" width="80" align="center">
+ <template slot-scope="scope">
+ <el-tag v-if="scope.row.hospital.hospState === 1" type="success" size="small">姝e父</el-tag>
+ <el-tag v-else type="info" size="small">鏈煡</el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鏃犵粨鏋滄彁绀� -->
+ <el-empty
+ v-if="searched && searchResults.length === 0"
+ description="鏈壘鍒板尮閰嶇殑鍖婚櫌"
+ :image-size="100">
+ </el-empty>
+
+ <!-- 缁熻淇℃伅 -->
+ <div v-if="searchResults.length > 0" style="margin-top: 10px; color: #909399;">
+ <i class="el-icon-info"></i>
+ 鍏辨壘鍒� <span style="color: #409EFF; font-weight: bold;">{{ searchResults.length }}</span> 涓尮閰嶇殑鍖婚櫌
+ </div>
+ </el-col>
+ </el-row>
+ </el-card>
+ </div>
+</template>
+
+<script>
+import request from '@/utils/request'
+
+export default {
+ name: 'HospitalTokenizer',
+ data() {
+ return {
+ // 鎵归噺鐢熸垚鐘舵��
+ generating: false,
+ generateResult: null,
+ currentTaskId: null,
+ taskProgress: null,
+ progressTimer: null,
+
+ // 鎼滅储琛ㄥ崟
+ searchForm: {
+ searchText: '',
+ pageSize: 30
+ },
+
+ // 鎼滅储鐘舵��
+ searching: false,
+ searched: false,
+ tokenizedKeywords: '',
+ searchResults: []
+ }
+ },
+ methods: {
+ /** 鎵归噺鐢熸垚鍒嗚瘝 */
+ handleGenerateKeywords() {
+ this.$confirm('纭瑕佷负鎵�鏈夊尰闄㈢敓鎴愬垎璇嶅悧锛熻繖鍙兘闇�瑕佸嚑鍒嗛挓鏃堕棿銆�', '纭鎿嶄綔', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ this.generating = true
+ this.generateResult = null
+ this.taskProgress = null
+
+ request({
+ url: '/system/hospital/generateKeywords',
+ method: 'get'
+ }).then(response => {
+ if (response.code === 200) {
+ // 鑾峰彇浠诲姟ID
+ this.currentTaskId = response.data.taskId
+ this.$message.success('鍒嗚瘝浠诲姟宸插惎鍔紝姝e湪鍚庡彴鎵ц...')
+
+ // 寮�濮嬭疆璇换鍔¤繘搴�
+ this.startProgressPolling()
+ } else {
+ this.generating = false
+ this.$message.error(response.msg || '浠诲姟鍚姩澶辫触')
+ }
+ }).catch(error => {
+ this.generating = false
+ this.$message.error('缃戠粶璇锋眰澶辫触')
+ console.error('鐢熸垚鍒嗚瘝澶辫触:', error)
+ })
+ }).catch(() => {
+ this.$message.info('宸插彇娑堟搷浣�')
+ })
+ },
+
+ /** 寮�濮嬭疆璇换鍔¤繘搴� */
+ startProgressPolling() {
+ this.queryTaskProgress()
+
+ // 姣�2绉掕疆璇竴娆�
+ this.progressTimer = setInterval(() => {
+ this.queryTaskProgress()
+ }, 2000)
+ },
+
+ /** 鏌ヨ浠诲姟杩涘害 */
+ queryTaskProgress() {
+ if (!this.currentTaskId) {
+ return
+ }
+
+ request({
+ url: '/system/hospital/getTaskProgress',
+ method: 'get',
+ params: {
+ taskId: this.currentTaskId
+ }
+ }).then(response => {
+ if (response.code === 200) {
+ this.taskProgress = response.data
+
+ // 鍒ゆ柇浠诲姟鏄惁瀹屾垚
+ if (this.taskProgress.status === 'SUCCESS') {
+ this.stopProgressPolling()
+ this.generating = false
+ this.generateResult = {
+ success: true,
+ title: '鐢熸垚鎴愬姛',
+ message: `鍏卞鐞� ${this.taskProgress.totalCount} 涓尯闄紝鎴愬姛 ${this.taskProgress.successCount} 涓紝澶辫触 ${this.taskProgress.failedCount} 涓猔
+ }
+ this.$message.success('鍖婚櫌鍒嗚瘝鐢熸垚瀹屾垚锛�')
+ } else if (this.taskProgress.status === 'FAILED') {
+ this.stopProgressPolling()
+ this.generating = false
+ this.generateResult = {
+ success: false,
+ title: '鐢熸垚澶辫触',
+ message: this.taskProgress.errorMessage || '浠诲姟鎵ц澶辫触'
+ }
+ this.$message.error('鍖婚櫌鍒嗚瘝鐢熸垚澶辫触锛�')
+ }
+ } else {
+ // 浠诲姟涓嶅瓨鍦ㄦ垨宸茶繃鏈�
+ this.stopProgressPolling()
+ this.generating = false
+ }
+ }).catch(error => {
+ console.error('鏌ヨ杩涘害澶辫触:', error)
+ })
+ },
+
+ /** 鍋滄杞 */
+ stopProgressPolling() {
+ if (this.progressTimer) {
+ clearInterval(this.progressTimer)
+ this.progressTimer = null
+ }
+ },
+
+ /** 閲嶇疆鐘舵�� */
+ resetStatus() {
+ this.generateResult = null
+ this.taskProgress = null
+ this.currentTaskId = null
+ this.stopProgressPolling()
+ },
+
+ /** 鎼滅储鍖婚櫌 */
+ handleSearch() {
+ if (!this.searchForm.searchText.trim()) {
+ this.$message.warning('璇疯緭鍏ユ悳绱㈡枃鏈�')
+ return
+ }
+
+ this.searching = true
+ this.searched = false
+ this.tokenizedKeywords = ''
+ this.searchResults = []
+
+ request({
+ url: '/system/hospital/searchByKeywords',
+ method: 'get',
+ params: {
+ searchText: this.searchForm.searchText,
+ pageSize: this.searchForm.pageSize
+ }
+ }).then(response => {
+ this.searching = false
+ this.searched = true
+
+ if (response.code === 200) {
+ this.searchResults = response.data || []
+
+ // 鎻愬彇鍒嗚瘝缁撴灉锛堜粠鏃ュ織涓級
+ if (this.searchResults.length > 0) {
+ this.$message.success(`鎵惧埌 ${this.searchResults.length} 涓尮閰嶇殑鍖婚櫌`)
+ // 妯℃嫙鏄剧ず鍒嗚瘝缁撴灉
+ this.tokenizedKeywords = this.generateMockKeywords(this.searchForm.searchText)
+ } else {
+ this.$message.info('鏈壘鍒板尮閰嶇殑鍖婚櫌锛岃灏濊瘯鍏朵粬鍏抽敭璇�')
+ }
+ } else {
+ this.$message.error(response.msg || '鎼滅储澶辫触')
+ }
+ }).catch(error => {
+ this.searching = false
+ this.searched = true
+ this.$message.error('鎼滅储澶辫触锛�' + (error.message || '缃戠粶閿欒'))
+ console.error('鎼滅储澶辫触:', error)
+ })
+ },
+
+ /** 鏍煎紡鍖栧湴鍧� */
+ formatAddress(row) {
+ const parts = []
+ if (row.hopsProvince) parts.push(row.hopsProvince)
+ if (row.hopsCity) parts.push(row.hopsCity)
+ if (row.hopsArea) parts.push(row.hopsArea)
+ if (row.hospAddress) parts.push(row.hospAddress)
+ return parts.join(' ')
+ },
+
+ /** 鑾峰彇鍒嗘暟鏍囩绫诲瀷 */
+ getScoreType(score) {
+ if (score >= 10) return 'danger' // 楂樺尮閰� - 绾㈣壊
+ if (score >= 5) return 'warning' // 涓尮閰� - 姗欒壊
+ if (score >= 3) return 'success' // 浣庡尮閰� - 缁胯壊
+ return '' // 鏋佷綆鍖归厤 - 榛樿
+ },
+
+ /** 鐢熸垚妯℃嫙鍒嗚瘝缁撴灉锛堢敤浜庡睍绀猴級 */
+ generateMockKeywords(text) {
+ // 杩欓噷绠�鍗曟ā鎷燂紝瀹為檯鍒嗚瘝鍦ㄥ悗绔畬鎴�
+ const keywords = []
+ for (let i = 0; i < text.length; i++) {
+ for (let len = 2; len <= Math.min(4, text.length - i); len++) {
+ keywords.push(text.substr(i, len))
+ }
+ }
+ return keywords.slice(0, 15).join(',')
+ }
+ },
+
+ beforeDestroy() {
+ // 缁勪欢閿�姣佹椂鍋滄杞
+ this.stopProgressPolling()
+ }
+}
+</script>
+
+<style scoped>
+.box-card {
+ margin: 20px;
+}
+
+.clearfix:before,
+.clearfix:after {
+ display: table;
+ content: "";
+}
+
+.clearfix:after {
+ clear: both;
+}
+
+.el-divider {
+ margin: 30px 0 20px 0;
+}
+
+.el-divider__text {
+ font-weight: bold;
+ font-size: 14px;
+}
+</style>
diff --git a/ruoyi-ui/src/views/system/ocr/config.vue b/ruoyi-ui/src/views/system/ocr/config.vue
new file mode 100644
index 0000000..6b5fe9e
--- /dev/null
+++ b/ruoyi-ui/src/views/system/ocr/config.vue
@@ -0,0 +1,277 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <div slot="header" class="clearfix">
+ <span>OCR鏈嶅姟閰嶇疆</span>
+ </div>
+
+ <el-tabs v-model="activeTab">
+ <el-tab-pane label="闃块噷浜慜CR閰嶇疆" name="ali">
+ <el-alert
+ title="闃块噷浜慜CR閰嶇疆璇存槑"
+ type="info"
+ :closable="false"
+ style="margin-bottom: 20px"
+ >
+ <div>
+ 閰嶇疆闃块噷浜慜CR鏈嶅姟鐨凙ccessKey淇℃伅銆傞厤缃悗閲嶅惎搴旂敤鐢熸晥銆�<br/>
+ <strong>娉ㄦ剰锛�</strong>璇峰Ε鍠勪繚绠ccessKey锛岄伩鍏嶆硠闇�
+ </div>
+ </el-alert>
+
+ <el-form
+ :model="aliForm"
+ :rules="rules"
+ ref="aliForm"
+ label-width="120px"
+ style="max-width: 600px;"
+ >
+ <el-form-item label="AccessKey ID" prop="accessKeyId">
+ <el-input
+ v-model="aliForm.accessKeyId"
+ placeholder="璇疯緭鍏ラ樋閲屼簯AccessKey ID"
+ show-password
+ :disabled="!canEditAli"
+ />
+ </el-form-item>
+
+ <el-form-item label="AccessKey Secret" prop="accessKeySecret">
+ <el-input
+ v-model="aliForm.accessKeySecret"
+ placeholder="璇疯緭鍏ラ樋閲屼簯AccessKey Secret"
+ show-password
+ :disabled="!canEditAli"
+ />
+ </el-form-item>
+
+ <el-form-item label="OCR鏈嶅姟绔偣">
+ <el-input
+ v-model="aliOcrEndpoint"
+ :disabled="true"
+ placeholder="闃块噷浜慜CR鏈嶅姟绔偣"
+ />
+ <div style="margin-top: 5px; color: #909399; font-size: 12px;">
+ 榛樿绔偣锛歿{ aliOcrEndpoint }}
+ </div>
+ </el-form-item>
+
+ <el-form-item>
+ <el-button
+ v-if="!canEditAli"
+ type="primary"
+ @click="handleEditAli"
+ >
+ <i class="el-icon-edit"></i> 缂栬緫閰嶇疆
+ </el-button>
+ <el-button
+ v-if="canEditAli"
+ type="success"
+ @click="handleSaveAli"
+ >
+ <i class="el-icon-check"></i> 淇濆瓨閰嶇疆
+ </el-button>
+ <el-button
+ v-if="canEditAli"
+ @click="handleCancelAli"
+ >
+ <i class="el-icon-close"></i> 鍙栨秷
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </el-tab-pane>
+
+ <el-tab-pane label="鐧惧害OCR閰嶇疆" name="baidu">
+ <el-alert
+ title="鐧惧害OCR閰嶇疆璇存槑"
+ type="info"
+ :closable="false"
+ style="margin-bottom: 20px"
+ >
+ <div>
+ 閰嶇疆鐧惧害AI寮�鏀惧钩鍙癘CR鏈嶅姟鐨凙ppID銆丄PI Key鍜孲ecret Key淇℃伅銆�<br/>
+ <strong>娉ㄦ剰锛�</strong>璇峰Ε鍠勪繚绠″瘑閽ヤ俊鎭紝閬垮厤娉勯湶
+ </div>
+ </el-alert>
+
+ <el-form
+ :model="baiduForm"
+ :rules="baiduRules"
+ ref="baiduForm"
+ label-width="120px"
+ style="max-width: 600px;"
+ >
+ <el-form-item label="App ID" prop="appId">
+ <el-input
+ v-model="baiduForm.appId"
+ placeholder="璇疯緭鍏ョ櫨搴CR App ID"
+ :disabled="!canEditBaidu"
+ />
+ </el-form-item>
+
+ <el-form-item label="API Key" prop="apiKey">
+ <el-input
+ v-model="baiduForm.apiKey"
+ placeholder="璇疯緭鍏ョ櫨搴CR API Key"
+ show-password
+ :disabled="!canEditBaidu"
+ />
+ </el-form-item>
+
+ <el-form-item label="Secret Key" prop="secretKey">
+ <el-input
+ v-model="baiduForm.secretKey"
+ placeholder="璇疯緭鍏ョ櫨搴CR Secret Key"
+ show-password
+ :disabled="!canEditBaidu"
+ />
+ </el-form-item>
+
+ <el-form-item>
+ <el-button
+ v-if="!canEditBaidu"
+ type="primary"
+ @click="handleEditBaidu"
+ >
+ <i class="el-icon-edit"></i> 缂栬緫閰嶇疆
+ </el-button>
+ <el-button
+ v-if="canEditBaidu"
+ type="success"
+ @click="handleSaveBaidu"
+ >
+ <i class="el-icon-check"></i> 淇濆瓨閰嶇疆
+ </el-button>
+ <el-button
+ v-if="canEditBaidu"
+ @click="handleCancelBaidu"
+ >
+ <i class="el-icon-close"></i> 鍙栨秷
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </el-tab-pane>
+ </el-tabs>
+ </el-card>
+ </div>
+</template>
+
+<script>
+export default {
+ name: "OCRConfig",
+ data() {
+ return {
+ activeTab: 'ali',
+ // 闃块噷浜慜CR閰嶇疆
+ canEditAli: false,
+ aliForm: {
+ accessKeyId: '',
+ accessKeySecret: ''
+ },
+ aliOcrEndpoint: 'ocr-api.cn-hangzhou.aliyuncs.com',
+ // 鐧惧害OCR閰嶇疆
+ canEditBaidu: false,
+ baiduForm: {
+ appId: '',
+ apiKey: '',
+ secretKey: ''
+ },
+ rules: {
+ accessKeyId: [
+ { required: true, message: '璇疯緭鍏ccessKey ID', trigger: 'blur' },
+ { min: 10, max: 64, message: '闀垮害鍦�10鍒�64涓瓧绗�', trigger: 'blur' }
+ ],
+ accessKeySecret: [
+ { required: true, message: '璇疯緭鍏ccessKey Secret', trigger: 'blur' },
+ { min: 10, max: 64, message: '闀垮害鍦�10鍒�64涓瓧绗�', trigger: 'blur' }
+ ]
+ },
+ baiduRules: {
+ appId: [
+ { required: true, message: '璇疯緭鍏pp ID', trigger: 'blur' },
+ { min: 5, max: 32, message: '闀垮害鍦�5鍒�32涓瓧绗�', trigger: 'blur' }
+ ],
+ apiKey: [
+ { required: true, message: '璇疯緭鍏PI Key', trigger: 'blur' },
+ { min: 10, max: 64, message: '闀垮害鍦�10鍒�64涓瓧绗�', trigger: 'blur' }
+ ],
+ secretKey: [
+ { required: true, message: '璇疯緭鍏ecret Key', trigger: 'blur' },
+ { min: 10, max: 64, message: '闀垮害鍦�10鍒�64涓瓧绗�', trigger: 'blur' }
+ ]
+ }
+ };
+ },
+ methods: {
+ /** 缂栬緫闃块噷浜慜CR閰嶇疆 */
+ handleEditAli() {
+ this.canEditAli = true;
+ },
+
+ /** 淇濆瓨闃块噷浜慜CR閰嶇疆 */
+ handleSaveAli() {
+ this.$refs.aliForm.validate(valid => {
+ if (valid) {
+ this.$confirm('淇濆瓨闃块噷浜慜CR閰嶇疆鍚庨渶瑕侀噸鍚簲鐢ㄦ墠鑳界敓鏁堬紝鏄惁缁х画锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ // 杩欓噷搴旇璋冪敤鍚庣API淇濆瓨閰嶇疆
+ this.$modal.msgSuccess('闃块噷浜慜CR閰嶇疆淇濆瓨鎴愬姛锛岃閲嶅惎搴旂敤浣块厤缃敓鏁�');
+ this.canEditAli = false;
+ });
+ }
+ });
+ },
+
+ /** 鍙栨秷闃块噷浜慜CR缂栬緫 */
+ handleCancelAli() {
+ this.canEditAli = false;
+ // 鎭㈠鍘熷鍊硷紙杩欓噷搴旇浠庡悗绔幏鍙栵級
+ this.aliForm = {
+ accessKeyId: '',
+ accessKeySecret: ''
+ };
+ },
+
+ /** 缂栬緫鐧惧害OCR閰嶇疆 */
+ handleEditBaidu() {
+ this.canEditBaidu = true;
+ },
+
+ /** 淇濆瓨鐧惧害OCR閰嶇疆 */
+ handleSaveBaidu() {
+ this.$refs.baiduForm.validate(valid => {
+ if (valid) {
+ this.$confirm('淇濆瓨鐧惧害OCR閰嶇疆鍚庨渶瑕侀噸鍚簲鐢ㄦ墠鑳界敓鏁堬紝鏄惁缁х画锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ // 杩欓噷搴旇璋冪敤鍚庣API淇濆瓨閰嶇疆
+ this.$modal.msgSuccess('鐧惧害OCR閰嶇疆淇濆瓨鎴愬姛锛岃閲嶅惎搴旂敤浣块厤缃敓鏁�');
+ this.canEditBaidu = false;
+ });
+ }
+ });
+ },
+
+ /** 鍙栨秷鐧惧害OCR缂栬緫 */
+ handleCancelBaidu() {
+ this.canEditBaidu = false;
+ // 鎭㈠鍘熷鍊硷紙杩欓噷搴旇浠庡悗绔幏鍙栵級
+ this.baiduForm = {
+ appId: '',
+ apiKey: '',
+ secretKey: ''
+ };
+ }
+ }
+};
+</script>
+
+<style scoped>
+.box-card {
+ margin-bottom: 20px;
+}
+</style>
\ No newline at end of file
diff --git a/ruoyi-ui/src/views/system/ocr/index.vue b/ruoyi-ui/src/views/system/ocr/index.vue
new file mode 100644
index 0000000..5a590ef
--- /dev/null
+++ b/ruoyi-ui/src/views/system/ocr/index.vue
@@ -0,0 +1,386 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <div slot="header" class="clearfix">
+ <span>OCR鍥惧儚璇嗗埆娴嬭瘯</span>
+ </div>
+
+ <!-- 鍔熻兘璇存槑 -->
+ <el-alert
+ title="鍔熻兘璇存槑"
+ type="info"
+ :closable="false"
+ style="margin-bottom: 20px"
+ >
+ <div>
+ 鏈〉闈㈢敤浜庢祴璇曢樋閲屼簯OCR銆佺櫨搴CR鍜岃吘璁簯OCR鍥惧儚璇嗗埆鍔熻兘銆傛敮鎸侀�氱敤鏂囧瓧璇嗗埆銆佸彂绁ㄨ瘑鍒�佽韩浠借瘉璇嗗埆銆佹墜鍐欎綋璇嗗埆绛夈��<br/>
+ <strong>鏀寔鏍煎紡锛�</strong>JPG銆丳NG銆丅MP绛夊父瑙佸浘鐗囨牸寮忥紱<strong>鏂囦欢澶у皬锛�</strong>涓嶈秴杩�4MB
+ </div>
+ </el-alert>
+
+ <!-- 璇嗗埆绫诲瀷閫夋嫨 -->
+ <el-form :inline="true" style="margin-bottom: 20px">
+ <el-form-item label="璇嗗埆绫诲瀷">
+ <el-select v-model="recognizeType" placeholder="璇烽�夋嫨璇嗗埆绫诲瀷" style="width: 200px">
+ <el-option label="閫氱敤鏂囧瓧璇嗗埆" value="General" />
+ <el-option label="鍙戠エ璇嗗埆" value="Invoice" />
+ <el-option label="韬唤璇佽瘑鍒�" value="IdCard" />
+ <el-option label="鎵嬪啓浣撹瘑鍒�" value="HandWriting" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="OCR鏈嶅姟">
+ <el-select v-model="provider" placeholder="璇烽�夋嫨OCR鏈嶅姟鎻愪緵鍟�" style="width: 200px">
+ <el-option label="闃块噷浜慜CR" value="ali" />
+ <el-option label="鐧惧害OCR" value="baidu" />
+ <el-option label="鑵捐浜慜CR" value="tencent" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎻愬彇瀛楁">
+ <el-input
+ v-if="provider === 'tencent' && recognizeType === 'HandWriting'"
+ v-model="itemNames"
+ placeholder="璇疯緭鍏ラ渶瑕佹彁鍙栫殑瀛楁锛岀敤閫楀彿鍒嗛殧"
+ style="width: 200px"
+ clearable
+ />
+ </el-form-item>
+ </el-form>
+
+ <!-- 鍥剧墖涓婁紶鍖哄煙 -->
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-card shadow="hover">
+ <div slot="header">
+ <span>鍥剧墖涓婁紶</span>
+ </div>
+
+ <el-upload
+ ref="upload"
+ class="upload-demo"
+ drag
+ action="#"
+ :auto-upload="false"
+ :on-change="handleFileChange"
+ :limit="1"
+ :file-list="fileList"
+ accept="image/*"
+ >
+ <i class="el-icon-upload"></i>
+ <div class="el-upload__text">灏嗗浘鐗囨嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <div class="el-upload__tip" slot="tip">
+ 鏀寔JPG銆丳NG銆丅MP鏍煎紡锛屾枃浠朵笉瓒呰繃4MB
+ </div>
+ </el-upload>
+
+ <div style="margin-top: 20px; text-align: center">
+ <el-button
+ type="primary"
+ :loading="recognizing"
+ :disabled="!fileList.length"
+ @click="handleRecognize"
+ >
+ <i class="el-icon-view"></i> 寮�濮嬭瘑鍒�
+ </el-button>
+ <el-button @click="handleClear">
+ <i class="el-icon-delete"></i> 娓呯┖
+ </el-button>
+ </div>
+
+ <!-- 鍥剧墖棰勮 -->
+ <div v-if="imagePreview" style="margin-top: 20px">
+ <el-divider>鍥剧墖棰勮</el-divider>
+ <div style="text-align: center">
+ <el-image
+ :src="imagePreview"
+ fit="contain"
+ style="max-width: 100%; max-height: 400px"
+ :preview-src-list="[imagePreview]"
+ />
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+
+ <!-- 璇嗗埆缁撴灉鍖哄煙 -->
+ <el-col :span="12">
+ <el-card shadow="hover" style="min-height: 500px">
+ <div slot="header">
+ <span>璇嗗埆缁撴灉</span>
+ <el-button
+ v-if="ocrResult"
+ style="float: right; padding: 3px 0"
+ type="text"
+ @click="handleCopyResult"
+ >
+ <i class="el-icon-document-copy"></i> 澶嶅埗缁撴灉
+ </el-button>
+ </div>
+
+ <!-- 鍔犺浇涓� -->
+ <div v-if="recognizing" style="text-align: center; padding: 50px 0">
+ <i class="el-icon-loading" style="font-size: 40px; color: #409EFF"></i>
+ <p style="margin-top: 20px; color: #909399">姝e湪璇嗗埆涓紝璇风◢鍊�...({{ provider === 'ali' ? '闃块噷浜�' : provider === 'baidu' ? '鐧惧害' : '鑵捐浜�' }})</p>
+ </div>
+
+ <!-- 璇嗗埆鎴愬姛 -->
+ <div v-else-if="ocrResult && ocrResult.success">
+ <el-alert
+ title="璇嗗埆鎴愬姛"
+ type="success"
+ :closable="false"
+ style="margin-bottom: 15px"
+ >
+ <div>鏈嶅姟鎻愪緵鍟�: {{ provider === 'ali' ? '闃块噷浜慜CR' : provider === 'baidu' ? '鐧惧害OCR' : '鑵捐浜慜CR' }}</div>
+ </el-alert>
+
+ <!-- 鎻愬彇鐨勫瓧娈典俊鎭� -->
+ <div v-if="extractedFields && Object.keys(extractedFields).length > 0">
+ <el-descriptions title="鎻愬彇瀛楁" :column="1" border>
+ <el-descriptions-item
+ v-for="(value, key) in extractedFields"
+ :key="key"
+ :label="getFieldLabel(key)"
+ >
+ {{ value }}
+ </el-descriptions-item>
+ </el-descriptions>
+ <el-divider />
+ </div>
+
+ <!-- 瀹屾暣璇嗗埆鍐呭 -->
+ <div>
+ <h4>瀹屾暣璇嗗埆鍐呭锛�</h4>
+ <el-input
+ type="textarea"
+ :value="getFullContent(ocrResult)"
+ :autosize="{ minRows: 10, maxRows: 20 }"
+ readonly
+ style="margin-top: 10px"
+ />
+ </div>
+
+ <!-- 鍘熷JSON鏁版嵁 -->
+ <el-collapse style="margin-top: 20px">
+ <el-collapse-item title="鏌ョ湅鍘熷JSON鏁版嵁" name="json">
+ <pre style="background: #f5f7fa; padding: 15px; border-radius: 4px; max-height: 400px; overflow: auto">{{ JSON.stringify(ocrResult, null, 2) }}</pre>
+ </el-collapse-item>
+ </el-collapse>
+ </div>
+
+ <!-- 璇嗗埆澶辫触 -->
+ <div v-else-if="ocrResult && !ocrResult.success">
+ <el-alert
+ title="璇嗗埆澶辫触"
+ :description="ocrResult.error || '鏈煡閿欒'"
+ type="error"
+ :closable="false"
+ >
+ <div>鏈嶅姟鎻愪緵鍟�: {{ provider === 'ali' ? '闃块噷浜慜CR' : provider === 'baidu' ? '鐧惧害OCR' : '鑵捐浜慜CR' }}</div>
+ </el-alert>
+ <div v-if="ocrResult.detail" style="margin-top: 15px">
+ <el-tag type="warning">{{ ocrResult.detail }}</el-tag>
+ </div>
+ </div>
+
+ <!-- 鏈紑濮嬭瘑鍒� -->
+ <div v-else style="text-align: center; padding: 50px 0; color: #909399">
+ <i class="el-icon-picture-outline" style="font-size: 60px"></i>
+ <p style="margin-top: 20px">璇蜂笂浼犲浘鐗囧苟鐐瑰嚮"寮�濮嬭瘑鍒�"鎸夐挳</p>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </el-card>
+ </div>
+</template>
+
+<script>
+import { recognizeImage, extractFields } from "@/api/system/ocr";
+
+export default {
+ name: "OCRTest",
+ data() {
+ return {
+ // 璇嗗埆绫诲瀷
+ recognizeType: "General",
+ // OCR鏈嶅姟鎻愪緵鍟�
+ provider: "ali",
+ // 鑵捐浜慜CR鎻愬彇瀛楁
+ itemNames: "鎮h�呭鍚�,鎬у埆,骞撮緞,韬唤璇佸彿,璇婃柇,闇�鏀粯杞繍璐圭敤,琛岀▼,寮�濮嬫椂闂�,缁撴潫鏃堕棿,瀹跺睘绛惧悕",
+ // 鏂囦欢鍒楄〃
+ fileList: [],
+ // 褰撳墠鏂囦欢
+ currentFile: null,
+ // 鍥剧墖棰勮
+ imagePreview: null,
+ // 璇嗗埆涓�
+ recognizing: false,
+ // OCR璇嗗埆缁撴灉
+ ocrResult: null,
+ // 鎻愬彇鐨勫瓧娈�
+ extractedFields: null
+ };
+ },
+ methods: {
+ /** 鏂囦欢閫夋嫨鍙樺寲 */
+ handleFileChange(file, fileList) {
+ // 闄愬埗鍙兘涓婁紶涓�涓枃浠�
+ if (fileList.length > 1) {
+ fileList.splice(0, 1);
+ }
+
+ this.fileList = fileList;
+ this.currentFile = file.raw;
+
+ // 鐢熸垚鍥剧墖棰勮
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ this.imagePreview = e.target.result;
+ };
+ reader.readAsDataURL(file.raw);
+
+ // 娓呯┖涔嬪墠鐨勮瘑鍒粨鏋�
+ this.ocrResult = null;
+ this.extractedFields = null;
+ },
+
+ /** 寮�濮嬭瘑鍒� */
+ handleRecognize() {
+ if (!this.currentFile) {
+ this.$modal.msgWarning("璇峰厛涓婁紶鍥剧墖");
+ return;
+ }
+
+ // 妫�鏌ユ枃浠跺ぇ灏忥紙4MB锛�
+ const maxSize = 4 * 1024 * 1024;
+ if (this.currentFile.size > maxSize) {
+ this.$modal.msgError("鍥剧墖澶у皬涓嶈兘瓒呰繃4MB");
+ return;
+ }
+
+ this.recognizing = true;
+ this.ocrResult = null;
+ this.extractedFields = null;
+
+ // 鏋勫缓FormData
+ const formData = new FormData();
+ formData.append("file", this.currentFile);
+ formData.append("type", this.recognizeType);
+ formData.append("provider", this.provider);
+
+ // 濡傛灉鏄吘璁簯OCR鎵嬪啓浣撹瘑鍒紝鍒欐坊鍔爄temNames鍙傛暟
+ if (this.provider === 'tencent' && this.recognizeType === 'HandWriting' && this.itemNames) {
+ const itemNamesArray = this.itemNames.split(',').map(item => item.trim()).filter(item => item);
+ itemNamesArray.forEach((itemName, index) => {
+ formData.append(`itemNames[${index}]`, itemName);
+ });
+ }
+
+ // 璋冪敤OCR璇嗗埆鎺ュ彛
+ recognizeImage(formData).then(response => {
+ this.ocrResult = response.data.ocrResult;
+
+ // 鑷姩鎻愬彇瀛楁
+ if (this.ocrResult.success) {
+ extractFields(this.ocrResult).then(res => {
+ this.extractedFields = res.data;
+ }).catch(() => {
+ // 鎻愬彇澶辫触涓嶅奖鍝嶄富娴佺▼
+ });
+ }
+
+ this.recognizing = false;
+ }).catch(error => {
+ this.$modal.msgError("OCR璇嗗埆澶辫触: " + (error.message || "鏈煡閿欒"));
+ this.recognizing = false;
+ });
+ },
+
+ /** 娓呯┖ */
+ handleClear() {
+ this.fileList = [];
+ this.currentFile = null;
+ this.imagePreview = null;
+ this.ocrResult = null;
+ this.extractedFields = null;
+ this.$refs.upload.clearFiles();
+ },
+
+ /** 澶嶅埗璇嗗埆缁撴灉 */
+ handleCopyResult() {
+ const content = this.getFullContent(this.ocrResult);
+ this.copyToClipboard(content);
+ this.$modal.msgSuccess("澶嶅埗鎴愬姛");
+ },
+
+ /** 澶嶅埗鍒板壀璐存澘 */
+ copyToClipboard(text) {
+ const textarea = document.createElement("textarea");
+ textarea.value = text;
+ document.body.appendChild(textarea);
+ textarea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textarea);
+ },
+
+ /** 鑾峰彇瀹屾暣璇嗗埆鍐呭 */
+ getFullContent(ocrResult) {
+ if (!ocrResult) return "";
+
+ if (ocrResult.content) {
+ return ocrResult.content;
+ }
+
+ // 濡傛灉鏈塸rism_wordsInfo锛屾嫾鎺ユ墍鏈夋枃瀛�
+ if (ocrResult.prism_wordsInfo && Array.isArray(ocrResult.prism_wordsInfo)) {
+ return ocrResult.prism_wordsInfo.map(item => item.word).join("\n");
+ }
+
+ return JSON.stringify(ocrResult, null, 2);
+ },
+
+ /** 鑾峰彇瀛楁鏍囩 */
+ getFieldLabel(key) {
+ const labelMap = {
+ totalAmount: "鎬婚噾棰�",
+ date: "鏃ユ湡",
+ remark: "澶囨敞",
+ fullText: "鍏ㄦ枃",
+ error: "閿欒淇℃伅"
+ };
+ return labelMap[key] || key;
+ }
+ }
+};
+</script>
+
+<style scoped>
+.box-card {
+ margin-bottom: 20px;
+}
+
+.upload-demo {
+ width: 100%;
+}
+
+.el-upload-dragger {
+ width: 100%;
+}
+
+pre {
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 12px;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.el-descriptions {
+ margin-top: 10px;
+}
+
+h4 {
+ margin: 15px 0 10px 0;
+ color: #303133;
+}
+</style>
\ No newline at end of file
diff --git a/ruoyi-ui/src/views/system/vehicleAlert/index.vue b/ruoyi-ui/src/views/system/vehicleAlert/index.vue
new file mode 100644
index 0000000..e4024c2
--- /dev/null
+++ b/ruoyi-ui/src/views/system/vehicleAlert/index.vue
@@ -0,0 +1,528 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="88px">
+ <el-form-item label="杞︾墝鍙�" prop="vehicleNo">
+ <el-input
+ v-model="queryParams.vehicleNo"
+ placeholder="璇疯緭鍏ヨ溅鐗屽彿"
+ clearable
+ size="small"
+ @keyup.enter.native="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍛婅鏃ユ湡" prop="alertDate">
+ <el-date-picker
+ v-model="queryParams.alertDate"
+ type="date"
+ value-format="yyyy-MM-dd"
+ placeholder="璇烽�夋嫨鍛婅鏃ユ湡"
+ clearable
+ size="small"
+ />
+ </el-form-item>
+ <el-form-item label="鍛婅鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable size="small">
+ <el-option label="鏈鐞�" value="0" />
+ <el-option label="宸插鐞�" value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="褰掑睘閮ㄩ棬" prop="deptId">
+ <el-select v-model="queryParams.deptId" placeholder="璇烽�夋嫨閮ㄩ棬" clearable size="small">
+ <el-option
+ v-for="dept in deptList"
+ :key="dept.deptId"
+ :label="dept.deptName"
+ :value="dept.deptId"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍛婅鏃堕棿">
+ <el-date-picker
+ v-model="dateRange"
+ size="small"
+ style="width: 240px"
+ value-format="yyyy-MM-dd"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="el-icon-check"
+ size="mini"
+ :disabled="multiple"
+ @click="handleBatchProcess"
+ v-hasPermi="['system:vehicleAlert:handle']"
+ >鎵归噺澶勭悊</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="el-icon-delete"
+ size="mini"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['system:vehicleAlert:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="el-icon-download"
+ size="mini"
+ @click="handleExport"
+ v-hasPermi="['system:vehicleAlert:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="info"
+ plain
+ icon="el-icon-refresh"
+ size="mini"
+ @click="refreshUnhandledCount"
+ >鍒锋柊缁熻</el-button>
+ </el-col>
+ <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <!-- 缁熻鍗$墖 -->
+ <el-row :gutter="20" class="mb8">
+ <el-col :span="6">
+ <el-card shadow="hover">
+ <div style="text-align: center;">
+ <div style="font-size: 14px; color: #909399;">鏈鐞嗗憡璀�</div>
+ <div style="font-size: 28px; color: #F56C6C; font-weight: bold; margin-top: 10px;">
+ {{ unhandledCount }}
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card shadow="hover">
+ <div style="text-align: center;">
+ <div style="font-size: 14px; color: #909399;">浠婃棩鍛婅</div>
+ <div style="font-size: 28px; color: #E6A23C; font-weight: bold; margin-top: 10px;">
+ {{ todayCount }}
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card shadow="hover">
+ <div style="text-align: center;">
+ <div style="font-size: 14px; color: #909399;">绱鍛婅杞﹁締</div>
+ <div style="font-size: 28px; color: #409EFF; font-weight: bold; margin-top: 10px;">
+ {{ totalVehicles }}
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card shadow="hover">
+ <div style="text-align: center;">
+ <div style="font-size: 14px; color: #909399;">绱鍛婅娆℃暟</div>
+ <div style="font-size: 28px; color: #67C23A; font-weight: bold; margin-top: 10px;">
+ {{ total }}
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <el-table v-loading="loading" :data="alertList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="鍛婅ID" align="center" prop="alertId" width="80" />
+ <el-table-column label="杞︾墝鍙�" align="center" prop="vehicleNo" width="120">
+ <template slot-scope="scope">
+ <el-tag>{{ scope.row.vehicleNo }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍛婅鏃ユ湡" align="center" prop="alertDate" width="120" />
+ <el-table-column label="鍛婅鏃堕棿" align="center" prop="alertTime" width="180">
+ <template slot-scope="scope">
+ <span>{{ parseTime(scope.row.alertTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="杩愯閲岀▼(鍏噷)" align="center" prop="mileage" width="140">
+ <template slot-scope="scope">
+ <el-tag type="danger" v-if="scope.row.mileage >= 10">
+ {{ scope.row.mileage }} km
+ </el-tag>
+ <span v-else>{{ scope.row.mileage }} km</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="褰撴棩鍛婅娆℃暟" align="center" prop="alertCount" width="120">
+ <template slot-scope="scope">
+ <el-tag type="warning" v-if="scope.row.alertCount >= 3">
+ 绗瑊{ scope.row.alertCount }}娆�
+ </el-tag>
+ <span v-else>绗瑊{ scope.row.alertCount }}娆�</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="褰掑睘閮ㄩ棬" align="center" prop="deptName" width="150" :show-overflow-tooltip="true" />
+ <el-table-column label="鍛婅鐘舵��" align="center" prop="status" width="100">
+ <template slot-scope="scope">
+ <el-tag v-if="scope.row.status === '0'" type="danger">鏈鐞�</el-tag>
+ <el-tag v-else type="success">宸插鐞�</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="閫氱煡鐘舵��" align="center" prop="notifyStatus" width="100">
+ <template slot-scope="scope">
+ <el-tag v-if="scope.row.notifyStatus === '0'" type="info">鏈彂閫�</el-tag>
+ <el-tag v-else-if="scope.row.notifyStatus === '1'" type="success">宸插彂閫�</el-tag>
+ <el-tag v-else type="danger">鍙戦�佸け璐�</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶勭悊浜�" align="center" prop="handlerName" width="100" :show-overflow-tooltip="true" />
+ <el-table-column label="澶勭悊鏃堕棿" align="center" prop="handleTime" width="180">
+ <template slot-scope="scope">
+ <span v-if="scope.row.handleTime">{{ parseTime(scope.row.handleTime) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width" width="180" fixed="right">
+ <template slot-scope="scope">
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-view"
+ @click="handleView(scope.row)"
+ v-hasPermi="['system:vehicleAlert:query']"
+ >璇︽儏</el-button>
+ <el-button
+ v-if="scope.row.status === '0'"
+ size="mini"
+ type="text"
+ icon="el-icon-check"
+ @click="handleProcess(scope.row)"
+ v-hasPermi="['system:vehicleAlert:handle']"
+ >澶勭悊</el-button>
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-delete"
+ @click="handleDelete(scope.row)"
+ v-hasPermi="['system:vehicleAlert:remove']"
+ >鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total>0"
+ :total="total"
+ :page.sync="queryParams.pageNum"
+ :limit.sync="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 鍛婅璇︽儏瀵硅瘽妗� -->
+ <el-dialog title="鍛婅璇︽儏" :visible.sync="detailOpen" width="700px" append-to-body>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="鍛婅ID">{{ detail.alertId }}</el-descriptions-item>
+ <el-descriptions-item label="杞︾墝鍙�">
+ <el-tag>{{ detail.vehicleNo }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍛婅鏃ユ湡">{{ detail.alertDate }}</el-descriptions-item>
+ <el-descriptions-item label="鍛婅鏃堕棿">{{ parseTime(detail.alertTime) }}</el-descriptions-item>
+ <el-descriptions-item label="杩愯閲岀▼">
+ <span style="color: #F56C6C; font-weight: bold;">{{ detail.mileage }} 鍏噷</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="褰撴棩鍛婅娆℃暟">绗瑊{ detail.alertCount }}娆�</el-descriptions-item>
+ <el-descriptions-item label="褰掑睘閮ㄩ棬" :span="2">{{ detail.deptName }}</el-descriptions-item>
+ <el-descriptions-item label="鍛婅鐘舵��">
+ <el-tag v-if="detail.status === '0'" type="danger">鏈鐞�</el-tag>
+ <el-tag v-else type="success">宸插鐞�</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="閫氱煡鐘舵��">
+ <el-tag v-if="detail.notifyStatus === '0'" type="info">鏈彂閫�</el-tag>
+ <el-tag v-else-if="detail.notifyStatus === '1'" type="success">宸插彂閫�</el-tag>
+ <el-tag v-else type="danger">鍙戦�佸け璐�</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="閫氱煡鐢ㄦ埛" :span="2">{{ detail.notifyUsers || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="閫氱煡娑堟伅" :span="2">{{ detail.notifyMessage || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="澶勭悊浜�">{{ detail.handlerName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="澶勭悊鏃堕棿">{{ parseTime(detail.handleTime) || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="澶勭悊澶囨敞" :span="2">{{ detail.handleRemark || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿" :span="2">{{ parseTime(detail.createTime) }}</el-descriptions-item>
+ </el-descriptions>
+ <div slot="footer" class="dialog-footer">
+ <el-button @click="detailOpen = false">鍏� 闂�</el-button>
+ </div>
+ </el-dialog>
+
+ <!-- 澶勭悊鍛婅瀵硅瘽妗� -->
+ <el-dialog title="澶勭悊鍛婅" :visible.sync="processOpen" width="500px" append-to-body>
+ <el-form ref="processForm" :model="processForm" :rules="processRules" label-width="100px">
+ <el-form-item label="澶勭悊澶囨敞" prop="handleRemark">
+ <el-input
+ v-model="processForm.handleRemark"
+ type="textarea"
+ :rows="4"
+ placeholder="璇疯緭鍏ュ鐞嗗娉紙蹇呭~锛�"
+ />
+ </el-form-item>
+ </el-form>
+ <div slot="footer" class="dialog-footer">
+ <el-button type="primary" @click="submitProcess">纭� 瀹�</el-button>
+ <el-button @click="processOpen = false">鍙� 娑�</el-button>
+ </div>
+ </el-dialog>
+
+ <!-- 鎵归噺澶勭悊瀵硅瘽妗� -->
+ <el-dialog title="鎵归噺澶勭悊鍛婅" :visible.sync="batchProcessOpen" width="500px" append-to-body>
+ <el-alert
+ :title="`宸查�夋嫨 ${ids.length} 鏉℃湭澶勭悊鍛婅璁板綍`"
+ type="info"
+ :closable="false"
+ style="margin-bottom: 20px;"
+ />
+ <el-form ref="batchProcessForm" :model="batchProcessForm" :rules="batchProcessRules" label-width="100px">
+ <el-form-item label="澶勭悊澶囨敞" prop="handleRemark">
+ <el-input
+ v-model="batchProcessForm.handleRemark"
+ type="textarea"
+ :rows="4"
+ placeholder="璇疯緭鍏ュ鐞嗗娉紙蹇呭~锛�"
+ />
+ </el-form-item>
+ </el-form>
+ <div slot="footer" class="dialog-footer">
+ <el-button type="primary" @click="submitBatchProcess">纭� 瀹�</el-button>
+ <el-button @click="batchProcessOpen = false">鍙� 娑�</el-button>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+import { listVehicleAlert, getVehicleAlert, delVehicleAlert, handleAlert, batchHandleAlert, getUnhandledCount, exportVehicleAlert } from "@/api/system/vehicleAlert";
+import { listDept } from "@/api/system/dept";
+
+export default {
+ name: "VehicleAlert",
+ data() {
+ return {
+ // 閬僵灞�
+ loading: true,
+ // 閫変腑鏁扮粍
+ ids: [],
+ // 闈炲崟涓鐢�
+ single: true,
+ // 闈炲涓鐢�
+ multiple: true,
+ // 鏄剧ず鎼滅储鏉′欢
+ showSearch: true,
+ // 鎬绘潯鏁�
+ total: 0,
+ // 杞﹁締寮傚父鍛婅琛ㄦ牸鏁版嵁
+ alertList: [],
+ // 閮ㄩ棬鍒楄〃
+ deptList: [],
+ // 寮瑰嚭灞傛爣棰�
+ title: "",
+ // 鏄惁鏄剧ず璇︽儏寮瑰嚭灞�
+ detailOpen: false,
+ // 鏄惁鏄剧ず澶勭悊寮瑰嚭灞�
+ processOpen: false,
+ // 鏄惁鏄剧ず鎵归噺澶勭悊寮瑰嚭灞�
+ batchProcessOpen: false,
+ // 鏃ユ湡鑼冨洿
+ dateRange: [],
+ // 璇︽儏鏁版嵁
+ detail: {},
+ // 澶勭悊琛ㄥ崟
+ processForm: {
+ alertId: null,
+ handleRemark: ''
+ },
+ // 鎵归噺澶勭悊琛ㄥ崟
+ batchProcessForm: {
+ handleRemark: ''
+ },
+ // 缁熻鏁版嵁
+ unhandledCount: 0,
+ todayCount: 0,
+ totalVehicles: 0,
+ // 鏌ヨ鍙傛暟
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ vehicleNo: null,
+ alertDate: null,
+ status: null,
+ deptId: null
+ },
+ // 澶勭悊琛ㄥ崟鏍¢獙
+ processRules: {
+ handleRemark: [
+ { required: true, message: "澶勭悊澶囨敞涓嶈兘涓虹┖", trigger: "blur" }
+ ]
+ },
+ // 鎵归噺澶勭悊琛ㄥ崟鏍¢獙
+ batchProcessRules: {
+ handleRemark: [
+ { required: true, message: "澶勭悊澶囨敞涓嶈兘涓虹┖", trigger: "blur" }
+ ]
+ }
+ };
+ },
+ created() {
+ this.getList();
+ this.getDeptList();
+ this.refreshUnhandledCount();
+ },
+ methods: {
+ /** 鏌ヨ杞﹁締寮傚父鍛婅鍒楄〃 */
+ getList() {
+ this.loading = true;
+ listVehicleAlert(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
+ this.alertList = response.rows;
+ this.total = response.total;
+ // 缁熻浠婃棩鍛婅鏁伴噺
+ const today = this.parseTime(new Date(), '{y}-{m}-{d}');
+ this.todayCount = this.alertList.filter(item => item.alertDate === today).length;
+ // 缁熻鍛婅杞﹁締鏁伴噺锛堝幓閲嶏級
+ const vehicleSet = new Set(this.alertList.map(item => item.vehicleId));
+ this.totalVehicles = vehicleSet.size;
+ this.loading = false;
+ });
+ },
+ /** 鏌ヨ閮ㄩ棬鍒楄〃 */
+ getDeptList() {
+ listDept().then(response => {
+ this.deptList = response.data;
+ });
+ },
+ /** 鍒锋柊鏈鐞嗗憡璀︾粺璁� */
+ refreshUnhandledCount() {
+ getUnhandledCount().then(response => {
+ this.unhandledCount = response.data;
+ });
+ },
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ handleQuery() {
+ this.queryParams.pageNum = 1;
+ this.getList();
+ },
+ /** 閲嶇疆鎸夐挳鎿嶄綔 */
+ resetQuery() {
+ this.dateRange = [];
+ this.resetForm("queryForm");
+ this.handleQuery();
+ },
+ // 澶氶�夋閫変腑鏁版嵁
+ handleSelectionChange(selection) {
+ this.ids = selection.map(item => item.alertId);
+ this.single = selection.length !== 1;
+ this.multiple = !selection.length;
+ },
+ /** 鏌ョ湅璇︽儏鎸夐挳鎿嶄綔 */
+ handleView(row) {
+ getVehicleAlert(row.alertId).then(response => {
+ this.detail = response.data;
+ this.detailOpen = true;
+ });
+ },
+ /** 澶勭悊鎸夐挳鎿嶄綔 */
+ handleProcess(row) {
+ this.processForm = {
+ alertId: row.alertId,
+ handleRemark: ''
+ };
+ this.processOpen = true;
+ this.$nextTick(() => {
+ this.$refs["processForm"].clearValidate();
+ });
+ },
+ /** 鎻愪氦澶勭悊 */
+ submitProcess() {
+ this.$refs["processForm"].validate(valid => {
+ if (valid) {
+ handleAlert(this.processForm.alertId, {
+ handleRemark: this.processForm.handleRemark
+ }).then(response => {
+ this.$modal.msgSuccess("澶勭悊鎴愬姛");
+ this.processOpen = false;
+ this.getList();
+ this.refreshUnhandledCount();
+ });
+ }
+ });
+ },
+ /** 鎵归噺澶勭悊鎸夐挳鎿嶄綔 */
+ handleBatchProcess() {
+ // 绛涢�夊嚭鏈鐞嗙殑璁板綍
+ const unhandledIds = this.ids.filter(id => {
+ const alert = this.alertList.find(item => item.alertId === id);
+ return alert && alert.status === '0';
+ });
+
+ if (unhandledIds.length === 0) {
+ this.$modal.msgWarning("璇烽�夋嫨鏈鐞嗙殑鍛婅璁板綍");
+ return;
+ }
+
+ this.ids = unhandledIds;
+ this.batchProcessForm = {
+ handleRemark: ''
+ };
+ this.batchProcessOpen = true;
+ this.$nextTick(() => {
+ this.$refs["batchProcessForm"].clearValidate();
+ });
+ },
+ /** 鎻愪氦鎵归噺澶勭悊 */
+ submitBatchProcess() {
+ this.$refs["batchProcessForm"].validate(valid => {
+ if (valid) {
+ batchHandleAlert(this.ids, {
+ handleRemark: this.batchProcessForm.handleRemark
+ }).then(response => {
+ this.$modal.msgSuccess("鎵归噺澶勭悊鎴愬姛");
+ this.batchProcessOpen = false;
+ this.getList();
+ this.refreshUnhandledCount();
+ });
+ }
+ });
+ },
+ /** 鍒犻櫎鎸夐挳鎿嶄綔 */
+ handleDelete(row) {
+ const alertIds = row.alertId || this.ids;
+ this.$modal.confirm('鏄惁纭鍒犻櫎鍛婅璁板綍缂栧彿涓�"' + alertIds + '"鐨勬暟鎹」锛�').then(function() {
+ return delVehicleAlert(alertIds);
+ }).then(() => {
+ this.getList();
+ this.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ }).catch(() => {});
+ },
+ /** 瀵煎嚭鎸夐挳鎿嶄綔 */
+ handleExport() {
+ this.download('system/vehicleAlert/export', {
+ ...this.queryParams
+ }, `杞﹁締寮傚父鍛婅_${new Date().getTime()}.xlsx`)
+ }
+ }
+};
+</script>
+
+<style scoped>
+.mb8 {
+ margin-bottom: 8px;
+}
+</style>
diff --git a/ruoyi-ui/src/views/system/vehicleAlertConfig/index.vue b/ruoyi-ui/src/views/system/vehicleAlertConfig/index.vue
new file mode 100644
index 0000000..ebc0eee
--- /dev/null
+++ b/ruoyi-ui/src/views/system/vehicleAlertConfig/index.vue
@@ -0,0 +1,486 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="88px">
+ <el-form-item label="閰嶇疆绫诲瀷" prop="configType">
+ <el-select v-model="queryParams.configType" placeholder="璇烽�夋嫨閰嶇疆绫诲瀷" clearable size="small">
+ <el-option label="鍏ㄥ眬閰嶇疆" value="GLOBAL" />
+ <el-option label="閮ㄩ棬閰嶇疆" value="DEPT" />
+ <el-option label="杞﹁締閰嶇疆" value="VEHICLE" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閮ㄩ棬" prop="deptId" v-if="queryParams.configType === 'DEPT'">
+ <el-select v-model="queryParams.deptId" placeholder="璇烽�夋嫨閮ㄩ棬" clearable size="small">
+ <el-option
+ v-for="dept in deptList"
+ :key="dept.deptId"
+ :label="dept.deptName"
+ :value="dept.deptId"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="杞﹁締" prop="vehicleId" v-if="queryParams.configType === 'VEHICLE'">
+ <el-select v-model="queryParams.vehicleId" placeholder="璇烽�夋嫨杞﹁締" clearable size="small" filterable>
+ <el-option
+ v-for="vehicle in vehicleList"
+ :key="vehicle.vehicleId"
+ :label="vehicle.vehicleNo"
+ :value="vehicle.vehicleId"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable size="small">
+ <el-option label="鍚敤" value="0" />
+ <el-option label="鍋滅敤" value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="el-icon-plus"
+ size="mini"
+ @click="handleAdd"
+ v-hasPermi="['system:vehicleAlertConfig:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="el-icon-edit"
+ size="mini"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['system:vehicleAlertConfig:edit']"
+ >淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="el-icon-delete"
+ size="mini"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['system:vehicleAlertConfig:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="el-icon-download"
+ size="mini"
+ @click="handleExport"
+ v-hasPermi="['system:vehicleAlertConfig:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <!-- 閰嶇疆璇存槑 -->
+ <el-alert
+ title="閰嶇疆璇存槑"
+ type="info"
+ :closable="false"
+ style="margin-bottom: 10px;">
+ <div slot="default">
+ <p>鈥� <strong>鍏ㄥ眬閰嶇疆</strong>锛氶�傜敤浜庢墍鏈夎溅杈嗙殑榛樿閰嶇疆</p>
+ <p>鈥� <strong>閮ㄩ棬閰嶇疆</strong>锛氶拡瀵圭壒瀹氶儴闂ㄧ殑杞﹁締閰嶇疆锛屼紭鍏堢骇楂樹簬鍏ㄥ眬閰嶇疆</p>
+ <p>鈥� <strong>杞﹁締閰嶇疆</strong>锛氶拡瀵圭壒瀹氳溅杈嗙殑涓�у寲閰嶇疆锛屼紭鍏堢骇鏈�楂�</p>
+ <p>鈥� <strong>閫氱煡鐢ㄦ埛</strong>锛氬涓敤鎴稩D鐢ㄨ嫳鏂囬�楀彿鍒嗛殧锛屽锛�1,2,3</p>
+ </div>
+ </el-alert>
+
+ <el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="閰嶇疆ID" align="center" prop="configId" width="80" />
+ <el-table-column label="閰嶇疆绫诲瀷" align="center" prop="configType" width="100">
+ <template slot-scope="scope">
+ <el-tag v-if="scope.row.configType === 'GLOBAL'" type="primary">鍏ㄥ眬閰嶇疆</el-tag>
+ <el-tag v-else-if="scope.row.configType === 'DEPT'" type="success">閮ㄩ棬閰嶇疆</el-tag>
+ <el-tag v-else type="warning">杞﹁締閰嶇疆</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="閮ㄩ棬/杞﹁締" align="center" prop="targetName" width="150">
+ <template slot-scope="scope">
+ <span v-if="scope.row.configType === 'GLOBAL'">-</span>
+ <span v-else>{{ scope.row.targetName || '-' }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="閲岀▼闃堝��(km)" align="center" prop="mileageThreshold" width="120">
+ <template slot-scope="scope">
+ <el-tag type="danger">{{ scope.row.mileageThreshold }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="姣忔棩鍛婅娆℃暟" align="center" prop="dailyAlertLimit" width="120" />
+ <el-table-column label="鍛婅闂撮殧(鍒嗛挓)" align="center" prop="alertInterval" width="120" />
+ <el-table-column label="閫氱煡鐢ㄦ埛" align="center" prop="notifyUserIds" width="120" :show-overflow-tooltip="true">
+ <template slot-scope="scope">
+ <span v-if="scope.row.notifyUserIds">{{ scope.row.notifyUserIds }}</span>
+ <span v-else style="color: #909399;">鏈厤缃�</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status" width="80">
+ <template slot-scope="scope">
+ <el-switch
+ v-model="scope.row.status"
+ active-value="0"
+ inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ ></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="180">
+ <template slot-scope="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" :show-overflow-tooltip="true" />
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width" width="150" fixed="right">
+ <template slot-scope="scope">
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-edit"
+ @click="handleUpdate(scope.row)"
+ v-hasPermi="['system:vehicleAlertConfig:edit']"
+ >淇敼</el-button>
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-delete"
+ @click="handleDelete(scope.row)"
+ v-hasPermi="['system:vehicleAlertConfig:remove']"
+ >鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total>0"
+ :total="total"
+ :page.sync="queryParams.pageNum"
+ :limit.sync="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 娣诲姞鎴栦慨鏀硅溅杈嗗憡璀﹂厤缃璇濇 -->
+ <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+ <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+ <el-form-item label="閰嶇疆绫诲瀷" prop="configType">
+ <el-radio-group v-model="form.configType" @change="handleConfigTypeChange">
+ <el-radio label="GLOBAL">鍏ㄥ眬閰嶇疆</el-radio>
+ <el-radio label="DEPT">閮ㄩ棬閰嶇疆</el-radio>
+ <el-radio label="VEHICLE">杞﹁締閰嶇疆</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="閮ㄩ棬" prop="deptId" v-if="form.configType === 'DEPT'">
+ <el-select v-model="form.deptId" placeholder="璇烽�夋嫨閮ㄩ棬" clearable style="width: 100%">
+ <el-option
+ v-for="dept in deptList"
+ :key="dept.deptId"
+ :label="dept.deptName"
+ :value="dept.deptId"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="杞﹁締" prop="vehicleId" v-if="form.configType === 'VEHICLE'">
+ <el-select v-model="form.vehicleId" placeholder="璇烽�夋嫨杞﹁締" clearable filterable style="width: 100%">
+ <el-option
+ v-for="vehicle in vehicleList"
+ :key="vehicle.vehicleId"
+ :label="vehicle.vehicleNo"
+ :value="vehicle.vehicleId"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閲岀▼闃堝��(km)" prop="mileageThreshold">
+ <el-input-number
+ v-model="form.mileageThreshold"
+ :min="1"
+ :max="1000"
+ :step="1"
+ placeholder="璇疯緭鍏ラ噷绋嬮槇鍊�"
+ style="width: 100%"
+ />
+ <span style="color: #909399; font-size: 12px;">杞﹁締鏈粦瀹氫换鍔℃椂锛岃秴杩囨閲岀▼灏嗚Е鍙戝憡璀�</span>
+ </el-form-item>
+ <el-form-item label="姣忔棩鍛婅娆℃暟" prop="dailyAlertLimit">
+ <el-input-number
+ v-model="form.dailyAlertLimit"
+ :min="1"
+ :max="100"
+ :step="1"
+ placeholder="璇疯緭鍏ユ瘡鏃ュ憡璀︽鏁伴檺鍒�"
+ style="width: 100%"
+ />
+ <span style="color: #909399; font-size: 12px;">姣忚締杞︽瘡澶╂渶澶氬憡璀︽鏁�</span>
+ </el-form-item>
+ <el-form-item label="鍛婅闂撮殧(鍒嗛挓)" prop="alertInterval">
+ <el-input-number
+ v-model="form.alertInterval"
+ :min="1"
+ :max="1440"
+ :step="1"
+ placeholder="璇疯緭鍏ュ憡璀﹂棿闅旀椂闂�"
+ style="width: 100%"
+ />
+ <span style="color: #909399; font-size: 12px;">涓ゆ鍛婅涔嬮棿鐨勬渶灏忔椂闂撮棿闅�</span>
+ </el-form-item>
+ <el-form-item label="閫氱煡鐢ㄦ埛ID" prop="notifyUserIds">
+ <el-input
+ v-model="form.notifyUserIds"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ラ�氱煡鐢ㄦ埛ID鍒楄〃锛屽涓敤鑻辨枃閫楀彿鍒嗛殧锛屽锛�1,2,3"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="form.status">
+ <el-radio label="0">鍚敤</el-radio>
+ <el-radio label="1">鍋滅敤</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <div slot="footer" class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+import { listVehicleAlertConfig, getVehicleAlertConfig, delVehicleAlertConfig, addVehicleAlertConfig, updateVehicleAlertConfig } from "@/api/system/vehicleAlertConfig";
+import { listDept } from "@/api/system/dept";
+import { listVehicle } from "@/api/system/vehicle";
+
+export default {
+ name: "VehicleAlertConfig",
+ data() {
+ return {
+ // 閬僵灞�
+ loading: true,
+ // 閫変腑鏁扮粍
+ ids: [],
+ // 闈炲崟涓鐢�
+ single: true,
+ // 闈炲涓鐢�
+ multiple: true,
+ // 鏄剧ず鎼滅储鏉′欢
+ showSearch: true,
+ // 鎬绘潯鏁�
+ total: 0,
+ // 杞﹁締鍛婅閰嶇疆琛ㄦ牸鏁版嵁
+ configList: [],
+ // 閮ㄩ棬鍒楄〃
+ deptList: [],
+ // 杞﹁締鍒楄〃
+ vehicleList: [],
+ // 寮瑰嚭灞傛爣棰�
+ title: "",
+ // 鏄惁鏄剧ず寮瑰嚭灞�
+ open: false,
+ // 鏌ヨ鍙傛暟
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ configType: null,
+ deptId: null,
+ vehicleId: null,
+ status: null
+ },
+ // 琛ㄥ崟鍙傛暟
+ form: {},
+ // 琛ㄥ崟鏍¢獙
+ rules: {
+ configType: [
+ { required: true, message: "閰嶇疆绫诲瀷涓嶈兘涓虹┖", trigger: "change" }
+ ],
+ deptId: [
+ { required: true, message: "閮ㄩ棬涓嶈兘涓虹┖", trigger: "change" }
+ ],
+ vehicleId: [
+ { required: true, message: "杞﹁締涓嶈兘涓虹┖", trigger: "change" }
+ ],
+ mileageThreshold: [
+ { required: true, message: "閲岀▼闃堝�间笉鑳戒负绌�", trigger: "blur" }
+ ],
+ dailyAlertLimit: [
+ { required: true, message: "姣忔棩鍛婅娆℃暟涓嶈兘涓虹┖", trigger: "blur" }
+ ],
+ alertInterval: [
+ { required: true, message: "鍛婅闂撮殧涓嶈兘涓虹┖", trigger: "blur" }
+ ]
+ }
+ };
+ },
+ created() {
+ this.getList();
+ this.getDeptList();
+ this.getVehicleList();
+ },
+ methods: {
+ /** 鏌ヨ杞﹁締鍛婅閰嶇疆鍒楄〃 */
+ getList() {
+ this.loading = true;
+ listVehicleAlertConfig(this.queryParams).then(response => {
+ this.configList = response.rows;
+ this.total = response.total;
+ this.loading = false;
+ });
+ },
+ /** 鏌ヨ閮ㄩ棬鍒楄〃 */
+ getDeptList() {
+ listDept().then(response => {
+ this.deptList = response.data;
+ });
+ },
+ /** 鏌ヨ杞﹁締鍒楄〃 */
+ getVehicleList() {
+ listVehicle({ pageNum: 1, pageSize: 10000 }).then(response => {
+ this.vehicleList = response.rows || [];
+ });
+ },
+ // 鍙栨秷鎸夐挳
+ cancel() {
+ this.open = false;
+ this.reset();
+ },
+ // 琛ㄥ崟閲嶇疆
+ reset() {
+ this.form = {
+ configId: null,
+ configType: "GLOBAL",
+ deptId: null,
+ vehicleId: null,
+ mileageThreshold: 10,
+ dailyAlertLimit: 5,
+ alertInterval: 5,
+ notifyUserIds: null,
+ status: "0",
+ remark: null
+ };
+ this.resetForm("form");
+ },
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ handleQuery() {
+ this.queryParams.pageNum = 1;
+ this.getList();
+ },
+ /** 閲嶇疆鎸夐挳鎿嶄綔 */
+ resetQuery() {
+ this.resetForm("queryForm");
+ this.handleQuery();
+ },
+ // 澶氶�夋閫変腑鏁版嵁
+ handleSelectionChange(selection) {
+ this.ids = selection.map(item => item.configId);
+ this.single = selection.length !== 1;
+ this.multiple = !selection.length;
+ },
+ /** 閰嶇疆绫诲瀷鏀瑰彉 */
+ handleConfigTypeChange(value) {
+ if (value === 'GLOBAL') {
+ this.form.deptId = null;
+ this.form.vehicleId = null;
+ } else if (value === 'DEPT') {
+ this.form.vehicleId = null;
+ } else if (value === 'VEHICLE') {
+ this.form.deptId = null;
+ }
+ },
+ /** 鐘舵�佷慨鏀� */
+ handleStatusChange(row) {
+ let text = row.status === "0" ? "鍚敤" : "鍋滅敤";
+ this.$modal.confirm('纭瑕�"' + text + '""' + row.configId + '"閰嶇疆鍚楋紵').then(function() {
+ return updateVehicleAlertConfig(row);
+ }).then(() => {
+ this.$modal.msgSuccess(text + "鎴愬姛");
+ }).catch(function() {
+ row.status = row.status === "0" ? "1" : "0";
+ });
+ },
+ /** 鏂板鎸夐挳鎿嶄綔 */
+ handleAdd() {
+ this.reset();
+ this.open = true;
+ this.title = "娣诲姞杞﹁締鍛婅閰嶇疆";
+ },
+ /** 淇敼鎸夐挳鎿嶄綔 */
+ handleUpdate(row) {
+ this.reset();
+ const configId = row.configId || this.ids[0];
+ getVehicleAlertConfig(configId).then(response => {
+ this.form = response.data;
+ this.open = true;
+ this.title = "淇敼杞﹁締鍛婅閰嶇疆";
+ });
+ },
+ /** 鎻愪氦鎸夐挳 */
+ submitForm() {
+ this.$refs["form"].validate(valid => {
+ if (valid) {
+ // 鏍规嵁閰嶇疆绫诲瀷娓呯┖涓嶉渶瑕佺殑瀛楁
+ if (this.form.configType === 'GLOBAL') {
+ this.form.deptId = null;
+ this.form.vehicleId = null;
+ } else if (this.form.configType === 'DEPT') {
+ this.form.vehicleId = null;
+ } else if (this.form.configType === 'VEHICLE') {
+ this.form.deptId = null;
+ }
+
+ if (this.form.configId != null) {
+ updateVehicleAlertConfig(this.form).then(response => {
+ this.$modal.msgSuccess("淇敼鎴愬姛");
+ this.open = false;
+ this.getList();
+ });
+ } else {
+ addVehicleAlertConfig(this.form).then(response => {
+ this.$modal.msgSuccess("鏂板鎴愬姛");
+ this.open = false;
+ this.getList();
+ });
+ }
+ }
+ });
+ },
+ /** 鍒犻櫎鎸夐挳鎿嶄綔 */
+ handleDelete(row) {
+ const configIds = row.configId || this.ids;
+ this.$modal.confirm('鏄惁纭鍒犻櫎閰嶇疆缂栧彿涓�"' + configIds + '"鐨勬暟鎹」锛�').then(function() {
+ return delVehicleAlertConfig(configIds);
+ }).then(() => {
+ this.getList();
+ this.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ }).catch(() => {});
+ },
+ /** 瀵煎嚭鎸夐挳鎿嶄綔 */
+ handleExport() {
+ this.download('system/vehicleAlertConfig/export', {
+ ...this.queryParams
+ }, `杞﹁締鍛婅閰嶇疆_${new Date().getTime()}.xlsx`)
+ }
+ }
+};
+</script>
+
+<style scoped>
+.mb8 {
+ margin-bottom: 8px;
+}
+</style>
diff --git a/ruoyi-ui/src/views/system/vehicleSync/index.vue b/ruoyi-ui/src/views/system/vehicleSync/index.vue
new file mode 100644
index 0000000..d7673fb
--- /dev/null
+++ b/ruoyi-ui/src/views/system/vehicleSync/index.vue
@@ -0,0 +1,245 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <div slot="header" class="clearfix">
+ <span>杞﹁締鍚屾绠$悊</span>
+ <el-button
+ style="float: right; padding: 3px 0"
+ type="text"
+ @click="handleRefresh"
+ >
+ <i class="el-icon-refresh"></i> 鍒锋柊
+ </el-button>
+ </div>
+
+ <!-- 璇存槑鎻愮ず -->
+ <el-alert
+ title="鍔熻兘璇存槑"
+ type="info"
+ :closable="false"
+ style="margin-bottom: 15px"
+ >
+ <div>
+ 鏈〉闈㈢敤浜庢煡鐪嬫棫绯荤粺涓殑杞﹁締鏁版嵁锛屽苟鎵嬪姩鍚屾鏈悓姝ョ殑杞﹁締鍒版柊绯荤粺銆�<br/>
+ <strong>宸插悓姝�</strong>锛氳溅杈嗗凡瀛樺湪浜庢柊绯荤粺涓紱<strong>鏈悓姝�</strong>锛氳溅杈嗚繕鏈悓姝ワ紝鍙墜鍔ㄦ坊鍔犲埌鏂扮郴缁熴��
+ </div>
+ </el-alert>
+
+ <!-- 绛涢�夋潯浠� -->
+ <el-form :inline="true" class="filter-form">
+ <el-form-item label="鍚屾鐘舵��">
+ <el-select v-model="filterSynced" placeholder="璇烽�夋嫨" @change="handleFilter" clearable>
+ <el-option label="鍏ㄩ儴" :value="null" />
+ <el-option label="鏈悓姝�" :value="false" />
+ <el-option label="宸插悓姝�" :value="true" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+
+ <!-- 鏁版嵁琛ㄦ牸 -->
+ <el-table
+ v-loading="loading"
+ :data="filteredVehicleList"
+ style="width: 100%"
+ border
+ >
+ <el-table-column label="杞﹁締ID(CarID)" prop="carId" width="120" align="center" />
+ <el-table-column label="杞︾墝鍙�" prop="vehicleNo" width="150" align="center">
+ <template slot-scope="scope">
+ <el-tag>{{ scope.row.vehicleNo }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曟嵁绫诲瀷缂栫爜" prop="carOrdClass" width="120" align="center" />
+ <el-table-column label="褰掑睘鍒嗗叕鍙�" prop="deptName" width="150" align="center" />
+ <el-table-column label="鍚屾鐘舵��" prop="synced" width="120" align="center">
+ <template slot-scope="scope">
+ <el-tag :type="scope.row.synced ? 'success' : 'warning'">
+ {{ scope.row.synced ? '宸插悓姝�' : '鏈悓姝�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏂扮郴缁熻溅杈咺D" prop="vehicleId" width="130" align="center">
+ <template slot-scope="scope">
+ <span v-if="scope.row.vehicleId">{{ scope.row.vehicleId }}</span>
+ <span v-else style="color: #909399">-</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template slot-scope="scope">
+ <el-button
+ v-if="!scope.row.synced"
+ size="mini"
+ type="primary"
+ icon="el-icon-plus"
+ @click="handleSync(scope.row)"
+ v-hasPermi="['system:vehicleSync:sync']"
+ >
+ 鍚屾
+ </el-button>
+ <el-tag v-else type="info">宸插悓姝�</el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <!-- 鍚屾瀵硅瘽妗� -->
+ <el-dialog
+ title="鎵嬪姩鍚屾杞﹁締"
+ :visible.sync="syncDialogVisible"
+ width="500px"
+ append-to-body
+ >
+ <el-form ref="syncForm" :model="syncForm" :rules="syncRules" label-width="100px">
+ <el-form-item label="鏃х郴缁烮D" prop="carId">
+ <el-input v-model="syncForm.carId" disabled />
+ </el-form-item>
+ <el-form-item label="杞︾墝鍙�" prop="vehicleNo">
+ <el-input v-model="syncForm.vehicleNo" disabled />
+ </el-form-item>
+ <el-form-item label="褰掑睘鍒嗗叕鍙�" prop="deptId">
+ <el-select v-model="syncForm.deptId" placeholder="璇烽�夋嫨褰掑睘鍒嗗叕鍙�" clearable style="width: 100%">
+ <el-option
+ v-for="dept in deptOptions"
+ :key="dept.deptId"
+ :label="dept.deptName"
+ :value="dept.deptId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <div slot="footer" class="dialog-footer">
+ <el-button @click="syncDialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="submitSync" :loading="syncLoading">纭� 瀹�</el-button>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+import { listVehicleSync, syncVehicle } from "@/api/system/vehicleSync";
+import { listDept } from "@/api/system/dept";
+
+export default {
+ name: "VehicleSync",
+ data() {
+ return {
+ // 鍔犺浇鐘舵��
+ loading: true,
+ // 杞﹁締鍒楄〃锛堝師濮嬫暟鎹級
+ vehicleList: [],
+ // 绛涢�夊悗鐨勮溅杈嗗垪琛�
+ filteredVehicleList: [],
+ // 绛涢�夋潯浠讹細鍚屾鐘舵��
+ filterSynced: null,
+ // 鍚屾瀵硅瘽妗嗘樉绀虹姸鎬�
+ syncDialogVisible: false,
+ // 鍚屾鍔犺浇鐘舵��
+ syncLoading: false,
+ // 鍚屾琛ㄥ崟
+ syncForm: {
+ carId: null,
+ vehicleNo: null,
+ deptId: null
+ },
+ // 閮ㄩ棬鏍戦�夐」
+ deptOptions: [],
+ // 琛ㄥ崟鏍¢獙瑙勫垯
+ syncRules: {
+ deptId: [
+ { required: true, message: "璇烽�夋嫨褰掑睘鍒嗗叕鍙�", trigger: "change" }
+ ]
+ }
+ };
+ },
+ created() {
+ this.getList();
+ this.getDeptList();
+ },
+ methods: {
+ /** 鏌ヨ杞﹁締鍚屾鍒楄〃 */
+ getList() {
+ this.loading = true;
+ listVehicleSync().then(response => {
+ this.vehicleList = response.rows;
+ this.applyFilter();
+ this.loading = false;
+ }).catch(() => {
+ this.loading = false;
+ });
+ },
+ /** 搴旂敤绛涢�夋潯浠� */
+ applyFilter() {
+ if (this.filterSynced === null) {
+ // 鏄剧ず鍏ㄩ儴
+ this.filteredVehicleList = this.vehicleList;
+ } else {
+ // 鏍规嵁鍚屾鐘舵�佺瓫閫�
+ this.filteredVehicleList = this.vehicleList.filter(
+ item => item.synced === this.filterSynced
+ );
+ }
+ },
+ /** 绛涢�夋潯浠跺彉鍖� */
+ handleFilter() {
+ this.applyFilter();
+ },
+ /** 鑾峰彇閮ㄩ棬鍒楄〃锛堝彧鏄剧ず鍒嗗叕鍙革細parent_id=100锛� */
+ getDeptList() {
+ listDept({ parentId: 100 }).then(response => {
+ // 杩囨护鍑哄垎鍏徃锛坧arent_id=100鐨勯儴闂級
+ if (response.data) {
+ this.deptOptions = response.data.filter(dept => dept.parentId === "100");
+ } else {
+ this.deptOptions = [];
+ }
+ });
+ },
+ /** 鍒锋柊鍒楄〃 */
+ handleRefresh() {
+ this.getList();
+ this.$modal.msgSuccess("鍒锋柊鎴愬姛");
+ },
+ /** 鍚屾鎸夐挳鎿嶄綔 */
+ handleSync(row) {
+ this.syncForm = {
+ carId: row.carId,
+ vehicleNo: row.vehicleNo,
+ deptId: row.deptId || null
+ };
+ this.syncDialogVisible = true;
+ this.$nextTick(() => {
+ this.$refs["syncForm"].clearValidate();
+ });
+ },
+ /** 鎻愪氦鍚屾 */
+ submitSync() {
+ this.$refs["syncForm"].validate(valid => {
+ if (valid) {
+ this.syncLoading = true;
+ syncVehicle(this.syncForm).then(response => {
+ this.$modal.msgSuccess("鍚屾鎴愬姛");
+ this.syncDialogVisible = false;
+ this.syncLoading = false;
+ this.getList();
+ }).catch(() => {
+ this.syncLoading = false;
+ });
+ }
+ });
+ }
+ }
+};
+</script>
+
+<style scoped>
+.box-card {
+ margin-bottom: 20px;
+}
+
+.filter-form {
+ margin-bottom: 15px;
+ padding: 10px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+}
+</style>
diff --git a/sql/hospital_tokenizer_menu.sql b/sql/hospital_tokenizer_menu.sql
new file mode 100644
index 0000000..7b17246
--- /dev/null
+++ b/sql/hospital_tokenizer_menu.sql
@@ -0,0 +1,24 @@
+-- 鍖婚櫌鍒嗚瘝娴嬭瘯鑿滃崟SQL
+-- 鎵ц姝よ剼鏈悗锛岄渶瑕佺粰绠$悊鍛樿鑹插垎閰嶈彍鍗曟潈闄�
+
+-- 鏌ユ壘绯荤粺绠$悊鑿滃崟ID
+SELECT @parentId := menu_id FROM sys_menu WHERE menu_name = '绯荤粺绠$悊' AND parent_id = 0;
+
+-- 鎻掑叆鍖婚櫌绠$悊涓�绾ц彍鍗�
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+VALUES
+('鍖婚櫌绠$悊', @parentId, 10, 'hospital', NULL, 1, 0, 'M', '0', '0', NULL, 'hospital', 'admin', sysdate(), '鍖婚櫌鍒嗚瘝绠$悊');
+
+-- 鑾峰彇鍒氭彃鍏ョ殑鍖婚櫌绠$悊鑿滃崟ID
+SELECT @hospitalMenuId := menu_id FROM sys_menu WHERE menu_name = '鍖婚櫌绠$悊' AND parent_id = @parentId;
+
+-- 鎻掑叆鍖婚櫌鍒嗚瘝娴嬭瘯浜岀骇鑿滃崟
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+VALUES
+('鍖婚櫌鍒嗚瘝娴嬭瘯', @hospitalMenuId, 1, 'tokenizer', 'system/hospital/tokenizer', 1, 0, 'C', '0', '0', 'system:hospital:tokenizer', 'search', 'admin', sysdate(), '鍖婚櫌鍒嗚瘝涓庢悳绱㈡祴璇曞伐鍏�');
+
+-- 璇存槑锛�
+-- 1. 鎵ц姝よ剼鏈悗锛岃彍鍗曚細鍑虹幇鍦�"绯荤粺绠$悊"涓�
+-- 2. 闇�瑕佸湪"绯荤粺绠$悊 > 瑙掕壊绠$悊"涓粰鐩稿簲瑙掕壊鍒嗛厤"鍖婚櫌绠$悊"鑿滃崟鏉冮檺
+-- 3. 鏉冮檺鏍囪瘑锛歴ystem:hospital:tokenizer
+-- 4. 鑿滃崟璺緞锛氱郴缁熺鐞� > 鍖婚櫌绠$悊 > 鍖婚櫌鍒嗚瘝娴嬭瘯
diff --git a/sql/ocr_module_menu.sql b/sql/ocr_module_menu.sql
new file mode 100644
index 0000000..989076b
--- /dev/null
+++ b/sql/ocr_module_menu.sql
@@ -0,0 +1,82 @@
+-- OCR妯″潡瀹屾暣鑿滃崟SQL
+-- 鍖呭惈OCR绠$悊銆佹祴璇曘�侀厤缃拰缃戠粶璇婃柇鍔熻兘
+
+-- 鑾峰彇绯荤粺宸ュ叿鑿滃崟ID锛堥�氬父鏄�3锛�
+SET @parentMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = '绯荤粺宸ュ叿' AND parent_id = 0 LIMIT 1);
+
+-- 濡傛灉娌℃湁绯荤粺宸ュ叿鑿滃崟锛屽垯浣跨敤绯荤粺绠$悊锛坧arent_id=1锛�
+SET @parentMenuId = IFNULL(@parentMenuId, 1);
+
+-- 鎻掑叆OCR绠$悊鑿滃崟
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT 'OCR绠$悊', @parentMenuId, 9, 'ocr', '', 1, 0, 'M', '0', '0', '', 'camera', 'admin', NOW(), '', NULL, 'OCR鏈嶅姟绠$悊鑿滃崟'
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE menu_name = 'OCR绠$悊' AND perms = ''
+);
+
+-- 鑾峰彇OCR绠$悊鑿滃崟ID
+SET @ocrParentMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = 'OCR绠$悊' LIMIT 1);
+
+-- 鎻掑叆OCR娴嬭瘯瀛愯彍鍗�
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT 'OCR娴嬭瘯', @ocrParentMenuId, 1, 'ocr', 'system/ocr/index', 1, 0, 'C', '0', '0', 'system:ocr:test', 'eye-open', 'admin', NOW(), '', NULL, 'OCR鍥惧儚璇嗗埆娴嬭瘯椤甸潰'
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE menu_name = 'OCR娴嬭瘯' AND perms = 'system:ocr:test'
+);
+
+-- 鎻掑叆OCR閰嶇疆瀛愯彍鍗�
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT 'OCR閰嶇疆', @ocrParentMenuId, 2, 'config', 'system/ocr/config', 1, 0, 'C', '0', '0', 'system:ocr:config', 'setting', 'admin', NOW(), '', NULL, 'OCR鏈嶅姟閰嶇疆椤甸潰'
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE menu_name = 'OCR閰嶇疆' AND perms = 'system:ocr:config'
+);
+
+-- 鎻掑叆OCR璇嗗埆鏉冮檺鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT 'OCR璇嗗埆', (SELECT menu_id FROM sys_menu WHERE perms = 'system:ocr:test' LIMIT 1), 1, '#', '', 1, 0, 'F', '0', '0', 'system:ocr:recognize', '#', 'admin', NOW(), '', NULL, 'OCR鍥惧儚璇嗗埆鏉冮檺'
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE perms = 'system:ocr:recognize'
+);
+
+-- 鎻掑叆OCR鏈嶅姟鎻愪緵鍟嗘煡璇㈡潈闄愭寜閽�
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT 'OCR鏈嶅姟鍟嗘煡璇�', (SELECT menu_id FROM sys_menu WHERE perms = 'system:ocr:test' LIMIT 1), 2, '#', '', 1, 0, 'F', '0', '0', 'system:ocr:providers', '#', 'admin', NOW(), '', NULL, 'OCR鏈嶅姟鎻愪緵鍟嗘煡璇㈡潈闄�'
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE perms = 'system:ocr:providers'
+);
+
+-- 鎻掑叆缃戠粶璇婃柇鑿滃崟
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '缃戠粶璇婃柇', @parentMenuId, 10, 'diag', '', 1, 0, 'M', '0', '0', '', 'link', 'admin', NOW(), '', NULL, '缃戠粶璇婃柇绠$悊鑿滃崟'
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE menu_name = '缃戠粶璇婃柇' AND perms = ''
+);
+
+-- 鎻掑叆OCR缃戠粶璇婃柇瀛愯彍鍗�
+SET @diagParentMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = '缃戠粶璇婃柇' LIMIT 1);
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT 'OCR杩炴帴璇婃柇', @diagParentMenuId, 1, 'ocrConnection', 'system/diag/ocrConnection', 1, 0, 'C', '0', '0', 'system:diag:ocr', 'monitor', 'admin', NOW(), '', NULL, 'OCR鏈嶅姟杩炴帴璇婃柇椤甸潰'
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE menu_name = 'OCR杩炴帴璇婃柇' AND perms = 'system:diag:ocr'
+);
+
+-- 鏌ヨ缁撴灉
+SELECT
+ menu_id,
+ menu_name,
+ parent_id,
+ path,
+ component,
+ perms,
+ icon,
+ '鑿滃崟娣诲姞鎴愬姛' AS status
+FROM sys_menu
+WHERE menu_name IN ('OCR绠$悊', 'OCR娴嬭瘯', 'OCR閰嶇疆', 'OCR璇嗗埆', 'OCR鏈嶅姟鍟嗘煡璇�', '缃戠粶璇婃柇', 'OCR杩炴帴璇婃柇')
+ORDER BY parent_id, order_num;
diff --git a/sql/ocr_test_menu.sql b/sql/ocr_test_menu.sql
new file mode 100644
index 0000000..2990e39
--- /dev/null
+++ b/sql/ocr_test_menu.sql
@@ -0,0 +1,41 @@
+-- OCR娴嬭瘯椤甸潰鑿滃崟SQL
+-- 娉ㄦ剰锛氳鏍规嵁瀹為檯鎯呭喌璋冩暣parent_id锛堢埗鑿滃崟ID锛�
+
+-- 鑾峰彇绯荤粺宸ュ叿鑿滃崟ID锛堥�氬父鏄�3锛�
+SET @parentMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = '绯荤粺宸ュ叿' AND parent_id = 0 LIMIT 1);
+
+-- 濡傛灉娌℃湁绯荤粺宸ュ叿鑿滃崟锛屽垯浣跨敤绯荤粺绠$悊锛坧arent_id=1锛�
+SET @parentMenuId = IFNULL(@parentMenuId, 1);
+
+-- 鎻掑叆OCR娴嬭瘯鑿滃崟
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT 'OCR娴嬭瘯', @parentMenuId, 10, 'ocr', 'system/ocr/index', 1, 0, 'C', '0', '0', 'system:ocr:test', 'eye-open', 'admin', NOW(), '', NULL, 'OCR鍥惧儚璇嗗埆娴嬭瘯椤甸潰'
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE menu_name = 'OCR娴嬭瘯' AND perms = 'system:ocr:test'
+);
+
+-- 鑾峰彇鍒氭彃鍏ョ殑OCR娴嬭瘯鑿滃崟ID
+SET @ocrMenuId = (SELECT menu_id FROM sys_menu WHERE perms = 'system:ocr:test' LIMIT 1);
+
+-- 鎻掑叆OCR璇嗗埆鏉冮檺鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT 'OCR璇嗗埆', @ocrMenuId, 1, '#', '', 1, 0, 'F', '0', '0', 'system:ocr:recognize', '#', 'admin', NOW(), '', NULL, 'OCR鍥惧儚璇嗗埆鏉冮檺'
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE perms = 'system:ocr:recognize'
+);
+
+-- 鏌ヨ缁撴灉
+SELECT
+ menu_id,
+ menu_name,
+ parent_id,
+ path,
+ component,
+ perms,
+ icon,
+ '鑿滃崟娣诲姞鎴愬姛' AS status
+FROM sys_menu
+WHERE menu_name IN ('OCR娴嬭瘯', 'OCR璇嗗埆')
+ORDER BY menu_id;
diff --git a/sql/tb_hosp_data_add_keywords.sql b/sql/tb_hosp_data_add_keywords.sql
new file mode 100644
index 0000000..312f964
--- /dev/null
+++ b/sql/tb_hosp_data_add_keywords.sql
@@ -0,0 +1,9 @@
+-- 涓哄尰闄㈡暟鎹〃娣诲姞鍒嗚瘝瀛楁
+-- 鎵ц鏃堕棿锛�2026-01-20
+
+-- 娣诲姞鍒嗚瘝瀛楁锛堝瓨鍌ㄤ互閫楀彿鍒嗛殧鐨勫叧閿瘝锛�
+ALTER TABLE `tb_hosp_data`
+ADD COLUMN `hosp_keywords` varchar(4000) DEFAULT NULL COMMENT '鍖婚櫌淇℃伅鍒嗚瘝锛堥�楀彿鍒嗛殧锛�' AFTER `hosp_level`;
+
+-- 涓哄垎璇嶅瓧娈垫坊鍔犵储寮曚互鎻愬崌鎼滅储鎬ц兘
+CREATE INDEX idx_hosp_keywords ON tb_hosp_data(hosp_keywords);
diff --git a/sql/vehicle_abnormal_alert.sql b/sql/vehicle_abnormal_alert.sql
new file mode 100644
index 0000000..39e4eaf
--- /dev/null
+++ b/sql/vehicle_abnormal_alert.sql
@@ -0,0 +1,183 @@
+-- 杞﹁締寮傚父杩愯鍛婅璁板綍琛�
+CREATE TABLE `tb_vehicle_abnormal_alert` (
+ `alert_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '鍛婅ID',
+ `vehicle_id` bigint(20) NOT NULL COMMENT '杞﹁締ID',
+ `vehicle_no` varchar(20) DEFAULT NULL COMMENT '杞︾墝鍙�',
+ `alert_date` date NOT NULL COMMENT '鍛婅鏃ユ湡',
+ `alert_time` datetime NOT NULL COMMENT '鍛婅鏃堕棿',
+ `mileage` decimal(10,3) DEFAULT 0.000 COMMENT '绱杩愯鍏噷鏁�(鍏噷)',
+ `alert_type` varchar(20) DEFAULT 'NO_TASK_MILEAGE' COMMENT '鍛婅绫诲瀷(NO_TASK_MILEAGE-鏃犱换鍔¤秴鍏噷)',
+ `alert_reason` varchar(500) DEFAULT NULL COMMENT '鍛婅鍘熷洜鎻忚堪',
+ `start_time` datetime DEFAULT NULL COMMENT '寮�濮嬭繍琛屾椂闂�',
+ `end_time` datetime DEFAULT NULL COMMENT '缁撴潫杩愯鏃堕棿',
+ `alert_count` int(11) DEFAULT 1 COMMENT '褰撴棩鍛婅娆℃暟',
+ `status` char(1) DEFAULT '0' COMMENT '鐘舵�侊紙0-鏈鐞� 1-宸插鐞� 2-宸插拷鐣ワ級',
+ `handler_id` bigint(20) DEFAULT NULL COMMENT '澶勭悊浜篒D',
+ `handler_name` varchar(64) DEFAULT NULL COMMENT '澶勭悊浜哄鍚�',
+ `handle_time` datetime DEFAULT NULL COMMENT '澶勭悊鏃堕棿',
+ `handle_remark` varchar(500) DEFAULT NULL COMMENT '澶勭悊澶囨敞',
+ `notify_status` char(1) DEFAULT '0' COMMENT '閫氱煡鐘舵�侊紙0-鏈彂閫� 1-宸插彂閫� 2-鍙戦�佸け璐ワ級',
+ `notify_time` datetime DEFAULT NULL COMMENT '閫氱煡鏃堕棿',
+ `notify_users` varchar(500) DEFAULT NULL COMMENT '閫氱煡鐢ㄦ埛ID鍒楄〃锛堥�楀彿鍒嗛殧锛�',
+ `dept_id` bigint(20) DEFAULT NULL COMMENT '褰掑睘閮ㄩ棬ID',
+ `dept_name` varchar(100) DEFAULT NULL COMMENT '褰掑睘閮ㄩ棬鍚嶇О',
+ `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ PRIMARY KEY (`alert_id`),
+ KEY `idx_vehicle_id` (`vehicle_id`),
+ KEY `idx_alert_date` (`alert_date`),
+ KEY `idx_alert_time` (`alert_time`),
+ KEY `idx_vehicle_date` (`vehicle_id`, `alert_date`),
+ KEY `idx_dept_id` (`dept_id`),
+ KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='杞﹁締寮傚父杩愯鍛婅璁板綍琛�';
+
+-- 杞﹁締寮傚父鍛婅閰嶇疆琛�
+DROP TABLE IF EXISTS `tb_vehicle_alert_config`;
+CREATE TABLE `tb_vehicle_alert_config` (
+ `config_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '閰嶇疆ID',
+ `config_type` varchar(50) NOT NULL COMMENT '閰嶇疆绫诲瀷(GLOBAL-鍏ㄥ眬/DEPT-閮ㄩ棬/VEHICLE-杞﹁締)',
+ `dept_id` bigint(20) DEFAULT NULL COMMENT '閮ㄩ棬ID锛堥儴闂ㄩ厤缃椂浣跨敤锛�',
+ `vehicle_id` bigint(20) DEFAULT NULL COMMENT '杞﹁締ID锛堣溅杈嗛厤缃椂浣跨敤锛�',
+ `mileage_threshold` decimal(10,3) DEFAULT 10.000 COMMENT '鍏噷鏁板憡璀﹂槇鍊�(鍏噷)',
+ `daily_alert_limit` int(11) DEFAULT 5 COMMENT '姣忔棩鏈�澶у憡璀︽鏁�',
+ `alert_interval` int(11) DEFAULT 5 COMMENT '鍛婅闂撮殧鏃堕棿(鍒嗛挓)',
+ `notify_user_ids` varchar(1000) DEFAULT NULL COMMENT '閫氱煡鐢ㄦ埛ID鍒楄〃锛堥�楀彿鍒嗛殧锛�',
+ `status` char(1) DEFAULT '0' COMMENT '鐘舵�侊紙0-鍚敤 1-鍋滅敤锛�',
+ `create_by` varchar(64) DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `update_by` varchar(64) DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `remark` varchar(500) DEFAULT NULL COMMENT '澶囨敞',
+ PRIMARY KEY (`config_id`),
+ UNIQUE KEY `uk_vehicle_config` (`config_type`, `vehicle_id`),
+ UNIQUE KEY `uk_dept_config` (`config_type`, `dept_id`),
+ KEY `idx_dept_id` (`dept_id`),
+ KEY `idx_vehicle_id` (`vehicle_id`),
+ KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='杞﹁締寮傚父鍛婅閰嶇疆琛�';
+
+-- 鎻掑叆鍏ㄥ眬榛樿閰嶇疆
+INSERT INTO `tb_vehicle_alert_config` (
+ `config_type`, `dept_id`, `vehicle_id`, `mileage_threshold`, `daily_alert_limit`,
+ `alert_interval`, `status`, `create_by`, `remark`
+) VALUES (
+ 'GLOBAL', NULL, NULL, 10.000, 5, 5, '0', 'admin', '鍏ㄥ眬榛樿閰嶇疆锛氳溅杈嗘棤浠诲姟瓒�10鍏噷鍛婅锛屾瘡澶╂渶澶�5娆★紝闂撮殧5鍒嗛挓'
+);
+
+-- 娣诲姞绯荤粺閰嶇疆鍙傛暟
+INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time)
+VALUES ('杞﹁締寮傚父鍛婅鍚敤寮�鍏�', 'vehicle.alert.enabled', 'true', 'Y', '鎺у埗杞﹁締寮傚父杩愯鍛婅鍔熻兘鐨勬�诲紑鍏炽�倀rue=鍚敤锛宖alse=绂佺敤', 'admin', NOW())
+ON DUPLICATE KEY UPDATE config_value = config_value;
+
+INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time)
+VALUES ('杞﹁締寮傚父鍛婅鍏噷鏁伴槇鍊�', 'vehicle.alert.mileage.threshold', '10', 'Y', '杞﹁締鏃犱换鍔¤繍琛岃秴杩囪鍏噷鏁版椂瑙﹀彂鍛婅锛堝崟浣嶏細鍏噷锛�', 'admin', NOW())
+ON DUPLICATE KEY UPDATE config_value = config_value;
+
+INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time)
+VALUES ('杞﹁締寮傚父鍛婅姣忔棩娆℃暟闄愬埗', 'vehicle.alert.daily.limit', '5', 'Y', '姣忓彴杞︽瘡澶╂渶澶氬憡璀︽鏁�', 'admin', NOW())
+ON DUPLICATE KEY UPDATE config_value = config_value;
+
+INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time)
+VALUES ('杞﹁締寮傚父鍛婅闂撮殧鏃堕棿', 'vehicle.alert.interval.minutes', '5', 'Y', '鍚屼竴杞﹁締涓ゆ鍛婅涔嬮棿鐨勬渶灏忛棿闅旀椂闂达紙鍗曚綅锛氬垎閽燂級', 'admin', NOW())
+ON DUPLICATE KEY UPDATE config_value = config_value;
+
+INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time)
+VALUES ('杞﹁締寮傚父鍛婅閫氱煡鐢ㄦ埛', 'vehicle.alert.notify.users', '', 'Y', '鎺ユ敹鍛婅閫氱煡鐨勭敤鎴稩D鍒楄〃锛屽涓敤鎴风敤閫楀彿鍒嗛殧銆備负绌烘椂鏍规嵁杞﹁締褰掑睘閮ㄩ棬鍙戦�佺粰鍒嗗叕鍙歌礋璐d汉', 'admin', NOW())
+ON DUPLICATE KEY UPDATE config_value = config_value;
+
+INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time)
+VALUES ('杞﹁締寮傚父鍛婅鐩戞帶鏃堕棿绐楀彛', 'vehicle.alert.time.window', '10', 'Y', '鐩戞帶鏃堕棿绐楀彛锛堝崟浣嶏細鍒嗛挓锛夛紝鐢ㄤ簬璁$畻杞﹁締鍦ㄨ鏃堕棿绐楀彛鍐呯殑杩愯鍏噷鏁�', 'admin', NOW())
+ON DUPLICATE KEY UPDATE config_value = config_value;
+
+-- 鍒涘缓瀹氭椂浠诲姟
+INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, update_by, update_time, remark)
+VALUES ('杞﹁締寮傚父杩愯鐩戞帶浠诲姟', 'DEFAULT', 'vehicleAbnormalAlertTask.monitorVehicleAbnormalRunning()', '0 */5 * * * ?', '3', '0', '1', 'admin', NOW(), 'admin', NOW(), '姣�5鍒嗛挓鎵ц涓�娆★紝鐩戞帶杞﹁締鏃犱换鍔¤秴鍏噷杩愯鎯呭喌')
+ON DUPLICATE KEY UPDATE invoke_target = invoke_target;
+
+-- =====================================================
+-- 鑿滃崟鏉冮檺閰嶇疆
+-- =====================================================
+
+-- 1. 鍒涘缓杞﹁締鐩戞帶鐖惰彍鍗曪紙濡傛灉涓嶅瓨鍦級
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '杞﹁締鐩戞帶', 0, 5, 'vehicle-monitor', NULL, 1, 0, 'M', '0', '0', '', 'monitor', 'admin', NOW(), 'admin', NOW(), '杞﹁締鐩戞帶绠$悊鐩綍'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_name = '杞﹁締鐩戞帶' AND menu_type = 'M');
+
+-- 鑾峰彇杞﹁締鐩戞帶鐖惰彍鍗旾D
+SET @vehicleMonitorMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = '杞﹁締鐩戞帶' AND menu_type = 'M' LIMIT 1);
+
+-- 2. 鍒涘缓杞﹁締寮傚父鍛婅鑿滃崟
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '杞﹁締寮傚父鍛婅', @vehicleMonitorMenuId, 1, 'vehicleAlert', 'system/vehicleAlert/index', 1, 0, 'C', '0', '0', 'system:vehicleAlert:list', 'warning', 'admin', NOW(), 'admin', NOW(), '杞﹁締寮傚父杩愯鍛婅绠$悊'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_name = '杞﹁締寮傚父鍛婅' AND perms = 'system:vehicleAlert:list');
+
+-- 鑾峰彇杞﹁締寮傚父鍛婅鑿滃崟ID
+SET @vehicleAlertMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = '杞﹁締寮傚父鍛婅' AND perms = 'system:vehicleAlert:list' LIMIT 1);
+
+-- 3. 鍒涘缓杞﹁締寮傚父鍛婅鍔熻兘鎸夐挳
+-- 3.1 鏌ヨ鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '鍛婅鏌ヨ', @vehicleAlertMenuId, 1, '#', '', 1, 0, 'F', '0', '0', 'system:vehicleAlert:query', '#', 'admin', NOW(), 'admin', NOW(), ''
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleAlert:query');
+
+-- 3.2 澶勭悊鍛婅鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '澶勭悊鍛婅', @vehicleAlertMenuId, 2, '#', '', 1, 0, 'F', '0', '0', 'system:vehicleAlert:handle', '#', 'admin', NOW(), 'admin', NOW(), ''
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleAlert:handle');
+
+-- 3.3 鍒犻櫎鍛婅鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '鍒犻櫎鍛婅', @vehicleAlertMenuId, 3, '#', '', 1, 0, 'F', '0', '0', 'system:vehicleAlert:remove', '#', 'admin', NOW(), 'admin', NOW(), ''
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleAlert:remove');
+
+-- 3.4 瀵煎嚭鍛婅鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '瀵煎嚭鍛婅', @vehicleAlertMenuId, 4, '#', '', 1, 0, 'F', '0', '0', 'system:vehicleAlert:export', '#', 'admin', NOW(), 'admin', NOW(), ''
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleAlert:export');
+
+-- 4. 鍒涘缓鍛婅閰嶇疆绠$悊鑿滃崟
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '鍛婅閰嶇疆绠$悊', @vehicleMonitorMenuId, 2, 'vehicleAlertConfig', 'system/vehicleAlertConfig/index', 1, 0, 'C', '0', '0', 'system:vehicleAlertConfig:list', 'edit', 'admin', NOW(), 'admin', NOW(), '杞﹁締鍛婅閰嶇疆绠$悊'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_name = '鍛婅閰嶇疆绠$悊' AND perms = 'system:vehicleAlertConfig:list');
+
+-- 鑾峰彇鍛婅閰嶇疆绠$悊鑿滃崟ID
+SET @vehicleAlertConfigMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = '鍛婅閰嶇疆绠$悊' AND perms = 'system:vehicleAlertConfig:list' LIMIT 1);
+
+-- 5. 鍒涘缓鍛婅閰嶇疆绠$悊鍔熻兘鎸夐挳
+-- 5.1 鏌ヨ鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '閰嶇疆鏌ヨ', @vehicleAlertConfigMenuId, 1, '#', '', 1, 0, 'F', '0', '0', 'system:vehicleAlertConfig:query', '#', 'admin', NOW(), 'admin', NOW(), ''
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleAlertConfig:query');
+
+-- 5.2 鏂板鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '鏂板閰嶇疆', @vehicleAlertConfigMenuId, 2, '#', '', 1, 0, 'F', '0', '0', 'system:vehicleAlertConfig:add', '#', 'admin', NOW(), 'admin', NOW(), ''
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleAlertConfig:add');
+
+-- 5.3 淇敼鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '淇敼閰嶇疆', @vehicleAlertConfigMenuId, 3, '#', '', 1, 0, 'F', '0', '0', 'system:vehicleAlertConfig:edit', '#', 'admin', NOW(), 'admin', NOW(), ''
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleAlertConfig:edit');
+
+-- 5.4 鍒犻櫎鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '鍒犻櫎閰嶇疆', @vehicleAlertConfigMenuId, 4, '#', '', 1, 0, 'F', '0', '0', 'system:vehicleAlertConfig:remove', '#', 'admin', NOW(), 'admin', NOW(), ''
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleAlertConfig:remove');
+
+-- 5.5 瀵煎嚭鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '瀵煎嚭閰嶇疆', @vehicleAlertConfigMenuId, 5, '#', '', 1, 0, 'F', '0', '0', 'system:vehicleAlertConfig:export', '#', 'admin', NOW(), 'admin', NOW(), ''
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleAlertConfig:export');
diff --git a/sql/vehicle_abnormal_alert_upgrade.sql b/sql/vehicle_abnormal_alert_upgrade.sql
new file mode 100644
index 0000000..27e379f
--- /dev/null
+++ b/sql/vehicle_abnormal_alert_upgrade.sql
@@ -0,0 +1,150 @@
+-- =====================================================
+-- 杞﹁締寮傚父杩愯鐩戞帶鍛婅鍔熻兘 - 鏁版嵁搴撳崌绾ц剼鏈�
+-- 鐢ㄤ簬鏇存柊宸插瓨鍦ㄧ殑琛ㄧ粨鏋�
+-- =====================================================
+
+-- 妫�鏌ュ苟淇敼 tb_vehicle_alert_config 琛ㄧ粨鏋�
+-- 濡傛灉琛ㄥ凡瀛樺湪浣嗗瓧娈典笉姝g‘锛岄渶瑕佸厛鍒犻櫎鏃у瓧娈碉紝鍐嶆坊鍔犳柊瀛楁
+
+-- 1. 鍒犻櫎鏃х殑鍞竴绱㈠紩
+ALTER TABLE `tb_vehicle_alert_config` DROP INDEX IF EXISTS `uk_type_target`;
+
+-- 2. 妫�鏌ユ槸鍚﹀瓨鍦� target_id 瀛楁锛屽鏋滃瓨鍦ㄥ垯鍒犻櫎
+SET @exist_target_id := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_alert_config'
+ AND COLUMN_NAME = 'target_id');
+
+SET @sql_drop_target_id = IF(@exist_target_id > 0,
+ 'ALTER TABLE `tb_vehicle_alert_config` DROP COLUMN `target_id`',
+ 'SELECT ''target_id column does not exist''');
+PREPARE stmt FROM @sql_drop_target_id;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 3. 娣诲姞 dept_id 瀛楁锛堝鏋滀笉瀛樺湪锛�
+SET @exist_dept_id := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_alert_config'
+ AND COLUMN_NAME = 'dept_id');
+
+SET @sql_add_dept_id = IF(@exist_dept_id = 0,
+ 'ALTER TABLE `tb_vehicle_alert_config` ADD COLUMN `dept_id` bigint(20) DEFAULT NULL COMMENT ''閮ㄩ棬ID锛堥儴闂ㄩ厤缃椂浣跨敤锛�'' AFTER `config_type`',
+ 'SELECT ''dept_id column already exists''');
+PREPARE stmt FROM @sql_add_dept_id;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 4. 娣诲姞 vehicle_id 瀛楁锛堝鏋滀笉瀛樺湪锛�
+SET @exist_vehicle_id := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_alert_config'
+ AND COLUMN_NAME = 'vehicle_id');
+
+SET @sql_add_vehicle_id = IF(@exist_vehicle_id = 0,
+ 'ALTER TABLE `tb_vehicle_alert_config` ADD COLUMN `vehicle_id` bigint(20) DEFAULT NULL COMMENT ''杞﹁締ID锛堣溅杈嗛厤缃椂浣跨敤锛�'' AFTER `dept_id`',
+ 'SELECT ''vehicle_id column already exists''');
+PREPARE stmt FROM @sql_add_vehicle_id;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 5. 妫�鏌ユ槸鍚﹀瓨鍦� notify_roles 瀛楁锛屽鏋滃瓨鍦ㄥ垯鍒犻櫎
+SET @exist_notify_roles := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_alert_config'
+ AND COLUMN_NAME = 'notify_roles');
+
+SET @sql_drop_notify_roles = IF(@exist_notify_roles > 0,
+ 'ALTER TABLE `tb_vehicle_alert_config` DROP COLUMN `notify_roles`',
+ 'SELECT ''notify_roles column does not exist''');
+PREPARE stmt FROM @sql_drop_notify_roles;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 6. 淇敼 enabled 瀛楁涓� status锛堝鏋滃瓨鍦� enabled锛�
+SET @exist_enabled := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_alert_config'
+ AND COLUMN_NAME = 'enabled');
+
+SET @sql_change_enabled = IF(@exist_enabled > 0,
+ 'ALTER TABLE `tb_vehicle_alert_config` CHANGE COLUMN `enabled` `status` char(1) DEFAULT ''0'' COMMENT ''鐘舵�侊紙0-鍚敤 1-鍋滅敤锛�''',
+ 'SELECT ''enabled column does not exist, may already be status''');
+PREPARE stmt FROM @sql_change_enabled;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 7. 鍒犻櫎鏃х储寮�
+ALTER TABLE `tb_vehicle_alert_config` DROP INDEX IF EXISTS `idx_target_id`;
+ALTER TABLE `tb_vehicle_alert_config` DROP INDEX IF EXISTS `idx_enabled`;
+
+-- 8. 鍒涘缓鏂扮殑鍞竴绱㈠紩
+-- 鍏堟鏌ョ储寮曟槸鍚﹀瓨鍦�
+SET @exist_uk_vehicle := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_alert_config'
+ AND INDEX_NAME = 'uk_vehicle_config');
+
+SET @sql_add_uk_vehicle = IF(@exist_uk_vehicle = 0,
+ 'ALTER TABLE `tb_vehicle_alert_config` ADD UNIQUE KEY `uk_vehicle_config` (`config_type`, `vehicle_id`)',
+ 'SELECT ''uk_vehicle_config index already exists''');
+PREPARE stmt FROM @sql_add_uk_vehicle;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @exist_uk_dept := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_alert_config'
+ AND INDEX_NAME = 'uk_dept_config');
+
+SET @sql_add_uk_dept = IF(@exist_uk_dept = 0,
+ 'ALTER TABLE `tb_vehicle_alert_config` ADD UNIQUE KEY `uk_dept_config` (`config_type`, `dept_id`)',
+ 'SELECT ''uk_dept_config index already exists''');
+PREPARE stmt FROM @sql_add_uk_dept;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 9. 鍒涘缓鏂扮殑鏅�氱储寮�
+SET @exist_idx_dept := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_alert_config'
+ AND INDEX_NAME = 'idx_dept_id');
+
+SET @sql_add_idx_dept = IF(@exist_idx_dept = 0,
+ 'ALTER TABLE `tb_vehicle_alert_config` ADD KEY `idx_dept_id` (`dept_id`)',
+ 'SELECT ''idx_dept_id index already exists''');
+PREPARE stmt FROM @sql_add_idx_dept;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @exist_idx_vehicle := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_alert_config'
+ AND INDEX_NAME = 'idx_vehicle_id');
+
+SET @sql_add_idx_vehicle = IF(@exist_idx_vehicle = 0,
+ 'ALTER TABLE `tb_vehicle_alert_config` ADD KEY `idx_vehicle_id` (`vehicle_id`)',
+ 'SELECT ''idx_vehicle_id index already exists''');
+PREPARE stmt FROM @sql_add_idx_vehicle;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @exist_idx_status := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_alert_config'
+ AND INDEX_NAME = 'idx_status');
+
+SET @sql_add_idx_status = IF(@exist_idx_status = 0,
+ 'ALTER TABLE `tb_vehicle_alert_config` ADD KEY `idx_status` (`status`)',
+ 'SELECT ''idx_status index already exists''');
+PREPARE stmt FROM @sql_add_idx_status;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 10. 鏇存柊宸叉湁鐨勫叏灞�閰嶇疆璁板綍
+UPDATE `tb_vehicle_alert_config`
+SET `dept_id` = NULL, `vehicle_id` = NULL
+WHERE `config_type` = 'GLOBAL';
+
+-- 鍗囩骇瀹屾垚鎻愮ず
+SELECT '鏁版嵁搴撳崌绾у畬鎴愶紒tb_vehicle_alert_config 琛ㄧ粨鏋勫凡鏇存柊' AS 'Status';
diff --git a/sql/vehicle_sync_menu.sql b/sql/vehicle_sync_menu.sql
new file mode 100644
index 0000000..63d49ec
--- /dev/null
+++ b/sql/vehicle_sync_menu.sql
@@ -0,0 +1,35 @@
+-- 杞﹁締鍚屾绠$悊鑿滃崟 SQL
+-- 鑿滃崟 ID鑷锛屽彲浠ユ牴鎹綘鐨勬暟鎹簱瀹為檯鎯呭喌璋冩暣
+
+-- 鐖惰彍鍗曪紙杞﹁締绠$悊锛塈D锛屽亣璁句负2020锛岄渶瑕佹牴鎹疄闄呮儏鍐佃皟鏁�
+SET @parentMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = '杞﹁締绠$悊' AND menu_type = 'M' LIMIT 1);
+
+-- 濡傛灉娌℃湁杞﹁締绠$悊鐖惰彍鍗曪紝鍏堝垱寤�
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '杞﹁締绠$悊', 0, 4, 'vehicle', NULL, 1, 0, 'M', '0', '0', '', 'car', 'admin', sysdate(), '', NULL, '杞﹁締绠$悊鐩綍'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_name = '杞﹁締绠$悊' AND menu_type = 'M');
+
+-- 閲嶆柊鑾峰彇鐖惰彍鍗旾D
+SET @parentMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = '杞﹁締绠$悊' AND menu_type = 'M' LIMIT 1);
+
+-- 杞﹁締鍚屾绠$悊鑿滃崟
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '杞﹁締鍚屾', @parentMenuId, 5, 'vehicleSync', 'system/vehicleSync/index', 1, 0, 'C', '0', '0', 'system:vehicleSync:list', 'upload', 'admin', sysdate(), '', NULL, '杞﹁締鍚屾鑿滃崟'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleSync:list');
+
+-- 鑾峰彇杞﹁締鍚屾鑿滃崟ID
+SET @vehicleSyncMenuId = (SELECT menu_id FROM sys_menu WHERE perms = 'system:vehicleSync:list' LIMIT 1);
+
+-- 杞﹁締鍚屾鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '鍚屾杞﹁締', @vehicleSyncMenuId, 1, '#', '', 1, 0, 'F', '0', '0', 'system:vehicleSync:sync', '#', 'admin', sysdate(), '', NULL, ''
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleSync:sync');
+
+-- 杞﹁締鍚屾鏌ヨ鎸夐挳
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '鏌ヨ', @vehicleSyncMenuId, 2, '#', '', 1, 0, 'F', '0', '0', 'system:vehicleSync:query', '#', 'admin', sysdate(), '', NULL, ''
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'system:vehicleSync:query');
diff --git "a/\345\214\273\351\231\242\344\277\241\346\201\257\345\210\206\350\257\215\346\220\234\347\264\242\345\212\237\350\203\275\350\257\264\346\230\216.md" "b/\345\214\273\351\231\242\344\277\241\346\201\257\345\210\206\350\257\215\346\220\234\347\264\242\345\212\237\350\203\275\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..22154cf
--- /dev/null
+++ "b/\345\214\273\351\231\242\344\277\241\346\201\257\345\210\206\350\257\215\346\220\234\347\264\242\345\212\237\350\203\275\350\257\264\346\230\216.md"
@@ -0,0 +1,274 @@
+# 鍖婚櫌淇℃伅鍒嗚瘝鎼滅储鍔熻兘璇存槑
+
+## 鍔熻兘姒傝堪
+
+鏈姛鑳藉疄鐜颁簡鍩轰簬涓枃鍒嗚瘝鐨勫尰闄俊鎭櫤鑳芥悳绱�,閫氳繃瀵瑰尰闄㈠悕绉般�佸湴鍧�绛変俊鎭繘琛屽垎璇嶅鐞�,鏀寔妯$硦鍖归厤鍜屾潈閲嶆帓搴�,澶у箙鎻愬崌鍖婚櫌鎼滅储鐨勫噯纭�у拰鐢ㄦ埛浣撻獙銆�
+
+## 瀹炵幇鍐呭
+
+### 1. 鏁版嵁搴撳眰闈�
+
+#### 1.1 鏂板鍒嗚瘝瀛楁
+- **鏂囦欢**: `sql/tb_hosp_data_add_keywords.sql`
+- **鍐呭**: 鍦� `tb_hosp_data` 琛ㄤ腑娣诲姞 `hosp_keywords` 瀛楁
+ ```sql
+ ALTER TABLE `tb_hosp_data`
+ ADD COLUMN `hosp_keywords` varchar(500) DEFAULT NULL COMMENT '鍖婚櫌淇℃伅鍒嗚瘝锛堥�楀彿鍒嗛殧锛�';
+
+ CREATE INDEX idx_hosp_keywords ON tb_hosp_data(hosp_keywords);
+ ```
+
+### 2. 瀹炰綋绫诲眰闈�
+
+#### 2.1 TbHospData 瀹炰綋鎵╁睍
+- **鏂囦欢**: `ruoyi-system/src/main/java/com/ruoyi/system/domain/TbHospData.java`
+- **鏂板瀛楁**:
+ - `hospKeywords` - 鍖婚櫌淇℃伅鍒嗚瘝锛堥�楀彿鍒嗛殧锛�
+- **鍖呭惈鏂规硶**: getter銆乻etter 鍙� toString
+
+### 3. 鍒嗚瘝宸ュ叿绫�
+
+#### 3.1 HospitalTokenizerUtil
+- **鏂囦欢**: `ruoyi-common/src/main/java/com/ruoyi/common/utils/HospitalTokenizerUtil.java`
+- **鏍稿績鍔熻兘**:
+ 1. **涓枃鍒嗚瘝**: 浣跨敤 N-Gram 绠楁硶瀵瑰尰闄俊鎭繘琛屽垎璇嶏紙2-4瀛楃缁勫悎锛�
+ 2. **鍋滅敤璇嶈繃婊�**: 鑷姩杩囨护"鍖婚櫌"銆�"甯�"銆�"鐪�"绛夊父瑙佸仠鐢ㄨ瘝
+ 3. **鍏抽敭璇嶆彁鍙�**: 浠庡尰闄㈠悕绉般�佺畝绉般�佺渷甯傚尯銆佸湴鍧�涓彁鍙栧叧閿瘝
+ 4. **鍖归厤搴﹁绠�**: 璁$畻涓や釜鍒嗚瘝闆嗗悎鐨勫尮閰嶅垎鏁�
+
+- **涓昏鏂规硶**:
+ ```java
+ // 鐢熸垚鍖婚櫌淇℃伅鐨勫垎璇�
+ public static String tokenize(String hospName, String hospShort,
+ String province, String city,
+ String area, String address)
+
+ // 瀵规悳绱㈡枃鏈繘琛屽垎璇�
+ public static String tokenizeSearchText(String text)
+
+ // 璁$畻涓や釜鍒嗚瘝闆嗗悎鐨勫尮閰嶅害
+ public static int calculateMatchScore(String keywords1, String keywords2)
+ ```
+
+### 4. Service 灞�
+
+#### 4.1 ITbHospDataService 鎺ュ彛鎵╁睍
+- **鏂囦欢**: `ruoyi-system/src/main/java/com/ruoyi/system/service/ITbHospDataService.java`
+- **鏂板鏂规硶**:
+ ```java
+ // 鎵归噺鐢熸垚骞舵洿鏂版墍鏈夊尰闄㈢殑鍒嗚瘝
+ int generateAllHospitalKeywords();
+
+ // 涓哄崟涓尰闄㈢敓鎴愬垎璇�
+ String generateKeywordsForHospital(TbHospData tbHospData);
+ ```
+
+#### 4.2 TbHospDataServiceImpl 瀹炵幇
+- **鏂囦欢**: `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TbHospDataServiceImpl.java`
+- **鍔熻兘**:
+ - 鎵归噺澶勭悊鎵�鏈夊尰闄㈡暟鎹�,鐢熸垚鍒嗚瘝骞舵洿鏂版暟鎹簱
+ - 鍗曚釜鍖婚櫌鍒嗚瘝鐢熸垚,鍦ㄦ柊澧�/鏇存柊鍖婚櫌鏃惰嚜鍔ㄨ皟鐢�
+
+#### 4.3 HospDataSyncServiceImpl 闆嗘垚
+- **鏂囦欢**: `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/HospDataSyncServiceImpl.java`
+- **鍔熻兘**: 鍦ㄥ尰闄㈠悓姝ユ椂鑷姩鐢熸垚鍒嗚瘝
+ - 浠� SQL Server 鍚屾鍖婚櫌鏁版嵁鏃�,鑷姩璋冪敤鍒嗚瘝鐢熸垚
+ - 纭繚鎵�鏈夊尰闄㈡暟鎹兘鏈夋渶鏂扮殑鍒嗚瘝淇℃伅
+
+### 5. Controller 灞�
+
+#### 5.1 HospDataController 鎵╁睍
+- **鏂囦欢**: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/HospDataController.java`
+
+##### 5.1.1 鎵归噺鐢熸垚鍒嗚瘝鎺ュ彛
+- **鎺ュ彛**: `GET /system/hospital/generateKeywords`
+- **鍔熻兘**: 鎵归噺鐢熸垚鎵�鏈夊尰闄㈢殑鍒嗚瘝锛堢鐞嗗憳鎺ュ彛锛�
+- **杩斿洖**: 鏇存柊鐨勫尰闄㈡暟閲�
+- **浣跨敤鍦烘櫙**:
+ - 棣栨閮ㄧ讲鏃跺垵濮嬪寲鎵�鏈夊尰闄㈠垎璇�
+ - 鍒嗚瘝绠楁硶鍗囩骇鍚庨噸鏂扮敓鎴愬垎璇�
+
+##### 5.1.2 鍩轰簬鍒嗚瘝鍖归厤鎼滅储鎺ュ彛
+- **鎺ュ彛**: `GET /system/hospital/searchByKeywords`
+- **鍙傛暟**:
+ - `searchText` (蹇呭~): 鎼滅储鏂囨湰锛堝尰闄㈠悕绉般�佸湴鍧�绛夛級
+ - `pageSize` (鍙��): 杩斿洖缁撴灉鏁伴噺,榛樿 50
+- **鍔熻兘娴佺▼**:
+ 1. 瀵瑰墠绔紶鍏ョ殑鎼滅储鏂囨湰杩涜鍒嗚瘝
+ 2. 鏌ヨ鎵�鏈夋甯哥姸鎬佺殑鍖婚櫌鏁版嵁
+ 3. 璁$畻姣忎釜鍖婚櫌涓庢悳绱㈡枃鏈殑鍖归厤鍒嗘暟
+ 4. 鎸夊尮閰嶅垎鏁伴檷搴忔帓搴忥紙鏉冮噸鎺掑簭锛�
+ 5. 杩斿洖鍖归厤鐨勫尰闄㈠垪琛�
+
+- **杩斿洖绀轰緥**:
+ ```json
+ {
+ "code": 200,
+ "msg": "鏌ヨ鎴愬姛",
+ "data": [
+ {
+ "hospId": 1001,
+ "hospName": "鍖椾含鍗忓拰鍖婚櫌",
+ "hospShort": "鍗忓拰鍖婚櫌",
+ "hopsProvince": "鍖椾含甯�",
+ "hopsCity": "鍖椾含甯�",
+ "hopsArea": "涓滃煄鍖�",
+ "hospAddress": "涓滃煄鍖哄竻搴滃洯1鍙�"
+ },
+ // ... 鏇村鍖婚櫌鏁版嵁锛堟寜鍖归厤搴︽帓搴忥級
+ ]
+ }
+ ```
+
+### 6. Mapper XML 鏇存柊
+
+#### 6.1 TbHospDataMapper.xml
+- **鏂囦欢**: `ruoyi-system/src/main/resources/mapper/system/TbHospDataMapper.xml`
+- **鏇存柊鍐呭**:
+ - ResultMap 娣诲姞 `hospKeywords` 瀛楁鏄犲皠
+ - SELECT 璇彞鍖呭惈 `hosp_keywords` 瀛楁
+ - INSERT 鍜� UPDATE 鏀寔 `hosp_keywords` 瀛楁
+
+## 浣跨敤璇存槑
+
+### 閮ㄧ讲姝ラ
+
+1. **鎵ц鏁版嵁搴撹剼鏈�**
+ ```bash
+ mysql -u鐢ㄦ埛鍚� -p鏁版嵁搴撳悕 < sql/tb_hosp_data_add_keywords.sql
+ ```
+
+2. **閲嶅惎搴旂敤**
+ ```bash
+ ./ry.sh restart
+ # 鎴� Windows 涓�
+ ry.bat
+ ```
+
+3. **鍒濆鍖栧垎璇嶆暟鎹�**
+ 璋冪敤鎵归噺鐢熸垚鍒嗚瘝鎺ュ彛:
+ ```bash
+ GET http://localhost:8080/system/hospital/generateKeywords
+ ```
+
+### 鍓嶇闆嗘垚绀轰緥
+
+#### 浣跨敤鍒嗚瘝鎼滅储鎺ュ彛
+```javascript
+// 鍦� uni-app 涓皟鐢�
+uni.request({
+ url: '/system/hospital/searchByKeywords',
+ method: 'GET',
+ data: {
+ searchText: '鍖椾含鍗忓拰涓滃煄鍖�',
+ pageSize: 20
+ },
+ success: (res) => {
+ if (res.data.code === 200) {
+ // 鍖婚櫌鍒楄〃宸叉寜鍖归厤搴︽帓搴�
+ const hospitals = res.data.data;
+ console.log('鎵惧埌鍖婚櫌:', hospitals.length);
+ }
+ }
+});
+```
+
+#### Vue.js 绀轰緥
+```javascript
+// 鍦� Vue 缁勪欢涓�
+methods: {
+ async searchHospitals(searchText) {
+ try {
+ const response = await this.$http.get('/system/hospital/searchByKeywords', {
+ params: {
+ searchText: searchText,
+ pageSize: 50
+ }
+ });
+
+ if (response.data.code === 200) {
+ this.hospitals = response.data.data;
+ // 鏁版嵁宸叉寜鍖归厤搴︽帓搴�,鍖归厤瓒婂ソ鐨勫尰闄㈣秺闈犲墠
+ }
+ } catch (error) {
+ console.error('鎼滅储澶辫触:', error);
+ }
+ }
+}
+```
+
+## 鍔熻兘鐗圭偣
+
+### 1. 鏅鸿兘鍒嗚瘝
+- 浣跨敤 N-Gram 绠楁硶鐢熸垚 2-4 瀛楃鐨勫叧閿瘝缁勫悎
+- 鑷姩鎻愬彇鍗曚釜涓枃瀛楃浣滀负琛ュ厖鍏抽敭璇�
+- 鏀寔瀵瑰尰闄㈠悕绉般�佺畝绉般�佺渷甯傚尯銆佸湴鍧�绛夊涓瓧娈靛垎璇�
+
+### 2. 鍋滅敤璇嶈繃婊�
+鑷姩杩囨护浠ヤ笅甯歌鍋滅敤璇�:
+- 閫氱敤璇�: "鍖婚櫌"銆�"璇婃墍"銆�"鍗敓"銆�"闄�"
+- 鍦板尯璇�: "甯�"銆�"鐪�"銆�"鍘�"銆�"鍖�"銆�"闀�"銆�"涔�"
+- 浣嶇疆璇�: "琛楅亾"銆�"璺�"銆�"鍙�"銆�"鏍�"銆�"鍗曞厓"銆�"瀹�"銆�"灞�"銆�"妤�"
+- 杩炴帴璇�: "鐨�"銆�"浜�"銆�"鍦�"銆�"涓�"銆�"鍜�"銆�"鍙�"绛�
+
+### 3. 鏉冮噸鎺掑簭
+- 鏍规嵁鍖归厤鐨勫叧閿瘝鏁伴噺璁$畻鍖归厤鍒嗘暟
+- 鍖归厤鐨勫垎璇嶈秺澶�,鎺掑悕瓒婇潬鍓�
+- 绮剧‘鍖归厤浼樺厛浜庢ā绯婂尮閰�
+
+### 4. 鑷姩鍖栭泦鎴�
+- 鍖婚櫌鍚屾鏃惰嚜鍔ㄧ敓鎴愬垎璇�
+- 鏂板鎴栨洿鏂板尰闄㈡椂鑷姩鏇存柊鍒嗚瘝
+- 鏃犻渶鎵嬪姩骞查,纭繚鏁版嵁涓�鑷存��
+
+## 鎬ц兘浼樺寲
+
+1. **绱㈠紩浼樺寲**: 鍦� `hosp_keywords` 瀛楁涓婂垱寤虹储寮�,鎻愬崌鏌ヨ鎬ц兘
+2. **鍐呭瓨璁$畻**: 鍖归厤搴﹁绠楀湪鍐呭瓨涓繘琛�,閬垮厤鏁版嵁搴撳帇鍔�
+3. **缁撴灉闄愬埗**: 鏀寔 `pageSize` 鍙傛暟闄愬埗杩斿洖鏁伴噺,鍑忓皯缃戠粶浼犺緭
+
+## 娉ㄦ剰浜嬮」
+
+1. **棣栨閮ㄧ讲**: 蹇呴』璋冪敤 `/generateKeywords` 鎺ュ彛鍒濆鍖栨墍鏈夊尰闄㈢殑鍒嗚瘝鏁版嵁
+2. **鏁版嵁鍚屾**: 鍖婚櫌鍚屾浼氳嚜鍔ㄦ洿鏂板垎璇�,鏃犻渶棰濆鎿嶄綔
+3. **鍒嗚瘝璐ㄩ噺**: 鍒嗚瘝绠楁硶鍩轰簬绠�鍗曠殑 N-Gram,瀵逛簬澶嶆潅鐨勫尰闄㈠悕绉板彲鑳介渶瑕佷紭鍖�
+4. **鍋滅敤璇嶇淮鎶�**: 鍙牴鎹疄闄呬笟鍔¢渶姹傚湪 `HospitalTokenizerUtil` 涓皟鏁村仠鐢ㄨ瘝鍒楄〃
+
+## 鍚庣画浼樺寲寤鸿
+
+1. **闆嗘垚绗笁鏂瑰垎璇�**:
+ - 鍙�冭檻闆嗘垚 IK Analyzer銆丠anLP 绛変笓涓氫腑鏂囧垎璇嶅簱
+ - 鎻愬崌鍒嗚瘝鍑嗙‘搴﹀拰鍙洖鐜�
+
+2. **鎷奸煶鏀寔**:
+ - 澧炲姞鎷奸煶绱㈠紩,鏀寔鎷奸煶棣栧瓧姣嶆悳绱�
+ - 渚嬪: "bjxhyy" 鍙互鍖归厤"鍖椾含鍗忓拰鍖婚櫌"
+
+3. **鍚屼箟璇嶆墿灞�**:
+ - 鏀寔鍚屼箟璇嶅尮閰�
+ - 渚嬪: "浜烘皯鍖婚櫌" 鍜� "浜烘皯鍖荤枟涓績"
+
+4. **鎼滅储鍘嗗彶**:
+ - 璁板綍鐢ㄦ埛鎼滅储鍘嗗彶
+ - 鏍规嵁鍘嗗彶鏁版嵁浼樺寲鎺掑簭绠楁硶
+
+5. **鍦扮悊浣嶇疆鏉冮噸**:
+ - 缁撳悎鐢ㄦ埛浣嶇疆淇℃伅
+ - 浼樺厛杩斿洖闄勮繎鐨勫尰闄�
+
+## 鎶�鏈爤
+
+- **鍚庣妗嗘灦**: Spring Boot + MyBatis
+- **鏁版嵁搴�**: MySQL 5.7+
+- **宸ュ叿绫�**: Apache Commons Lang3
+- **鏃ュ織**: SLF4J + Logback
+
+## 鑱旂郴鏂瑰紡
+
+濡傛湁闂鎴栧缓璁�,璇疯仈绯诲紑鍙戝洟闃熴��
+
+---
+
+**鏂囨。鐗堟湰**: v1.0
+**鏇存柊鏃ユ湡**: 2026-01-20
+**寮�鍙戣��**: RuoYi Team
diff --git "a/\345\214\273\351\231\242\345\210\206\350\257\215\346\220\234\347\264\242-\345\277\253\351\200\237\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/\345\214\273\351\231\242\345\210\206\350\257\215\346\220\234\347\264\242-\345\277\253\351\200\237\344\275\277\347\224\250\346\214\207\345\215\227.md"
new file mode 100644
index 0000000..e1776dc
--- /dev/null
+++ "b/\345\214\273\351\231\242\345\210\206\350\257\215\346\220\234\347\264\242-\345\277\253\351\200\237\344\275\277\347\224\250\346\214\207\345\215\227.md"
@@ -0,0 +1,432 @@
+# 鍖婚櫌鍒嗚瘝鎼滅储鍔熻兘 - 蹇�熶娇鐢ㄦ寚鍗�
+
+## 涓�銆侀儴缃叉楠わ紙4姝ュ畬鎴愶級
+
+### 姝ラ1: 闆嗘垚 HanLP 鍒嗚瘝搴�
+
+**璇存槑**: 宸查泦鎴� HanLP 涓撲笟涓枃鍒嗚瘝搴擄紝鏇夸唬浜嗙畝鍗曠殑 N-Gram 绠楁硶锛屽垎璇嶅噯纭害鏇撮珮銆�
+
+鍦� `ruoyi-common/pom.xml` 涓凡娣诲姞锛�
+```xml
+<!-- HanLP 涓枃鍒嗚瘝搴� -->
+<dependency>
+ <groupId>com.hankcs</groupId>
+ <artifactId>hanlp</artifactId>
+ <version>portable-1.8.4</version>
+</dependency>
+```
+
+### 姝ラ2: 鎵ц鏁版嵁搴撹剼鏈�
+```bash
+# 杩涘叆 SQL 鐩綍
+cd sql
+
+# 1. 鎵ц鑴氭湰娣诲姞鍒嗚瘝瀛楁
+mysql -uroot -p浣犵殑瀵嗙爜 浣犵殑鏁版嵁搴撳悕 < tb_hosp_data_add_keywords.sql
+
+# 2. 娣诲姞鍚庡彴鑿滃崟鏉冮檺
+mysql -uroot -p浣犵殑瀵嗙爜 浣犵殑鏁版嵁搴撳悕 < hospital_tokenizer_menu.sql
+```
+
+### 姝ラ3: 閲嶅惎搴旂敤
+```bash
+# Linux/Mac
+./ry.sh restart
+
+# Windows
+ry.bat
+```
+
+### 姝ラ4: 浣跨敤鍚庡彴娴嬭瘯鐣岄潰鍒濆鍖栧垎璇�
+
+**鏂瑰紡1: 閫氳繃鍚庡彴鐣岄潰锛堟帹鑽愶級**
+
+1. 鐧诲綍鍚庡彴绠$悊绯荤粺
+2. 杩涘叆鑿滃崟锛�**绯荤粺绠$悊 > 鍖婚櫌绠$悊 > 鍖婚櫌鍒嗚瘝娴嬭瘯**
+3. 鐐瑰嚮銆�**鎵归噺鐢熸垚鎵�鏈夊尰闄㈠垎璇�**銆嶆寜閽�
+4. 绛夊緟鐢熸垚瀹屾垚
+
+**鏂瑰紡2: 閫氳繃 API 鎺ュ彛**
+
+浣跨敤 Postman 鎴栨祻瑙堝櫒璁块棶锛�
+```
+GET http://localhost:8080/system/hospital/generateKeywords
+```
+
+绛夊緟杩斿洖缁撴灉,鏄剧ず鎴愬姛鐢熸垚鐨勫尰闄㈡暟閲忋��
+
+---
+
+## 浜屻�佸悗鍙版祴璇曠晫闈娇鐢�
+
+### 璁块棶璺緞
+**绯荤粺绠$悊 > 鍖婚櫌绠$悊 > 鍖婚櫌鍒嗚瘝娴嬭瘯**
+
+### 鍔熻兘璇存槑
+
+#### 1. 鎵归噺鍒嗚瘝绠$悊
+- **鍔熻兘**: 涓烘墍鏈夊尰闄㈡壒閲忕敓鎴愬垎璇嶆暟鎹�
+- **浣跨敤鍦烘櫙**:
+ - 棣栨閮ㄧ讲鏃跺垵濮嬪寲
+ - 鍒嗚瘝绠楁硶鍗囩骇鍚庨噸鏂扮敓鎴�
+ - 鏁版嵁淇鍚庢洿鏂�
+- **鎿嶄綔**: 鐐瑰嚮銆屾壒閲忕敓鎴愭墍鏈夊尰闄㈠垎璇嶃�嶆寜閽�
+- **鑰楁椂**: 鏍规嵁鍖婚櫌鏁伴噺锛岄�氬父闇�瑕� 1-5 鍒嗛挓
+
+#### 2. 鍖婚櫌鍖归厤娴嬭瘯
+- **鍔熻兘**: 娴嬭瘯鍒嗚瘝鎼滅储鏁堟灉
+- **浣跨敤鏂规硶**:
+ 1. 鍦ㄦ悳绱㈡杈撳叆鍖婚櫌鍚嶇О銆佸湴鍧�鎴栧叧閿瘝
+ 2. 璁剧疆杩斿洖缁撴灉鏁伴噺锛堥粯璁� 30 鏉★級
+ 3. 鐐瑰嚮銆屾悳绱€�嶆寜閽�
+ 4. 鏌ョ湅鍖归厤缁撴灉锛堟寜鐩稿叧搴︽帓搴忥級
+
+#### 3. 娴嬭瘯绀轰緥
+```
+杈撳叆: "鍖椾含鍗忓拰鍖婚櫌"
+缁撴灉: 鏄剧ず鎵�鏈夊寘鍚�滃寳浜�濆拰鈥滃崗鍜屸�濈殑鍖婚櫌
+
+杈撳叆: "涓婃捣鐟為噾鍖婚櫌鍗㈡咕鍖�"
+缁撴灉: 涓婃捣鐟為噾鍖婚櫌鍙婂叾鍒嗛櫌鎺掑湪鏈�鍓�
+
+杈撳叆: "浜烘皯鍖婚櫌鏈濋槼鍖�"
+缁撴灉: 鏈濋槼鍖虹殑浜烘皯鍖婚櫌鐩稿叧缁撴灉
+```
+
+### 鐣岄潰鎴浘璇存槑
+
+1. **鎵归噺鍒嗚瘝鍖哄煙**
+ - 灞曠ず鎻愮ず淇℃伅
+ - 鐢熸垚鎸夐挳锛堝甫 loading 鐘舵�侊級
+ - 缁撴灉鏄剧ず锛堟垚鍔�/澶辫触锛�
+
+2. **鎼滅储娴嬭瘯鍖哄煙**
+ - 鎼滅储杈撳叆妗�
+ - 缁撴灉鏁伴噺璁剧疆
+ - 鍒嗚瘝缁撴灉灞曠ず锛堟爣绛惧舰寮忥級
+ - 鍖婚櫌鍒楄〃琛ㄦ牸
+ - 缁熻淇℃伅
+
+---
+
+## 涓夈�丄PI 鎺ュ彛浣跨敤
+
+### 鎺ュ彛1: 鎵归噺鐢熸垚鍒嗚瘝锛堢鐞嗗憳锛�
+
+**鎺ュ彛**: `GET /system/hospital/generateKeywords`
+
+**璇存槑**: 鎵归噺涓烘墍鏈夊尰闄㈢敓鎴愬垎璇�,鐢ㄤ簬鍒濆鍖栨垨閲嶆柊鐢熸垚
+
+**璇锋眰绀轰緥**:
+```bash
+curl -X GET "http://localhost:8080/system/hospital/generateKeywords"
+```
+
+**杩斿洖绀轰緥**:
+```json
+{
+ "code": 200,
+ "msg": "鎴愬姛鐢熸垚 1523 涓尰闄㈢殑鍒嗚瘝"
+}
+```
+
+---
+
+### 鎺ュ彛2: 鍒嗚瘝鎼滅储鍖婚櫌锛堟牳蹇冨姛鑳斤級
+
+**鎺ュ彛**: `GET /system/hospital/searchByKeywords`
+
+**鍙傛暟**:
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 | 绀轰緥 |
+|-----|------|------|------|------|
+| searchText | String | 鏄� | 鎼滅储鏂囨湰 | "鍖椾含鍗忓拰涓滃煄鍖�" |
+| pageSize | Integer | 鍚� | 杩斿洖鏁伴噺,榛樿50 | 20 |
+
+**璇锋眰绀轰緥**:
+```bash
+# 鎼滅储 "鍖椾含鍗忓拰鍖婚櫌"
+curl -X GET "http://localhost:8080/system/hospital/searchByKeywords?searchText=鍖椾含鍗忓拰鍖婚櫌&pageSize=20"
+
+# 鎼滅储 "涓婃捣鐟為噾"
+curl -X GET "http://localhost:8080/system/hospital/searchByKeywords?searchText=涓婃捣鐟為噾"
+```
+
+**杩斿洖绀轰緥**:
+```json
+{
+ "code": 200,
+ "msg": "鏌ヨ鎴愬姛",
+ "data": [
+ {
+ "hospId": 1001,
+ "hospName": "鍖椾含鍗忓拰鍖婚櫌",
+ "hospShort": "鍗忓拰鍖婚櫌",
+ "hopsProvince": "鍖椾含甯�",
+ "hopsCity": "鍖椾含甯�",
+ "hopsArea": "涓滃煄鍖�",
+ "hospAddress": "涓滃煄鍖哄竻搴滃洯1鍙�",
+ "hospTel": "010-69156114"
+ },
+ {
+ "hospId": 1002,
+ "hospName": "棣栭兘鍖荤澶у闄勫睘鍖椾含鍗忓拰鍖婚櫌瑗块櫌",
+ "hospShort": "鍗忓拰瑗块櫌",
+ "hopsProvince": "鍖椾含甯�",
+ "hopsCity": "鍖椾含甯�",
+ "hopsArea": "瑗垮煄鍖�",
+ "hospAddress": "瑗垮煄鍖哄ぇ鏈ㄤ粨鑳″悓41鍙�"
+ }
+ // ... 鏇村鍖归厤缁撴灉锛堟寜鍖归厤搴﹁嚜鍔ㄦ帓搴忥級
+ ]
+}
+```
+
+---
+
+## 鍥涖�佸墠绔泦鎴愮ず渚�
+
+### uni-app 闆嗘垚
+
+```javascript
+// 鍦ㄤ綘鐨勯〉闈㈡垨缁勪欢涓�
+export default {
+ data() {
+ return {
+ searchText: '',
+ hospitals: []
+ }
+ },
+ methods: {
+ // 鎼滅储鍖婚櫌
+ searchHospitals() {
+ if (!this.searchText.trim()) {
+ uni.showToast({
+ title: '璇疯緭鍏ユ悳绱㈠唴瀹�',
+ icon: 'none'
+ });
+ return;
+ }
+
+ uni.showLoading({ title: '鎼滅储涓�...' });
+
+ uni.request({
+ url: this.$baseUrl + '/system/hospital/searchByKeywords',
+ method: 'GET',
+ data: {
+ searchText: this.searchText,
+ pageSize: 30
+ },
+ success: (res) => {
+ uni.hideLoading();
+
+ if (res.data.code === 200) {
+ this.hospitals = res.data.data;
+ console.log('鎵惧埌鍖婚櫌:', this.hospitals.length);
+
+ if (this.hospitals.length === 0) {
+ uni.showToast({
+ title: '鏈壘鍒板尮閰嶇殑鍖婚櫌',
+ icon: 'none'
+ });
+ }
+ } else {
+ uni.showToast({
+ title: res.data.msg || '鎼滅储澶辫触',
+ icon: 'none'
+ });
+ }
+ },
+ fail: (err) => {
+ uni.hideLoading();
+ uni.showToast({
+ title: '缃戠粶璇锋眰澶辫触',
+ icon: 'none'
+ });
+ console.error('鎼滅储澶辫触:', err);
+ }
+ });
+ },
+
+ // 閫夋嫨鍖婚櫌
+ selectHospital(hospital) {
+ // 灏嗛�変腑鐨勫尰闄俊鎭洖濉埌琛ㄥ崟
+ console.log('閫夋嫨浜嗗尰闄�:', hospital.hospName);
+ // 浣犵殑涓氬姟閫昏緫...
+ }
+ }
+}
+```
+
+### Vue.js + Axios 闆嗘垚
+
+```javascript
+// 鍦� Vue 缁勪欢涓�
+<template>
+ <div>
+ <el-input
+ v-model="searchText"
+ placeholder="杈撳叆鍖婚櫌鍚嶇О鎴栧湴鍧�"
+ @keyup.enter="searchHospitals">
+ <el-button slot="append" icon="el-icon-search" @click="searchHospitals"></el-button>
+ </el-input>
+
+ <el-table :data="hospitals" v-loading="loading">
+ <el-table-column prop="hospName" label="鍖婚櫌鍚嶇О"></el-table-column>
+ <el-table-column prop="hopsCity" label="鍩庡競"></el-table-column>
+ <el-table-column prop="hospAddress" label="鍦板潃"></el-table-column>
+ <el-table-column label="鎿嶄綔">
+ <template slot-scope="scope">
+ <el-button size="mini" @click="selectHospital(scope.row)">閫夋嫨</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ searchText: '',
+ hospitals: [],
+ loading: false
+ }
+ },
+ methods: {
+ async searchHospitals() {
+ if (!this.searchText.trim()) {
+ this.$message.warning('璇疯緭鍏ユ悳绱㈠唴瀹�');
+ return;
+ }
+
+ this.loading = true;
+
+ try {
+ const response = await this.$http.get('/system/hospital/searchByKeywords', {
+ params: {
+ searchText: this.searchText,
+ pageSize: 50
+ }
+ });
+
+ if (response.data.code === 200) {
+ this.hospitals = response.data.data;
+
+ if (this.hospitals.length === 0) {
+ this.$message.info('鏈壘鍒板尮閰嶇殑鍖婚櫌');
+ } else {
+ this.$message.success(`鎵惧埌 ${this.hospitals.length} 涓尮閰嶇殑鍖婚櫌`);
+ }
+ } else {
+ this.$message.error(response.data.msg || '鎼滅储澶辫触');
+ }
+ } catch (error) {
+ console.error('鎼滅储澶辫触:', error);
+ this.$message.error('缃戠粶璇锋眰澶辫触');
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ selectHospital(hospital) {
+ // 澶勭悊閫夋嫨鍖婚櫌鐨勯�昏緫
+ console.log('閫夋嫨浜嗗尰闄�:', hospital);
+ this.$emit('hospital-selected', hospital);
+ }
+ }
+}
+</script>
+```
+
+---
+
+## 浜斻�佸姛鑳借鏄�
+
+### 1. 鍒嗚瘝鐗圭偣
+- **HanLP 涓撲笟鍒嗚瘝**: 浣跨敤涓氱晫閫氱敤鐨� HanLP 涓枃鍒嗚瘝搴�
+- **鏅鸿兘鍒嗚瘝**: 鑷姩灏嗗尰闄㈠悕绉般�佸湴鍧�绛夊垎瑙d负澶氫釜鍏抽敭璇�
+- **鍋滅敤璇嶈繃婊�**: 杩囨护鈥滃尰闄⑩�濄�佲�滃競鈥濄�佲�滅渷鈥濈瓑甯歌璇�
+- **闄嶇骇鏂规**: HanLP 澶辫触鏃惰嚜鍔ㄩ檷绾у埌 N-Gram 绠楁硶
+- **鏀寔妯$硦鍖归厤**: 杈撳叆閮ㄥ垎鍏抽敭璇嶅嵆鍙尮閰�
+
+### 2. 鏉冮噸鎺掑簭
+- 鍖归厤鐨勫叧閿瘝瓒婂,鎺掑悕瓒婇潬鍓�
+- 瀹屽叏鍖归厤浼樺厛浜庨儴鍒嗗尮閰�
+- 鑷姩鎸夌浉鍏冲害鎺掑簭,鏃犻渶鎵嬪姩绛涢��
+
+### 3. 浣跨敤鍦烘櫙
+- 鉁� 鐢ㄦ埛杈撳叆 "鍖椾含鍗忓拰" 鈫� 鍖归厤鎵�鏈夊甫"鍖椾含"鍜�"鍗忓拰"鐨勫尰闄�
+- 鉁� 鐢ㄦ埛杈撳叆 "涓滃煄鍖轰汉姘�" 鈫� 鍖归厤"涓滃煄鍖�"鍜�"浜烘皯"鐩稿叧鐨勫尰闄�
+- 鉁� 鐢ㄦ埛杈撳叆 "鐟為噾鍖婚櫌鍗㈡咕" 鈫� 鍖归厤涓婃捣鐟為噾鍖婚櫌鍙婂叾鍒嗛櫌
+
+---
+
+## 鍏�佸父瑙侀棶棰�
+
+### Q1: 棣栨閮ㄧ讲鍚庢悳绱笉鍒颁换浣曠粨鏋�?
+**A**: 闇�瑕佸厛璋冪敤 `/generateKeywords` 鎺ュ彛鍒濆鍖栧垎璇嶆暟鎹��
+
+### Q2: 鏂板鎴栦慨鏀瑰尰闄㈠悗闇�瑕侀噸鏂扮敓鎴愬垎璇嶅悧?
+**A**: 涓嶉渶瑕併�傜郴缁熶細鑷姩鍦ㄦ柊澧�/淇敼/鍚屾鍖婚櫌鏃剁敓鎴愬垎璇嶃��
+
+### Q3: 鎼滅储閫熷害鎱㈡�庝箞鍔�?
+**A**:
+1. 纭宸茬粡鍦� `hosp_keywords` 瀛楁涓婂垱寤虹储寮�
+2. 閫傚綋鍑忓皬 `pageSize` 鍙傛暟鍊�
+3. 鑰冭檻澧炲姞鏈嶅姟鍣ㄥ唴瀛�
+
+### Q4: 鍚庡彴鑿滃崟鐪嬩笉鍒扳�滃尰闄㈢鐞嗏��?
+**A**:
+1. 纭鎵ц浜� `hospital_tokenizer_menu.sql` 鑴氭湰
+2. 鍦ㄢ�滅郴缁熺鐞� > 瑙掕壊绠$悊鈥濅腑锛岀粰褰撳墠瑙掕壊鍒嗛厤鑿滃崟鏉冮檺
+3. 閲嶆柊鐧诲綍鍚庡彴绯荤粺
+
+### Q5: HanLP 鍒嗚瘝搴撲笅杞芥參鎬庝箞鍔�?
+**A**:
+1. 浣跨敤闃块噷浜戞垨鑵捐浜� Maven 闀滃儚
+2. 鎴栬�呮墜鍔ㄤ笅杞� HanLP jar 鍖呮斁鍒版湰鍦� Maven 浠撳簱
+
+### Q6: 濡備綍浼樺寲鎼滅储鍑嗙‘搴�?
+**A**:
+1. 璋冩暣 `HospitalTokenizerUtil` 涓殑鍋滅敤璇嶅垪琛�
+2. 鏍规嵁涓氬姟闇�姹傛坊鍔犲尰闄㈠埆鍚嶆槧灏�
+3. 浣跨敤鍚庡彴娴嬭瘯鐣岄潰鍙嶅娴嬭瘯鍜岃皟浼�
+
+---
+
+## 涓冦�佺淮鎶ゅ缓璁�
+
+### 瀹氭湡缁存姢
+```bash
+# 寤鸿姣忔湀閲嶆柊鐢熸垚涓�娆″垎璇嶏紙鍙�夛級
+GET /system/hospital/generateKeywords
+```
+
+### 鐩戞帶鏃ュ織
+鏌ョ湅搴旂敤鏃ュ織涓殑鍒嗚瘝鐢熸垚淇℃伅:
+```bash
+tail -f logs/sys-info.log | grep "鍖婚櫌鍒嗚瘝"
+```
+
+### 鏁版嵁澶囦唤
+```bash
+# 瀹氭湡澶囦唤鍖婚櫌鏁版嵁
+mysqldump -u鐢ㄦ埛鍚� -p鏁版嵁搴撳悕 tb_hosp_data > hosp_data_backup.sql
+```
+
+---
+
+## 涓冦�佹妧鏈敮鎸�
+
+濡傞亣鍒伴棶棰�,璇锋鏌�:
+1. 鏁版嵁搴撳瓧娈垫槸鍚︽纭坊鍔�
+2. 搴旂敤鏄惁姝e父閲嶅惎
+3. 鍒嗚瘝鏁版嵁鏄惁宸插垵濮嬪寲
+4. 鎺ュ彛鏉冮檺鏄惁閰嶇疆姝g‘
+
+璇︾粏鎶�鏈枃妗h鍙傝��: `鍖婚櫌淇℃伅鍒嗚瘝鎼滅储鍔熻兘璇存槑.md`
+
+---
+
+**鐗堟湰**: v1.0
+**鏇存柊鏃ユ湡**: 2026-01-20
--
Gitblit v1.9.1