# 急救转运任务出发地目的地自动填充功能说明 ## 功能概述 在创建急救转运任务时,自动将**转出医院地址**作为**出发地**,将**转入医院地址**作为**目的地**,写入主任务表(`sys_task`)的相应字段,便于任务调度、路径规划和统计分析。 ## 业务背景 急救转运任务的核心就是将患者从一个医院转运到另一个医院: - **转出医院** = 任务的起点(出发地) - **转入医院** = 任务的终点(目的地) 将这些信息写入主任务表的好处: 1. **统一数据结构**:所有类型任务(急救转运、福祉车、保养等)都有出发地和目的地 2. **便于路径规划**:GPS导航可以直接使用出发地和目的地坐标 3. **统计分析**:可以统计各区域的任务量、行驶距离等 4. **任务调度**:调度系统可以根据地理位置分配最近的车辆和人员 5. **旧系统兼容**:便于与旧系统的数据同步 ## 实现位置 **前端文件**: `app/pages/task/create-emergency.vue` **后端实体**: `com.ruoyi.system.domain.SysTask` ## 技术实现 ### 1. 前端数据提交 在 `buildSubmitData()` 方法中,添加出发地和目的地字段: ```javascript const submitData = { taskType: 'EMERGENCY_TRANSFER', vehicleIds: this.selectedVehicleId ? [this.selectedVehicleId] : [], assigneeIds: this.selectedStaff.map(staff => staff.userId), transferTime: this.taskForm.transferTime, documentTypeId: this.selectedDocumentTypeId, taskTypeId: this.selectedEmergencyTaskTypeId, // 将转出医院地址作为出发地,转入医院地址作为目的地 departureAddress: this.taskForm.hospitalOut.address || '', destinationAddress: this.taskForm.hospitalIn.address || '', patient: { ... }, hospitalOut: this.taskForm.hospitalOut, hospitalIn: this.taskForm.hospitalIn, transferDistance: this.taskForm.transferDistance ? parseFloat(this.taskForm.transferDistance) : null, price: this.taskForm.price ? parseFloat(this.taskForm.price) : null } ``` ### 2. GPS坐标同步 如果有GPS坐标信息,同时写入出发地和目的地的经纬度: ```javascript if (this.addressCoordinates.hospitalOutAddress) { // 转出医院GPS坐标写入扩展表 if (!submitData.hospitalOut) submitData.hospitalOut = {} submitData.hospitalOut.longitude = this.addressCoordinates.hospitalOutAddress.lng submitData.hospitalOut.latitude = this.addressCoordinates.hospitalOutAddress.lat // 同时写入主任务表的出发地经纬度 submitData.departureLongitude = this.addressCoordinates.hospitalOutAddress.lng submitData.departureLatitude = this.addressCoordinates.hospitalOutAddress.lat } if (this.addressCoordinates.hospitalInAddress) { // 转入医院GPS坐标写入扩展表 if (!submitData.hospitalIn) submitData.hospitalIn = {} submitData.hospitalIn.longitude = this.addressCoordinates.hospitalInAddress.lng submitData.hospitalIn.latitude = this.addressCoordinates.hospitalInAddress.lat // 同时写入主任务表的目的地经纬度 submitData.destinationLongitude = this.addressCoordinates.hospitalInAddress.lng submitData.destinationLatitude = this.addressCoordinates.hospitalInAddress.lat } ``` ### 3. 后端数据接收 主任务表(`sys_task`)相关字段: ```sql -- 出发地址 departure_address VARCHAR(500) -- 目的地址 destination_address VARCHAR(500) -- 出发地经度 departure_longitude DECIMAL(10,7) -- 出发地纬度 departure_latitude DECIMAL(10,7) -- 目的地经度 destination_longitude DECIMAL(10,7) -- 目的地纬度 destination_latitude DECIMAL(10,7) ``` 对应的Java实体字段: ```java public class SysTask extends BaseEntity { /** 出发地址 */ private String departureAddress; /** 目的地址 */ private String destinationAddress; /** 出发地经度 */ private java.math.BigDecimal departureLongitude; /** 出发地纬度 */ private java.math.BigDecimal departureLatitude; /** 目的地经度 */ private java.math.BigDecimal destinationLongitude; /** 目的地纬度 */ private java.math.BigDecimal destinationLatitude; // ... getters and setters } ``` ## 数据流程 ### 数据流转图 ``` 用户选择转出医院 ↓ 选择医院后,自动填充 taskForm.hospitalOut.address (例如:"广东省广州市天河区中山大道123号") ↓ 用户选择转入医院 ↓ 选择医院后,自动填充 taskForm.hospitalIn.address (例如:"广东省广州市越秀区解放路456号") ↓ 用户点击"保存" ↓ 调用 buildSubmitData() ↓ 设置 departureAddress = taskForm.hospitalOut.address 设置 destinationAddress = taskForm.hospitalIn.address ↓ 如果有GPS坐标,同时设置经纬度字段 ↓ 提交到后端 ↓ 保存到 sys_task 表 ``` ## 数据结构 ### 前端表单数据 ```javascript taskForm: { hospitalOut: { id: 123, // 医院ID name: "广州市第一人民医院", // 医院名称 department: "急诊科", // 科室 departmentId: 10, // 科室ID bedNumber: "A101", // 床号 address: "广东省广州市天河区中山大道123号", // 完整地址 longitude: 113.3245, // 经度(如果有GPS) latitude: 23.1291 // 纬度(如果有GPS) }, hospitalIn: { id: 456, name: "广州市中医医院", department: "骨科", departmentId: 15, bedNumber: "B205", address: "广东省广州市越秀区解放路456号", longitude: 113.2654, latitude: 23.1389 } } ``` ### 提交数据结构 ```javascript { taskType: "EMERGENCY_TRANSFER", vehicleIds: [101], assigneeIds: [1, 2, 3], // 主任务表字段 departureAddress: "广东省广州市天河区中山大道123号", destinationAddress: "广东省广州市越秀区解放路456号", departureLongitude: 113.3245, departureLatitude: 23.1291, destinationLongitude: 113.2654, destinationLatitude: 23.1389, // 扩展信息 hospitalOut: { id: 123, name: "广州市第一人民医院", department: "急诊科", departmentId: 10, bedNumber: "A101", address: "广东省广州市天河区中山大道123号", longitude: 113.3245, latitude: 23.1291 }, hospitalIn: { id: 456, name: "广州市中医医院", department: "骨科", departmentId: 15, bedNumber: "B205", address: "广东省广州市越秀区解放路456号", longitude: 113.2654, latitude: 23.1389 }, patient: { ... }, transferDistance: 15.5, price: 300.00 } ``` ### 数据库存储 **主任务表(sys_task)**: | 字段 | 值 | 说明 | |------|-----|------| | task_id | 10001 | 任务ID | | task_type | EMERGENCY_TRANSFER | 任务类型 | | departure_address | 广东省广州市天河区中山大道123号 | 出发地址(转出医院) | | destination_address | 广东省广州市越秀区解放路456号 | 目的地址(转入医院) | | departure_longitude | 113.3245 | 出发地经度 | | departure_latitude | 23.1291 | 出发地纬度 | | destination_longitude | 113.2654 | 目的地经度 | | destination_latitude | 23.1389 | 目的地纬度 | **扩展信息表(sys_task_emergency)**: | 字段 | 值 | 说明 | |------|-----|------| | emergency_id | 20001 | 扩展信息ID | | task_id | 10001 | 关联任务ID | | hospital_out_id | 123 | 转出医院ID | | hospital_out_name | 广州市第一人民医院 | 转出医院名称 | | hospital_out_department_id | 10 | 转出科室ID | | hospital_out_department | 急诊科 | 转出科室 | | hospital_out_address | 广东省广州市天河区中山大道123号 | 转出医院地址 | | hospital_in_id | 456 | 转入医院ID | | hospital_in_name | 广州市中医医院 | 转入医院名称 | | hospital_in_department_id | 15 | 转入科室ID | | hospital_in_department | 骨科 | 转入科室 | | hospital_in_address | 广东省广州市越秀区解放路456号 | 转入医院地址 | ## 应用场景 ### 1. 任务调度 调度系统可以根据出发地坐标,计算距离最近的可用车辆: ```javascript // 查询离出发地最近的车辆 SELECT v.*, SQRT(POW(v.current_longitude - task.departure_longitude, 2) + POW(v.current_latitude - task.departure_latitude, 2)) AS distance FROM tb_vehicle_info v JOIN sys_task task ON task.task_id = ? WHERE v.status = 'AVAILABLE' ORDER BY distance LIMIT 5 ``` ### 2. 路径规划 GPS导航可以直接使用出发地和目的地坐标: ```javascript // 调用地图API规划路线 const route = await mapAPI.planRoute({ origin: { longitude: task.departureLongitude, latitude: task.departureLatitude }, destination: { longitude: task.destinationLongitude, latitude: task.destinationLatitude } }) // 显示预计时间和距离 console.log('预计行驶时间:', route.duration, '分钟') console.log('预计行驶距离:', route.distance, '公里') ``` ### 3. 统计分析 按区域统计任务量: ```sql -- 统计各区域的任务数量 SELECT SUBSTRING(departure_address, 1, CHARINDEX('市', departure_address)) AS region, COUNT(*) AS task_count, AVG(estimated_distance) AS avg_distance FROM sys_task WHERE task_type = 'EMERGENCY_TRANSFER' AND create_time >= '2025-01-01' GROUP BY SUBSTRING(departure_address, 1, CHARINDEX('市', departure_address)) ORDER BY task_count DESC ``` ### 4. 任务列表展示 在任务列表中显示出发地和目的地: ```vue {{ task.taskCode }} 急救转运 {{ task.departureAddress }} {{ task.destinationAddress }} ``` ## 异常处理 ### 1. 医院地址为空 **场景**: 用户选择了医院但地址未能获取 **处理**: ```javascript departureAddress: this.taskForm.hospitalOut.address || '', destinationAddress: this.taskForm.hospitalIn.address || '', ``` **结果**: 使用空字符串,不影响任务创建 ### 2. GPS坐标缺失 **场景**: 医院数据中没有GPS坐标信息 **处理**: ```javascript if (this.addressCoordinates.hospitalOutAddress) { // 只在有坐标时才设置 submitData.departureLongitude = ... submitData.departureLatitude = ... } ``` **结果**: 经纬度字段为 NULL,不影响任务创建 ### 3. 地址过长 **场景**: 完整地址超过数据库字段长度限制(500字符) **处理**: 数据库字段定义为 `VARCHAR(500)`,通常足够 **备选方案**: 如果确实超长,可以在前端截取: ```javascript departureAddress: (this.taskForm.hospitalOut.address || '').substring(0, 500), destinationAddress: (this.taskForm.hospitalIn.address || '').substring(0, 500), ``` ## 数据一致性 ### 双重存储策略 出发地和目的地信息在两个地方存储: 1. **主任务表(sys_task)** - `departure_address` / `destination_address` - `departure_longitude` / `departure_latitude` - `destination_longitude` / `destination_latitude` 2. **扩展信息表(sys_task_emergency)** - `hospital_out_address` / `hospital_in_address` - `hospital_out_longitude` / `hospital_out_latitude` - `hospital_in_longitude` / `hospital_in_latitude` ### 为什么要双重存储? 1. **主任务表**: 供通用任务调度、统计使用,所有任务类型统一 2. **扩展表**: 保留完整的医院信息,包括医院ID、名称、科室等 ### 数据同步保证 前端代码确保数据一致: ```javascript // 地址一致 departureAddress: this.taskForm.hospitalOut.address hospitalOut.address: this.taskForm.hospitalOut.address // GPS坐标一致 departureLongitude: this.addressCoordinates.hospitalOutAddress.lng hospitalOut.longitude: this.addressCoordinates.hospitalOutAddress.lng ``` ## 相关规范 根据项目记忆: 1. **急救转运扩展信息存储方案**(memory: 5b8a95d1): - ✅ 急救转运任务的扩展信息采用独立的 sys_task_emergency 表 - ✅ 与主任务表通过任务ID关联 - ✅ 本次修改确保了主任务表和扩展表的数据同步 2. **旧系统同步参数扩展规则**(memory: cabfc07d): - ✅ 需要传递医院ID和科室ID - ✅ 地址信息便于旧系统展示和调度 ## 测试建议 ### 测试场景1:正常创建任务 **操作**: 1. 选择转出医院:"广州市第一人民医院" 2. 地址自动填充:"广东省广州市天河区中山大道123号" 3. 选择转入医院:"广州市中医医院" 4. 地址自动填充:"广东省广州市越秀区解放路456号" 5. 填写其他信息并保存 **验证**: ```sql SELECT task_id, departure_address, destination_address, departure_longitude, departure_latitude, destination_longitude, destination_latitude FROM sys_task WHERE task_id = ? ``` **预期结果**: - `departure_address` = "广东省广州市天河区中山大道123号" - `destination_address` = "广东省广州市越秀区解放路456号" - 如果有GPS坐标,经纬度字段有值 - 如果无GPS坐标,经纬度字段为 NULL ### 测试场景2:医院地址为空 **操作**: 1. 选择的医院数据中没有地址信息 2. 保存任务 **预期结果**: - `departure_address` = ""(空字符串) - `destination_address` = ""(空字符串) - 任务创建成功,不报错 ### 测试场景3:地址变更 **操作**: 1. 先选择医院A,地址自动填充 2. 又选择医院B,地址应该更新 3. 保存任务 **预期结果**: - 保存的地址是最后选择的医院B的地址 - 不会残留医院A的地址 ### 测试场景4:任务列表显示 **操作**: 1. 创建任务后 2. 在任务列表查看 **预期结果**: - 任务列表正确显示出发地和目的地 - GPS地图能正确定位两个位置 ## 优化建议 ### 1. 地址简化显示 完整地址可能过长,可以提供简化版本: ```javascript // 提取关键信息:市 + 区 + 主要道路 function simplifyAddress(fullAddress) { // "广东省广州市天河区中山大道123号" → "天河区中山大道" const match = fullAddress.match(/(.+?市)?(.+?区)(.+?)(\d+号)?/) if (match) { return (match[2] || '') + (match[3] || '') } return fullAddress } // 在任务列表中使用简化地址 const displayAddress = simplifyAddress(task.departureAddress) ``` ### 2. 地址验证 确保地址格式正确: ```javascript function validateAddress(address) { if (!address || address.length < 5) { return false } // 检查是否包含关键词 const keywords = ['省', '市', '区', '县', '路', '街', '道', '号'] return keywords.some(keyword => address.includes(keyword)) } ``` ### 3. 自动计算距离 如果有GPS坐标,自动计算两点距离: ```javascript function calculateDistance(lat1, lng1, lat2, lng2) { const R = 6371 // 地球半径(公里) const dLat = (lat2 - lat1) * Math.PI / 180 const dLng = (lng2 - lng1) * Math.PI / 180 const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng/2) * Math.sin(dLng/2) const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) return R * c } // 自动填充转运距离 if (this.addressCoordinates.hospitalOutAddress && this.addressCoordinates.hospitalInAddress) { const distance = calculateDistance( this.addressCoordinates.hospitalOutAddress.lat, this.addressCoordinates.hospitalOutAddress.lng, this.addressCoordinates.hospitalInAddress.lat, this.addressCoordinates.hospitalInAddress.lng ) this.taskForm.transferDistance = distance.toFixed(2) } ``` ## 版本历史 - **2025-01-25 v1**: 初始版本 - ✅ 将转出医院地址写入 departure_address - ✅ 将转入医院地址写入 destination_address - ✅ 同时写入GPS坐标到 departure/destination longitude/latitude - ✅ 确保主任务表和扩展表数据一致 - ✅ 添加异常处理(地址为空的情况)