wlzboy
2025-09-21 0b80903f3d48b3c39570c097a4334cb7eb71d08f
feat:通用任务初始化
2个文件已修改
36个文件已添加
6686 ■■■■■ 已修改文件
README_TASK.md 183 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
prd/任务车辆关联.md 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
prd/通用任务管理功能说明.md 1156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskVehicleController.java 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTask.java 369 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskAttachment.java 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskLog.java 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskVehicle.java 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/enums/TaskStatus.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/enums/TaskType.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/enums/TaskVehicleStatus.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/enums/VehicleType.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskCreateVO.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskQueryVO.java 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskStatisticsVO.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskUpdateVO.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskAttachmentMapper.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskLogMapper.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskVehicleMapper.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 567 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/TaskCodeGenerator.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/TaskStatusValidator.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskAttachmentMapper.xml 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskLogMapper.xml 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskVehicleMapper.xml 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/api/task.js 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/router/index.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/task/general/detail.vue 564 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/task/general/index.vue 535 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/task_dict_data.sql 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/task_menu.sql 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/task_tables.sql 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/vehicle_info.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
README_TASK.md
New file
@@ -0,0 +1,183 @@
# é€šç”¨ä»»åŠ¡ç®¡ç†åŠŸèƒ½ä½¿ç”¨è¯´æ˜Ž
## åŠŸèƒ½æ¦‚è¿°
通用任务管理功能是基于若依框架开发的完整任务管理系统,支持维修保养、加油任务、其他任务类型的全生命周期管理。
## ä¸»è¦åŠŸèƒ½
### 1. ä»»åŠ¡ç®¡ç†
- **任务创建**:支持创建维修保养、加油任务、其他类型任务
- **任务查询**:支持按任务编号、类型、状态、时间范围等条件查询
- **任务编辑**:支持修改任务描述、地址、时间、执行人等信息
- **任务删除**:支持单个或批量删除任务
- **任务分配**:支持将任务分配给指定执行人
- **状态管理**:支持任务状态流转(待开始→任务中→已完成,任何状态可取消)
### 2. è½¦è¾†ç®¡ç†
- **车辆分配**:支持将车辆分配给任务
- **批量分配**:支持一次性分配多辆车给任务
- **取消分配**:支持取消车辆与任务的关联
- **车辆查询**:支持查询可用车辆列表
### 3. é™„件管理
- **文件上传**:支持上传任务相关附件(PDF、DOC、图片等)
- **文件下载**:支持下载已上传的附件
- **文件删除**:支持删除不需要的附件
### 4. æ“ä½œæ—¥å¿—
- **日志记录**:自动记录所有任务操作日志
- **日志查询**:支持查看任务的操作历史
- **操作追踪**:记录操作人、操作时间、操作内容等详细信息
### 5. ç»Ÿè®¡åŠŸèƒ½
- **任务统计**:提供任务总数、各状态任务数量、超时任务等统计信息
- **我的任务**:支持查看当前用户创建或分配的任务
- **超时提醒**:自动识别和提醒超时任务
## æ•°æ®åº“表结构
### æ ¸å¿ƒè¡¨
1. **sys_task** - ä»»åŠ¡ä¸»è¡¨
2. **sys_task_vehicle** - ä»»åŠ¡è½¦è¾†å…³è”è¡¨
3. **sys_task_attachment** - ä»»åŠ¡é™„ä»¶è¡¨
4. **sys_task_log** - ä»»åŠ¡æ“ä½œæ—¥å¿—è¡¨
### æ•°æ®å­—å…¸
- **sys_task_type** - ä»»åŠ¡ç±»åž‹å­—å…¸
- **sys_task_status** - ä»»åŠ¡çŠ¶æ€å­—å…¸
- **sys_vehicle_type** - è½¦è¾†ç±»åž‹å­—å…¸
- **sys_vehicle_status** - è½¦è¾†çŠ¶æ€å­—å…¸
- **sys_task_vehicle_status** - ä»»åŠ¡è½¦è¾†å…³è”çŠ¶æ€å­—å…¸
## éƒ¨ç½²æ­¥éª¤
### 1. æ•°æ®åº“初始化
```sql
-- æ‰§è¡Œè¡¨ç»“构创建脚本
source sql/task_tables.sql;
-- æ‰§è¡Œæ•°æ®å­—典初始化脚本
source sql/task_dict_data.sql;
-- æ‰§è¡Œèœå•权限初始化脚本
source sql/task_menu.sql;
```
### 2. åŽç«¯éƒ¨ç½²
1. ç¡®ä¿æ‰€æœ‰Java文件已正确放置到对应目录
2. é‡æ–°ç¼–译项目:`mvn clean compile`
3. é‡å¯åº”用服务
### 3. å‰ç«¯éƒ¨ç½²
1. ç¡®ä¿æ‰€æœ‰Vue文件已正确放置到对应目录
2. é‡æ–°æž„建前端:`npm run build:prod`
3. éƒ¨ç½²åˆ°Web服务器
### 4. æƒé™é…ç½®
1. ç™»å½•系统管理后台
2. è¿›å…¥"系统管理" â†’ "菜单管理"
3. ç¡®è®¤ä»»åŠ¡ç®¡ç†ç›¸å…³èœå•å·²æ­£ç¡®åˆ›å»º
4. ä¸ºç›¸åº”角色分配任务管理权限
## API接口说明
### ä»»åŠ¡ç®¡ç†æŽ¥å£
- `GET /task/list` - æŸ¥è¯¢ä»»åŠ¡åˆ—è¡¨
- `GET /task/{taskId}` - èŽ·å–ä»»åŠ¡è¯¦æƒ…
- `POST /task` - åˆ›å»ºä»»åŠ¡
- `PUT /task` - æ›´æ–°ä»»åŠ¡
- `DELETE /task/{taskIds}` - åˆ é™¤ä»»åŠ¡
- `PUT /task/{taskId}/assign` - åˆ†é…ä»»åŠ¡
- `PUT /task/{taskId}/status` - æ›´æ–°ä»»åŠ¡çŠ¶æ€
### è½¦è¾†ç®¡ç†æŽ¥å£
- `GET /task/vehicle/list/{taskId}` - æŸ¥è¯¢ä»»åŠ¡å…³è”è½¦è¾†
- `GET /task/vehicle/available` - æŸ¥è¯¢å¯ç”¨è½¦è¾†
- `POST /task/vehicle/assign/{taskId}` - åˆ†é…è½¦è¾†
- `POST /task/vehicle/assign-batch/{taskId}` - æ‰¹é‡åˆ†é…è½¦è¾†
- `DELETE /task/vehicle/{taskId}/{vehicleId}` - å–消车辆分配
### é™„件管理接口
- `GET /task/attachment/list/{taskId}` - æŸ¥è¯¢ä»»åС附件
- `POST /task/attachment/upload/{taskId}` - ä¸Šä¼ é™„ä»¶
- `DELETE /task/attachment/{attachmentId}` - åˆ é™¤é™„ä»¶
### ç»Ÿè®¡æŽ¥å£
- `GET /task/statistics` - èŽ·å–ä»»åŠ¡ç»Ÿè®¡ä¿¡æ¯
- `GET /task/overdue` - èŽ·å–è¶…æ—¶ä»»åŠ¡åˆ—è¡¨
- `GET /task/my` - èŽ·å–æˆ‘çš„ä»»åŠ¡åˆ—è¡¨
## ä½¿ç”¨è¯´æ˜Ž
### 1. åˆ›å»ºä»»åŠ¡
1. è¿›å…¥"任务管理" â†’ "通用任务"
2. ç‚¹å‡»"新增"按钮
3. å¡«å†™ä»»åŠ¡ä¿¡æ¯ï¼š
   - ä»»åŠ¡ç±»åž‹ï¼šé€‰æ‹©ç»´ä¿®ä¿å…»ã€åŠ æ²¹ä»»åŠ¡æˆ–å…¶ä»–
   - ä»»åŠ¡æè¿°ï¼šè¯¦ç»†æè¿°ä»»åŠ¡å†…å®¹
   - å‡ºå‘地址:任务起始地点
   - ç›®çš„地址:任务目标地点
   - è®¡åˆ’时间:设置计划开始和结束时间
   - æ‰§è¡Œäººï¼šé€‰æ‹©ä»»åŠ¡æ‰§è¡Œäºº
   - å¤‡æ³¨ï¼šå…¶ä»–说明信息
4. ç‚¹å‡»"确定"保存
### 2. åˆ†é…è½¦è¾†
1. åœ¨ä»»åŠ¡åˆ—è¡¨ä¸­ç‚¹å‡»"查看"进入任务详情
2. åœ¨"关联车辆"区域点击"分配车辆"
3. é€‰æ‹©è¦åˆ†é…çš„车辆(可多选)
4. å¡«å†™åˆ†é…å¤‡æ³¨
5. ç‚¹å‡»"确定"完成分配
### 3. ä¸Šä¼ é™„ä»¶
1. åœ¨ä»»åŠ¡è¯¦æƒ…é¡µé¢ç‚¹å‡»"上传附件"
2. é€‰æ‹©è¦ä¸Šä¼ çš„æ–‡ä»¶ï¼ˆæ”¯æŒPDF、DOC、图片等格式)
3. æ–‡ä»¶å¤§å°ä¸è¶…过10MB
4. ä¸Šä¼ å®ŒæˆåŽå¯åœ¨é™„件列表中查看和下载
### 4. çŠ¶æ€ç®¡ç†
1. åœ¨ä»»åŠ¡åˆ—è¡¨æˆ–è¯¦æƒ…é¡µé¢ç‚¹å‡»"状态变更"
2. é€‰æ‹©æ–°çš„任务状态
3. å¡«å†™çŠ¶æ€å˜æ›´å¤‡æ³¨
4. ç³»ç»Ÿä¼šè‡ªåŠ¨è®°å½•çŠ¶æ€å˜æ›´æ—¥å¿—
## æƒé™è¯´æ˜Ž
### èœå•权限
- `task:general:view` - ä»»åŠ¡æŸ¥çœ‹æƒé™
- `task:general:query` - ä»»åŠ¡æŸ¥è¯¢æƒé™
- `task:general:add` - ä»»åŠ¡æ–°å¢žæƒé™
- `task:general:edit` - ä»»åŠ¡ä¿®æ”¹æƒé™
- `task:general:remove` - ä»»åŠ¡åˆ é™¤æƒé™
- `task:general:assign` - ä»»åŠ¡åˆ†é…æƒé™
- `task:general:status` - çŠ¶æ€å˜æ›´æƒé™
- `task:general:export` - ä»»åŠ¡å¯¼å‡ºæƒé™
### æ•°æ®æƒé™
系统基于部门进行数据隔离,用户只能查看和操作本部门及下级部门的数据。
## æ³¨æ„äº‹é¡¹
1. **任务编号**:系统自动生成,格式为TASK+日期+序号
2. **状态流转**:必须按照业务规则进行状态流转,不允许跳跃式变更
3. **文件上传**:支持的文件类型和大小有限制,请按要求上传
4. **数据备份**:建议定期备份任务相关数据
5. **性能优化**:大量数据时建议使用分页查询
## æ•…障排除
### å¸¸è§é—®é¢˜
1. **菜单不显示**:检查菜单权限配置和角色分配
2. **接口调用失败**:检查后端服务是否正常启动
3. **文件上传失败**:检查文件大小和格式是否符合要求
4. **状态变更失败**:检查状态流转规则是否允许
### æ—¥å¿—查看
- åŽç«¯æ—¥å¿—:查看应用日志文件
- å‰ç«¯æ—¥å¿—:打开浏览器开发者工具查看控制台
- æ•°æ®åº“日志:查看数据库操作日志
## æŠ€æœ¯æ”¯æŒ
如有问题请联系系统管理员或开发团队。
prd/ÈÎÎñ³µÁ¾¹ØÁª.md
New file
@@ -0,0 +1,301 @@
# ä»»åŠ¡è½¦è¾†å…³è”å…³ç³»è®¾è®¡
## å…³ç³»æ¦‚è¿°
根据业务需求,任务管理系统需要建立以下关联关系:
1. **任务 â†” è½¦è¾†**:一个任务可以关联多辆车,一辆车可以执行多个任务
2. **车辆 â†” æœºæž„**:每辆车归属于一个机构
3. **任务 â†” æ‰§è¡Œäºº**:每个任务分配给一个执行人
4. **执行人 â†” æœºæž„**:每个执行人归属于一个机构
## æ•°æ®åº“关系设计
### 1. æ ¸å¿ƒå…³è”关系
```mermaid
erDiagram
    sys_task ||--o{ sys_task_vehicle : "一对多"
    tb_vehicle_info ||--o{ sys_task_vehicle : "一对多"
    sys_dept ||--o{ tb_vehicle_info : "一对多"
    sys_dept ||--o{ sys_task : "一对多"
    sys_user ||--o{ sys_task : "一对多"
    sys_dept ||--o{ sys_user : "一对多"
    sys_task {
        bigint task_id PK
        varchar task_code UK
        varchar task_type
        varchar task_status
        bigint creator_id FK
        bigint assignee_id FK
        bigint dept_id FK
        bigint vehicle_id FK
    }
    tb_vehicle_info {
        bigint vehicle_id PK
        varchar platform_code
        varchar vehicle_no UK
        varchar vehicle_type
        varchar vehicle_brand
        varchar vehicle_model
        varchar vehicle_color
        char vehicle_status
        varchar device_id
        bigint dept_id FK
    }
    sys_task_vehicle {
        bigint id PK
        bigint task_id FK
        bigint vehicle_id FK
        datetime assign_time
        varchar assign_by
        varchar status
        varchar remark
    }
    sys_dept {
        bigint dept_id PK
        varchar dept_name
        bigint parent_id
    }
    sys_user {
        bigint user_id PK
        varchar user_name
        bigint dept_id FK
    }
```
### 2. å…³è”关系说明
#### 2.1 ä»»åŠ¡ä¸Žè½¦è¾†å…³è”
- **关系类型**:多对多(通过中间表 `sys_task_vehicle`)
- **业务规则**:
  - **一个任务可以分配多辆车**(如:主车+备用车、多车协同作业)
  - **一辆车可以执行多个任务**(按时间顺序,避免冲突)
  - **任务车辆关联有独立状态管理**(已分配、执行中、已完成、已取消)
  - **支持批量分配和取消分配**
  - **车辆分配时验证车辆状态和可用性**
#### 2.2 è½¦è¾†ä¸Žæœºæž„关联
- **关系类型**:多对一
- **业务规则**:
  - æ¯è¾†è½¦å¿…须归属于一个机构
  - æœºæž„删除时,车辆归属设置为NULL(软删除)
  - æ”¯æŒæŒ‰æœºæž„查询车辆
#### 2.3 ä»»åŠ¡ä¸Žæ‰§è¡Œäººå…³è”
- **关系类型**:多对一
- **业务规则**:
  - æ¯ä¸ªä»»åŠ¡åˆ†é…ç»™ä¸€ä¸ªæ‰§è¡Œäºº
  - æ‰§è¡Œäººå¿…须与任务在同一机构或上级机构
  - æ”¯æŒä»»åŠ¡é‡æ–°åˆ†é…
#### 2.4 æ‰§è¡Œäººä¸Žæœºæž„关联
- **关系类型**:多对一
- **业务规则**:
  - æ¯ä¸ªæ‰§è¡Œäººå½’属于一个机构
  - æ”¯æŒæœºæž„层级权限控制
## æ•°æ®æƒé™æŽ§åˆ¶
### 1. æœºæž„数据隔离
```sql
-- æŸ¥è¯¢ä»»åŠ¡æ—¶æŒ‰æœºæž„è¿‡æ»¤
SELECT t.* FROM sys_task t
WHERE t.dept_id IN (
    SELECT dept_id FROM sys_dept
    WHERE FIND_IN_SET(dept_id, @user_dept_ids)
);
-- æŸ¥è¯¢è½¦è¾†æ—¶æŒ‰æœºæž„过滤
SELECT v.* FROM tb_vehicle_info v
WHERE v.dept_id IN (
    SELECT dept_id FROM sys_dept
    WHERE FIND_IN_SET(dept_id, @user_dept_ids)
);
```
### 2. æƒé™éªŒè¯è§„则
- **任务创建**:只能创建本机构及下级机构的任务
- **车辆分配**:只能分配本机构及下级机构的车辆
- **执行人分配**:只能分配给本机构及下级机构的用户
- **数据查询**:只能查看本机构及下级机构的数据
## ä¸šåŠ¡åœºæ™¯ç¤ºä¾‹
### 1. åˆ›å»ºç»´ä¿®ä»»åŠ¡å¹¶åˆ†é…å¤šè¾†è½¦
```sql
-- 1. åˆ›å»ºä»»åŠ¡
INSERT INTO sys_task (task_code, task_type, task_status, creator_id, dept_id, ...)
VALUES ('TASK202401150001', 'MAINTENANCE', 'PENDING', 100, 200, ...);
-- 2. åˆ†é…å¤šè¾†è½¦ï¼ˆä¸»è½¦+备用车)
INSERT INTO sys_task_vehicle (task_id, vehicle_id, assign_by, status, remark)
VALUES
(1, 10, 'admin', 'ASSIGNED', '分配主维修车'),
(1, 11, 'admin', 'ASSIGNED', '分配备用维修车'),
(1, 12, 'admin', 'ASSIGNED', '分配工具车');
-- 3. åˆ†é…æ‰§è¡Œäºº
UPDATE sys_task SET assignee_id = 150 WHERE task_id = 1;
```
### 2. æŸ¥è¯¢æœºæž„下的所有任务和车辆
```sql
-- æŸ¥è¯¢æœºæž„任务
SELECT t.task_code, t.task_type, t.task_status,
       u.user_name as assignee_name,
       d.dept_name
FROM sys_task t
LEFT JOIN sys_user u ON t.assignee_id = u.user_id
LEFT JOIN sys_dept d ON t.dept_id = d.dept_id
WHERE t.dept_id = 200;
-- æŸ¥è¯¢æœºæž„车辆
SELECT v.vehicle_no, v.vehicle_type, v.vehicle_status,
       d.dept_name
FROM tb_vehicle_info v
LEFT JOIN sys_dept d ON v.dept_id = d.dept_id
WHERE v.dept_id = 200;
```
### 3. æŸ¥è¯¢ä»»åŠ¡çš„è½¦è¾†åˆ†é…æƒ…å†µ
```sql
SELECT t.task_code, t.task_type,
       v.vehicle_no, v.vehicle_type,
       tv.assign_time, tv.status, tv.remark
FROM sys_task t
LEFT JOIN sys_task_vehicle tv ON t.task_id = tv.task_id
LEFT JOIN tb_vehicle_info v ON tv.vehicle_id = v.vehicle_id
WHERE t.task_id = 1;
```
## æŽ¥å£è®¾è®¡æ‰©å±•
### 1. è½¦è¾†ç®¡ç†æŽ¥å£
```javascript
// æŸ¥è¯¢æœºæž„车辆列表
export function listVehicleByDept(deptId, query) {
  return request({
    url: '/api/vehicle/list-by-dept/' + deptId,
    method: 'get',
    params: query
  })
}
// æŸ¥è¯¢å¯ç”¨è½¦è¾†ï¼ˆæœªåˆ†é…ä»»åŠ¡çš„è½¦è¾†ï¼‰
export function listAvailableVehicles(deptId, taskType) {
  return request({
    url: '/api/vehicle/available',
    method: 'get',
    params: { deptId, taskType }
  })
}
```
### 2. ä»»åŠ¡è½¦è¾†åˆ†é…æŽ¥å£
```javascript
// æ‰¹é‡åˆ†é…è½¦è¾†
export function assignVehiclesToTask(taskId, vehicleIds, remark) {
  return request({
    url: '/api/task/' + taskId + '/assign-vehicles',
    method: 'post',
    data: { vehicleIds, remark }
  })
}
// æŸ¥è¯¢ä»»åŠ¡è½¦è¾†ä½¿ç”¨æƒ…å†µ
export function getTaskVehicleUsage(taskId) {
  return request({
    url: '/api/task/' + taskId + '/vehicle-usage',
    method: 'get'
  })
}
```
## æ•°æ®ä¸€è‡´æ€§ä¿è¯
### 1. å¤–键约束
- ä»»åŠ¡è¡¨å¼•ç”¨è½¦è¾†è¡¨ï¼š`ON DELETE SET NULL`
- ä»»åŠ¡è½¦è¾†å…³è”è¡¨ï¼š`ON DELETE CASCADE`
- è½¦è¾†è¡¨å¼•用机构表:`ON DELETE SET NULL`
### 2. ä¸šåŠ¡è§„åˆ™éªŒè¯
- è½¦è¾†åˆ†é…æ—¶éªŒè¯è½¦è¾†çŠ¶æ€ï¼ˆå¿…é¡»ä¸ºæ­£å¸¸çŠ¶æ€ï¼‰
- ä»»åŠ¡åˆ†é…æ—¶éªŒè¯æ‰§è¡Œäººæƒé™ï¼ˆåŒæœºæž„æˆ–ä¸Šçº§æœºæž„ï¼‰
- æœºæž„删除时处理关联数据(软删除或转移)
### 3. äº‹åŠ¡æŽ§åˆ¶
```java
@Transactional
public void assignMultipleVehiclesToTask(Long taskId, List<Long> vehicleIds, String remark) {
    // 1. éªŒè¯ä»»åŠ¡çŠ¶æ€
    Task task = taskMapper.selectById(taskId);
    if (task.getStatus() == TaskStatus.COMPLETED) {
        throw new BusinessException("已完成的任务不能分配车辆");
    }
    List<TaskVehicle> assignedVehicles = new ArrayList<>();
    for (Long vehicleId : vehicleIds) {
        // 2. éªŒè¯è½¦è¾†çŠ¶æ€å’Œå¯ç”¨æ€§
        Vehicle vehicle = vehicleMapper.selectById(vehicleId);
        if (vehicle.getStatus() != VehicleStatus.ACTIVE) {
            throw new BusinessException("车辆ID " + vehicleId + " çŠ¶æ€å¼‚å¸¸ï¼Œä¸èƒ½åˆ†é…");
        }
        // 3. æ£€æŸ¥è½¦è¾†æ˜¯å¦å·²è¢«å…¶ä»–任务占用
        if (isVehicleAssignedToOtherTask(vehicleId, taskId)) {
            throw new BusinessException("车辆ID " + vehicleId + " å·²è¢«å…¶ä»–任务占用");
        }
        // 4. åˆ›å»ºå…³è”记录
        TaskVehicle taskVehicle = new TaskVehicle();
        taskVehicle.setTaskId(taskId);
        taskVehicle.setVehicleId(vehicleId);
        taskVehicle.setStatus(TaskVehicleStatus.ASSIGNED);
        taskVehicle.setRemark(remark);
        taskVehicleMapper.insert(taskVehicle);
        assignedVehicles.add(taskVehicle);
    }
    // 5. è®°å½•操作日志
    logTaskOperation(taskId, "ASSIGN_MULTIPLE_VEHICLES",
        "分配了 " + vehicleIds.size() + " è¾†è½¦: " + vehicleIds.toString());
}
@Transactional
public void unassignVehicleFromTask(Long taskId, Long vehicleId) {
    // 1. éªŒè¯ä»»åŠ¡è½¦è¾†å…³è”æ˜¯å¦å­˜åœ¨
    TaskVehicle taskVehicle = taskVehicleMapper.selectByTaskAndVehicle(taskId, vehicleId);
    if (taskVehicle == null) {
        throw new BusinessException("任务车辆关联不存在");
    }
    // 2. éªŒè¯æ˜¯å¦å¯ä»¥å–消分配
    if (taskVehicle.getStatus() == TaskVehicleStatus.COMPLETED) {
        throw new BusinessException("已完成的任务车辆关联不能取消");
    }
    // 3. åˆ é™¤å…³è”记录
    taskVehicleMapper.deleteById(taskVehicle.getId());
    // 4. è®°å½•操作日志
    logTaskOperation(taskId, "UNASSIGN_VEHICLE", "取消分配车辆ID: " + vehicleId);
}
```
## æ€»ç»“
通过以上设计,实现了任务、车辆、机构和执行人之间的完整关联关系:
1. **数据完整性**:通过外键约束保证数据一致性
2. **权限控制**:基于机构的数据隔离和权限验证
3. **业务灵活性**:支持多车辆分配和状态管理
4. **扩展性**:预留了设备ID等字段,支持GPS定位等功能扩展
这个设计既满足了当前的业务需求,又为未来的功能扩展留下了空间。
prd/ͨÓÃÈÎÎñ¹ÜÀí¹¦ÄÜ˵Ã÷.md
New file
@@ -0,0 +1,1156 @@
# é€šç”¨ä»»åŠ¡ç®¡ç†åŠŸèƒ½è¯´æ˜Ž
## åŠŸèƒ½æ¦‚è¿°
根据需求文档,已完成通用任务和转运任务的后台管理功能开发,包括:
1. **通用任务管理** - æ”¯æŒç»´ä¿®ä¿å…»ã€åŠ æ²¹ä»»åŠ¡ã€å…¶ä»–ä»»åŠ¡ç±»åž‹
2. **完整的CRUD操作** - åˆ›å»ºã€æŸ¥è¯¢ã€æ›´æ–°ã€åˆ é™¤ã€åˆ†é…ã€çŠ¶æ€ç®¡ç†
3. **权限控制** - åŸºäºŽè§’色的操作权限控制
4. **数据隔离** - åŸºäºŽæœºæž„的数据权限隔离
## æŠ€æœ¯æž¶æž„
### åŽç«¯æž¶æž„ (ruoyi-task模块)
- é‡‡ç”¨DDD架构设计,分层明确
- **领域层**: ä»»åŠ¡èšåˆæ ¹ã€å€¼å¯¹è±¡ã€é¢†åŸŸæœåŠ¡
- **应用层**: CQRS命令查询分离、应用服务
- **基础设施层**: æŒä¹…化、消息推送
- **接口层**: REST API控制器
### å‰ç«¯æž¶æž„ (ruoyi-ui模块)
- **页面组件**: ä»»åŠ¡åˆ—è¡¨ã€ä»»åŠ¡è¯¦æƒ…ã€ä»»åŠ¡åˆ›å»º
- **API服务**: ç»Ÿä¸€çš„API接口调用
- **路由配置**: åŠ¨æ€è·¯ç”±å’Œæƒé™æŽ§åˆ¶
## æ ¸å¿ƒåŠŸèƒ½
### 1. ä»»åŠ¡ç±»åž‹ç®¡ç†
#### é€šç”¨ä»»åŠ¡ç±»åž‹
- **维修保养** (MAINTENANCE) - è½¦è¾†ç»´ä¿®ä¿å…»ä»»åŠ¡
- **加油任务** (FUEL) - è½¦è¾†åŠ æ²¹ä»»åŠ¡
- **其他** (OTHER) - å…¶ä»–类型任务
### 2. ä»»åŠ¡çŠ¶æ€ç®¡ç†
#### ä»»åŠ¡çŠ¶æ€å®šä¹‰
- **待开始** (PENDING) - ä»»åŠ¡å·²åˆ›å»ºï¼Œç­‰å¾…å¼€å§‹
- **任务中** (IN_PROGRESS) - ä»»åŠ¡å·²ç»å¼€å§‹
- **已完成** (COMPLETED) - ä»»åŠ¡å·²å®Œæˆ
- **已取消** (CANCELLED) - ä»»åŠ¡å·²å–æ¶ˆ
#### ä»»åŠ¡çŠ¶æ€æµè½¬è§„åˆ™
##### ä»»åŠ¡çŠ¶æ€æµè½¬è§„åˆ™
- **PENDING** â†’ **IN_PROGRESS** â†’ **COMPLETED**
- ä»»ä½•状态都可以流转到 **CANCELLED**(除了 **COMPLETED**)
##### çŠ¶æ€æµè½¬çº¦æŸ
1. ä»»åŠ¡çŠ¶æ€æµè½¬å¿…é¡»ç¬¦åˆä¸šåŠ¡é€»è¾‘é¡ºåº
2. æ”¯æŒçŠ¶æ€å›žé€€ï¼ˆå¦‚ä»Ž IN_PROGRESS å›žåˆ° PENDING)
3. ä»»åŠ¡å–æ¶ˆåŽä¸èƒ½å†æ¬¡æ¿€æ´»
### 3. ä»»åŠ¡å­—æ®µè®¾è®¡
#### é€šç”¨å­—段
- ä»»åŠ¡ç¼–å·ã€ä»»åŠ¡åˆ›å»ºäººï¼Œä»»åŠ¡å½’å±žæœºæž„
- ä»»åŠ¡ç±»åž‹ã€ä»»åŠ¡çŠ¶æ€
- ä»»åŠ¡ç›®çš„åœ°å€ã€ä»»åŠ¡å‡ºå‘åœ°å€
- è®¡åˆ’开始时间、计划结束时间
- å®žé™…开始时间、实际结束时间
- ä»»åŠ¡å¤‡æ³¨ã€é™„ä»¶åˆ—è¡¨
## æ•°æ®åº“设计
### 1. æ ¸å¿ƒè¡¨ç»“æž„
#### 1.1 ä»»åŠ¡ä¸»è¡¨ (sys_task)
```sql
CREATE TABLE sys_task (
    task_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '任务ID',
    task_code VARCHAR(50) NOT NULL UNIQUE COMMENT '任务编号',
    task_type VARCHAR(20) NOT NULL COMMENT '任务类型:MAINTENANCE-维修保养,FUEL-加油任务,OTHER-其他',
    task_status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态:PENDING-待开始,IN_PROGRESS-任务中,COMPLETED-已完成,CANCELLED-已取消',
    task_description varchar(1000) COMMENT '任务描述',
    -- åœ°å€ä¿¡æ¯
    departure_address VARCHAR(500) COMMENT '出发地址',
    destination_address VARCHAR(500) COMMENT '目的地址',
    -- æ—¶é—´ä¿¡æ¯
    planned_start_time DATETIME COMMENT '计划开始时间',
    planned_end_time DATETIME COMMENT '计划结束时间',
    actual_start_time DATETIME COMMENT '实际开始时间',
    actual_end_time DATETIME COMMENT '实际结束时间',
    -- äººå‘˜ä¿¡æ¯
    creator_id BIGINT NOT NULL COMMENT '创建人ID',
    assignee_id BIGINT COMMENT '执行人ID',
    dept_id BIGINT NOT NULL COMMENT '归属部门ID',
    -- ç³»ç»Ÿå­—段
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    create_by VARCHAR(64) NOT NULL COMMENT '创建者',
    update_by VARCHAR(64) COMMENT '更新者',
    remark VARCHAR(500) COMMENT '备注',
    del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
    INDEX idx_task_code (task_code),
    INDEX idx_task_type (task_type),
    INDEX idx_task_status (task_status),
    INDEX idx_creator_id (creator_id),
    INDEX idx_assignee_id (assignee_id),
    INDEX idx_dept_id (dept_id),
    INDEX idx_planned_start_time (planned_start_time),
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务管理表';
```
#### 1.2 è½¦è¾†ä¿¡æ¯è¡¨ (tb_vehicle_info)
```sql
-- åŸºäºŽçŽ°æœ‰è½¦è¾†è¡¨ç»“æž„ï¼Œæ·»åŠ æœºæž„å…³è”å­—æ®µ
CREATE TABLE tb_vehicle_info (
    vehicle_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '车辆ID',
    platform_code VARCHAR(50) NOT NULL COMMENT '平台标识(A/B)',
    vehicle_no VARCHAR(50) NOT NULL COMMENT '车牌号',
    vehicle_type VARCHAR(50) NOT NULL COMMENT '车辆类型:AMBULANCE-救护车,TRANSFER-转运车,MAINTENANCE-维修车',
    vehicle_brand VARCHAR(50) COMMENT '车辆品牌',
    vehicle_model VARCHAR(50) COMMENT '车辆型号',
    vehicle_color VARCHAR(20) COMMENT '车辆颜色',
    vehicle_status CHAR(1) DEFAULT '0' COMMENT '车辆状态(0正常 1停用)',
    device_id VARCHAR(50) DEFAULT NULL COMMENT '设备ID',
    -- æœºæž„关联(新增字段)
    dept_id BIGINT(20) DEFAULT NULL COMMENT '归属机构ID',
    -- ç³»ç»Ÿå­—段
    create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
    create_time DATETIME COMMENT '创建时间',
    update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
    update_time DATETIME COMMENT '更新时间',
    remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
    PRIMARY KEY (vehicle_id),
    INDEX idx_vehicle_no (vehicle_no),
    INDEX idx_vehicle_type (vehicle_type),
    INDEX idx_vehicle_status (vehicle_status),
    INDEX idx_dept_id (dept_id),
    INDEX idx_platform_code (platform_code),
    FOREIGN KEY (dept_id) REFERENCES sys_dept(dept_id) ON DELETE SET NULL
) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COMMENT='车辆信息表';
```
#### 1.3 ä»»åŠ¡è½¦è¾†å…³è”è¡¨ (sys_task_vehicle)
```sql
CREATE TABLE sys_task_vehicle (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '关联ID',
    task_id BIGINT NOT NULL COMMENT '任务ID',
    vehicle_id BIGINT NOT NULL COMMENT '车辆ID',
    assign_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '分配时间',
    assign_by VARCHAR(64) NOT NULL COMMENT '分配人',
    status VARCHAR(20) DEFAULT 'ASSIGNED' COMMENT '关联状态:ASSIGNED-已分配,ACTIVE-执行中,COMPLETED-已完成,CANCELLED-已取消',
    remark VARCHAR(500) COMMENT '备注',
    INDEX idx_task_id (task_id),
    INDEX idx_vehicle_id (vehicle_id),
    INDEX idx_status (status),
    INDEX idx_assign_time (assign_time),
    UNIQUE KEY uk_task_vehicle (task_id, vehicle_id),
    FOREIGN KEY (task_id) REFERENCES sys_task(task_id) ON DELETE CASCADE,
    FOREIGN KEY (vehicle_id) REFERENCES tb_vehicle_info(vehicle_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务车辆关联表';
```
#### 1.4 ä»»åŠ¡é™„ä»¶è¡¨ (sys_task_attachment)
```sql
CREATE TABLE sys_task_attachment (
    attachment_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '附件ID',
    task_id BIGINT NOT NULL COMMENT '任务ID',
    file_name VARCHAR(255) NOT NULL COMMENT '文件名',
    file_path VARCHAR(500) NOT NULL COMMENT '文件路径',
    file_size BIGINT COMMENT '文件大小(字节)',
    file_type VARCHAR(50) COMMENT '文件类型',
    upload_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
    upload_by VARCHAR(64) NOT NULL COMMENT '上传者',
    INDEX idx_task_id (task_id),
    FOREIGN KEY (task_id) REFERENCES sys_task(task_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务附件表';
```
#### 1.3 ä»»åŠ¡æ“ä½œæ—¥å¿—è¡¨ (sys_task_log)
```sql
CREATE TABLE sys_task_log (
    log_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID',
    task_id BIGINT NOT NULL COMMENT '任务ID',
    operation_type VARCHAR(20) NOT NULL COMMENT '操作类型:CREATE-创建,UPDATE-更新,ASSIGN-分配,STATUS_CHANGE-状态变更,DELETE-删除',
    operation_desc VARCHAR(500) COMMENT '操作描述',
    old_value TEXT COMMENT '操作前值',
    new_value TEXT COMMENT '操作后值',
    operator_id BIGINT NOT NULL COMMENT '操作人ID',
    operator_name VARCHAR(64) NOT NULL COMMENT '操作人姓名',
    operation_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
    ip_address VARCHAR(128) COMMENT 'IP地址',
    INDEX idx_task_id (task_id),
    INDEX idx_operation_type (operation_type),
    INDEX idx_operator_id (operator_id),
    INDEX idx_operation_time (operation_time),
    FOREIGN KEY (task_id) REFERENCES sys_task(task_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务操作日志表';
```
### 2. æ•°æ®å­—典设计
#### 2.1 ä»»åŠ¡ç±»åž‹å­—å…¸
```sql
INSERT INTO sys_dict_type VALUES ('sys_task_type', '任务类型', '0', 'admin', sysdate(), '', null, '任务类型列表');
INSERT INTO sys_dict_data VALUES (1, 1, '维修保养', 'MAINTENANCE', 'sys_task_type', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '维修保养任务');
INSERT INTO sys_dict_data VALUES (2, 2, '加油任务', 'FUEL', 'sys_task_type', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '加油任务');
INSERT INTO sys_dict_data VALUES (3, 3, '其他', 'OTHER', 'sys_task_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '其他类型任务');
```
#### 2.2 ä»»åŠ¡çŠ¶æ€å­—å…¸
```sql
INSERT INTO sys_dict_type VALUES ('sys_task_status', '任务状态', '0', 'admin', sysdate(), '', null, '任务状态列表');
INSERT INTO sys_dict_data VALUES (4, 1, '待开始', 'PENDING', 'sys_task_status', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '任务已创建,等待开始');
INSERT INTO sys_dict_data VALUES (5, 2, '任务中', 'IN_PROGRESS', 'sys_task_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '任务已经开始');
INSERT INTO sys_dict_data VALUES (6, 3, '已完成', 'COMPLETED', 'sys_task_status', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '任务已完成');
INSERT INTO sys_dict_data VALUES (7, 4, '已取消', 'CANCELLED', 'sys_task_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '任务已取消');
```
#### 2.3 è½¦è¾†ç±»åž‹å­—å…¸
```sql
INSERT INTO sys_dict_type VALUES ('sys_vehicle_type', '车辆类型', '0', 'admin', sysdate(), '', null, '车辆类型列表');
INSERT INTO sys_dict_data VALUES (8, 1, '救护车', 'AMBULANCE', 'sys_vehicle_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '救护车');
INSERT INTO sys_dict_data VALUES (9, 2, '转运车', 'TRANSFER', 'sys_vehicle_type', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '转运车');
INSERT INTO sys_dict_data VALUES (10, 3, '维修车', 'MAINTENANCE', 'sys_vehicle_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '维修车');
```
#### 2.4 è½¦è¾†çŠ¶æ€å­—å…¸
```sql
INSERT INTO sys_dict_type VALUES ('sys_vehicle_status', '车辆状态', '0', 'admin', sysdate(), '', null, '车辆状态列表');
INSERT INTO sys_dict_data VALUES (11, 1, '正常', '0', 'sys_vehicle_status', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '车辆正常使用');
INSERT INTO sys_dict_data VALUES (12, 2, '停用', '1', 'sys_vehicle_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '车辆停用');
```
#### 2.5 ä»»åŠ¡è½¦è¾†å…³è”çŠ¶æ€å­—å…¸
```sql
INSERT INTO sys_dict_type VALUES ('sys_task_vehicle_status', '任务车辆关联状态', '0', 'admin', sysdate(), '', null, '任务车辆关联状态列表');
INSERT INTO sys_dict_data VALUES (15, 1, '已分配', 'ASSIGNED', 'sys_task_vehicle_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '车辆已分配给任务');
INSERT INTO sys_dict_data VALUES (16, 2, '执行中', 'ACTIVE', 'sys_task_vehicle_status', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '车辆正在执行任务');
INSERT INTO sys_dict_data VALUES (17, 3, '已完成', 'COMPLETED', 'sys_task_vehicle_status', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '车辆任务已完成');
INSERT INTO sys_dict_data VALUES (18, 4, '已取消', 'CANCELLED', 'sys_task_vehicle_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '车辆任务已取消');
```
## æŽ¥å£è®¾è®¡
### 1. REST API è§„范
#### 1.1 ä»»åŠ¡ç®¡ç†æŽ¥å£
##### 1.1.1 åˆ›å»ºä»»åŠ¡
```
POST /api/task
Content-Type: application/json
Request Body:
{
    "taskType": "MAINTENANCE",
    "taskTitle": "车辆维修保养",
    "taskDescription": "定期保养检查",
    "departureAddress": "北京市朝阳区",
    "destinationAddress": "北京市海淀区维修厂",
    "plannedStartTime": "2024-01-15 09:00:00",
    "plannedEndTime": "2024-01-15 17:00:00",
    "assigneeId": 100,
    "remark": "紧急维修"
}
Response:
{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "taskId": 1,
        "taskCode": "TASK202401150001"
    }
}
```
##### 1.1.2 æŸ¥è¯¢ä»»åŠ¡åˆ—è¡¨
```
GET /api/task/list?pageNum=1&pageSize=10&taskType=MAINTENANCE&taskStatus=PENDING
Response:
{
    "code": 200,
    "msg": "查询成功",
    "rows": [
        {
            "taskId": 1,
            "taskCode": "TASK202401150001",
            "taskType": "MAINTENANCE",
            "taskStatus": "PENDING",
            "taskTitle": "车辆维修保养",
            "departureAddress": "北京市朝阳区",
            "destinationAddress": "北京市海淀区维修厂",
            "plannedStartTime": "2024-01-15 09:00:00",
            "plannedEndTime": "2024-01-15 17:00:00",
            "creatorName": "张三",
            "assigneeName": "李四",
            "createTime": "2024-01-15 08:30:00"
        }
    ],
    "total": 1
}
```
##### 1.1.3 èŽ·å–ä»»åŠ¡è¯¦æƒ…
```
GET /api/task/{taskId}
Response:
{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "taskId": 1,
        "taskCode": "TASK202401150001",
        "taskType": "MAINTENANCE",
        "taskStatus": "PENDING",
        "taskTitle": "车辆维修保养",
        "taskDescription": "定期保养检查",
        "departureAddress": "北京市朝阳区",
        "destinationAddress": "北京市海淀区维修厂",
        "plannedStartTime": "2024-01-15 09:00:00",
        "plannedEndTime": "2024-01-15 17:00:00",
        "actualStartTime": null,
        "actualEndTime": null,
        "creatorName": "张三",
        "assigneeName": "李四",
        "remark": "紧急维修",
        "attachments": [],
        "operationLogs": [],
        "assignedVehicles": [
            {
                "id": 1,
                "vehicleId": 1,
                "vehicleNo": "京A12345",
                "vehicleType": "AMBULANCE",
                "assignTime": "2024-01-15 09:00:00",
                "assignBy": "张三",
                "status": "ASSIGNED",
                "remark": "分配救护车执行任务"
            }
        ]
    }
}
```
##### 1.1.4 æ›´æ–°ä»»åŠ¡
```
PUT /api/task/{taskId}
Content-Type: application/json
Request Body:
{
    "taskTitle": "车辆维修保养(更新)",
    "taskDescription": "定期保养检查,增加安全检查",
    "plannedStartTime": "2024-01-15 10:00:00",
    "assigneeId": 101,
    "remark": "更新后的备注"
}
Response:
{
    "code": 200,
    "msg": "操作成功"
}
```
##### 1.1.5 æ›´æ–°ä»»åŠ¡çŠ¶æ€
```
PUT /api/task/{taskId}/status
Content-Type: application/json
Request Body:
{
    "taskStatus": "IN_PROGRESS",
    "actualStartTime": "2024-01-15 10:15:00",
    "remark": "任务开始执行"
}
Response:
{
    "code": 200,
    "msg": "操作成功"
}
```
##### 1.1.6 åˆ†é…ä»»åŠ¡
```
PUT /api/task/{taskId}/assign
Content-Type: application/json
Request Body:
{
    "assigneeId": 102,
    "remark": "重新分配给王五"
}
Response:
{
    "code": 200,
    "msg": "操作成功"
}
```
##### 1.1.7 åˆ é™¤ä»»åŠ¡
```
DELETE /api/task/{taskIds}
Response:
{
    "code": 200,
    "msg": "操作成功"
}
```
#### 1.2 ä»»åŠ¡é™„ä»¶æŽ¥å£
##### 1.2.1 ä¸Šä¼ é™„ä»¶
```
POST /api/task/{taskId}/attachment
Content-Type: multipart/form-data
Request Body:
- file: æ–‡ä»¶å†…容
Response:
{
    "code": 200,
    "msg": "上传成功",
    "data": {
        "attachmentId": 1,
        "fileName": "维修报告.pdf",
        "filePath": "/uploads/task/2024/01/15/xxx.pdf"
    }
}
```
##### 1.2.2 åˆ é™¤é™„ä»¶
```
DELETE /api/task/attachment/{attachmentId}
Response:
{
    "code": 200,
    "msg": "操作成功"
}
```
#### 1.3 è½¦è¾†ç®¡ç†æŽ¥å£
##### 1.3.1 æŸ¥è¯¢è½¦è¾†åˆ—表
```
GET /api/vehicle/list?pageNum=1&pageSize=10&vehicleType=AMBULANCE&vehicleStatus=ACTIVE&deptId=100
Response:
{
    "code": 200,
    "msg": "查询成功",
    "rows": [
        {
            "vehicleId": 1,
            "platformCode": "A",
            "vehicleNo": "京A12345",
            "vehicleType": "AMBULANCE",
            "vehicleBrand": "奔驰",
            "vehicleModel": "Sprinter",
            "vehicleStatus": "0",
            "deviceId": "DEV001",
            "deptId": 100,
            "deptName": "北京急救中心",
            "createTime": "2024-01-15 08:30:00"
        }
    ],
    "total": 1
}
```
##### 1.3.2 èŽ·å–è½¦è¾†è¯¦æƒ…
```
GET /api/vehicle/{vehicleId}
Response:
{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "vehicleId": 1,
        "platformCode": "A",
        "vehicleNo": "京A12345",
        "vehicleType": "AMBULANCE",
        "vehicleBrand": "奔驰",
        "vehicleModel": "Sprinter",
        "vehicleColor": "白色",
        "vehicleStatus": "0",
        "deviceId": "DEV001",
        "deptId": 100,
        "deptName": "北京急救中心",
        "createBy": "admin",
        "createTime": "2024-01-15 08:30:00",
        "updateBy": "admin",
        "updateTime": "2024-01-15 10:30:00",
        "remark": "车辆状态良好"
    }
}
```
##### 1.3.3 åˆ†é…è½¦è¾†ç»™ä»»åŠ¡
```
POST /api/task/{taskId}/assign-vehicle
Content-Type: application/json
Request Body:
{
    "vehicleId": 1,
    "remark": "分配救护车执行任务"
}
Response:
{
    "code": 200,
    "msg": "操作成功"
}
```
##### 1.3.4 æ‰¹é‡åˆ†é…è½¦è¾†ç»™ä»»åŠ¡
```
POST /api/task/{taskId}/assign-vehicles
Content-Type: application/json
Request Body:
{
    "vehicleIds": [1, 2, 3],
    "remark": "分配多辆车执行任务"
}
Response:
{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "successCount": 3,
        "failedCount": 0,
        "details": [
            {
                "vehicleId": 1,
                "vehicleNo": "京A12345",
                "status": "success",
                "message": "分配成功"
            },
            {
                "vehicleId": 2,
                "vehicleNo": "京A12346",
                "status": "success",
                "message": "分配成功"
            },
            {
                "vehicleId": 3,
                "vehicleNo": "京A12347",
                "status": "success",
                "message": "分配成功"
            }
        ]
    }
}
```
##### 1.3.5 å–消任务车辆分配
```
DELETE /api/task/{taskId}/vehicle/{vehicleId}
Response:
{
    "code": 200,
    "msg": "操作成功"
}
```
##### 1.3.6 æŸ¥è¯¢ä»»åŠ¡å…³è”çš„è½¦è¾†
```
GET /api/task/{taskId}/vehicles
Response:
{
    "code": 200,
    "msg": "查询成功",
    "data": [
        {
            "id": 1,
            "taskId": 1,
            "vehicleId": 1,
            "vehicleNo": "京A12345",
            "vehicleType": "AMBULANCE",
            "assignTime": "2024-01-15 09:00:00",
            "assignBy": "张三",
            "status": "ASSIGNED",
            "remark": "分配救护车执行任务"
        }
    ]
}
```
##### 1.3.7 æŸ¥è¯¢å¯ç”¨è½¦è¾†
```
GET /api/vehicle/available?deptId=100&taskType=MAINTENANCE
Response:
{
    "code": 200,
    "msg": "查询成功",
    "data": [
        {
            "vehicleId": 1,
            "vehicleNo": "京A12345",
            "vehicleType": "AMBULANCE",
            "vehicleBrand": "奔驰",
            "vehicleModel": "Sprinter",
            "vehicleStatus": "0",
            "deptName": "北京急救中心",
            "currentLocation": "北京市朝阳区"
        },
        {
            "vehicleId": 2,
            "vehicleNo": "京A12346",
            "vehicleType": "AMBULANCE",
            "vehicleBrand": "奔驰",
            "vehicleModel": "Sprinter",
            "vehicleStatus": "0",
            "deptName": "北京急救中心",
            "currentLocation": "北京市海淀区"
        }
    ]
}
```
#### 1.4 ä»»åŠ¡ç»Ÿè®¡æŽ¥å£
##### 1.4.1 ä»»åŠ¡ç»Ÿè®¡æ¦‚è§ˆ
```
GET /api/task/statistics
Response:
{
    "code": 200,
    "msg": "查询成功",
    "data": {
        "totalTasks": 100,
        "pendingTasks": 20,
        "inProgressTasks": 30,
        "completedTasks": 45,
        "cancelledTasks": 5,
        "todayTasks": 8,
        "overdueTasks": 3,
        "vehicleUtilization": 85.5
    }
}
```
## ç¨‹åºè®¾è®¡
### 1. åŽç«¯æž¶æž„设计
#### 1.1 æ¨¡å—结构
```
ruoyi-task/
├── src/main/java/com/ruoyi/task/
│   â”œâ”€â”€ controller/          # æŽ§åˆ¶å™¨å±‚
│   â”‚   â”œâ”€â”€ TaskController.java
│   â”‚   â”œâ”€â”€ TaskAttachmentController.java
│   â”‚   â”œâ”€â”€ VehicleController.java
│   â”‚   â””── TaskVehicleController.java
│   â”œâ”€â”€ service/            # æœåС层
│   â”‚   â”œâ”€â”€ ITaskService.java
│   â”‚   â”œâ”€â”€ impl/TaskServiceImpl.java
│   â”‚   â”œâ”€â”€ ITaskAttachmentService.java
│   â”‚   â”œâ”€â”€ impl/TaskAttachmentServiceImpl.java
│   â”‚   â”œâ”€â”€ IVehicleService.java
│   â”‚   â”œâ”€â”€ impl/VehicleServiceImpl.java
│   â”‚   â”œâ”€â”€ ITaskVehicleService.java
│   â”‚   â””── impl/TaskVehicleServiceImpl.java
│   â”œâ”€â”€ domain/             # é¢†åŸŸå±‚
│   â”‚   â”œâ”€â”€ Task.java
│   â”‚   â”œâ”€â”€ TaskAttachment.java
│   â”‚   â”œâ”€â”€ TaskLog.java
│   â”‚   â”œâ”€â”€ Vehicle.java
│   â”‚   â”œâ”€â”€ TaskVehicle.java
│   â”‚   â”œâ”€â”€ enums/
│   â”‚   â”‚   â”œâ”€â”€ TaskType.java
│   â”‚   â”‚   â”œâ”€â”€ TaskStatus.java
│   â”‚   â”‚   â”œâ”€â”€ VehicleType.java
│   â”‚   â”‚   â”œâ”€â”€ VehicleStatus.java
│   â”‚   â”‚   â””── TaskVehicleStatus.java
│   â”‚   â””── vo/
│   â”‚       â”œâ”€â”€ TaskQueryVO.java
│   â”‚       â”œâ”€â”€ TaskCreateVO.java
│   â”‚       â”œâ”€â”€ TaskUpdateVO.java
│   â”‚       â”œâ”€â”€ VehicleQueryVO.java
│   â”‚       â”œâ”€â”€ VehicleCreateVO.java
│   â”‚       â””── TaskVehicleAssignVO.java
│   â”œâ”€â”€ mapper/             # æ•°æ®è®¿é—®å±‚
│   â”‚   â”œâ”€â”€ TaskMapper.java
│   â”‚   â”œâ”€â”€ TaskAttachmentMapper.java
│   â”‚   â”œâ”€â”€ TaskLogMapper.java
│   â”‚   â”œâ”€â”€ VehicleMapper.java
│   â”‚   â””── TaskVehicleMapper.java
│   â”œâ”€â”€ config/             # é…ç½®ç±»
│   â”‚   â””── TaskConfig.java
│   â””── utils/              # å·¥å…·ç±»
│       â”œâ”€â”€ TaskCodeGenerator.java
│       â”œâ”€â”€ TaskStatusValidator.java
│       â””── VehicleCodeGenerator.java
└── src/main/resources/
    â””── mapper/
        â”œâ”€â”€ TaskMapper.xml
        â”œâ”€â”€ TaskAttachmentMapper.xml
        â”œâ”€â”€ TaskLogMapper.xml
        â”œâ”€â”€ VehicleMapper.xml
        â””── TaskVehicleMapper.xml
```
#### 1.2 æ ¸å¿ƒç±»è®¾è®¡
##### 1.2.1 ä»»åŠ¡å®žä½“ç±» (Task.java)
```java
/**
 * ä»»åŠ¡å®žä½“ç±»
 * åŒ…含任务的所有属性和业务方法
 */
public class Task extends BaseEntity {
    private Long taskId;
    private String taskCode;
    private TaskType taskType;
    private TaskStatus taskStatus;
    private String taskTitle;
    private String taskDescription;
    private String departureAddress;
    private String destinationAddress;
    private Date plannedStartTime;
    private Date plannedEndTime;
    private Date actualStartTime;
    private Date actualEndTime;
    private Long creatorId;
    private Long assigneeId;
    private Long deptId;
    // å…³è”车辆列表(通过中间表查询)
    private List<TaskVehicle> assignedVehicles;
    // ä¸šåŠ¡æ–¹æ³•
    public boolean canChangeStatus(TaskStatus newStatus);
    public boolean isOverdue();
    public long getDuration();
    public void start();
    public void complete();
    public void cancel();
    public List<Vehicle> getAssignedVehicles();
    public boolean hasVehicle(Long vehicleId);
}
```
##### 1.2.2 ä»»åŠ¡æœåŠ¡æŽ¥å£ (ITaskService.java)
```java
/**
 * ä»»åŠ¡æœåŠ¡æŽ¥å£
 * å®šä¹‰ä»»åŠ¡ç›¸å…³çš„ä¸šåŠ¡æ“ä½œ
 */
public interface ITaskService {
    // åŸºç¡€CRUD操作
    List<Task> selectTaskList(TaskQueryVO queryVO);
    Task selectTaskById(Long taskId);
    int insertTask(TaskCreateVO createVO);
    int updateTask(TaskUpdateVO updateVO);
    int deleteTaskByIds(Long[] taskIds);
    // ä¸šåŠ¡æ“ä½œ
    int assignTask(Long taskId, Long assigneeId, String remark);
    int changeTaskStatus(Long taskId, TaskStatus newStatus, String remark);
    int uploadAttachment(Long taskId, MultipartFile file);
    int deleteAttachment(Long attachmentId);
    // è½¦è¾†ç®¡ç†æ“ä½œ
    int assignVehicleToTask(Long taskId, Long vehicleId, String remark);
    int unassignVehicleFromTask(Long taskId, Long vehicleId);
    int assignMultipleVehiclesToTask(Long taskId, List<Long> vehicleIds, String remark);
    List<TaskVehicle> getTaskVehicles(Long taskId);
    List<Vehicle> getAvailableVehicles(Long deptId, String taskType);
    // ç»Ÿè®¡æŸ¥è¯¢
    TaskStatisticsVO getTaskStatistics();
    List<Task> selectOverdueTasks();
    List<Task> selectMyTasks(Long userId);
}
```
#### 1.3 ä¸šåŠ¡è§„åˆ™è®¾è®¡
##### 1.3.1 ä»»åŠ¡çŠ¶æ€æµè½¬è§„åˆ™
```java
/**
 * ä»»åŠ¡çŠ¶æ€æµè½¬éªŒè¯å™¨
 */
@Component
public class TaskStatusValidator {
    private static final Map<TaskStatus, Set<TaskStatus>> ALLOWED_TRANSITIONS = new HashMap<>();
    static {
        // PENDING -> IN_PROGRESS, CANCELLED
        ALLOWED_TRANSITIONS.put(PENDING, Set.of(IN_PROGRESS, CANCELLED));
        // IN_PROGRESS -> COMPLETED, CANCELLED, PENDING
        ALLOWED_TRANSITIONS.put(IN_PROGRESS, Set.of(COMPLETED, CANCELLED, PENDING));
        // COMPLETED -> ä¸å…è®¸ä»»ä½•状态变更
        ALLOWED_TRANSITIONS.put(COMPLETED, Set.of());
        // CANCELLED -> ä¸å…è®¸ä»»ä½•状态变更
        ALLOWED_TRANSITIONS.put(CANCELLED, Set.of());
    }
    public boolean canTransition(TaskStatus from, TaskStatus to) {
        return ALLOWED_TRANSITIONS.get(from).contains(to);
    }
}
```
##### 1.3.2 ä»»åŠ¡ç¼–å·ç”Ÿæˆè§„åˆ™
```java
/**
 * ä»»åŠ¡ç¼–å·ç”Ÿæˆå™¨
 * æ ¼å¼ï¼šTASK + YYYYMMDD + 4位序号
 */
@Component
public class TaskCodeGenerator {
    public String generateTaskCode() {
        String dateStr = DateUtils.formatDate(new Date(), "yyyyMMdd");
        String sequence = getNextSequence(dateStr);
        return "TASK" + dateStr + sequence;
    }
    private String getNextSequence(String dateStr) {
        // æŸ¥è¯¢å½“日最大序号并递增
        // å®žçŽ°é€»è¾‘...
    }
}
```
### 2. å‰ç«¯æž¶æž„设计
#### 2.1 é¡µé¢ç»„件结构
```
src/views/task/
├── index.vue              # ä»»åŠ¡åˆ—è¡¨é¡µé¢
├── detail.vue             # ä»»åŠ¡è¯¦æƒ…é¡µé¢
├── create.vue             # åˆ›å»ºä»»åŠ¡é¡µé¢
├── edit.vue               # ç¼–辑任务页面
└── components/
    â”œâ”€â”€ TaskList.vue       # ä»»åŠ¡åˆ—è¡¨ç»„ä»¶
    â”œâ”€â”€ TaskForm.vue       # ä»»åŠ¡è¡¨å•ç»„ä»¶
    â”œâ”€â”€ TaskStatus.vue     # ä»»åŠ¡çŠ¶æ€ç»„ä»¶
    â”œâ”€â”€ TaskAttachment.vue # ä»»åŠ¡é™„ä»¶ç»„ä»¶
    â””── TaskLog.vue        # ä»»åŠ¡æ—¥å¿—ç»„ä»¶
```
#### 2.2 API服务设计
```javascript
// src/api/task.js
import request from '@/utils/request'
// ä»»åŠ¡ç®¡ç†API
export function listTask(query) {
  return request({
    url: '/api/task/list',
    method: 'get',
    params: query
  })
}
export function getTask(taskId) {
  return request({
    url: '/api/task/' + taskId,
    method: 'get'
  })
}
export function addTask(data) {
  return request({
    url: '/api/task',
    method: 'post',
    data: data
  })
}
export function updateTask(data) {
  return request({
    url: '/api/task/' + data.taskId,
    method: 'put',
    data: data
  })
}
export function deleteTask(taskIds) {
  return request({
    url: '/api/task/' + taskIds,
    method: 'delete'
  })
}
export function assignTask(taskId, data) {
  return request({
    url: '/api/task/' + taskId + '/assign',
    method: 'put',
    data: data
  })
}
export function changeTaskStatus(taskId, data) {
  return request({
    url: '/api/task/' + taskId + '/status',
    method: 'put',
    data: data
  })
}
// é™„件管理API
export function uploadAttachment(taskId, file) {
  const formData = new FormData()
  formData.append('file', file)
  return request({
    url: '/api/task/' + taskId + '/attachment',
    method: 'post',
    data: formData,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })
}
export function deleteAttachment(attachmentId) {
  return request({
    url: '/api/task/attachment/' + attachmentId,
    method: 'delete'
  })
}
// ç»Ÿè®¡API
export function getTaskStatistics() {
  return request({
    url: '/api/task/statistics',
    method: 'get'
  })
}
// è½¦è¾†ç®¡ç†API
export function listVehicleByDept(deptId, query) {
  return request({
    url: '/api/vehicle/list-by-dept/' + deptId,
    method: 'get',
    params: query
  })
}
export function listAvailableVehicles(deptId, taskType) {
  return request({
    url: '/api/vehicle/available',
    method: 'get',
    params: { deptId, taskType }
  })
}
export function assignVehiclesToTask(taskId, vehicleIds, remark) {
  return request({
    url: '/api/task/' + taskId + '/assign-vehicles',
    method: 'post',
    data: { vehicleIds, remark }
  })
}
export function getTaskVehicleUsage(taskId) {
  return request({
    url: '/api/task/' + taskId + '/vehicle-usage',
    method: 'get'
  })
}
```
### 3. æƒé™æŽ§åˆ¶è®¾è®¡
#### 3.1 èœå•权限配置
```sql
-- ä»»åŠ¡ç®¡ç†èœå•
INSERT INTO sys_menu VALUES (2000, '任务管理', 0, 5, 'task', null, '', 1, 0, 'M', '0', '0', '', 'task', 'admin', sysdate(), '', null, '任务管理目录');
-- é€šç”¨ä»»åŠ¡èœå•
INSERT INTO sys_menu VALUES (2001, '通用任务', 2000, 1, 'general', 'task/general/index', '', 1, 0, 'C', '0', '0', 'task:general:view', 'list', 'admin', sysdate(), '', null, '通用任务菜单');
-- ä»»åŠ¡ç®¡ç†æŒ‰é’®æƒé™
INSERT INTO sys_menu VALUES (2002, '任务查询', 2001, 1, '', '', '', 1, 0, 'F', '0', '0', 'task:general:query', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2003, '任务新增', 2001, 2, '', '', '', 1, 0, 'F', '0', '0', 'task:general:add', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2004, '任务修改', 2001, 3, '', '', '', 1, 0, 'F', '0', '0', 'task:general:edit', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2005, '任务删除', 2001, 4, '', '', '', 1, 0, 'F', '0', '0', 'task:general:remove', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2006, '任务分配', 2001, 5, '', '', '', 1, 0, 'F', '0', '0', 'task:general:assign', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2007, '状态变更', 2001, 6, '', '', '', 1, 0, 'F', '0', '0', 'task:general:status', '#', 'admin', sysdate(), '', null, '');
```
#### 3.2 æ•°æ®æƒé™æŽ§åˆ¶
```java
/**
 * ä»»åŠ¡æ•°æ®æƒé™æŽ§åˆ¶
 * åŸºäºŽéƒ¨é—¨è¿›è¡Œæ•°æ®éš”离
 */
@Aspect
@Component
public class TaskDataScopeAspect {
    @Before("@annotation(dataScope)")
    public void doBefore(JoinPoint point, DataScope dataScope) {
        // èŽ·å–å½“å‰ç”¨æˆ·éƒ¨é—¨æƒé™
        // æ·»åŠ æ•°æ®æƒé™è¿‡æ»¤æ¡ä»¶
        // å®žçŽ°é€»è¾‘...
    }
}
```
### 4. å¼‚常处理设计
#### 4.1 ä¸šåŠ¡å¼‚å¸¸å®šä¹‰
```java
/**
 * ä»»åŠ¡ç›¸å…³ä¸šåŠ¡å¼‚å¸¸
 */
public class TaskException extends RuntimeException {
    private String code;
    private String message;
    public TaskException(String code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }
}
/**
 * ä»»åŠ¡å¼‚å¸¸å¸¸é‡
 */
public class TaskErrorCode {
    public static final String TASK_NOT_FOUND = "TASK_001";
    public static final String TASK_STATUS_INVALID = "TASK_002";
    public static final String TASK_ASSIGNEE_INVALID = "TASK_003";
    public static final String TASK_PERMISSION_DENIED = "TASK_004";
}
```
#### 4.2 å…¨å±€å¼‚常处理
```java
/**
 * å…¨å±€å¼‚常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(TaskException.class)
    public AjaxResult handleTaskException(TaskException e) {
        return AjaxResult.error(e.getCode(), e.getMessage());
    }
}
```
## éƒ¨ç½²å’Œé…ç½®
### 1. çŽ¯å¢ƒè¦æ±‚
- JDK 1.8+
- MySQL 5.7+
- Redis 3.0+
- Maven 3.6+
### 2. é…ç½®æ–‡ä»¶
```yaml
# application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ry-task?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: password
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
# ä»»åŠ¡ç›¸å…³é…ç½®
task:
  # ä»»åŠ¡ç¼–å·å‰ç¼€
  code-prefix: TASK
  # é™„件上传路径
  upload-path: /uploads/task/
  # æœ€å¤§é™„件大小(MB)
  max-file-size: 10
  # å…è®¸çš„æ–‡ä»¶ç±»åž‹
  allowed-file-types: pdf,doc,docx,jpg,jpeg,png
```
### 3. æ•°æ®åº“初始化
```sql
-- æ‰§è¡Œæ•°æ®åº“表创建脚本
source sql/task_tables.sql;
-- æ‰§è¡Œæ•°æ®å­—典初始化脚本
source sql/task_dict_data.sql;
-- æ‰§è¡Œèœå•权限初始化脚本
source sql/task_menu.sql;
```
## æµ‹è¯•ç­–ç•¥
### 1. å•元测试
- æœåŠ¡å±‚ä¸šåŠ¡é€»è¾‘æµ‹è¯•
- æ•°æ®è®¿é—®å±‚测试
- å·¥å…·ç±»æµ‹è¯•
### 2. é›†æˆæµ‹è¯•
- API接口测试
- æ•°æ®åº“操作测试
- æƒé™æŽ§åˆ¶æµ‹è¯•
### 3. æ€§èƒ½æµ‹è¯•
- å¹¶å‘创建任务测试
- å¤§æ•°æ®é‡æŸ¥è¯¢æµ‹è¯•
- æ–‡ä»¶ä¸Šä¼ æ€§èƒ½æµ‹è¯•
## ç›‘控和日志
### 1. ä¸šåŠ¡ç›‘æŽ§
- ä»»åŠ¡åˆ›å»ºæ•°é‡ç»Ÿè®¡
- ä»»åŠ¡å®ŒæˆçŽ‡ç»Ÿè®¡
- ä»»åŠ¡è¶…æ—¶çŽ‡ç»Ÿè®¡
### 2. ç³»ç»Ÿç›‘控
- API响应时间监控
- æ•°æ®åº“连接池监控
- æ–‡ä»¶å­˜å‚¨ç©ºé—´ç›‘控
### 3. æ—¥å¿—记录
- æ“ä½œæ—¥å¿—记录
- å¼‚常日志记录
- æ€§èƒ½æ—¥å¿—记录
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java
New file
@@ -0,0 +1,84 @@
package com.ruoyi.web.controller.task;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import com.ruoyi.system.domain.SysTask;
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.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
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.SysTaskAttachment;
import com.ruoyi.system.service.ISysTaskService;
/**
 * ä»»åС附件Controller
 *
 * @author ruoyi
 * @date 2024-01-15
 */
@RestController
@RequestMapping("/task/attachment")
public class SysTaskAttachmentController extends BaseController {
    @Autowired
    private ISysTaskService sysTaskService;
    /**
     * æŸ¥è¯¢ä»»åŠ¡é™„ä»¶åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('task:general:query')")
    @GetMapping("/list/{taskId}")
    public AjaxResult list(@PathVariable("taskId") Long taskId) {
        SysTask task = sysTaskService.getTaskDetail(taskId);
        return success(task.getAttachments());
    }
    /**
     * ä¸Šä¼ ä»»åС附件
     */
    @PreAuthorize("@ss.hasPermi('task:general:edit')")
    @Log(title = "任务附件", businessType = BusinessType.INSERT)
    @PostMapping("/upload/{taskId}")
    public AjaxResult upload(@PathVariable("taskId") Long taskId, @RequestParam("file") MultipartFile file) {
        try {
            int result = sysTaskService.uploadAttachment(taskId, file);
            if (result > 0) {
                return success("上传成功");
            } else {
                return error("上传失败");
            }
        } catch (Exception e) {
            return error("上传失败:" + e.getMessage());
        }
    }
    /**
     * åˆ é™¤ä»»åС附件
     */
    @PreAuthorize("@ss.hasPermi('task:general:edit')")
    @Log(title = "任务附件", businessType = BusinessType.DELETE)
    @DeleteMapping("/{attachmentId}")
    public AjaxResult remove(@PathVariable("attachmentId") Long attachmentId) {
        try {
            int result = sysTaskService.deleteAttachment(attachmentId);
            if (result > 0) {
                return success("删除成功");
            } else {
                return error("删除失败");
            }
        } catch (Exception e) {
            return error("删除失败:" + e.getMessage());
        }
    }
}
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
New file
@@ -0,0 +1,205 @@
package com.ruoyi.web.controller.task;
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.SysTask;
import com.ruoyi.system.domain.vo.TaskQueryVO;
import com.ruoyi.system.domain.vo.TaskCreateVO;
import com.ruoyi.system.domain.vo.TaskUpdateVO;
import com.ruoyi.system.domain.vo.TaskStatisticsVO;
import com.ruoyi.system.domain.enums.TaskStatus;
import com.ruoyi.system.service.ISysTaskService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
/**
 * ä»»åŠ¡ç®¡ç†Controller
 *
 * @author ruoyi
 * @date 2024-01-15
 */
@RestController
@RequestMapping("/task")
public class SysTaskController extends BaseController {
    @Autowired
    private ISysTaskService sysTaskService;
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç®¡ç†åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('task:general:query')")
    @GetMapping("/list")
    public TableDataInfo list(TaskQueryVO queryVO) {
        startPage();
        List<SysTask> list = sysTaskService.selectSysTaskList(queryVO);
        return getDataTable(list);
    }
    /**
     * å¯¼å‡ºä»»åŠ¡ç®¡ç†åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('task:general:export')")
    @Log(title = "任务管理", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, TaskQueryVO queryVO) {
        List<SysTask> list = sysTaskService.selectSysTaskList(queryVO);
        ExcelUtil<SysTask> util = new ExcelUtil<SysTask>(SysTask.class);
        util.exportExcel(response, list, "任务管理数据");
    }
    /**
     * èŽ·å–ä»»åŠ¡ç®¡ç†è¯¦ç»†ä¿¡æ¯
     */
    @PreAuthorize("@ss.hasPermi('task:general:query')")
    @GetMapping(value = "/{taskId}")
    public AjaxResult getInfo(@PathVariable("taskId") Long taskId) {
        return success(sysTaskService.getTaskDetail(taskId));
    }
    /**
     * æ–°å¢žä»»åŠ¡ç®¡ç†
     */
    @PreAuthorize("@ss.hasPermi('task:general:add')")
    @Log(title = "任务管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@RequestBody TaskCreateVO createVO) {
        return toAjax(sysTaskService.insertSysTask(createVO));
    }
    /**
     * ä¿®æ”¹ä»»åŠ¡ç®¡ç†
     */
    @PreAuthorize("@ss.hasPermi('task:general:edit')")
    @Log(title = "任务管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@RequestBody TaskUpdateVO updateVO) {
        return toAjax(sysTaskService.updateSysTask(updateVO));
    }
    /**
     * åˆ é™¤ä»»åŠ¡ç®¡ç†
     */
    @PreAuthorize("@ss.hasPermi('task:general:remove')")
    @Log(title = "任务管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{taskIds}")
    public AjaxResult remove(@PathVariable Long[] taskIds) {
        return toAjax(sysTaskService.deleteSysTaskByTaskIds(taskIds));
    }
    /**
     * åˆ†é…ä»»åŠ¡
     */
    @PreAuthorize("@ss.hasPermi('task:general:assign')")
    @Log(title = "任务分配", businessType = BusinessType.UPDATE)
    @PutMapping("/{taskId}/assign")
    public AjaxResult assignTask(@PathVariable Long taskId, @RequestBody AssignTaskRequest request) {
        return toAjax(sysTaskService.assignTask(taskId, request.getAssigneeId(), request.getRemark()));
    }
    /**
     * æ›´æ–°ä»»åŠ¡çŠ¶æ€
     */
    @PreAuthorize("@ss.hasPermi('task:general:status')")
    @Log(title = "任务状态变更", businessType = BusinessType.UPDATE)
    @PutMapping("/{taskId}/status")
    public AjaxResult changeTaskStatus(@PathVariable Long taskId, @RequestBody ChangeStatusRequest request) {
        TaskStatus newStatus = TaskStatus.getByCode(request.getTaskStatus());
        if (newStatus == null) {
            return error("无效的任务状态");
        }
        return toAjax(sysTaskService.changeTaskStatus(taskId, newStatus, request.getRemark()));
    }
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç»Ÿè®¡ä¿¡æ¯
     */
    @PreAuthorize("@ss.hasPermi('task:general:query')")
    @GetMapping("/statistics")
    public AjaxResult getStatistics() {
        TaskStatisticsVO statistics = sysTaskService.getTaskStatistics();
        return success(statistics);
    }
    /**
     * æŸ¥è¯¢è¶…时任务列表
     */
    @PreAuthorize("@ss.hasPermi('task:general:query')")
    @GetMapping("/overdue")
    public AjaxResult getOverdueTasks() {
        List<SysTask> list = sysTaskService.selectOverdueTasks();
        return success(list);
    }
    /**
     * æŸ¥è¯¢æˆ‘的任务列表
     */
    @PreAuthorize("@ss.hasPermi('task:general:query')")
    @GetMapping("/my")
    public AjaxResult getMyTasks() {
        List<SysTask> list = sysTaskService.selectMyTasks(getUserId());
        return success(list);
    }
    /**
     * åˆ†é…ä»»åŠ¡è¯·æ±‚å¯¹è±¡
     */
    public static class AssignTaskRequest {
        private Long assigneeId;
        private String remark;
        public Long getAssigneeId() {
            return assigneeId;
        }
        public void setAssigneeId(Long assigneeId) {
            this.assigneeId = assigneeId;
        }
        public String getRemark() {
            return remark;
        }
        public void setRemark(String remark) {
            this.remark = remark;
        }
    }
    /**
     * å˜æ›´çŠ¶æ€è¯·æ±‚å¯¹è±¡
     */
    public static class ChangeStatusRequest {
        private String taskStatus;
        private String remark;
        public String getTaskStatus() {
            return taskStatus;
        }
        public void setTaskStatus(String taskStatus) {
            this.taskStatus = taskStatus;
        }
        public String getRemark() {
            return remark;
        }
        public void setRemark(String remark) {
            this.remark = remark;
        }
    }
}
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskVehicleController.java
New file
@@ -0,0 +1,158 @@
package com.ruoyi.web.controller.task;
import java.util.List;
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.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.RequestParam;
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.SysTaskVehicle;
import com.ruoyi.system.service.ISysTaskService;
/**
 * ä»»åŠ¡è½¦è¾†å…³è”Controller
 *
 * @author ruoyi
 * @date 2024-01-15
 */
@RestController
@RequestMapping("/task/vehicle")
public class SysTaskVehicleController extends BaseController {
    @Autowired
    private ISysTaskService sysTaskService;
    /**
     * æŸ¥è¯¢ä»»åŠ¡å…³è”çš„è½¦è¾†åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('task:general:query')")
    @GetMapping("/list/{taskId}")
    public AjaxResult list(@PathVariable("taskId") Long taskId) {
        List<SysTaskVehicle> list = sysTaskService.getTaskVehicles(taskId);
        return success(list);
    }
    /**
     * æŸ¥è¯¢å¯ç”¨è½¦è¾†åˆ—表
     */
    @PreAuthorize("@ss.hasPermi('task:general:query')")
    @GetMapping("/available")
    public AjaxResult getAvailableVehicles(@RequestParam Long deptId, @RequestParam(required = false) String taskType) {
        List<SysTaskVehicle> list = sysTaskService.getAvailableVehicles(deptId, taskType);
        return success(list);
    }
    /**
     * åˆ†é…è½¦è¾†ç»™ä»»åŠ¡
     */
    @PreAuthorize("@ss.hasPermi('task:general:assign')")
    @Log(title = "任务车辆分配", businessType = BusinessType.INSERT)
    @PostMapping("/assign/{taskId}")
    public AjaxResult assignVehicle(@PathVariable("taskId") Long taskId, @RequestBody AssignVehicleRequest request) {
        try {
            int result = sysTaskService.assignVehicleToTask(taskId, request.getVehicleId(), request.getRemark());
            if (result > 0) {
                return success("分配成功");
            } else {
                return error("分配失败");
            }
        } catch (Exception e) {
            return error("分配失败:" + e.getMessage());
        }
    }
    /**
     * æ‰¹é‡åˆ†é…è½¦è¾†ç»™ä»»åŠ¡
     */
    @PreAuthorize("@ss.hasPermi('task:general:assign')")
    @Log(title = "任务车辆批量分配", businessType = BusinessType.INSERT)
    @PostMapping("/assign-batch/{taskId}")
    public AjaxResult assignVehicles(@PathVariable("taskId") Long taskId, @RequestBody BatchAssignVehicleRequest request) {
        try {
            int result = sysTaskService.assignMultipleVehiclesToTask(taskId, request.getVehicleIds(), request.getRemark());
            if (result > 0) {
                return success("批量分配成功,共分配 " + result + " è¾†è½¦");
            } else {
                return error("批量分配失败");
            }
        } catch (Exception e) {
            return error("批量分配失败:" + e.getMessage());
        }
    }
    /**
     * å–消任务车辆分配
     */
    @PreAuthorize("@ss.hasPermi('task:general:assign')")
    @Log(title = "取消任务车辆分配", businessType = BusinessType.DELETE)
    @DeleteMapping("/{taskId}/{vehicleId}")
    public AjaxResult unassignVehicle(@PathVariable("taskId") Long taskId, @PathVariable("vehicleId") Long vehicleId) {
        try {
            int result = sysTaskService.unassignVehicleFromTask(taskId, vehicleId);
            if (result > 0) {
                return success("取消分配成功");
            } else {
                return error("取消分配失败");
            }
        } catch (Exception e) {
            return error("取消分配失败:" + e.getMessage());
        }
    }
    /**
     * åˆ†é…è½¦è¾†è¯·æ±‚对象
     */
    public static class AssignVehicleRequest {
        private Long vehicleId;
        private String remark;
        public Long getVehicleId() {
            return vehicleId;
        }
        public void setVehicleId(Long vehicleId) {
            this.vehicleId = vehicleId;
        }
        public String getRemark() {
            return remark;
        }
        public void setRemark(String remark) {
            this.remark = remark;
        }
    }
    /**
     * æ‰¹é‡åˆ†é…è½¦è¾†è¯·æ±‚对象
     */
    public static class BatchAssignVehicleRequest {
        private List<Long> vehicleIds;
        private String remark;
        public List<Long> getVehicleIds() {
            return vehicleIds;
        }
        public void setVehicleIds(List<Long> vehicleIds) {
            this.vehicleIds = vehicleIds;
        }
        public String getRemark() {
            return remark;
        }
        public void setRemark(String remark) {
            this.remark = remark;
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTask.java
New file
@@ -0,0 +1,369 @@
package com.ruoyi.system.domain;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.system.domain.enums.TaskStatus;
import com.ruoyi.system.domain.enums.TaskType;
/**
 * ä»»åŠ¡ç®¡ç†å¯¹è±¡ sys_task
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public class SysTask extends BaseEntity {
    private static final long serialVersionUID = 1L;
    /** ä»»åŠ¡ID */
    private Long taskId;
    /** ä»»åŠ¡ç¼–å· */
    @Excel(name = "任务编号")
    private String taskCode;
    /** ä»»åŠ¡ç±»åž‹ */
    @Excel(name = "任务类型", readConverterExp = "MAINTENANCE=维修保养,FUEL=加油任务,OTHER=其他")
    private String taskType;
    /** ä»»åŠ¡çŠ¶æ€ */
    @Excel(name = "任务状态", readConverterExp = "PENDING=待开始,IN_PROGRESS=任务中,COMPLETED=已完成,CANCELLED=已取消")
    private String taskStatus;
    /** ä»»åŠ¡æè¿° */
    @Excel(name = "任务描述")
    private String taskDescription;
    /** å‡ºå‘地址 */
    @Excel(name = "出发地址")
    private String departureAddress;
    /** ç›®çš„地址 */
    @Excel(name = "目的地址")
    private String destinationAddress;
    /** è®¡åˆ’开始时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @Excel(name = "计划开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date plannedStartTime;
    /** è®¡åˆ’结束时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @Excel(name = "计划结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date plannedEndTime;
    /** å®žé™…开始时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @Excel(name = "实际开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date actualStartTime;
    /** å®žé™…结束时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @Excel(name = "实际结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date actualEndTime;
    /** åˆ›å»ºäººID */
    @Excel(name = "创建人ID")
    private Long creatorId;
    /** æ‰§è¡ŒäººID */
    @Excel(name = "执行人ID")
    private Long assigneeId;
    /** å½’属部门ID */
    @Excel(name = "归属部门ID")
    private Long deptId;
    /** åˆ›å»ºäººå§“名 */
    @Excel(name = "创建人")
    private String creatorName;
    /** æ‰§è¡Œäººå§“名 */
    @Excel(name = "执行人")
    private String assigneeName;
    /** éƒ¨é—¨åç§° */
    @Excel(name = "部门名称")
    private String deptName;
    /** åˆ é™¤æ ‡å¿—(0代表存在 2代表删除) */
    private String delFlag;
    /** å…³è”车辆列表 */
    private List<SysTaskVehicle> assignedVehicles;
    /** é™„件列表 */
    private List<SysTaskAttachment> attachments;
    /** æ“ä½œæ—¥å¿—列表 */
    private List<SysTaskLog> operationLogs;
    public void setTaskId(Long taskId) {
        this.taskId = taskId;
    }
    public Long getTaskId() {
        return taskId;
    }
    public void setTaskCode(String taskCode) {
        this.taskCode = taskCode;
    }
    public String getTaskCode() {
        return taskCode;
    }
    public void setTaskType(String taskType) {
        this.taskType = taskType;
    }
    public String getTaskType() {
        return taskType;
    }
    public void setTaskStatus(String taskStatus) {
        this.taskStatus = taskStatus;
    }
    public String getTaskStatus() {
        return taskStatus;
    }
    public void setTaskDescription(String taskDescription) {
        this.taskDescription = taskDescription;
    }
    public String getTaskDescription() {
        return taskDescription;
    }
    public void setDepartureAddress(String departureAddress) {
        this.departureAddress = departureAddress;
    }
    public String getDepartureAddress() {
        return departureAddress;
    }
    public void setDestinationAddress(String destinationAddress) {
        this.destinationAddress = destinationAddress;
    }
    public String getDestinationAddress() {
        return destinationAddress;
    }
    public void setPlannedStartTime(Date plannedStartTime) {
        this.plannedStartTime = plannedStartTime;
    }
    public Date getPlannedStartTime() {
        return plannedStartTime;
    }
    public void setPlannedEndTime(Date plannedEndTime) {
        this.plannedEndTime = plannedEndTime;
    }
    public Date getPlannedEndTime() {
        return plannedEndTime;
    }
    public void setActualStartTime(Date actualStartTime) {
        this.actualStartTime = actualStartTime;
    }
    public Date getActualStartTime() {
        return actualStartTime;
    }
    public void setActualEndTime(Date actualEndTime) {
        this.actualEndTime = actualEndTime;
    }
    public Date getActualEndTime() {
        return actualEndTime;
    }
    public void setCreatorId(Long creatorId) {
        this.creatorId = creatorId;
    }
    public Long getCreatorId() {
        return creatorId;
    }
    public void setAssigneeId(Long assigneeId) {
        this.assigneeId = assigneeId;
    }
    public Long getAssigneeId() {
        return assigneeId;
    }
    public void setDeptId(Long deptId) {
        this.deptId = deptId;
    }
    public Long getDeptId() {
        return deptId;
    }
    public void setCreatorName(String creatorName) {
        this.creatorName = creatorName;
    }
    public String getCreatorName() {
        return creatorName;
    }
    public void setAssigneeName(String assigneeName) {
        this.assigneeName = assigneeName;
    }
    public String getAssigneeName() {
        return assigneeName;
    }
    public void setDeptName(String deptName) {
        this.deptName = deptName;
    }
    public String getDeptName() {
        return deptName;
    }
    public void setDelFlag(String delFlag) {
        this.delFlag = delFlag;
    }
    public String getDelFlag() {
        return delFlag;
    }
    public void setAssignedVehicles(List<SysTaskVehicle> assignedVehicles) {
        this.assignedVehicles = assignedVehicles;
    }
    public List<SysTaskVehicle> getAssignedVehicles() {
        return assignedVehicles;
    }
    public void setAttachments(List<SysTaskAttachment> attachments) {
        this.attachments = attachments;
    }
    public List<SysTaskAttachment> getAttachments() {
        return attachments;
    }
    public void setOperationLogs(List<SysTaskLog> operationLogs) {
        this.operationLogs = operationLogs;
    }
    public List<SysTaskLog> getOperationLogs() {
        return operationLogs;
    }
    /**
     * åˆ¤æ–­æ˜¯å¦å¯ä»¥å˜æ›´çŠ¶æ€
     */
    public boolean canChangeStatus(TaskStatus newStatus) {
        TaskStatus currentStatus = TaskStatus.getByCode(this.taskStatus);
        if (currentStatus == null || newStatus == null) {
            return false;
        }
        // çŠ¶æ€æµè½¬è§„åˆ™
        switch (currentStatus) {
            case PENDING:
                return newStatus == TaskStatus.IN_PROGRESS || newStatus == TaskStatus.CANCELLED;
            case IN_PROGRESS:
                return newStatus == TaskStatus.COMPLETED || newStatus == TaskStatus.CANCELLED || newStatus == TaskStatus.PENDING;
            case COMPLETED:
            case CANCELLED:
                return false;
            default:
                return false;
        }
    }
    /**
     * åˆ¤æ–­æ˜¯å¦è¶…æ—¶
     */
    public boolean isOverdue() {
        if (plannedEndTime == null) {
            return false;
        }
        return new Date().after(plannedEndTime) && !TaskStatus.COMPLETED.getCode().equals(taskStatus);
    }
    /**
     * èŽ·å–ä»»åŠ¡æŒç»­æ—¶é—´ï¼ˆåˆ†é’Ÿï¼‰
     */
    public long getDuration() {
        if (actualStartTime != null && actualEndTime != null) {
            return (actualEndTime.getTime() - actualStartTime.getTime()) / (1000 * 60);
        }
        return 0;
    }
    /**
     * å¼€å§‹ä»»åŠ¡
     */
    public void start() {
        this.taskStatus = TaskStatus.IN_PROGRESS.getCode();
        this.actualStartTime = new Date();
    }
    /**
     * å®Œæˆä»»åŠ¡
     */
    public void complete() {
        this.taskStatus = TaskStatus.COMPLETED.getCode();
        this.actualEndTime = new Date();
    }
    /**
     * å–消任务
     */
    public void cancel() {
        this.taskStatus = TaskStatus.CANCELLED.getCode();
    }
    /**
     * æ£€æŸ¥æ˜¯å¦åˆ†é…äº†æŒ‡å®šè½¦è¾†
     */
    public boolean hasVehicle(Long vehicleId) {
        if (assignedVehicles == null || vehicleId == null) {
            return false;
        }
        return assignedVehicles.stream().anyMatch(v -> vehicleId.equals(v.getVehicleId()));
    }
    @Override
    public String toString() {
        return "SysTask{" +
                "taskId=" + taskId +
                ", taskCode='" + taskCode + '\'' +
                ", taskType='" + taskType + '\'' +
                ", taskStatus='" + taskStatus + '\'' +
                ", taskDescription='" + taskDescription + '\'' +
                ", departureAddress='" + departureAddress + '\'' +
                ", destinationAddress='" + destinationAddress + '\'' +
                ", plannedStartTime=" + plannedStartTime +
                ", plannedEndTime=" + plannedEndTime +
                ", actualStartTime=" + actualStartTime +
                ", actualEndTime=" + actualEndTime +
                ", creatorId=" + creatorId +
                ", assigneeId=" + assigneeId +
                ", deptId=" + deptId +
                ", creatorName='" + creatorName + '\'' +
                ", assigneeName='" + assigneeName + '\'' +
                ", deptName='" + deptName + '\'' +
                '}';
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskAttachment.java
New file
@@ -0,0 +1,126 @@
package com.ruoyi.system.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
 * ä»»åŠ¡é™„ä»¶å¯¹è±¡ sys_task_attachment
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public class SysTaskAttachment extends BaseEntity {
    private static final long serialVersionUID = 1L;
    /** é™„ä»¶ID */
    private Long attachmentId;
    /** ä»»åŠ¡ID */
    @Excel(name = "任务ID")
    private Long taskId;
    /** æ–‡ä»¶å */
    @Excel(name = "文件名")
    private String fileName;
    /** æ–‡ä»¶è·¯å¾„ */
    @Excel(name = "文件路径")
    private String filePath;
    /** æ–‡ä»¶å¤§å°ï¼ˆå­—节) */
    @Excel(name = "文件大小")
    private Long fileSize;
    /** æ–‡ä»¶ç±»åž‹ */
    @Excel(name = "文件类型")
    private String fileType;
    /** ä¸Šä¼ æ—¶é—´ */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @Excel(name = "上传时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date uploadTime;
    /** ä¸Šä¼ è€… */
    @Excel(name = "上传者")
    private String uploadBy;
    public void setAttachmentId(Long attachmentId) {
        this.attachmentId = attachmentId;
    }
    public Long getAttachmentId() {
        return attachmentId;
    }
    public void setTaskId(Long taskId) {
        this.taskId = taskId;
    }
    public Long getTaskId() {
        return taskId;
    }
    public void setFileName(String fileName) {
        this.fileName = fileName;
    }
    public String getFileName() {
        return fileName;
    }
    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }
    public String getFilePath() {
        return filePath;
    }
    public void setFileSize(Long fileSize) {
        this.fileSize = fileSize;
    }
    public Long getFileSize() {
        return fileSize;
    }
    public void setFileType(String fileType) {
        this.fileType = fileType;
    }
    public String getFileType() {
        return fileType;
    }
    public void setUploadTime(Date uploadTime) {
        this.uploadTime = uploadTime;
    }
    public Date getUploadTime() {
        return uploadTime;
    }
    public void setUploadBy(String uploadBy) {
        this.uploadBy = uploadBy;
    }
    public String getUploadBy() {
        return uploadBy;
    }
    @Override
    public String toString() {
        return "SysTaskAttachment{" +
                "attachmentId=" + attachmentId +
                ", taskId=" + taskId +
                ", fileName='" + fileName + '\'' +
                ", filePath='" + filePath + '\'' +
                ", fileSize=" + fileSize +
                ", fileType='" + fileType + '\'' +
                ", uploadTime=" + uploadTime +
                ", uploadBy='" + uploadBy + '\'' +
                '}';
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskLog.java
New file
@@ -0,0 +1,152 @@
package com.ruoyi.system.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
 * ä»»åŠ¡æ“ä½œæ—¥å¿—å¯¹è±¡ sys_task_log
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public class SysTaskLog extends BaseEntity {
    private static final long serialVersionUID = 1L;
    /** æ—¥å¿—ID */
    private Long logId;
    /** ä»»åŠ¡ID */
    @Excel(name = "任务ID")
    private Long taskId;
    /** æ“ä½œç±»åž‹ */
    @Excel(name = "操作类型", readConverterExp = "CREATE=创建,UPDATE=更新,ASSIGN=分配,STATUS_CHANGE=状态变更,DELETE=删除")
    private String operationType;
    /** æ“ä½œæè¿° */
    @Excel(name = "操作描述")
    private String operationDesc;
    /** æ“ä½œå‰å€¼ */
    @Excel(name = "操作前值")
    private String oldValue;
    /** æ“ä½œåŽå€¼ */
    @Excel(name = "操作后值")
    private String newValue;
    /** æ“ä½œäººID */
    @Excel(name = "操作人ID")
    private Long operatorId;
    /** æ“ä½œäººå§“名 */
    @Excel(name = "操作人姓名")
    private String operatorName;
    /** æ“ä½œæ—¶é—´ */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date operationTime;
    /** IP地址 */
    @Excel(name = "IP地址")
    private String ipAddress;
    public void setLogId(Long logId) {
        this.logId = logId;
    }
    public Long getLogId() {
        return logId;
    }
    public void setTaskId(Long taskId) {
        this.taskId = taskId;
    }
    public Long getTaskId() {
        return taskId;
    }
    public void setOperationType(String operationType) {
        this.operationType = operationType;
    }
    public String getOperationType() {
        return operationType;
    }
    public void setOperationDesc(String operationDesc) {
        this.operationDesc = operationDesc;
    }
    public String getOperationDesc() {
        return operationDesc;
    }
    public void setOldValue(String oldValue) {
        this.oldValue = oldValue;
    }
    public String getOldValue() {
        return oldValue;
    }
    public void setNewValue(String newValue) {
        this.newValue = newValue;
    }
    public String getNewValue() {
        return newValue;
    }
    public void setOperatorId(Long operatorId) {
        this.operatorId = operatorId;
    }
    public Long getOperatorId() {
        return operatorId;
    }
    public void setOperatorName(String operatorName) {
        this.operatorName = operatorName;
    }
    public String getOperatorName() {
        return operatorName;
    }
    public void setOperationTime(Date operationTime) {
        this.operationTime = operationTime;
    }
    public Date getOperationTime() {
        return operationTime;
    }
    public void setIpAddress(String ipAddress) {
        this.ipAddress = ipAddress;
    }
    public String getIpAddress() {
        return ipAddress;
    }
    @Override
    public String toString() {
        return "SysTaskLog{" +
                "logId=" + logId +
                ", taskId=" + taskId +
                ", operationType='" + operationType + '\'' +
                ", operationDesc='" + operationDesc + '\'' +
                ", oldValue='" + oldValue + '\'' +
                ", newValue='" + newValue + '\'' +
                ", operatorId=" + operatorId +
                ", operatorName='" + operatorName + '\'' +
                ", operationTime=" + operationTime +
                ", ipAddress='" + ipAddress + '\'' +
                '}';
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskVehicle.java
New file
@@ -0,0 +1,152 @@
package com.ruoyi.system.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
 * ä»»åŠ¡è½¦è¾†å…³è”å¯¹è±¡ sys_task_vehicle
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public class SysTaskVehicle extends BaseEntity {
    private static final long serialVersionUID = 1L;
    /** å…³è”ID */
    private Long id;
    /** ä»»åŠ¡ID */
    @Excel(name = "任务ID")
    private Long taskId;
    /** è½¦è¾†ID */
    @Excel(name = "车辆ID")
    private Long vehicleId;
    /** åˆ†é…æ—¶é—´ */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @Excel(name = "分配时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date assignTime;
    /** åˆ†é…äºº */
    @Excel(name = "分配人")
    private String assignBy;
    /** å…³è”状态 */
    @Excel(name = "关联状态", readConverterExp = "ASSIGNED=已分配,ACTIVE=执行中,COMPLETED=已完成,CANCELLED=已取消")
    private String status;
    /** è½¦ç‰Œå· */
    @Excel(name = "车牌号")
    private String vehicleNo;
    /** è½¦è¾†ç±»åž‹ */
    @Excel(name = "车辆类型", readConverterExp = "AMBULANCE=救护车,TRANSFER=转运车,MAINTENANCE=维修车")
    private String vehicleType;
    /** è½¦è¾†å“ç‰Œ */
    @Excel(name = "车辆品牌")
    private String vehicleBrand;
    /** è½¦è¾†åž‹å· */
    @Excel(name = "车辆型号")
    private String vehicleModel;
    public void setId(Long id) {
        this.id = id;
    }
    public Long getId() {
        return id;
    }
    public void setTaskId(Long taskId) {
        this.taskId = taskId;
    }
    public Long getTaskId() {
        return taskId;
    }
    public void setVehicleId(Long vehicleId) {
        this.vehicleId = vehicleId;
    }
    public Long getVehicleId() {
        return vehicleId;
    }
    public void setAssignTime(Date assignTime) {
        this.assignTime = assignTime;
    }
    public Date getAssignTime() {
        return assignTime;
    }
    public void setAssignBy(String assignBy) {
        this.assignBy = assignBy;
    }
    public String getAssignBy() {
        return assignBy;
    }
    public void setStatus(String status) {
        this.status = status;
    }
    public String getStatus() {
        return status;
    }
    public void setVehicleNo(String vehicleNo) {
        this.vehicleNo = vehicleNo;
    }
    public String getVehicleNo() {
        return vehicleNo;
    }
    public void setVehicleType(String vehicleType) {
        this.vehicleType = vehicleType;
    }
    public String getVehicleType() {
        return vehicleType;
    }
    public void setVehicleBrand(String vehicleBrand) {
        this.vehicleBrand = vehicleBrand;
    }
    public String getVehicleBrand() {
        return vehicleBrand;
    }
    public void setVehicleModel(String vehicleModel) {
        this.vehicleModel = vehicleModel;
    }
    public String getVehicleModel() {
        return vehicleModel;
    }
    @Override
    public String toString() {
        return "SysTaskVehicle{" +
                "id=" + id +
                ", taskId=" + taskId +
                ", vehicleId=" + vehicleId +
                ", assignTime=" + assignTime +
                ", assignBy='" + assignBy + '\'' +
                ", status='" + status + '\'' +
                ", vehicleNo='" + vehicleNo + '\'' +
                ", vehicleType='" + vehicleType + '\'' +
                ", vehicleBrand='" + vehicleBrand + '\'' +
                ", vehicleModel='" + vehicleModel + '\'' +
                '}';
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/enums/TaskStatus.java
New file
@@ -0,0 +1,46 @@
package com.ruoyi.system.domain.enums;
/**
 * ä»»åŠ¡çŠ¶æ€æžšä¸¾
 *
 * @author ruoyi
 */
public enum TaskStatus {
    /** å¾…开始 */
    PENDING("PENDING", "待开始"),
    /** ä»»åС䏭 */
    IN_PROGRESS("IN_PROGRESS", "任务中"),
    /** å·²å®Œæˆ */
    COMPLETED("COMPLETED", "已完成"),
    /** å·²å–消 */
    CANCELLED("CANCELLED", "已取消");
    private final String code;
    private final String info;
    TaskStatus(String code, String info) {
        this.code = code;
        this.info = info;
    }
    public String getCode() {
        return code;
    }
    public String getInfo() {
        return info;
    }
    public static TaskStatus getByCode(String code) {
        for (TaskStatus status : values()) {
            if (status.getCode().equals(code)) {
                return status;
            }
        }
        return null;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/enums/TaskType.java
New file
@@ -0,0 +1,43 @@
package com.ruoyi.system.domain.enums;
/**
 * ä»»åŠ¡ç±»åž‹æžšä¸¾
 *
 * @author ruoyi
 */
public enum TaskType {
    /** ç»´ä¿®ä¿å…» */
    MAINTENANCE("MAINTENANCE", "维修保养"),
    /** åŠ æ²¹ä»»åŠ¡ */
    FUEL("FUEL", "加油任务"),
    /** å…¶ä»– */
    OTHER("OTHER", "其他");
    private final String code;
    private final String info;
    TaskType(String code, String info) {
        this.code = code;
        this.info = info;
    }
    public String getCode() {
        return code;
    }
    public String getInfo() {
        return info;
    }
    public static TaskType getByCode(String code) {
        for (TaskType type : values()) {
            if (type.getCode().equals(code)) {
                return type;
            }
        }
        return null;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/enums/TaskVehicleStatus.java
New file
@@ -0,0 +1,46 @@
package com.ruoyi.system.domain.enums;
/**
 * ä»»åŠ¡è½¦è¾†å…³è”çŠ¶æ€æžšä¸¾
 *
 * @author ruoyi
 */
public enum TaskVehicleStatus {
    /** å·²åˆ†é… */
    ASSIGNED("ASSIGNED", "已分配"),
    /** æ‰§è¡Œä¸­ */
    ACTIVE("ACTIVE", "执行中"),
    /** å·²å®Œæˆ */
    COMPLETED("COMPLETED", "已完成"),
    /** å·²å–消 */
    CANCELLED("CANCELLED", "已取消");
    private final String code;
    private final String info;
    TaskVehicleStatus(String code, String info) {
        this.code = code;
        this.info = info;
    }
    public String getCode() {
        return code;
    }
    public String getInfo() {
        return info;
    }
    public static TaskVehicleStatus getByCode(String code) {
        for (TaskVehicleStatus status : values()) {
            if (status.getCode().equals(code)) {
                return status;
            }
        }
        return null;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/enums/VehicleType.java
New file
@@ -0,0 +1,43 @@
package com.ruoyi.system.domain.enums;
/**
 * è½¦è¾†ç±»åž‹æžšä¸¾
 *
 * @author ruoyi
 */
public enum VehicleType {
    /** æ•‘护车 */
    AMBULANCE("AMBULANCE", "救护车"),
    /** è½¬è¿è½¦ */
    TRANSFER("TRANSFER", "转运车"),
    /** ç»´ä¿®è½¦ */
    MAINTENANCE("MAINTENANCE", "维修车");
    private final String code;
    private final String info;
    VehicleType(String code, String info) {
        this.code = code;
        this.info = info;
    }
    public String getCode() {
        return code;
    }
    public String getInfo() {
        return info;
    }
    public static VehicleType getByCode(String code) {
        for (VehicleType type : values()) {
            if (type.getCode().equals(code)) {
                return type;
            }
        }
        return null;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskCreateVO.java
New file
@@ -0,0 +1,103 @@
package com.ruoyi.system.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
 * ä»»åŠ¡åˆ›å»ºå¯¹è±¡
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public class TaskCreateVO {
    /** ä»»åŠ¡ç±»åž‹ */
    private String taskType;
    /** ä»»åŠ¡æè¿° */
    private String taskDescription;
    /** å‡ºå‘地址 */
    private String departureAddress;
    /** ç›®çš„地址 */
    private String destinationAddress;
    /** è®¡åˆ’开始时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date plannedStartTime;
    /** è®¡åˆ’结束时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date plannedEndTime;
    /** æ‰§è¡ŒäººID */
    private Long assigneeId;
    /** å¤‡æ³¨ */
    private String remark;
    public String getTaskType() {
        return taskType;
    }
    public void setTaskType(String taskType) {
        this.taskType = taskType;
    }
    public String getTaskDescription() {
        return taskDescription;
    }
    public void setTaskDescription(String taskDescription) {
        this.taskDescription = taskDescription;
    }
    public String getDepartureAddress() {
        return departureAddress;
    }
    public void setDepartureAddress(String departureAddress) {
        this.departureAddress = departureAddress;
    }
    public String getDestinationAddress() {
        return destinationAddress;
    }
    public void setDestinationAddress(String destinationAddress) {
        this.destinationAddress = destinationAddress;
    }
    public Date getPlannedStartTime() {
        return plannedStartTime;
    }
    public void setPlannedStartTime(Date plannedStartTime) {
        this.plannedStartTime = plannedStartTime;
    }
    public Date getPlannedEndTime() {
        return plannedEndTime;
    }
    public void setPlannedEndTime(Date plannedEndTime) {
        this.plannedEndTime = plannedEndTime;
    }
    public Long getAssigneeId() {
        return assigneeId;
    }
    public void setAssigneeId(Long assigneeId) {
        this.assigneeId = assigneeId;
    }
    public String getRemark() {
        return remark;
    }
    public void setRemark(String remark) {
        this.remark = remark;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskQueryVO.java
New file
@@ -0,0 +1,140 @@
package com.ruoyi.system.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
/**
 * ä»»åŠ¡æŸ¥è¯¢å¯¹è±¡ sys_task
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public class TaskQueryVO extends BaseEntity {
    private static final long serialVersionUID = 1L;
    /** ä»»åŠ¡ç¼–å· */
    private String taskCode;
    /** ä»»åŠ¡ç±»åž‹ */
    private String taskType;
    /** ä»»åŠ¡çŠ¶æ€ */
    private String taskStatus;
    /** åˆ›å»ºäººID */
    private Long creatorId;
    /** æ‰§è¡ŒäººID */
    private Long assigneeId;
    /** å½’属部门ID */
    private Long deptId;
    /** è®¡åˆ’开始时间-开始 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date plannedStartTimeBegin;
    /** è®¡åˆ’开始时间-结束 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date plannedStartTimeEnd;
    /** è®¡åˆ’结束时间-开始 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date plannedEndTimeBegin;
    /** è®¡åˆ’结束时间-结束 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date plannedEndTimeEnd;
    /** æ˜¯å¦è¶…æ—¶ */
    private Boolean overdue;
    public String getTaskCode() {
        return taskCode;
    }
    public void setTaskCode(String taskCode) {
        this.taskCode = taskCode;
    }
    public String getTaskType() {
        return taskType;
    }
    public void setTaskType(String taskType) {
        this.taskType = taskType;
    }
    public String getTaskStatus() {
        return taskStatus;
    }
    public void setTaskStatus(String taskStatus) {
        this.taskStatus = taskStatus;
    }
    public Long getCreatorId() {
        return creatorId;
    }
    public void setCreatorId(Long creatorId) {
        this.creatorId = creatorId;
    }
    public Long getAssigneeId() {
        return assigneeId;
    }
    public void setAssigneeId(Long assigneeId) {
        this.assigneeId = assigneeId;
    }
    public Long getDeptId() {
        return deptId;
    }
    public void setDeptId(Long deptId) {
        this.deptId = deptId;
    }
    public Date getPlannedStartTimeBegin() {
        return plannedStartTimeBegin;
    }
    public void setPlannedStartTimeBegin(Date plannedStartTimeBegin) {
        this.plannedStartTimeBegin = plannedStartTimeBegin;
    }
    public Date getPlannedStartTimeEnd() {
        return plannedStartTimeEnd;
    }
    public void setPlannedStartTimeEnd(Date plannedStartTimeEnd) {
        this.plannedStartTimeEnd = plannedStartTimeEnd;
    }
    public Date getPlannedEndTimeBegin() {
        return plannedEndTimeBegin;
    }
    public void setPlannedEndTimeBegin(Date plannedEndTimeBegin) {
        this.plannedEndTimeBegin = plannedEndTimeBegin;
    }
    public Date getPlannedEndTimeEnd() {
        return plannedEndTimeEnd;
    }
    public void setPlannedEndTimeEnd(Date plannedEndTimeEnd) {
        this.plannedEndTimeEnd = plannedEndTimeEnd;
    }
    public Boolean getOverdue() {
        return overdue;
    }
    public void setOverdue(Boolean overdue) {
        this.overdue = overdue;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskStatisticsVO.java
New file
@@ -0,0 +1,98 @@
package com.ruoyi.system.domain.vo;
/**
 * ä»»åŠ¡ç»Ÿè®¡å¯¹è±¡
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public class TaskStatisticsVO {
    /** æ€»ä»»åŠ¡æ•° */
    private Long totalTasks;
    /** å¾…开始任务数 */
    private Long pendingTasks;
    /** ä»»åŠ¡ä¸­æ•°é‡ */
    private Long inProgressTasks;
    /** å·²å®Œæˆä»»åŠ¡æ•° */
    private Long completedTasks;
    /** å·²å–消任务数 */
    private Long cancelledTasks;
    /** ä»Šæ—¥ä»»åŠ¡æ•° */
    private Long todayTasks;
    /** è¶…时任务数 */
    private Long overdueTasks;
    /** è½¦è¾†åˆ©ç”¨çއ */
    private Double vehicleUtilization;
    public Long getTotalTasks() {
        return totalTasks;
    }
    public void setTotalTasks(Long totalTasks) {
        this.totalTasks = totalTasks;
    }
    public Long getPendingTasks() {
        return pendingTasks;
    }
    public void setPendingTasks(Long pendingTasks) {
        this.pendingTasks = pendingTasks;
    }
    public Long getInProgressTasks() {
        return inProgressTasks;
    }
    public void setInProgressTasks(Long inProgressTasks) {
        this.inProgressTasks = inProgressTasks;
    }
    public Long getCompletedTasks() {
        return completedTasks;
    }
    public void setCompletedTasks(Long completedTasks) {
        this.completedTasks = completedTasks;
    }
    public Long getCancelledTasks() {
        return cancelledTasks;
    }
    public void setCancelledTasks(Long cancelledTasks) {
        this.cancelledTasks = cancelledTasks;
    }
    public Long getTodayTasks() {
        return todayTasks;
    }
    public void setTodayTasks(Long todayTasks) {
        this.todayTasks = todayTasks;
    }
    public Long getOverdueTasks() {
        return overdueTasks;
    }
    public void setOverdueTasks(Long overdueTasks) {
        this.overdueTasks = overdueTasks;
    }
    public Double getVehicleUtilization() {
        return vehicleUtilization;
    }
    public void setVehicleUtilization(Double vehicleUtilization) {
        this.vehicleUtilization = vehicleUtilization;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskUpdateVO.java
New file
@@ -0,0 +1,103 @@
package com.ruoyi.system.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
 * ä»»åŠ¡æ›´æ–°å¯¹è±¡
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public class TaskUpdateVO {
    /** ä»»åŠ¡ID */
    private Long taskId;
    /** ä»»åŠ¡æè¿° */
    private String taskDescription;
    /** å‡ºå‘地址 */
    private String departureAddress;
    /** ç›®çš„地址 */
    private String destinationAddress;
    /** è®¡åˆ’开始时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date plannedStartTime;
    /** è®¡åˆ’结束时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date plannedEndTime;
    /** æ‰§è¡ŒäººID */
    private Long assigneeId;
    /** å¤‡æ³¨ */
    private String remark;
    public Long getTaskId() {
        return taskId;
    }
    public void setTaskId(Long taskId) {
        this.taskId = taskId;
    }
    public String getTaskDescription() {
        return taskDescription;
    }
    public void setTaskDescription(String taskDescription) {
        this.taskDescription = taskDescription;
    }
    public String getDepartureAddress() {
        return departureAddress;
    }
    public void setDepartureAddress(String departureAddress) {
        this.departureAddress = departureAddress;
    }
    public String getDestinationAddress() {
        return destinationAddress;
    }
    public void setDestinationAddress(String destinationAddress) {
        this.destinationAddress = destinationAddress;
    }
    public Date getPlannedStartTime() {
        return plannedStartTime;
    }
    public void setPlannedStartTime(Date plannedStartTime) {
        this.plannedStartTime = plannedStartTime;
    }
    public Date getPlannedEndTime() {
        return plannedEndTime;
    }
    public void setPlannedEndTime(Date plannedEndTime) {
        this.plannedEndTime = plannedEndTime;
    }
    public Long getAssigneeId() {
        return assigneeId;
    }
    public void setAssigneeId(Long assigneeId) {
        this.assigneeId = assigneeId;
    }
    public String getRemark() {
        return remark;
    }
    public void setRemark(String remark) {
        this.remark = remark;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskAttachmentMapper.java
New file
@@ -0,0 +1,77 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.SysTaskAttachment;
/**
 * ä»»åС附件Mapper接口
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public interface SysTaskAttachmentMapper {
    /**
     * æŸ¥è¯¢ä»»åС附件
     *
     * @param attachmentId ä»»åС附件䏻键
     * @return ä»»åС附件
     */
    public SysTaskAttachment selectSysTaskAttachmentByAttachmentId(Long attachmentId);
    /**
     * æŸ¥è¯¢ä»»åŠ¡é™„ä»¶åˆ—è¡¨
     *
     * @param sysTaskAttachment ä»»åС附件
     * @return ä»»åŠ¡é™„ä»¶é›†åˆ
     */
    public List<SysTaskAttachment> selectSysTaskAttachmentList(SysTaskAttachment sysTaskAttachment);
    /**
     * æ ¹æ®ä»»åŠ¡ID查询附件列表
     *
     * @param taskId ä»»åŠ¡ID
     * @return ä»»åŠ¡é™„ä»¶é›†åˆ
     */
    public List<SysTaskAttachment> selectSysTaskAttachmentByTaskId(Long taskId);
    /**
     * æ–°å¢žä»»åС附件
     *
     * @param sysTaskAttachment ä»»åС附件
     * @return ç»“æžœ
     */
    public int insertSysTaskAttachment(SysTaskAttachment sysTaskAttachment);
    /**
     * ä¿®æ”¹ä»»åС附件
     *
     * @param sysTaskAttachment ä»»åС附件
     * @return ç»“æžœ
     */
    public int updateSysTaskAttachment(SysTaskAttachment sysTaskAttachment);
    /**
     * åˆ é™¤ä»»åС附件
     *
     * @param attachmentId ä»»åС附件䏻键
     * @return ç»“æžœ
     */
    public int deleteSysTaskAttachmentByAttachmentId(Long attachmentId);
    /**
     * æ‰¹é‡åˆ é™¤ä»»åС附件
     *
     * @param attachmentIds éœ€è¦åˆ é™¤çš„æ•°æ®ä¸»é”®é›†åˆ
     * @return ç»“æžœ
     */
    public int deleteSysTaskAttachmentByAttachmentIds(Long[] attachmentIds);
    /**
     * æ ¹æ®ä»»åŠ¡ID删除附件
     *
     * @param taskId ä»»åŠ¡ID
     * @return ç»“æžœ
     */
    public int deleteSysTaskAttachmentByTaskId(Long taskId);
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskLogMapper.java
New file
@@ -0,0 +1,77 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.SysTaskLog;
/**
 * ä»»åŠ¡æ“ä½œæ—¥å¿—Mapper接口
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public interface SysTaskLogMapper {
    /**
     * æŸ¥è¯¢ä»»åŠ¡æ“ä½œæ—¥å¿—
     *
     * @param logId ä»»åŠ¡æ“ä½œæ—¥å¿—ä¸»é”®
     * @return ä»»åŠ¡æ“ä½œæ—¥å¿—
     */
    public SysTaskLog selectSysTaskLogByLogId(Long logId);
    /**
     * æŸ¥è¯¢ä»»åŠ¡æ“ä½œæ—¥å¿—åˆ—è¡¨
     *
     * @param sysTaskLog ä»»åŠ¡æ“ä½œæ—¥å¿—
     * @return ä»»åŠ¡æ“ä½œæ—¥å¿—é›†åˆ
     */
    public List<SysTaskLog> selectSysTaskLogList(SysTaskLog sysTaskLog);
    /**
     * æ ¹æ®ä»»åŠ¡ID查询操作日志列表
     *
     * @param taskId ä»»åŠ¡ID
     * @return ä»»åŠ¡æ“ä½œæ—¥å¿—é›†åˆ
     */
    public List<SysTaskLog> selectSysTaskLogByTaskId(Long taskId);
    /**
     * æ–°å¢žä»»åŠ¡æ“ä½œæ—¥å¿—
     *
     * @param sysTaskLog ä»»åŠ¡æ“ä½œæ—¥å¿—
     * @return ç»“æžœ
     */
    public int insertSysTaskLog(SysTaskLog sysTaskLog);
    /**
     * ä¿®æ”¹ä»»åŠ¡æ“ä½œæ—¥å¿—
     *
     * @param sysTaskLog ä»»åŠ¡æ“ä½œæ—¥å¿—
     * @return ç»“æžœ
     */
    public int updateSysTaskLog(SysTaskLog sysTaskLog);
    /**
     * åˆ é™¤ä»»åŠ¡æ“ä½œæ—¥å¿—
     *
     * @param logId ä»»åŠ¡æ“ä½œæ—¥å¿—ä¸»é”®
     * @return ç»“æžœ
     */
    public int deleteSysTaskLogByLogId(Long logId);
    /**
     * æ‰¹é‡åˆ é™¤ä»»åŠ¡æ“ä½œæ—¥å¿—
     *
     * @param logIds éœ€è¦åˆ é™¤çš„æ•°æ®ä¸»é”®é›†åˆ
     * @return ç»“æžœ
     */
    public int deleteSysTaskLogByLogIds(Long[] logIds);
    /**
     * æ ¹æ®ä»»åŠ¡ID删除操作日志
     *
     * @param taskId ä»»åŠ¡ID
     * @return ç»“æžœ
     */
    public int deleteSysTaskLogByTaskId(Long taskId);
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java
New file
@@ -0,0 +1,109 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.vo.TaskQueryVO;
import com.ruoyi.system.domain.vo.TaskStatisticsVO;
/**
 * ä»»åŠ¡ç®¡ç†Mapper接口
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public interface SysTaskMapper {
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç®¡ç†
     *
     * @param taskId ä»»åŠ¡ç®¡ç†ä¸»é”®
     * @return ä»»åŠ¡ç®¡ç†
     */
    public SysTask selectSysTaskByTaskId(Long taskId);
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç®¡ç†åˆ—è¡¨
     *
     * @param sysTask ä»»åŠ¡ç®¡ç†
     * @return ä»»åŠ¡ç®¡ç†é›†åˆ
     */
    public List<SysTask> selectSysTaskList(TaskQueryVO queryVO);
    /**
     * æ–°å¢žä»»åŠ¡ç®¡ç†
     *
     * @param sysTask ä»»åŠ¡ç®¡ç†
     * @return ç»“æžœ
     */
    public int insertSysTask(SysTask sysTask);
    /**
     * ä¿®æ”¹ä»»åŠ¡ç®¡ç†
     *
     * @param sysTask ä»»åŠ¡ç®¡ç†
     * @return ç»“æžœ
     */
    public int updateSysTask(SysTask sysTask);
    /**
     * åˆ é™¤ä»»åŠ¡ç®¡ç†
     *
     * @param taskId ä»»åŠ¡ç®¡ç†ä¸»é”®
     * @return ç»“æžœ
     */
    public int deleteSysTaskByTaskId(Long taskId);
    /**
     * æ‰¹é‡åˆ é™¤ä»»åŠ¡ç®¡ç†
     *
     * @param taskIds éœ€è¦åˆ é™¤çš„æ•°æ®ä¸»é”®é›†åˆ
     * @return ç»“æžœ
     */
    public int deleteSysTaskByTaskIds(Long[] taskIds);
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç»Ÿè®¡ä¿¡æ¯
     *
     * @return ä»»åŠ¡ç»Ÿè®¡ä¿¡æ¯
     */
    public TaskStatisticsVO selectTaskStatistics();
    /**
     * æŸ¥è¯¢è¶…时任务列表
     *
     * @return è¶…时任务集合
     */
    public List<SysTask> selectOverdueTasks();
    /**
     * æŸ¥è¯¢æˆ‘的任务列表
     *
     * @param userId ç”¨æˆ·ID
     * @return æˆ‘的任务集合
     */
    public List<SysTask> selectMyTasks(Long userId);
    /**
     * æ ¹æ®ä»»åŠ¡ç¼–å·æŸ¥è¯¢ä»»åŠ¡
     *
     * @param taskCode ä»»åŠ¡ç¼–å·
     * @return ä»»åŠ¡ä¿¡æ¯
     */
    public SysTask selectSysTaskByTaskCode(String taskCode);
    /**
     * æ›´æ–°ä»»åŠ¡çŠ¶æ€
     *
     * @param sysTask ä»»åŠ¡ä¿¡æ¯
     * @return ç»“æžœ
     */
    public int updateTaskStatus(SysTask sysTask);
    /**
     * åˆ†é…ä»»åŠ¡
     *
     * @param sysTask ä»»åŠ¡ä¿¡æ¯
     * @return ç»“æžœ
     */
    public int assignTask(SysTask sysTask);
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskVehicleMapper.java
New file
@@ -0,0 +1,103 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.SysTaskVehicle;
/**
 * ä»»åŠ¡è½¦è¾†å…³è”Mapper接口
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public interface SysTaskVehicleMapper {
    /**
     * æŸ¥è¯¢ä»»åŠ¡è½¦è¾†å…³è”
     *
     * @param id ä»»åŠ¡è½¦è¾†å…³è”ä¸»é”®
     * @return ä»»åŠ¡è½¦è¾†å…³è”
     */
    public SysTaskVehicle selectSysTaskVehicleById(Long id);
    /**
     * æŸ¥è¯¢ä»»åŠ¡è½¦è¾†å…³è”åˆ—è¡¨
     *
     * @param sysTaskVehicle ä»»åŠ¡è½¦è¾†å…³è”
     * @return ä»»åŠ¡è½¦è¾†å…³è”é›†åˆ
     */
    public List<SysTaskVehicle> selectSysTaskVehicleList(SysTaskVehicle sysTaskVehicle);
    /**
     * æ ¹æ®ä»»åŠ¡ID查询关联车辆列表
     *
     * @param taskId ä»»åŠ¡ID
     * @return ä»»åŠ¡è½¦è¾†å…³è”é›†åˆ
     */
    public List<SysTaskVehicle> selectSysTaskVehicleByTaskId(Long taskId);
    /**
     * æ–°å¢žä»»åŠ¡è½¦è¾†å…³è”
     *
     * @param sysTaskVehicle ä»»åŠ¡è½¦è¾†å…³è”
     * @return ç»“æžœ
     */
    public int insertSysTaskVehicle(SysTaskVehicle sysTaskVehicle);
    /**
     * ä¿®æ”¹ä»»åŠ¡è½¦è¾†å…³è”
     *
     * @param sysTaskVehicle ä»»åŠ¡è½¦è¾†å…³è”
     * @return ç»“æžœ
     */
    public int updateSysTaskVehicle(SysTaskVehicle sysTaskVehicle);
    /**
     * åˆ é™¤ä»»åŠ¡è½¦è¾†å…³è”
     *
     * @param id ä»»åŠ¡è½¦è¾†å…³è”ä¸»é”®
     * @return ç»“æžœ
     */
    public int deleteSysTaskVehicleById(Long id);
    /**
     * æ‰¹é‡åˆ é™¤ä»»åŠ¡è½¦è¾†å…³è”
     *
     * @param ids éœ€è¦åˆ é™¤çš„æ•°æ®ä¸»é”®é›†åˆ
     * @return ç»“æžœ
     */
    public int deleteSysTaskVehicleByIds(Long[] ids);
    /**
     * æ ¹æ®ä»»åŠ¡ID删除车辆关联
     *
     * @param taskId ä»»åŠ¡ID
     * @return ç»“æžœ
     */
    public int deleteSysTaskVehicleByTaskId(Long taskId);
    /**
     * æ ¹æ®ä»»åŠ¡ID和车辆ID删除关联
     *
     * @param taskId ä»»åŠ¡ID
     * @param vehicleId è½¦è¾†ID
     * @return ç»“æžœ
     */
    public int deleteSysTaskVehicleByTaskIdAndVehicleId(Long taskId, Long vehicleId);
    /**
     * æ£€æŸ¥ä»»åŠ¡è½¦è¾†å…³è”æ˜¯å¦å­˜åœ¨
     *
     * @param taskId ä»»åŠ¡ID
     * @param vehicleId è½¦è¾†ID
     * @return ç»“æžœ
     */
    public int checkTaskVehicleExists(Long taskId, Long vehicleId);
    /**
     * æ‰¹é‡æ–°å¢žä»»åŠ¡è½¦è¾†å…³è”
     *
     * @param sysTaskVehicleList ä»»åŠ¡è½¦è¾†å…³è”åˆ—è¡¨
     * @return ç»“æžœ
     */
    public int batchInsertSysTaskVehicle(List<SysTaskVehicle> sysTaskVehicleList);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
New file
@@ -0,0 +1,174 @@
package com.ruoyi.system.service;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskVehicle;
import com.ruoyi.system.domain.SysTaskAttachment;
import com.ruoyi.system.domain.vo.TaskQueryVO;
import com.ruoyi.system.domain.vo.TaskCreateVO;
import com.ruoyi.system.domain.vo.TaskUpdateVO;
import com.ruoyi.system.domain.vo.TaskStatisticsVO;
import com.ruoyi.system.domain.enums.TaskStatus;
/**
 * ä»»åŠ¡ç®¡ç†Service接口
 *
 * @author ruoyi
 * @date 2024-01-15
 */
public interface ISysTaskService {
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç®¡ç†
     *
     * @param taskId ä»»åŠ¡ç®¡ç†ä¸»é”®
     * @return ä»»åŠ¡ç®¡ç†
     */
    public SysTask selectSysTaskByTaskId(Long taskId);
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç®¡ç†åˆ—è¡¨
     *
     * @param queryVO ä»»åŠ¡æŸ¥è¯¢å¯¹è±¡
     * @return ä»»åŠ¡ç®¡ç†é›†åˆ
     */
    public List<SysTask> selectSysTaskList(TaskQueryVO queryVO);
    /**
     * æ–°å¢žä»»åŠ¡ç®¡ç†
     *
     * @param createVO ä»»åŠ¡åˆ›å»ºå¯¹è±¡
     * @return ç»“æžœ
     */
    public int insertSysTask(TaskCreateVO createVO);
    /**
     * ä¿®æ”¹ä»»åŠ¡ç®¡ç†
     *
     * @param updateVO ä»»åŠ¡æ›´æ–°å¯¹è±¡
     * @return ç»“æžœ
     */
    public int updateSysTask(TaskUpdateVO updateVO);
    /**
     * æ‰¹é‡åˆ é™¤ä»»åŠ¡ç®¡ç†
     *
     * @param taskIds éœ€è¦åˆ é™¤çš„任务管理主键集合
     * @return ç»“æžœ
     */
    public int deleteSysTaskByTaskIds(Long[] taskIds);
    /**
     * åˆ†é…ä»»åŠ¡
     *
     * @param taskId ä»»åŠ¡ID
     * @param assigneeId æ‰§è¡ŒäººID
     * @param remark å¤‡æ³¨
     * @return ç»“æžœ
     */
    public int assignTask(Long taskId, Long assigneeId, String remark);
    /**
     * å˜æ›´ä»»åŠ¡çŠ¶æ€
     *
     * @param taskId ä»»åŠ¡ID
     * @param newStatus æ–°çŠ¶æ€
     * @param remark å¤‡æ³¨
     * @return ç»“æžœ
     */
    public int changeTaskStatus(Long taskId, TaskStatus newStatus, String remark);
    /**
     * ä¸Šä¼ ä»»åС附件
     *
     * @param taskId ä»»åŠ¡ID
     * @param file æ–‡ä»¶
     * @return ç»“æžœ
     */
    public int uploadAttachment(Long taskId, MultipartFile file);
    /**
     * åˆ é™¤ä»»åС附件
     *
     * @param attachmentId é™„ä»¶ID
     * @return ç»“æžœ
     */
    public int deleteAttachment(Long attachmentId);
    /**
     * åˆ†é…è½¦è¾†ç»™ä»»åŠ¡
     *
     * @param taskId ä»»åŠ¡ID
     * @param vehicleId è½¦è¾†ID
     * @param remark å¤‡æ³¨
     * @return ç»“æžœ
     */
    public int assignVehicleToTask(Long taskId, Long vehicleId, String remark);
    /**
     * å–消任务车辆分配
     *
     * @param taskId ä»»åŠ¡ID
     * @param vehicleId è½¦è¾†ID
     * @return ç»“æžœ
     */
    public int unassignVehicleFromTask(Long taskId, Long vehicleId);
    /**
     * æ‰¹é‡åˆ†é…è½¦è¾†ç»™ä»»åŠ¡
     *
     * @param taskId ä»»åŠ¡ID
     * @param vehicleIds è½¦è¾†ID列表
     * @param remark å¤‡æ³¨
     * @return ç»“æžœ
     */
    public int assignMultipleVehiclesToTask(Long taskId, List<Long> vehicleIds, String remark);
    /**
     * æŸ¥è¯¢ä»»åŠ¡å…³è”çš„è½¦è¾†
     *
     * @param taskId ä»»åŠ¡ID
     * @return ä»»åŠ¡è½¦è¾†å…³è”åˆ—è¡¨
     */
    public List<SysTaskVehicle> getTaskVehicles(Long taskId);
    /**
     * æŸ¥è¯¢å¯ç”¨è½¦è¾†
     *
     * @param deptId éƒ¨é—¨ID
     * @param taskType ä»»åŠ¡ç±»åž‹
     * @return å¯ç”¨è½¦è¾†åˆ—表
     */
    public List<SysTaskVehicle> getAvailableVehicles(Long deptId, String taskType);
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç»Ÿè®¡ä¿¡æ¯
     *
     * @return ä»»åŠ¡ç»Ÿè®¡ä¿¡æ¯
     */
    public TaskStatisticsVO getTaskStatistics();
    /**
     * æŸ¥è¯¢è¶…时任务列表
     *
     * @return è¶…时任务列表
     */
    public List<SysTask> selectOverdueTasks();
    /**
     * æŸ¥è¯¢æˆ‘的任务列表
     *
     * @param userId ç”¨æˆ·ID
     * @return æˆ‘的任务列表
     */
    public List<SysTask> selectMyTasks(Long userId);
    /**
     * èŽ·å–ä»»åŠ¡è¯¦æƒ…ï¼ˆåŒ…å«å…³è”æ•°æ®ï¼‰
     *
     * @param taskId ä»»åŠ¡ID
     * @return ä»»åŠ¡è¯¦æƒ…
     */
    public SysTask getTaskDetail(Long taskId);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
New file
@@ -0,0 +1,567 @@
package com.ruoyi.system.service.impl;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskVehicle;
import com.ruoyi.system.domain.SysTaskAttachment;
import com.ruoyi.system.domain.SysTaskLog;
import com.ruoyi.system.domain.vo.TaskQueryVO;
import com.ruoyi.system.domain.vo.TaskCreateVO;
import com.ruoyi.system.domain.vo.TaskUpdateVO;
import com.ruoyi.system.domain.vo.TaskStatisticsVO;
import com.ruoyi.system.domain.enums.TaskStatus;
import com.ruoyi.system.mapper.SysTaskMapper;
import com.ruoyi.system.mapper.SysTaskVehicleMapper;
import com.ruoyi.system.mapper.SysTaskAttachmentMapper;
import com.ruoyi.system.mapper.SysTaskLogMapper;
import com.ruoyi.system.service.ISysTaskService;
/**
 * ä»»åŠ¡ç®¡ç†Service业务层处理
 *
 * @author ruoyi
 * @date 2024-01-15
 */
@Service
public class SysTaskServiceImpl implements ISysTaskService {
    @Autowired
    private SysTaskMapper sysTaskMapper;
    @Autowired
    private SysTaskVehicleMapper sysTaskVehicleMapper;
    @Autowired
    private SysTaskAttachmentMapper sysTaskAttachmentMapper;
    @Autowired
    private SysTaskLogMapper sysTaskLogMapper;
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç®¡ç†
     *
     * @param taskId ä»»åŠ¡ç®¡ç†ä¸»é”®
     * @return ä»»åŠ¡ç®¡ç†
     */
    @Override
    public SysTask selectSysTaskByTaskId(Long taskId) {
        return sysTaskMapper.selectSysTaskByTaskId(taskId);
    }
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç®¡ç†åˆ—è¡¨
     *
     * @param queryVO ä»»åŠ¡æŸ¥è¯¢å¯¹è±¡
     * @return ä»»åŠ¡ç®¡ç†
     */
    @Override
    public List<SysTask> selectSysTaskList(TaskQueryVO queryVO) {
        return sysTaskMapper.selectSysTaskList(queryVO);
    }
    /**
     * æ–°å¢žä»»åŠ¡ç®¡ç†
     *
     * @param createVO ä»»åŠ¡åˆ›å»ºå¯¹è±¡
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int insertSysTask(TaskCreateVO createVO) {
        SysTask task = new SysTask();
        task.setTaskCode(generateTaskCode());
        task.setTaskType(createVO.getTaskType());
        task.setTaskStatus(TaskStatus.PENDING.getCode());
        task.setTaskDescription(createVO.getTaskDescription());
        task.setDepartureAddress(createVO.getDepartureAddress());
        task.setDestinationAddress(createVO.getDestinationAddress());
        task.setPlannedStartTime(createVO.getPlannedStartTime());
        task.setPlannedEndTime(createVO.getPlannedEndTime());
        task.setAssigneeId(createVO.getAssigneeId());
        task.setCreatorId(SecurityUtils.getUserId());
        task.setDeptId(SecurityUtils.getDeptId());
        task.setCreateBy(SecurityUtils.getUsername());
        task.setCreateTime(DateUtils.getNowDate());
        task.setRemark(createVO.getRemark());
        task.setDelFlag("0");
        int result = sysTaskMapper.insertSysTask(task);
        // è®°å½•操作日志
        if (result > 0) {
            recordTaskLog(task.getTaskId(), "CREATE", "创建任务", null,
                         "任务类型:" + createVO.getTaskType(), SecurityUtils.getUserId(), SecurityUtils.getUsername());
        }
        return result;
    }
    /**
     * ä¿®æ”¹ä»»åŠ¡ç®¡ç†
     *
     * @param updateVO ä»»åŠ¡æ›´æ–°å¯¹è±¡
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int updateSysTask(TaskUpdateVO updateVO) {
        SysTask oldTask = sysTaskMapper.selectSysTaskByTaskId(updateVO.getTaskId());
        if (oldTask == null) {
            throw new RuntimeException("任务不存在");
        }
        SysTask task = new SysTask();
        task.setTaskId(updateVO.getTaskId());
        task.setTaskDescription(updateVO.getTaskDescription());
        task.setDepartureAddress(updateVO.getDepartureAddress());
        task.setDestinationAddress(updateVO.getDestinationAddress());
        task.setPlannedStartTime(updateVO.getPlannedStartTime());
        task.setPlannedEndTime(updateVO.getPlannedEndTime());
        task.setAssigneeId(updateVO.getAssigneeId());
        task.setUpdateBy(SecurityUtils.getUsername());
        task.setUpdateTime(DateUtils.getNowDate());
        task.setRemark(updateVO.getRemark());
        int result = sysTaskMapper.updateSysTask(task);
        // è®°å½•操作日志
        if (result > 0) {
            recordTaskLog(updateVO.getTaskId(), "UPDATE", "更新任务",
                         buildTaskDescription(oldTask), buildTaskDescription(task),
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
        }
        return result;
    }
    /**
     * æ‰¹é‡åˆ é™¤ä»»åŠ¡ç®¡ç†
     *
     * @param taskIds éœ€è¦åˆ é™¤çš„任务管理主键
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int deleteSysTaskByTaskIds(Long[] taskIds) {
        int result = 0;
        for (Long taskId : taskIds) {
            // è®°å½•删除日志
            recordTaskLog(taskId, "DELETE", "删除任务", null, null,
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
            result += sysTaskMapper.deleteSysTaskByTaskId(taskId);
        }
        return result;
    }
    /**
     * åˆ†é…ä»»åŠ¡
     *
     * @param taskId ä»»åŠ¡ID
     * @param assigneeId æ‰§è¡ŒäººID
     * @param remark å¤‡æ³¨
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int assignTask(Long taskId, Long assigneeId, String remark) {
        SysTask task = new SysTask();
        task.setTaskId(taskId);
        task.setAssigneeId(assigneeId);
        task.setUpdateBy(SecurityUtils.getUsername());
        int result = sysTaskMapper.assignTask(task);
        // è®°å½•操作日志
        if (result > 0) {
            recordTaskLog(taskId, "ASSIGN", "分配任务", null,
                         "分配给用户ID:" + assigneeId + ",备注:" + remark,
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
        }
        return result;
    }
    /**
     * å˜æ›´ä»»åŠ¡çŠ¶æ€
     *
     * @param taskId ä»»åŠ¡ID
     * @param newStatus æ–°çŠ¶æ€
     * @param remark å¤‡æ³¨
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int changeTaskStatus(Long taskId, TaskStatus newStatus, String remark) {
        SysTask oldTask = sysTaskMapper.selectSysTaskByTaskId(taskId);
        if (oldTask == null) {
            throw new RuntimeException("任务不存在");
        }
        // éªŒè¯çŠ¶æ€æµè½¬æ˜¯å¦åˆæ³•
        TaskStatus oldTaskStatus = TaskStatus.getByCode(oldTask.getTaskStatus());
        if (!oldTask.canChangeStatus(newStatus)) {
            throw new RuntimeException("状态流转不合法:从 " + oldTaskStatus.getInfo() + " åˆ° " + newStatus.getInfo());
        }
        SysTask task = new SysTask();
        task.setTaskId(taskId);
        task.setTaskStatus(newStatus.getCode());
        task.setUpdateBy(SecurityUtils.getUsername());
        // æ ¹æ®çŠ¶æ€è®¾ç½®ç›¸åº”çš„æ—¶é—´
        if (newStatus == TaskStatus.IN_PROGRESS && oldTask.getActualStartTime() == null) {
            task.setActualStartTime(DateUtils.getNowDate());
        } else if (newStatus == TaskStatus.COMPLETED) {
            task.setActualEndTime(DateUtils.getNowDate());
        }
        int result = sysTaskMapper.updateTaskStatus(task);
        // è®°å½•操作日志
        if (result > 0) {
            recordTaskLog(taskId, "STATUS_CHANGE", "状态变更",
                         "状态:" + oldTaskStatus.getInfo(),
                         "状态:" + newStatus.getInfo() + ",备注:" + remark,
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
        }
        return result;
    }
    /**
     * ä¸Šä¼ ä»»åС附件
     *
     * @param taskId ä»»åŠ¡ID
     * @param file æ–‡ä»¶
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int uploadAttachment(Long taskId, MultipartFile file) {
        try {
            // ä¸Šä¼ æ–‡ä»¶
            String fileName = FileUploadUtils.upload("/task", file);
            String filePath = FileUploadUtils.getDefaultBaseDir() + fileName;
            SysTaskAttachment attachment = new SysTaskAttachment();
            attachment.setTaskId(taskId);
            attachment.setFileName(file.getOriginalFilename());
            attachment.setFilePath(filePath);
            attachment.setFileSize(file.getSize());
            attachment.setFileType(getFileType(file.getOriginalFilename()));
            attachment.setUploadTime(DateUtils.getNowDate());
            attachment.setUploadBy(SecurityUtils.getUsername());
            int result = sysTaskAttachmentMapper.insertSysTaskAttachment(attachment);
            // è®°å½•操作日志
            if (result > 0) {
                recordTaskLog(taskId, "UPDATE", "上传附件", null,
                             "上传文件:" + file.getOriginalFilename(),
                             SecurityUtils.getUserId(), SecurityUtils.getUsername());
            }
            return result;
        } catch (IOException e) {
            throw new RuntimeException("文件上传失败:" + e.getMessage());
        }
    }
    /**
     * åˆ é™¤ä»»åС附件
     *
     * @param attachmentId é™„ä»¶ID
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int deleteAttachment(Long attachmentId) {
        SysTaskAttachment attachment = sysTaskAttachmentMapper.selectSysTaskAttachmentByAttachmentId(attachmentId);
        if (attachment == null) {
            throw new RuntimeException("附件不存在");
        }
        // åˆ é™¤ç‰©ç†æ–‡ä»¶
        try {
            FileUtils.deleteFile(attachment.getFilePath());
        } catch (Exception e) {
            // å¿½ç•¥æ–‡ä»¶åˆ é™¤å¤±è´¥
        }
        int result = sysTaskAttachmentMapper.deleteSysTaskAttachmentByAttachmentId(attachmentId);
        // è®°å½•操作日志
        if (result > 0) {
            recordTaskLog(attachment.getTaskId(), "UPDATE", "删除附件",
                         "删除文件:" + attachment.getFileName(), null,
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
        }
        return result;
    }
    /**
     * åˆ†é…è½¦è¾†ç»™ä»»åŠ¡
     *
     * @param taskId ä»»åŠ¡ID
     * @param vehicleId è½¦è¾†ID
     * @param remark å¤‡æ³¨
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int assignVehicleToTask(Long taskId, Long vehicleId, String remark) {
        // æ£€æŸ¥æ˜¯å¦å·²ç»åˆ†é…
        int exists = sysTaskVehicleMapper.checkTaskVehicleExists(taskId, vehicleId);
        if (exists > 0) {
            throw new RuntimeException("车辆已经分配给该任务");
        }
        SysTaskVehicle taskVehicle = new SysTaskVehicle();
        taskVehicle.setTaskId(taskId);
        taskVehicle.setVehicleId(vehicleId);
        taskVehicle.setAssignTime(DateUtils.getNowDate());
        taskVehicle.setAssignBy(SecurityUtils.getUsername());
        taskVehicle.setStatus("ASSIGNED");
        taskVehicle.setRemark(remark);
        int result = sysTaskVehicleMapper.insertSysTaskVehicle(taskVehicle);
        // è®°å½•操作日志
        if (result > 0) {
            recordTaskLog(taskId, "ASSIGN", "分配车辆", null,
                         "分配车辆ID:" + vehicleId + ",备注:" + remark,
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
        }
        return result;
    }
    /**
     * å–消任务车辆分配
     *
     * @param taskId ä»»åŠ¡ID
     * @param vehicleId è½¦è¾†ID
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int unassignVehicleFromTask(Long taskId, Long vehicleId) {
        int result = sysTaskVehicleMapper.deleteSysTaskVehicleByTaskIdAndVehicleId(taskId, vehicleId);
        // è®°å½•操作日志
        if (result > 0) {
            recordTaskLog(taskId, "ASSIGN", "取消车辆分配",
                         "取消车辆ID:" + vehicleId, null,
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
        }
        return result;
    }
    /**
     * æ‰¹é‡åˆ†é…è½¦è¾†ç»™ä»»åŠ¡
     *
     * @param taskId ä»»åŠ¡ID
     * @param vehicleIds è½¦è¾†ID列表
     * @param remark å¤‡æ³¨
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int assignMultipleVehiclesToTask(Long taskId, List<Long> vehicleIds, String remark) {
        List<SysTaskVehicle> taskVehicles = new ArrayList<>();
        Date now = DateUtils.getNowDate();
        String assignBy = SecurityUtils.getUsername();
        for (Long vehicleId : vehicleIds) {
            // æ£€æŸ¥æ˜¯å¦å·²ç»åˆ†é…
            int exists = sysTaskVehicleMapper.checkTaskVehicleExists(taskId, vehicleId);
            if (exists == 0) {
                SysTaskVehicle taskVehicle = new SysTaskVehicle();
                taskVehicle.setTaskId(taskId);
                taskVehicle.setVehicleId(vehicleId);
                taskVehicle.setAssignTime(now);
                taskVehicle.setAssignBy(assignBy);
                taskVehicle.setStatus("ASSIGNED");
                taskVehicle.setRemark(remark);
                taskVehicles.add(taskVehicle);
            }
        }
        int result = 0;
        if (!taskVehicles.isEmpty()) {
            result = sysTaskVehicleMapper.batchInsertSysTaskVehicle(taskVehicles);
        }
        // è®°å½•操作日志
        if (result > 0) {
            recordTaskLog(taskId, "ASSIGN", "批量分配车辆", null,
                         "分配车辆数量:" + result + ",备注:" + remark,
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
        }
        return result;
    }
    /**
     * æŸ¥è¯¢ä»»åŠ¡å…³è”çš„è½¦è¾†
     *
     * @param taskId ä»»åŠ¡ID
     * @return ä»»åŠ¡è½¦è¾†å…³è”åˆ—è¡¨
     */
    @Override
    public List<SysTaskVehicle> getTaskVehicles(Long taskId) {
        return sysTaskVehicleMapper.selectSysTaskVehicleByTaskId(taskId);
    }
    /**
     * æŸ¥è¯¢å¯ç”¨è½¦è¾†
     *
     * @param deptId éƒ¨é—¨ID
     * @param taskType ä»»åŠ¡ç±»åž‹
     * @return å¯ç”¨è½¦è¾†åˆ—表
     */
    @Override
    public List<SysTaskVehicle> getAvailableVehicles(Long deptId, String taskType) {
        // è¿™é‡Œéœ€è¦æ ¹æ®ä¸šåŠ¡é€»è¾‘æŸ¥è¯¢å¯ç”¨è½¦è¾†
        // æš‚时返回空列表,实际实现需要查询车辆表
        return new ArrayList<>();
    }
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç»Ÿè®¡ä¿¡æ¯
     *
     * @return ä»»åŠ¡ç»Ÿè®¡ä¿¡æ¯
     */
    @Override
    public TaskStatisticsVO getTaskStatistics() {
        return sysTaskMapper.selectTaskStatistics();
    }
    /**
     * æŸ¥è¯¢è¶…时任务列表
     *
     * @return è¶…时任务列表
     */
    @Override
    public List<SysTask> selectOverdueTasks() {
        return sysTaskMapper.selectOverdueTasks();
    }
    /**
     * æŸ¥è¯¢æˆ‘的任务列表
     *
     * @param userId ç”¨æˆ·ID
     * @return æˆ‘的任务列表
     */
    @Override
    public List<SysTask> selectMyTasks(Long userId) {
        return sysTaskMapper.selectMyTasks(userId);
    }
    /**
     * èŽ·å–ä»»åŠ¡è¯¦æƒ…ï¼ˆåŒ…å«å…³è”æ•°æ®ï¼‰
     *
     * @param taskId ä»»åŠ¡ID
     * @return ä»»åŠ¡è¯¦æƒ…
     */
    @Override
    public SysTask getTaskDetail(Long taskId) {
        SysTask task = sysTaskMapper.selectSysTaskByTaskId(taskId);
        if (task != null) {
            // æŸ¥è¯¢å…³è”车辆
            task.setAssignedVehicles(sysTaskVehicleMapper.selectSysTaskVehicleByTaskId(taskId));
            // æŸ¥è¯¢é™„ä»¶
            task.setAttachments(sysTaskAttachmentMapper.selectSysTaskAttachmentByTaskId(taskId));
            // æŸ¥è¯¢æ“ä½œæ—¥å¿—
            task.setOperationLogs(sysTaskLogMapper.selectSysTaskLogByTaskId(taskId));
        }
        return task;
    }
    /**
     * ç”Ÿæˆä»»åŠ¡ç¼–å·
     *
     * @return ä»»åŠ¡ç¼–å·
     */
    private String generateTaskCode() {
        String dateStr = DateUtils.dateTimeNow("yyyyMMdd");
        return "TASK" + dateStr + "0001";
    }
    /**
     * è®°å½•任务操作日志
     *
     * @param taskId ä»»åŠ¡ID
     * @param operationType æ“ä½œç±»åž‹
     * @param operationDesc æ“ä½œæè¿°
     * @param oldValue æ“ä½œå‰å€¼
     * @param newValue æ“ä½œåŽå€¼
     * @param operatorId æ“ä½œäººID
     * @param operatorName æ“ä½œäººå§“名
     */
    private void recordTaskLog(Long taskId, String operationType, String operationDesc,
                              String oldValue, String newValue, Long operatorId, String operatorName) {
        SysTaskLog log = new SysTaskLog();
        log.setTaskId(taskId);
        log.setOperationType(operationType);
        log.setOperationDesc(operationDesc);
        log.setOldValue(oldValue);
        log.setNewValue(newValue);
        log.setOperatorId(operatorId);
        log.setOperatorName(operatorName);
        log.setOperationTime(DateUtils.getNowDate());
        // è¿™é‡Œå¯ä»¥èŽ·å–IP地址
        log.setIpAddress("127.0.0.1");
        sysTaskLogMapper.insertSysTaskLog(log);
    }
    /**
     * æž„建任务描述
     *
     * @param task ä»»åŠ¡å¯¹è±¡
     * @return ä»»åŠ¡æè¿°
     */
    private String buildTaskDescription(SysTask task) {
        StringBuilder sb = new StringBuilder();
        sb.append("任务编号:").append(task.getTaskCode()).append(",");
        sb.append("任务类型:").append(task.getTaskType()).append(",");
        sb.append("任务状态:").append(task.getTaskStatus()).append(",");
        if (StringUtils.isNotEmpty(task.getTaskDescription())) {
            sb.append("任务描述:").append(task.getTaskDescription()).append(",");
        }
        return sb.toString();
    }
    /**
     * èŽ·å–æ–‡ä»¶ç±»åž‹
     *
     * @param fileName æ–‡ä»¶å
     * @return æ–‡ä»¶ç±»åž‹
     */
    private String getFileType(String fileName) {
        if (StringUtils.isEmpty(fileName)) {
            return "";
        }
        int lastDotIndex = fileName.lastIndexOf(".");
        if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
            return fileName.substring(lastDotIndex + 1).toLowerCase();
        }
        return "";
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/TaskCodeGenerator.java
New file
@@ -0,0 +1,38 @@
package com.ruoyi.system.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.stereotype.Component;
import com.ruoyi.common.utils.DateUtils;
/**
 * ä»»åŠ¡ç¼–å·ç”Ÿæˆå™¨
 *
 * @author ruoyi
 */
@Component
public class TaskCodeGenerator {
    /**
     * ç”Ÿæˆä»»åŠ¡ç¼–å·
     * æ ¼å¼ï¼šTASK + YYYYMMDD + 4位序号
     *
     * @return ä»»åŠ¡ç¼–å·
     */
    public String generateTaskCode() {
        String dateStr = DateUtils.dateTimeNow("yyyyMMdd");
        return "TASK" + dateStr + "0001";
    }
    /**
     * æ ¹æ®æ—¥æœŸç”Ÿæˆä»»åŠ¡ç¼–å·
     *
     * @param date æ—¥æœŸ
     * @return ä»»åŠ¡ç¼–å·
     */
    public String generateTaskCode(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
        String dateStr = sdf.format(date);
        return "TASK" + dateStr + "0001";
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/TaskStatusValidator.java
New file
@@ -0,0 +1,66 @@
package com.ruoyi.system.utils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Component;
import com.ruoyi.system.domain.enums.TaskStatus;
/**
 * ä»»åŠ¡çŠ¶æ€æµè½¬éªŒè¯å™¨
 *
 * @author ruoyi
 */
@Component
public class TaskStatusValidator {
    private static final Map<TaskStatus, Set<TaskStatus>> ALLOWED_TRANSITIONS = new HashMap<>();
    static {
        // PENDING -> IN_PROGRESS, CANCELLED
        Set<TaskStatus> pendingTransitions = new HashSet<>();
        pendingTransitions.add(TaskStatus.IN_PROGRESS);
        pendingTransitions.add(TaskStatus.CANCELLED);
        ALLOWED_TRANSITIONS.put(TaskStatus.PENDING, pendingTransitions);
        // IN_PROGRESS -> COMPLETED, CANCELLED, PENDING
        Set<TaskStatus> inProgressTransitions = new HashSet<>();
        inProgressTransitions.add(TaskStatus.COMPLETED);
        inProgressTransitions.add(TaskStatus.CANCELLED);
        inProgressTransitions.add(TaskStatus.PENDING);
        ALLOWED_TRANSITIONS.put(TaskStatus.IN_PROGRESS, inProgressTransitions);
        // COMPLETED -> ä¸å…è®¸ä»»ä½•状态变更
        ALLOWED_TRANSITIONS.put(TaskStatus.COMPLETED, new HashSet<>());
        // CANCELLED -> ä¸å…è®¸ä»»ä½•状态变更
        ALLOWED_TRANSITIONS.put(TaskStatus.CANCELLED, new HashSet<>());
    }
    /**
     * éªŒè¯çŠ¶æ€æµè½¬æ˜¯å¦åˆæ³•
     *
     * @param from åŽŸçŠ¶æ€
     * @param to ç›®æ ‡çŠ¶æ€
     * @return æ˜¯å¦å…è®¸æµè½¬
     */
    public boolean canTransition(TaskStatus from, TaskStatus to) {
        if (from == null || to == null) {
            return false;
        }
        return ALLOWED_TRANSITIONS.get(from).contains(to);
    }
    /**
     * èŽ·å–å…è®¸çš„çŠ¶æ€æµè½¬åˆ—è¡¨
     *
     * @param from åŽŸçŠ¶æ€
     * @return å…è®¸çš„目标状态集合
     */
    public Set<TaskStatus> getAllowedTransitions(TaskStatus from) {
        if (from == null) {
            return new HashSet<>();
        }
        return ALLOWED_TRANSITIONS.get(from);
    }
}
ruoyi-system/src/main/resources/mapper/system/SysTaskAttachmentMapper.xml
New file
@@ -0,0 +1,95 @@
<?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.SysTaskAttachmentMapper">
    <resultMap type="SysTaskAttachment" id="SysTaskAttachmentResult">
        <result property="attachmentId"     column="attachment_id"     />
        <result property="taskId"           column="task_id"           />
        <result property="fileName"         column="file_name"         />
        <result property="filePath"         column="file_path"         />
        <result property="fileSize"         column="file_size"         />
        <result property="fileType"         column="file_type"         />
        <result property="uploadTime"       column="upload_time"       />
        <result property="uploadBy"         column="upload_by"         />
    </resultMap>
    <sql id="selectSysTaskAttachmentVo">
        select attachment_id, task_id, file_name, file_path, file_size, file_type, upload_time, upload_by
        from sys_task_attachment
    </sql>
    <select id="selectSysTaskAttachmentList" parameterType="SysTaskAttachment" resultMap="SysTaskAttachmentResult">
        <include refid="selectSysTaskAttachmentVo"/>
        <where>
            <if test="taskId != null "> and task_id = #{taskId}</if>
            <if test="fileName != null  and fileName != ''"> and file_name like concat('%', #{fileName}, '%')</if>
            <if test="fileType != null  and fileType != ''"> and file_type = #{fileType}</if>
            <if test="uploadBy != null  and uploadBy != ''"> and upload_by like concat('%', #{uploadBy}, '%')</if>
        </where>
        order by upload_time desc
    </select>
    <select id="selectSysTaskAttachmentByAttachmentId" parameterType="Long" resultMap="SysTaskAttachmentResult">
        <include refid="selectSysTaskAttachmentVo"/>
        where attachment_id = #{attachmentId}
    </select>
    <select id="selectSysTaskAttachmentByTaskId" parameterType="Long" resultMap="SysTaskAttachmentResult">
        <include refid="selectSysTaskAttachmentVo"/>
        where task_id = #{taskId}
        order by upload_time desc
    </select>
    <insert id="insertSysTaskAttachment" parameterType="SysTaskAttachment" useGeneratedKeys="true" keyProperty="attachmentId">
        insert into sys_task_attachment
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="taskId != null">task_id,</if>
            <if test="fileName != null and fileName != ''">file_name,</if>
            <if test="filePath != null and filePath != ''">file_path,</if>
            <if test="fileSize != null">file_size,</if>
            <if test="fileType != null">file_type,</if>
            <if test="uploadTime != null">upload_time,</if>
            <if test="uploadBy != null and uploadBy != ''">upload_by,</if>
         </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="taskId != null">#{taskId},</if>
            <if test="fileName != null and fileName != ''">#{fileName},</if>
            <if test="filePath != null and filePath != ''">#{filePath},</if>
            <if test="fileSize != null">#{fileSize},</if>
            <if test="fileType != null">#{fileType},</if>
            <if test="uploadTime != null">#{uploadTime},</if>
            <if test="uploadBy != null and uploadBy != ''">#{uploadBy},</if>
         </trim>
    </insert>
    <update id="updateSysTaskAttachment" parameterType="SysTaskAttachment">
        update sys_task_attachment
        <trim prefix="SET" suffixOverrides=",">
            <if test="taskId != null">task_id = #{taskId},</if>
            <if test="fileName != null and fileName != ''">file_name = #{fileName},</if>
            <if test="filePath != null and filePath != ''">file_path = #{filePath},</if>
            <if test="fileSize != null">file_size = #{fileSize},</if>
            <if test="fileType != null">file_type = #{fileType},</if>
            <if test="uploadTime != null">upload_time = #{uploadTime},</if>
            <if test="uploadBy != null and uploadBy != ''">upload_by = #{uploadBy},</if>
        </trim>
        where attachment_id = #{attachmentId}
    </update>
    <delete id="deleteSysTaskAttachmentByAttachmentId" parameterType="Long">
        delete from sys_task_attachment where attachment_id = #{attachmentId}
    </delete>
    <delete id="deleteSysTaskAttachmentByAttachmentIds" parameterType="String">
        delete from sys_task_attachment where attachment_id in
        <foreach item="attachmentId" collection="array" open="(" separator="," close=")">
            #{attachmentId}
        </foreach>
    </delete>
    <delete id="deleteSysTaskAttachmentByTaskId" parameterType="Long">
        delete from sys_task_attachment where task_id = #{taskId}
    </delete>
</mapper>
ruoyi-system/src/main/resources/mapper/system/SysTaskLogMapper.xml
New file
@@ -0,0 +1,104 @@
<?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.SysTaskLogMapper">
    <resultMap type="SysTaskLog" id="SysTaskLogResult">
        <result property="logId"            column="log_id"            />
        <result property="taskId"           column="task_id"           />
        <result property="operationType"    column="operation_type"    />
        <result property="operationDesc"    column="operation_desc"    />
        <result property="oldValue"         column="old_value"         />
        <result property="newValue"         column="new_value"         />
        <result property="operatorId"       column="operator_id"       />
        <result property="operatorName"     column="operator_name"     />
        <result property="operationTime"    column="operation_time"    />
        <result property="ipAddress"        column="ip_address"        />
    </resultMap>
    <sql id="selectSysTaskLogVo">
        select log_id, task_id, operation_type, operation_desc, old_value, new_value,
               operator_id, operator_name, operation_time, ip_address
        from sys_task_log
    </sql>
    <select id="selectSysTaskLogList" parameterType="SysTaskLog" resultMap="SysTaskLogResult">
        <include refid="selectSysTaskLogVo"/>
        <where>
            <if test="taskId != null "> and task_id = #{taskId}</if>
            <if test="operationType != null  and operationType != ''"> and operation_type = #{operationType}</if>
            <if test="operatorId != null "> and operator_id = #{operatorId}</if>
            <if test="operatorName != null  and operatorName != ''"> and operator_name like concat('%', #{operatorName}, '%')</if>
        </where>
        order by operation_time desc
    </select>
    <select id="selectSysTaskLogByLogId" parameterType="Long" resultMap="SysTaskLogResult">
        <include refid="selectSysTaskLogVo"/>
        where log_id = #{logId}
    </select>
    <select id="selectSysTaskLogByTaskId" parameterType="Long" resultMap="SysTaskLogResult">
        <include refid="selectSysTaskLogVo"/>
        where task_id = #{taskId}
        order by operation_time desc
    </select>
    <insert id="insertSysTaskLog" parameterType="SysTaskLog" useGeneratedKeys="true" keyProperty="logId">
        insert into sys_task_log
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="taskId != null">task_id,</if>
            <if test="operationType != null and operationType != ''">operation_type,</if>
            <if test="operationDesc != null">operation_desc,</if>
            <if test="oldValue != null">old_value,</if>
            <if test="newValue != null">new_value,</if>
            <if test="operatorId != null">operator_id,</if>
            <if test="operatorName != null and operatorName != ''">operator_name,</if>
            <if test="operationTime != null">operation_time,</if>
            <if test="ipAddress != null">ip_address,</if>
         </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="taskId != null">#{taskId},</if>
            <if test="operationType != null and operationType != ''">#{operationType},</if>
            <if test="operationDesc != null">#{operationDesc},</if>
            <if test="oldValue != null">#{oldValue},</if>
            <if test="newValue != null">#{newValue},</if>
            <if test="operatorId != null">#{operatorId},</if>
            <if test="operatorName != null and operatorName != ''">#{operatorName},</if>
            <if test="operationTime != null">#{operationTime},</if>
            <if test="ipAddress != null">#{ipAddress},</if>
         </trim>
    </insert>
    <update id="updateSysTaskLog" parameterType="SysTaskLog">
        update sys_task_log
        <trim prefix="SET" suffixOverrides=",">
            <if test="taskId != null">task_id = #{taskId},</if>
            <if test="operationType != null and operationType != ''">operation_type = #{operationType},</if>
            <if test="operationDesc != null">operation_desc = #{operationDesc},</if>
            <if test="oldValue != null">old_value = #{oldValue},</if>
            <if test="newValue != null">new_value = #{newValue},</if>
            <if test="operatorId != null">operator_id = #{operatorId},</if>
            <if test="operatorName != null and operatorName != ''">operator_name = #{operatorName},</if>
            <if test="operationTime != null">operation_time = #{operationTime},</if>
            <if test="ipAddress != null">ip_address = #{ipAddress},</if>
        </trim>
        where log_id = #{logId}
    </update>
    <delete id="deleteSysTaskLogByLogId" parameterType="Long">
        delete from sys_task_log where log_id = #{logId}
    </delete>
    <delete id="deleteSysTaskLogByLogIds" parameterType="String">
        delete from sys_task_log where log_id in
        <foreach item="logId" collection="array" open="(" separator="," close=")">
            #{logId}
        </foreach>
    </delete>
    <delete id="deleteSysTaskLogByTaskId" parameterType="Long">
        delete from sys_task_log where task_id = #{taskId}
    </delete>
</mapper>
ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml
New file
@@ -0,0 +1,198 @@
<?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.SysTaskMapper">
    <resultMap type="SysTask" id="SysTaskResult">
        <result property="taskId"           column="task_id"           />
        <result property="taskCode"         column="task_code"         />
        <result property="taskType"         column="task_type"         />
        <result property="taskStatus"       column="task_status"       />
        <result property="taskDescription"  column="task_description"  />
        <result property="departureAddress" column="departure_address" />
        <result property="destinationAddress" column="destination_address" />
        <result property="plannedStartTime" column="planned_start_time" />
        <result property="plannedEndTime"   column="planned_end_time"  />
        <result property="actualStartTime"  column="actual_start_time" />
        <result property="actualEndTime"    column="actual_end_time"   />
        <result property="creatorId"        column="creator_id"        />
        <result property="assigneeId"       column="assignee_id"       />
        <result property="deptId"           column="dept_id"           />
        <result property="createTime"       column="create_time"       />
        <result property="updateTime"       column="update_time"       />
        <result property="createBy"         column="create_by"         />
        <result property="updateBy"         column="update_by"         />
        <result property="remark"           column="remark"            />
        <result property="delFlag"          column="del_flag"          />
        <result property="creatorName"      column="creator_name"      />
        <result property="assigneeName"     column="assignee_name"     />
        <result property="deptName"         column="dept_name"         />
    </resultMap>
    <sql id="selectSysTaskVo">
        select t.task_id, t.task_code, t.task_type, t.task_status, t.task_description,
               t.departure_address, t.destination_address, t.planned_start_time, t.planned_end_time,
               t.actual_start_time, t.actual_end_time, t.creator_id, t.assignee_id, t.dept_id,
               t.create_time, t.update_time, t.create_by, t.update_by, t.remark, t.del_flag,
               u1.nick_name as creator_name, u2.nick_name as assignee_name, d.dept_name
        from sys_task t
        left join sys_user u1 on t.creator_id = u1.user_id
        left join sys_user u2 on t.assignee_id = u2.user_id
        left join sys_dept d on t.dept_id = d.dept_id
    </sql>
    <select id="selectSysTaskList" parameterType="TaskQueryVO" resultMap="SysTaskResult">
        <include refid="selectSysTaskVo"/>
        <where>
            t.del_flag = '0'
            <if test="taskCode != null  and taskCode != ''"> and t.task_code like concat('%', #{taskCode}, '%')</if>
            <if test="taskType != null  and taskType != ''"> and t.task_type = #{taskType}</if>
            <if test="taskStatus != null  and taskStatus != ''"> and t.task_status = #{taskStatus}</if>
            <if test="creatorId != null "> and t.creator_id = #{creatorId}</if>
            <if test="assigneeId != null "> and t.assignee_id = #{assigneeId}</if>
            <if test="deptId != null "> and t.dept_id = #{deptId}</if>
            <if test="plannedStartTimeBegin != null "> and t.planned_start_time &gt;= #{plannedStartTimeBegin}</if>
            <if test="plannedStartTimeEnd != null "> and t.planned_start_time &lt;= #{plannedStartTimeEnd}</if>
            <if test="plannedEndTimeBegin != null "> and t.planned_end_time &gt;= #{plannedEndTimeBegin}</if>
            <if test="plannedEndTimeEnd != null "> and t.planned_end_time &lt;= #{plannedEndTimeEnd}</if>
            <if test="overdue != null and overdue == true"> and t.planned_end_time &lt; now() and t.task_status != 'COMPLETED'</if>
        </where>
        order by t.create_time desc
    </select>
    <select id="selectSysTaskByTaskId" parameterType="Long" resultMap="SysTaskResult">
        <include refid="selectSysTaskVo"/>
        where t.task_id = #{taskId} and t.del_flag = '0'
    </select>
    <select id="selectSysTaskByTaskCode" parameterType="String" resultMap="SysTaskResult">
        <include refid="selectSysTaskVo"/>
        where t.task_code = #{taskCode} and t.del_flag = '0'
    </select>
    <select id="selectOverdueTasks" resultMap="SysTaskResult">
        <include refid="selectSysTaskVo"/>
        where t.del_flag = '0' and t.planned_end_time &lt; now() and t.task_status != 'COMPLETED'
        order by t.planned_end_time asc
    </select>
    <select id="selectMyTasks" parameterType="Long" resultMap="SysTaskResult">
        <include refid="selectSysTaskVo"/>
        where t.del_flag = '0' and (t.creator_id = #{userId} or t.assignee_id = #{userId})
        order by t.create_time desc
    </select>
    <select id="selectTaskStatistics" resultType="TaskStatisticsVO">
        select
            count(*) as totalTasks,
            sum(case when task_status = 'PENDING' then 1 else 0 end) as pendingTasks,
            sum(case when task_status = 'IN_PROGRESS' then 1 else 0 end) as inProgressTasks,
            sum(case when task_status = 'COMPLETED' then 1 else 0 end) as completedTasks,
            sum(case when task_status = 'CANCELLED' then 1 else 0 end) as cancelledTasks,
            sum(case when date(create_time) = curdate() then 1 else 0 end) as todayTasks,
            sum(case when planned_end_time &lt; now() and task_status != 'COMPLETED' then 1 else 0 end) as overdueTasks,
            round(sum(case when task_status = 'IN_PROGRESS' then 1 else 0 end) * 100.0 / count(*), 2) as vehicleUtilization
        from sys_task
        where del_flag = '0'
    </select>
    <insert id="insertSysTask" parameterType="SysTask" useGeneratedKeys="true" keyProperty="taskId">
        insert into sys_task
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="taskCode != null and taskCode != ''">task_code,</if>
            <if test="taskType != null and taskType != ''">task_type,</if>
            <if test="taskStatus != null and taskStatus != ''">task_status,</if>
            <if test="taskDescription != null">task_description,</if>
            <if test="departureAddress != null">departure_address,</if>
            <if test="destinationAddress != null">destination_address,</if>
            <if test="plannedStartTime != null">planned_start_time,</if>
            <if test="plannedEndTime != null">planned_end_time,</if>
            <if test="actualStartTime != null">actual_start_time,</if>
            <if test="actualEndTime != null">actual_end_time,</if>
            <if test="creatorId != null">creator_id,</if>
            <if test="assigneeId != null">assignee_id,</if>
            <if test="deptId != null">dept_id,</if>
            <if test="createTime != null">create_time,</if>
            <if test="updateTime != null">update_time,</if>
            <if test="createBy != null">create_by,</if>
            <if test="updateBy != null">update_by,</if>
            <if test="remark != null">remark,</if>
            <if test="delFlag != null">del_flag,</if>
         </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="taskCode != null and taskCode != ''">#{taskCode},</if>
            <if test="taskType != null and taskType != ''">#{taskType},</if>
            <if test="taskStatus != null and taskStatus != ''">#{taskStatus},</if>
            <if test="taskDescription != null">#{taskDescription},</if>
            <if test="departureAddress != null">#{departureAddress},</if>
            <if test="destinationAddress != null">#{destinationAddress},</if>
            <if test="plannedStartTime != null">#{plannedStartTime},</if>
            <if test="plannedEndTime != null">#{plannedEndTime},</if>
            <if test="actualStartTime != null">#{actualStartTime},</if>
            <if test="actualEndTime != null">#{actualEndTime},</if>
            <if test="creatorId != null">#{creatorId},</if>
            <if test="assigneeId != null">#{assigneeId},</if>
            <if test="deptId != null">#{deptId},</if>
            <if test="createTime != null">#{createTime},</if>
            <if test="updateTime != null">#{updateTime},</if>
            <if test="createBy != null">#{createBy},</if>
            <if test="updateBy != null">#{updateBy},</if>
            <if test="remark != null">#{remark},</if>
            <if test="delFlag != null">#{delFlag},</if>
         </trim>
    </insert>
    <update id="updateSysTask" parameterType="SysTask">
        update sys_task
        <trim prefix="SET" suffixOverrides=",">
            <if test="taskCode != null and taskCode != ''">task_code = #{taskCode},</if>
            <if test="taskType != null and taskType != ''">task_type = #{taskType},</if>
            <if test="taskStatus != null and taskStatus != ''">task_status = #{taskStatus},</if>
            <if test="taskDescription != null">task_description = #{taskDescription},</if>
            <if test="departureAddress != null">departure_address = #{departureAddress},</if>
            <if test="destinationAddress != null">destination_address = #{destinationAddress},</if>
            <if test="plannedStartTime != null">planned_start_time = #{plannedStartTime},</if>
            <if test="plannedEndTime != null">planned_end_time = #{plannedEndTime},</if>
            <if test="actualStartTime != null">actual_start_time = #{actualStartTime},</if>
            <if test="actualEndTime != null">actual_end_time = #{actualEndTime},</if>
            <if test="creatorId != null">creator_id = #{creatorId},</if>
            <if test="assigneeId != null">assignee_id = #{assigneeId},</if>
            <if test="deptId != null">dept_id = #{deptId},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
            <if test="updateBy != null">update_by = #{updateBy},</if>
            <if test="remark != null">remark = #{remark},</if>
            <if test="delFlag != null">del_flag = #{delFlag},</if>
        </trim>
        where task_id = #{taskId}
    </update>
    <update id="updateTaskStatus" parameterType="SysTask">
        update sys_task set
            task_status = #{taskStatus},
            <if test="actualStartTime != null">actual_start_time = #{actualStartTime},</if>
            <if test="actualEndTime != null">actual_end_time = #{actualEndTime},</if>
            update_time = now(),
            update_by = #{updateBy}
        where task_id = #{taskId}
    </update>
    <update id="assignTask" parameterType="SysTask">
        update sys_task set
            assignee_id = #{assigneeId},
            update_time = now(),
            update_by = #{updateBy}
        where task_id = #{taskId}
    </update>
    <delete id="deleteSysTaskByTaskId" parameterType="Long">
        update sys_task set del_flag = '2' where task_id = #{taskId}
    </delete>
    <delete id="deleteSysTaskByTaskIds" parameterType="String">
        update sys_task set del_flag = '2' where task_id in
        <foreach item="taskId" collection="array" open="(" separator="," close=")">
            #{taskId}
        </foreach>
    </delete>
</mapper>
ruoyi-system/src/main/resources/mapper/system/SysTaskVehicleMapper.xml
New file
@@ -0,0 +1,114 @@
<?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.SysTaskVehicleMapper">
    <resultMap type="SysTaskVehicle" id="SysTaskVehicleResult">
        <result property="id"               column="id"               />
        <result property="taskId"           column="task_id"          />
        <result property="vehicleId"        column="vehicle_id"       />
        <result property="assignTime"       column="assign_time"      />
        <result property="assignBy"         column="assign_by"        />
        <result property="status"           column="status"           />
        <result property="remark"           column="remark"           />
        <result property="vehicleNo"        column="vehicle_no"       />
        <result property="vehicleType"      column="vehicle_type"     />
        <result property="vehicleBrand"     column="vehicle_brand"    />
        <result property="vehicleModel"     column="vehicle_model"    />
    </resultMap>
    <sql id="selectSysTaskVehicleVo">
        select tv.id, tv.task_id, tv.vehicle_id, tv.assign_time, tv.assign_by, tv.status, tv.remark,
               v.vehicle_no, v.vehicle_type, v.vehicle_brand, v.vehicle_model
        from sys_task_vehicle tv
        left join tb_vehicle_info v on tv.vehicle_id = v.vehicle_id
    </sql>
    <select id="selectSysTaskVehicleList" parameterType="SysTaskVehicle" resultMap="SysTaskVehicleResult">
        <include refid="selectSysTaskVehicleVo"/>
        <where>
            <if test="taskId != null "> and tv.task_id = #{taskId}</if>
            <if test="vehicleId != null "> and tv.vehicle_id = #{vehicleId}</if>
            <if test="status != null  and status != ''"> and tv.status = #{status}</if>
            <if test="assignBy != null  and assignBy != ''"> and tv.assign_by like concat('%', #{assignBy}, '%')</if>
        </where>
        order by tv.assign_time desc
    </select>
    <select id="selectSysTaskVehicleById" parameterType="Long" resultMap="SysTaskVehicleResult">
        <include refid="selectSysTaskVehicleVo"/>
        where tv.id = #{id}
    </select>
    <select id="selectSysTaskVehicleByTaskId" parameterType="Long" resultMap="SysTaskVehicleResult">
        <include refid="selectSysTaskVehicleVo"/>
        where tv.task_id = #{taskId}
        order by tv.assign_time desc
    </select>
    <select id="checkTaskVehicleExists" resultType="int">
        select count(1) from sys_task_vehicle
        where task_id = #{taskId} and vehicle_id = #{vehicleId}
    </select>
    <insert id="insertSysTaskVehicle" parameterType="SysTaskVehicle" useGeneratedKeys="true" keyProperty="id">
        insert into sys_task_vehicle
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="taskId != null">task_id,</if>
            <if test="vehicleId != null">vehicle_id,</if>
            <if test="assignTime != null">assign_time,</if>
            <if test="assignBy != null and assignBy != ''">assign_by,</if>
            <if test="status != null and status != ''">status,</if>
            <if test="remark != null">remark,</if>
         </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="taskId != null">#{taskId},</if>
            <if test="vehicleId != null">#{vehicleId},</if>
            <if test="assignTime != null">#{assignTime},</if>
            <if test="assignBy != null and assignBy != ''">#{assignBy},</if>
            <if test="status != null and status != ''">#{status},</if>
            <if test="remark != null">#{remark},</if>
         </trim>
    </insert>
    <insert id="batchInsertSysTaskVehicle" parameterType="java.util.List">
        insert into sys_task_vehicle (task_id, vehicle_id, assign_time, assign_by, status, remark)
        values
        <foreach collection="list" item="item" separator=",">
            (#{item.taskId}, #{item.vehicleId}, #{item.assignTime}, #{item.assignBy}, #{item.status}, #{item.remark})
        </foreach>
    </insert>
    <update id="updateSysTaskVehicle" parameterType="SysTaskVehicle">
        update sys_task_vehicle
        <trim prefix="SET" suffixOverrides=",">
            <if test="taskId != null">task_id = #{taskId},</if>
            <if test="vehicleId != null">vehicle_id = #{vehicleId},</if>
            <if test="assignTime != null">assign_time = #{assignTime},</if>
            <if test="assignBy != null and assignBy != ''">assign_by = #{assignBy},</if>
            <if test="status != null and status != ''">status = #{status},</if>
            <if test="remark != null">remark = #{remark},</if>
        </trim>
        where id = #{id}
    </update>
    <delete id="deleteSysTaskVehicleById" parameterType="Long">
        delete from sys_task_vehicle where id = #{id}
    </delete>
    <delete id="deleteSysTaskVehicleByIds" parameterType="String">
        delete from sys_task_vehicle where id in
        <foreach item="id" collection="array" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
    <delete id="deleteSysTaskVehicleByTaskId" parameterType="Long">
        delete from sys_task_vehicle where task_id = #{taskId}
    </delete>
    <delete id="deleteSysTaskVehicleByTaskIdAndVehicleId">
        delete from sys_task_vehicle where task_id = #{taskId} and vehicle_id = #{vehicleId}
    </delete>
</mapper>
ruoyi-ui/src/api/task.js
New file
@@ -0,0 +1,159 @@
import request from '@/utils/request'
// æŸ¥è¯¢ä»»åŠ¡ç®¡ç†åˆ—è¡¨
export function listTask(query) {
  return request({
    url: '/task/list',
    method: 'get',
    params: query
  })
}
// æŸ¥è¯¢ä»»åŠ¡ç®¡ç†è¯¦ç»†
export function getTask(taskId) {
  return request({
    url: '/task/' + taskId,
    method: 'get'
  })
}
// æ–°å¢žä»»åŠ¡ç®¡ç†
export function addTask(data) {
  return request({
    url: '/task',
    method: 'post',
    data: data
  })
}
// ä¿®æ”¹ä»»åŠ¡ç®¡ç†
export function updateTask(data) {
  return request({
    url: '/task',
    method: 'put',
    data: data
  })
}
// åˆ é™¤ä»»åŠ¡ç®¡ç†
export function delTask(taskIds) {
  return request({
    url: '/task/' + taskIds,
    method: 'delete'
  })
}
// åˆ†é…ä»»åŠ¡
export function assignTask(taskId, data) {
  return request({
    url: '/task/' + taskId + '/assign',
    method: 'put',
    data: data
  })
}
// æ›´æ–°ä»»åŠ¡çŠ¶æ€
export function changeTaskStatus(taskId, data) {
  return request({
    url: '/task/' + taskId + '/status',
    method: 'put',
    data: data
  })
}
// æŸ¥è¯¢ä»»åŠ¡ç»Ÿè®¡ä¿¡æ¯
export function getTaskStatistics() {
  return request({
    url: '/task/statistics',
    method: 'get'
  })
}
// æŸ¥è¯¢è¶…时任务列表
export function getOverdueTasks() {
  return request({
    url: '/task/overdue',
    method: 'get'
  })
}
// æŸ¥è¯¢æˆ‘的任务列表
export function getMyTasks() {
  return request({
    url: '/task/my',
    method: 'get'
  })
}
// ä¸Šä¼ ä»»åС附件
export function uploadAttachment(taskId, file) {
  const formData = new FormData()
  formData.append('file', file)
  return request({
    url: '/task/attachment/upload/' + taskId,
    method: 'post',
    data: formData,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })
}
// åˆ é™¤ä»»åС附件
export function deleteAttachment(attachmentId) {
  return request({
    url: '/task/attachment/' + attachmentId,
    method: 'delete'
  })
}
// æŸ¥è¯¢ä»»åŠ¡é™„ä»¶åˆ—è¡¨
export function getTaskAttachments(taskId) {
  return request({
    url: '/task/attachment/list/' + taskId,
    method: 'get'
  })
}
// æŸ¥è¯¢ä»»åŠ¡å…³è”çš„è½¦è¾†åˆ—è¡¨
export function getTaskVehicles(taskId) {
  return request({
    url: '/task/vehicle/list/' + taskId,
    method: 'get'
  })
}
// æŸ¥è¯¢å¯ç”¨è½¦è¾†åˆ—表
export function getAvailableVehicles(deptId, taskType) {
  return request({
    url: '/task/vehicle/available',
    method: 'get',
    params: { deptId, taskType }
  })
}
// åˆ†é…è½¦è¾†ç»™ä»»åŠ¡
export function assignVehicleToTask(taskId, data) {
  return request({
    url: '/task/vehicle/assign/' + taskId,
    method: 'post',
    data: data
  })
}
// æ‰¹é‡åˆ†é…è½¦è¾†ç»™ä»»åŠ¡
export function assignVehiclesToTask(taskId, data) {
  return request({
    url: '/task/vehicle/assign-batch/' + taskId,
    method: 'post',
    data: data
  })
}
// å–消任务车辆分配
export function unassignVehicleFromTask(taskId, vehicleId) {
  return request({
    url: '/task/vehicle/' + taskId + '/' + vehicleId,
    method: 'delete'
  })
}
ruoyi-ui/src/router/index.js
@@ -197,6 +197,20 @@
        meta: { title: '修改生成配置', activeMenu: '/tool/gen' }
      }
    ]
  },
  {
    path: '/task/general-detail',
    component: Layout,
    hidden: true,
    permissions: ['task:general:query'],
    children: [
      {
        path: 'index/:taskId(\\d+)',
        component: () => import('@/views/task/general/detail'),
        name: 'TaskDetail',
        meta: { title: '任务详情', activeMenu: '/task/general' }
      }
    ]
  }
]
ruoyi-ui/src/views/task/general/detail.vue
New file
@@ -0,0 +1,564 @@
<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="goBack">返回</el-button>
      </div>
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <el-descriptions title="基本信息" :column="2" border>
        <el-descriptions-item label="任务编号">{{ taskDetail.taskCode }}</el-descriptions-item>
        <el-descriptions-item label="任务类型">
          <dict-tag :options="dict.type.sys_task_type" :value="taskDetail.taskType"/>
        </el-descriptions-item>
        <el-descriptions-item label="任务状态">
          <dict-tag :options="dict.type.sys_task_status" :value="taskDetail.taskStatus"/>
        </el-descriptions-item>
        <el-descriptions-item label="创建人">{{ taskDetail.creatorName }}</el-descriptions-item>
        <el-descriptions-item label="执行人">{{ taskDetail.assigneeName }}</el-descriptions-item>
        <el-descriptions-item label="部门">{{ taskDetail.deptName }}</el-descriptions-item>
        <el-descriptions-item label="任务描述" :span="2">{{ taskDetail.taskDescription }}</el-descriptions-item>
        <el-descriptions-item label="出发地址" :span="2">{{ taskDetail.departureAddress }}</el-descriptions-item>
        <el-descriptions-item label="目的地址" :span="2">{{ taskDetail.destinationAddress }}</el-descriptions-item>
        <el-descriptions-item label="计划开始时间">{{ parseTime(taskDetail.plannedStartTime) }}</el-descriptions-item>
        <el-descriptions-item label="计划结束时间">{{ parseTime(taskDetail.plannedEndTime) }}</el-descriptions-item>
        <el-descriptions-item label="实际开始时间">{{ parseTime(taskDetail.actualStartTime) }}</el-descriptions-item>
        <el-descriptions-item label="实际结束时间">{{ parseTime(taskDetail.actualEndTime) }}</el-descriptions-item>
        <el-descriptions-item label="创建时间">{{ parseTime(taskDetail.createTime) }}</el-descriptions-item>
        <el-descriptions-item label="更新时间">{{ parseTime(taskDetail.updateTime) }}</el-descriptions-item>
        <el-descriptions-item label="备注" :span="2">{{ taskDetail.remark }}</el-descriptions-item>
      </el-descriptions>
      <!-- æ“ä½œæŒ‰é’® -->
      <div style="margin-top: 20px; text-align: center;">
        <el-button type="primary" @click="handleEdit" v-hasPermi="['task:general:edit']">编辑任务</el-button>
        <el-button type="success" @click="handleAssign" v-hasPermi="['task:general:assign']">分配任务</el-button>
        <el-button type="warning" @click="handleStatusChange" v-hasPermi="['task:general:status']">状态变更</el-button>
        <el-button type="info" @click="handleVehicleAssign" v-hasPermi="['task:general:assign']">分配车辆</el-button>
      </div>
    </el-card>
    <!-- å…³è”车辆 -->
    <el-card class="box-card" style="margin-top: 20px;">
      <div slot="header" class="clearfix">
        <span>关联车辆</span>
        <el-button style="float: right; padding: 3px 0" type="text" @click="handleVehicleAssign" v-hasPermi="['task:general:assign']">分配车辆</el-button>
      </div>
      <el-table :data="taskDetail.assignedVehicles" v-loading="vehicleLoading">
        <el-table-column label="车牌号" align="center" prop="vehicleNo" />
        <el-table-column label="车辆类型" align="center" prop="vehicleType">
          <template slot-scope="scope">
            <dict-tag :options="dict.type.sys_vehicle_type" :value="scope.row.vehicleType"/>
          </template>
        </el-table-column>
        <el-table-column label="车辆品牌" align="center" prop="vehicleBrand" />
        <el-table-column label="车辆型号" align="center" prop="vehicleModel" />
        <el-table-column label="分配时间" align="center" prop="assignTime" width="180">
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.assignTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
          </template>
        </el-table-column>
        <el-table-column label="分配人" align="center" prop="assignBy" />
        <el-table-column label="状态" align="center" prop="status">
          <template slot-scope="scope">
            <dict-tag :options="dict.type.sys_task_vehicle_status" :value="scope.row.status"/>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
          <template slot-scope="scope">
            <el-button
              size="mini"
              type="text"
              icon="el-icon-delete"
              @click="handleUnassignVehicle(scope.row)"
              v-hasPermi="['task:general:assign']"
            >取消分配</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- ä»»åС附件 -->
    <el-card class="box-card" style="margin-top: 20px;">
      <div slot="header" class="clearfix">
        <span>任务附件</span>
        <el-button style="float: right; padding: 3px 0" type="text" @click="handleUpload" v-hasPermi="['task:general:edit']">上传附件</el-button>
      </div>
      <el-table :data="taskDetail.attachments" v-loading="attachmentLoading">
        <el-table-column label="文件名" align="center" prop="fileName" />
        <el-table-column label="文件类型" align="center" prop="fileType" />
        <el-table-column label="文件大小" align="center" prop="fileSize">
          <template slot-scope="scope">
            <span>{{ formatFileSize(scope.row.fileSize) }}</span>
          </template>
        </el-table-column>
        <el-table-column label="上传时间" align="center" prop="uploadTime" width="180">
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.uploadTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
          </template>
        </el-table-column>
        <el-table-column label="上传者" align="center" prop="uploadBy" />
        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
          <template slot-scope="scope">
            <el-button
              size="mini"
              type="text"
              icon="el-icon-download"
              @click="handleDownload(scope.row)"
            >下载</el-button>
            <el-button
              size="mini"
              type="text"
              icon="el-icon-delete"
              @click="handleDeleteAttachment(scope.row)"
              v-hasPermi="['task:general:edit']"
            >删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- æ“ä½œæ—¥å¿— -->
    <el-card class="box-card" style="margin-top: 20px;">
      <div slot="header" class="clearfix">
        <span>操作日志</span>
      </div>
      <el-timeline>
        <el-timeline-item
          v-for="log in taskDetail.operationLogs"
          :key="log.logId"
          :timestamp="parseTime(log.operationTime)"
          placement="top"
        >
          <el-card>
            <h4>{{ log.operationDesc }}</h4>
            <p>操作人:{{ log.operatorName }}</p>
            <p v-if="log.oldValue">操作前:{{ log.oldValue }}</p>
            <p v-if="log.newValue">操作后:{{ log.newValue }}</p>
          </el-card>
        </el-timeline-item>
      </el-timeline>
    </el-card>
    <!-- ç¼–辑任务对话框 -->
    <el-dialog :title="'编辑任务'" :visible.sync="editOpen" width="600px" append-to-body>
      <el-form ref="editForm" :model="editForm" :rules="editRules" label-width="80px">
        <el-form-item label="任务描述" prop="taskDescription">
          <el-input v-model="editForm.taskDescription" type="textarea" placeholder="请输入任务描述" />
        </el-form-item>
        <el-form-item label="出发地址" prop="departureAddress">
          <el-input v-model="editForm.departureAddress" placeholder="请输入出发地址" />
        </el-form-item>
        <el-form-item label="目的地址" prop="destinationAddress">
          <el-input v-model="editForm.destinationAddress" placeholder="请输入目的地址" />
        </el-form-item>
        <el-form-item label="计划开始时间" prop="plannedStartTime">
          <el-date-picker clearable
            v-model="editForm.plannedStartTime"
            type="datetime"
            value-format="yyyy-MM-dd HH:mm:ss"
            placeholder="请选择计划开始时间">
          </el-date-picker>
        </el-form-item>
        <el-form-item label="计划结束时间" prop="plannedEndTime">
          <el-date-picker clearable
            v-model="editForm.plannedEndTime"
            type="datetime"
            value-format="yyyy-MM-dd HH:mm:ss"
            placeholder="请选择计划结束时间">
          </el-date-picker>
        </el-form-item>
        <el-form-item label="执行人" prop="assigneeId">
          <el-select v-model="editForm.assigneeId" placeholder="请选择执行人" clearable>
            <el-option
              v-for="user in userList"
              :key="user.userId"
              :label="user.nickName"
              :value="user.userId"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="editForm.remark" type="textarea" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitEdit">ç¡® å®š</el-button>
        <el-button @click="cancelEdit">取 æ¶ˆ</el-button>
      </div>
    </el-dialog>
    <!-- åˆ†é…ä»»åŠ¡å¯¹è¯æ¡† -->
    <el-dialog title="分配任务" :visible.sync="assignOpen" width="500px" append-to-body>
      <el-form ref="assignForm" :model="assignForm" :rules="assignRules" label-width="80px">
        <el-form-item label="执行人" prop="assigneeId">
          <el-select v-model="assignForm.assigneeId" placeholder="请选择执行人" clearable>
            <el-option
              v-for="user in userList"
              :key="user.userId"
              :label="user.nickName"
              :value="user.userId"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="assignForm.remark" type="textarea" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitAssign">ç¡® å®š</el-button>
        <el-button @click="cancelAssign">取 æ¶ˆ</el-button>
      </div>
    </el-dialog>
    <!-- çŠ¶æ€å˜æ›´å¯¹è¯æ¡† -->
    <el-dialog title="状态变更" :visible.sync="statusOpen" width="500px" append-to-body>
      <el-form ref="statusForm" :model="statusForm" :rules="statusRules" label-width="80px">
        <el-form-item label="任务状态" prop="taskStatus">
          <el-select v-model="statusForm.taskStatus" placeholder="请选择任务状态">
            <el-option
              v-for="dict in dict.type.sys_task_status"
              :key="dict.value"
              :label="dict.label"
              :value="dict.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="statusForm.remark" type="textarea" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitStatusChange">ç¡® å®š</el-button>
        <el-button @click="cancelStatusChange">取 æ¶ˆ</el-button>
      </div>
    </el-dialog>
    <!-- åˆ†é…è½¦è¾†å¯¹è¯æ¡† -->
    <el-dialog title="分配车辆" :visible.sync="vehicleAssignOpen" width="600px" append-to-body>
      <el-form ref="vehicleAssignForm" :model="vehicleAssignForm" :rules="vehicleAssignRules" label-width="80px">
        <el-form-item label="车辆" prop="vehicleIds">
          <el-select v-model="vehicleAssignForm.vehicleIds" placeholder="请选择车辆" multiple clearable>
            <el-option
              v-for="vehicle in availableVehicles"
              :key="vehicle.vehicleId"
              :label="vehicle.vehicleNo + ' (' + vehicle.vehicleType + ')'"
              :value="vehicle.vehicleId"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="vehicleAssignForm.remark" type="textarea" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitVehicleAssign">ç¡® å®š</el-button>
        <el-button @click="cancelVehicleAssign">取 æ¶ˆ</el-button>
      </div>
    </el-dialog>
    <!-- ä¸Šä¼ é™„件对话框 -->
    <el-dialog title="上传附件" :visible.sync="uploadOpen" width="500px" append-to-body>
      <el-upload
        class="upload-demo"
        drag
        :action="uploadUrl"
        :headers="uploadHeaders"
        :data="uploadData"
        :on-success="handleUploadSuccess"
        :on-error="handleUploadError"
        :before-upload="beforeUpload"
        multiple>
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <div class="el-upload__tip" slot="tip">只能上传jpg/png/pdf/doc/docx文件,且不超过10MB</div>
      </el-upload>
    </el-dialog>
  </div>
</template>
<script>
import { getTask, updateTask, assignTask, changeTaskStatus, uploadAttachment, deleteAttachment, getTaskVehicles, getAvailableVehicles, assignVehiclesToTask, unassignVehicleFromTask } from "@/api/task";
import { listUser } from "@/api/system/user";
import { getToken } from "@/utils/auth";
export default {
  name: "TaskDetail",
  dicts: ['sys_task_type', 'sys_task_status', 'sys_vehicle_type', 'sys_task_vehicle_status'],
  data() {
    return {
      // ä»»åŠ¡è¯¦æƒ…
      taskDetail: {
        assignedVehicles: [],
        attachments: [],
        operationLogs: []
      },
      // æ˜¯å¦æ˜¾ç¤ºç¼–辑对话框
      editOpen: false,
      // æ˜¯å¦æ˜¾ç¤ºåˆ†é…å¯¹è¯æ¡†
      assignOpen: false,
      // æ˜¯å¦æ˜¾ç¤ºçŠ¶æ€å˜æ›´å¯¹è¯æ¡†
      statusOpen: false,
      // æ˜¯å¦æ˜¾ç¤ºè½¦è¾†åˆ†é…å¯¹è¯æ¡†
      vehicleAssignOpen: false,
      // æ˜¯å¦æ˜¾ç¤ºä¸Šä¼ å¯¹è¯æ¡†
      uploadOpen: false,
      // ç¼–辑表单
      editForm: {},
      // åˆ†é…è¡¨å•
      assignForm: {},
      // çŠ¶æ€å˜æ›´è¡¨å•
      statusForm: {},
      // è½¦è¾†åˆ†é…è¡¨å•
      vehicleAssignForm: {},
      // ç”¨æˆ·åˆ—表
      userList: [],
      // å¯ç”¨è½¦è¾†åˆ—表
      availableVehicles: [],
      // åŠ è½½çŠ¶æ€
      vehicleLoading: false,
      attachmentLoading: false,
      // ä¸Šä¼ ç›¸å…³
      uploadUrl: process.env.VUE_APP_BASE_API + "/task/attachment/upload/" + this.$route.params.taskId,
      uploadHeaders: {
        Authorization: "Bearer " + getToken()
      },
      uploadData: {},
      // è¡¨å•校验
      editRules: {
        taskDescription: [
          { required: true, message: "任务描述不能为空", trigger: "blur" }
        ],
        plannedStartTime: [
          { required: true, message: "计划开始时间不能为空", trigger: "blur" }
        ],
        plannedEndTime: [
          { required: true, message: "计划结束时间不能为空", trigger: "blur" }
        ]
      },
      assignRules: {
        assigneeId: [
          { required: true, message: "执行人不能为空", trigger: "change" }
        ]
      },
      statusRules: {
        taskStatus: [
          { required: true, message: "任务状态不能为空", trigger: "change" }
        ]
      },
      vehicleAssignRules: {
        vehicleIds: [
          { required: true, message: "车辆不能为空", trigger: "change" }
        ]
      }
    };
  },
  created() {
    this.getTaskDetail();
    this.getUserList();
  },
  methods: {
    /** èŽ·å–ä»»åŠ¡è¯¦æƒ… */
    getTaskDetail() {
      getTask(this.$route.params.taskId).then(response => {
        this.taskDetail = response.data;
      });
    },
    /** èŽ·å–ç”¨æˆ·åˆ—è¡¨ */
    getUserList() {
      listUser().then(response => {
        this.userList = response.rows;
      });
    },
    /** èŽ·å–å¯ç”¨è½¦è¾†åˆ—è¡¨ */
    getAvailableVehicleList() {
      getAvailableVehicles(this.taskDetail.deptId, this.taskDetail.taskType).then(response => {
        this.availableVehicles = response.data;
      });
    },
    /** è¿”回 */
    goBack() {
      this.$router.go(-1);
    },
    /** ç¼–辑任务 */
    handleEdit() {
      this.editForm = {
        taskId: this.taskDetail.taskId,
        taskDescription: this.taskDetail.taskDescription,
        departureAddress: this.taskDetail.departureAddress,
        destinationAddress: this.taskDetail.destinationAddress,
        plannedStartTime: this.taskDetail.plannedStartTime,
        plannedEndTime: this.taskDetail.plannedEndTime,
        assigneeId: this.taskDetail.assigneeId,
        remark: this.taskDetail.remark
      };
      this.editOpen = true;
    },
    /** åˆ†é…ä»»åŠ¡ */
    handleAssign() {
      this.assignForm = {
        taskId: this.taskDetail.taskId,
        assigneeId: this.taskDetail.assigneeId,
        remark: null
      };
      this.assignOpen = true;
    },
    /** çŠ¶æ€å˜æ›´ */
    handleStatusChange() {
      this.statusForm = {
        taskId: this.taskDetail.taskId,
        taskStatus: this.taskDetail.taskStatus,
        remark: null
      };
      this.statusOpen = true;
    },
    /** åˆ†é…è½¦è¾† */
    handleVehicleAssign() {
      this.getAvailableVehicleList();
      this.vehicleAssignForm = {
        taskId: this.taskDetail.taskId,
        vehicleIds: [],
        remark: null
      };
      this.vehicleAssignOpen = true;
    },
    /** ä¸Šä¼ é™„ä»¶ */
    handleUpload() {
      this.uploadOpen = true;
    },
    /** å–消车辆分配 */
    handleUnassignVehicle(row) {
      this.$modal.confirm('是否确认取消车辆"' + row.vehicleNo + '"的分配?').then(() => {
        return unassignVehicleFromTask(this.taskDetail.taskId, row.vehicleId);
      }).then(() => {
        this.$modal.msgSuccess("取消分配成功");
        this.getTaskDetail();
      }).catch(() => {});
    },
    /** ä¸‹è½½é™„ä»¶ */
    handleDownload(row) {
      window.open(row.filePath);
    },
    /** åˆ é™¤é™„ä»¶ */
    handleDeleteAttachment(row) {
      this.$modal.confirm('是否确认删除附件"' + row.fileName + '"?').then(() => {
        return deleteAttachment(row.attachmentId);
      }).then(() => {
        this.$modal.msgSuccess("删除成功");
        this.getTaskDetail();
      }).catch(() => {});
    },
    /** æäº¤ç¼–辑 */
    submitEdit() {
      this.$refs["editForm"].validate(valid => {
        if (valid) {
          updateTask(this.editForm).then(response => {
            this.$modal.msgSuccess("修改成功");
            this.editOpen = false;
            this.getTaskDetail();
          });
        }
      });
    },
    /** æäº¤åˆ†é… */
    submitAssign() {
      this.$refs["assignForm"].validate(valid => {
        if (valid) {
          assignTask(this.assignForm.taskId, this.assignForm).then(response => {
            this.$modal.msgSuccess("分配成功");
            this.assignOpen = false;
            this.getTaskDetail();
          });
        }
      });
    },
    /** æäº¤çŠ¶æ€å˜æ›´ */
    submitStatusChange() {
      this.$refs["statusForm"].validate(valid => {
        if (valid) {
          changeTaskStatus(this.statusForm.taskId, this.statusForm).then(response => {
            this.$modal.msgSuccess("状态变更成功");
            this.statusOpen = false;
            this.getTaskDetail();
          });
        }
      });
    },
    /** æäº¤è½¦è¾†åˆ†é… */
    submitVehicleAssign() {
      this.$refs["vehicleAssignForm"].validate(valid => {
        if (valid) {
          assignVehiclesToTask(this.vehicleAssignForm.taskId, this.vehicleAssignForm).then(response => {
            this.$modal.msgSuccess("分配成功");
            this.vehicleAssignOpen = false;
            this.getTaskDetail();
          });
        }
      });
    },
    /** å–消编辑 */
    cancelEdit() {
      this.editOpen = false;
      this.editForm = {};
    },
    /** å–消分配 */
    cancelAssign() {
      this.assignOpen = false;
      this.assignForm = {};
    },
    /** å–消状态变更 */
    cancelStatusChange() {
      this.statusOpen = false;
      this.statusForm = {};
    },
    /** å–消车辆分配 */
    cancelVehicleAssign() {
      this.vehicleAssignOpen = false;
      this.vehicleAssignForm = {};
    },
    /** ä¸Šä¼ å‰æ£€æŸ¥ */
    beforeUpload(file) {
      const isValidType = ['image/jpeg', 'image/png', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'].includes(file.type);
      const isLt10M = file.size / 1024 / 1024 < 10;
      if (!isValidType) {
        this.$message.error('只能上传 JPG/PNG/PDF/DOC/DOCX æ ¼å¼çš„æ–‡ä»¶!');
      }
      if (!isLt10M) {
        this.$message.error('上传文件大小不能超过 10MB!');
      }
      return isValidType && isLt10M;
    },
    /** ä¸Šä¼ æˆåŠŸ */
    handleUploadSuccess(response, file, fileList) {
      this.$modal.msgSuccess("上传成功");
      this.uploadOpen = false;
      this.getTaskDetail();
    },
    /** ä¸Šä¼ å¤±è´¥ */
    handleUploadError(err, file, fileList) {
      this.$modal.msgError("上传失败");
    },
    /** æ ¼å¼åŒ–文件大小 */
    formatFileSize(size) {
      if (size < 1024) {
        return size + ' B';
      } else if (size < 1024 * 1024) {
        return (size / 1024).toFixed(2) + ' KB';
      } else {
        return (size / 1024 / 1024).toFixed(2) + ' MB';
      }
    }
  }
};
</script>
<style scoped>
.box-card {
  margin-bottom: 20px;
}
</style>
ruoyi-ui/src/views/task/general/index.vue
New file
@@ -0,0 +1,535 @@
<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="任务编号" prop="taskCode">
        <el-input
          v-model="queryParams.taskCode"
          placeholder="请输入任务编号"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="任务类型" prop="taskType">
        <el-select v-model="queryParams.taskType" placeholder="请选择任务类型" clearable>
          <el-option
            v-for="dict in dict.type.sys_task_type"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="任务状态" prop="taskStatus">
        <el-select v-model="queryParams.taskStatus" placeholder="请选择任务状态" clearable>
          <el-option
            v-for="dict in dict.type.sys_task_status"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="计划开始时间">
        <el-date-picker
          v-model="dateRange"
          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="primary"
          plain
          icon="el-icon-plus"
          size="mini"
          @click="handleAdd"
          v-hasPermi="['task:general: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="['task:general: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="['task:general: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="['task:general:export']"
        >导出</el-button>
      </el-col>
      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>
    <el-table v-loading="loading" :data="taskList" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="任务编号" align="center" prop="taskCode" />
      <el-table-column label="任务类型" align="center" prop="taskType">
        <template slot-scope="scope">
          <dict-tag :options="dict.type.sys_task_type" :value="scope.row.taskType"/>
        </template>
      </el-table-column>
      <el-table-column label="任务状态" align="center" prop="taskStatus">
        <template slot-scope="scope">
          <dict-tag :options="dict.type.sys_task_status" :value="scope.row.taskStatus"/>
        </template>
      </el-table-column>
      <el-table-column label="任务描述" align="center" prop="taskDescription" show-overflow-tooltip />
      <el-table-column label="出发地址" align="center" prop="departureAddress" show-overflow-tooltip />
      <el-table-column label="目的地址" align="center" prop="destinationAddress" show-overflow-tooltip />
      <el-table-column label="计划开始时间" align="center" prop="plannedStartTime" width="180">
        <template slot-scope="scope">
          <span>{{ parseTime(scope.row.plannedStartTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
        </template>
      </el-table-column>
      <el-table-column label="计划结束时间" align="center" prop="plannedEndTime" width="180">
        <template slot-scope="scope">
          <span>{{ parseTime(scope.row.plannedEndTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
        </template>
      </el-table-column>
      <el-table-column label="创建人" align="center" prop="creatorName" />
      <el-table-column label="执行人" align="center" prop="assigneeName" />
      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
        <template slot-scope="scope">
          <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template slot-scope="scope">
          <el-button
            size="mini"
            type="text"
            icon="el-icon-view"
            @click="handleView(scope.row)"
            v-hasPermi="['task:general:query']"
          >查看</el-button>
          <el-button
            size="mini"
            type="text"
            icon="el-icon-edit"
            @click="handleUpdate(scope.row)"
            v-hasPermi="['task:general:edit']"
          >修改</el-button>
          <el-button
            size="mini"
            type="text"
            icon="el-icon-user"
            @click="handleAssign(scope.row)"
            v-hasPermi="['task:general:assign']"
          >分配</el-button>
          <el-button
            size="mini"
            type="text"
            icon="el-icon-refresh"
            @click="handleStatusChange(scope.row)"
            v-hasPermi="['task:general:status']"
          >状态</el-button>
          <el-button
            size="mini"
            type="text"
            icon="el-icon-delete"
            @click="handleDelete(scope.row)"
            v-hasPermi="['task:general: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="80px">
        <el-form-item label="任务类型" prop="taskType">
          <el-select v-model="form.taskType" placeholder="请选择任务类型">
            <el-option
              v-for="dict in dict.type.sys_task_type"
              :key="dict.value"
              :label="dict.label"
              :value="dict.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="任务描述" prop="taskDescription">
          <el-input v-model="form.taskDescription" type="textarea" placeholder="请输入任务描述" />
        </el-form-item>
        <el-form-item label="出发地址" prop="departureAddress">
          <el-input v-model="form.departureAddress" placeholder="请输入出发地址" />
        </el-form-item>
        <el-form-item label="目的地址" prop="destinationAddress">
          <el-input v-model="form.destinationAddress" placeholder="请输入目的地址" />
        </el-form-item>
        <el-form-item label="计划开始时间" prop="plannedStartTime">
          <el-date-picker clearable
            v-model="form.plannedStartTime"
            type="datetime"
            value-format="yyyy-MM-dd HH:mm:ss"
            placeholder="请选择计划开始时间">
          </el-date-picker>
        </el-form-item>
        <el-form-item label="计划结束时间" prop="plannedEndTime">
          <el-date-picker clearable
            v-model="form.plannedEndTime"
            type="datetime"
            value-format="yyyy-MM-dd HH:mm:ss"
            placeholder="请选择计划结束时间">
          </el-date-picker>
        </el-form-item>
        <el-form-item label="执行人" prop="assigneeId">
          <el-select v-model="form.assigneeId" placeholder="请选择执行人" clearable>
            <el-option
              v-for="user in userList"
              :key="user.userId"
              :label="user.nickName"
              :value="user.userId"
            />
          </el-select>
        </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>
    <!-- ä»»åŠ¡åˆ†é…å¯¹è¯æ¡† -->
    <el-dialog title="任务分配" :visible.sync="assignOpen" width="500px" append-to-body>
      <el-form ref="assignForm" :model="assignForm" :rules="assignRules" label-width="80px">
        <el-form-item label="执行人" prop="assigneeId">
          <el-select v-model="assignForm.assigneeId" placeholder="请选择执行人" clearable>
            <el-option
              v-for="user in userList"
              :key="user.userId"
              :label="user.nickName"
              :value="user.userId"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="assignForm.remark" type="textarea" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitAssign">ç¡® å®š</el-button>
        <el-button @click="cancelAssign">取 æ¶ˆ</el-button>
      </div>
    </el-dialog>
    <!-- çŠ¶æ€å˜æ›´å¯¹è¯æ¡† -->
    <el-dialog title="状态变更" :visible.sync="statusOpen" width="500px" append-to-body>
      <el-form ref="statusForm" :model="statusForm" :rules="statusRules" label-width="80px">
        <el-form-item label="任务状态" prop="taskStatus">
          <el-select v-model="statusForm.taskStatus" placeholder="请选择任务状态">
            <el-option
              v-for="dict in dict.type.sys_task_status"
              :key="dict.value"
              :label="dict.label"
              :value="dict.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="statusForm.remark" type="textarea" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitStatusChange">ç¡® å®š</el-button>
        <el-button @click="cancelStatusChange">取 æ¶ˆ</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import { listTask, getTask, delTask, addTask, updateTask, assignTask, changeTaskStatus } from "@/api/task";
import { listUser } from "@/api/system/user";
export default {
  name: "Task",
  dicts: ['sys_task_type', 'sys_task_status'],
  data() {
    return {
      // é®ç½©å±‚
      loading: true,
      // é€‰ä¸­æ•°ç»„
      ids: [],
      // éžå•个禁用
      single: true,
      // éžå¤šä¸ªç¦ç”¨
      multiple: true,
      // æ˜¾ç¤ºæœç´¢æ¡ä»¶
      showSearch: true,
      // æ€»æ¡æ•°
      total: 0,
      // ä»»åŠ¡ç®¡ç†è¡¨æ ¼æ•°æ®
      taskList: [],
      // å¼¹å‡ºå±‚标题
      title: "",
      // æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚
      open: false,
      // æ˜¯å¦æ˜¾ç¤ºåˆ†é…å¼¹å‡ºå±‚
      assignOpen: false,
      // æ˜¯å¦æ˜¾ç¤ºçŠ¶æ€å˜æ›´å¼¹å‡ºå±‚
      statusOpen: false,
      // æ—¥æœŸèŒƒå›´
      dateRange: [],
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        taskCode: null,
        taskType: null,
        taskStatus: null,
        plannedStartTimeBegin: null,
        plannedStartTimeEnd: null,
      },
      // è¡¨å•参数
      form: {},
      // åˆ†é…è¡¨å•
      assignForm: {},
      // çŠ¶æ€å˜æ›´è¡¨å•
      statusForm: {},
      // ç”¨æˆ·åˆ—表
      userList: [],
      // è¡¨å•校验
      rules: {
        taskType: [
          { required: true, message: "任务类型不能为空", trigger: "change" }
        ],
        taskDescription: [
          { required: true, message: "任务描述不能为空", trigger: "blur" }
        ],
        plannedStartTime: [
          { required: true, message: "计划开始时间不能为空", trigger: "blur" }
        ],
        plannedEndTime: [
          { required: true, message: "计划结束时间不能为空", trigger: "blur" }
        ]
      },
      // åˆ†é…è¡¨å•校验
      assignRules: {
        assigneeId: [
          { required: true, message: "执行人不能为空", trigger: "change" }
        ]
      },
      // çŠ¶æ€å˜æ›´è¡¨å•æ ¡éªŒ
      statusRules: {
        taskStatus: [
          { required: true, message: "任务状态不能为空", trigger: "change" }
        ]
      }
    };
  },
  created() {
    this.getList();
    this.getUserList();
  },
  methods: {
    /** æŸ¥è¯¢ä»»åŠ¡ç®¡ç†åˆ—è¡¨ */
    getList() {
      this.loading = true;
      if (this.dateRange != null && this.dateRange.length === 2) {
        this.queryParams.plannedStartTimeBegin = this.dateRange[0];
        this.queryParams.plannedStartTimeEnd = this.dateRange[1];
      }
      listTask(this.queryParams).then(response => {
        this.taskList = response.rows;
        this.total = response.total;
        this.loading = false;
      });
    },
    /** æŸ¥è¯¢ç”¨æˆ·åˆ—表 */
    getUserList() {
      listUser().then(response => {
        this.userList = response.rows;
      });
    },
    // å–消按钮
    cancel() {
      this.open = false;
      this.reset();
    },
    // è¡¨å•重置
    reset() {
      this.form = {
        taskId: null,
        taskType: null,
        taskDescription: null,
        departureAddress: null,
        destinationAddress: null,
        plannedStartTime: null,
        plannedEndTime: null,
        assigneeId: null,
        remark: null
      };
      this.resetForm("form");
    },
    /** æœç´¢æŒ‰é’®æ“ä½œ */
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    /** é‡ç½®æŒ‰é’®æ“ä½œ */
    resetQuery() {
      this.dateRange = [];
      this.resetForm("queryForm");
      this.handleQuery();
    },
    // å¤šé€‰æ¡†é€‰ä¸­æ•°æ®
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.taskId)
      this.single = selection.length!==1
      this.multiple = !selection.length
    },
    /** æ–°å¢žæŒ‰é’®æ“ä½œ */
    handleAdd() {
      this.reset();
      this.open = true;
      this.title = "添加任务管理";
    },
    /** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
    handleUpdate(row) {
      this.reset();
      const taskId = row.taskId || this.ids
      getTask(taskId).then(response => {
        this.form = response.data;
        this.open = true;
        this.title = "修改任务管理";
      });
    },
    /** æŸ¥çœ‹æŒ‰é’®æ“ä½œ */
    handleView(row) {
      this.$router.push('/task/general-detail/index/' + row.taskId);
    },
    /** åˆ†é…æŒ‰é’®æ“ä½œ */
    handleAssign(row) {
      this.assignForm = {
        taskId: row.taskId,
        assigneeId: row.assigneeId,
        remark: null
      };
      this.assignOpen = true;
    },
    /** çŠ¶æ€å˜æ›´æŒ‰é’®æ“ä½œ */
    handleStatusChange(row) {
      this.statusForm = {
        taskId: row.taskId,
        taskStatus: row.taskStatus,
        remark: null
      };
      this.statusOpen = true;
    },
    /** æäº¤æŒ‰é’® */
    submitForm() {
      this.$refs["form"].validate(valid => {
        if (valid) {
          if (this.form.taskId != null) {
            updateTask(this.form).then(response => {
              this.$modal.msgSuccess("修改成功");
              this.open = false;
              this.getList();
            });
          } else {
            addTask(this.form).then(response => {
              this.$modal.msgSuccess("新增成功");
              this.open = false;
              this.getList();
            });
          }
        }
      });
    },
    /** æäº¤åˆ†é… */
    submitAssign() {
      this.$refs["assignForm"].validate(valid => {
        if (valid) {
          assignTask(this.assignForm.taskId, this.assignForm).then(response => {
            this.$modal.msgSuccess("分配成功");
            this.assignOpen = false;
            this.getList();
          });
        }
      });
    },
    /** æäº¤çŠ¶æ€å˜æ›´ */
    submitStatusChange() {
      this.$refs["statusForm"].validate(valid => {
        if (valid) {
          changeTaskStatus(this.statusForm.taskId, this.statusForm).then(response => {
            this.$modal.msgSuccess("状态变更成功");
            this.statusOpen = false;
            this.getList();
          });
        }
      });
    },
    /** åˆ é™¤æŒ‰é’®æ“ä½œ */
    handleDelete(row) {
      const taskIds = row.taskId || this.ids;
      this.$modal.confirm('是否确认删除任务管理编号为"' + taskIds + '"的数据项?').then(function() {
        return delTask(taskIds);
      }).then(() => {
        this.getList();
        this.$modal.msgSuccess("删除成功");
      }).catch(() => {});
    },
    /** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
    handleExport() {
      this.download('task/export', {
        ...this.queryParams
      }, `task_${new Date().getTime()}.xlsx`)
    },
    /** å–消分配 */
    cancelAssign() {
      this.assignOpen = false;
      this.assignForm = {};
    },
    /** å–消状态变更 */
    cancelStatusChange() {
      this.statusOpen = false;
      this.statusForm = {};
    }
  }
};
</script>
sql/task_dict_data.sql
New file
@@ -0,0 +1,39 @@
-- ----------------------------
-- é€šç”¨ä»»åŠ¡ç®¡ç†æ•°æ®å­—å…¸
-- ----------------------------
-- 1. ä»»åŠ¡ç±»åž‹å­—å…¸
-- ----------------------------
INSERT INTO sys_dict_type VALUES ('sys_task_type', '任务类型', '0', 'admin', sysdate(), '', null, '任务类型列表');
INSERT INTO sys_dict_data VALUES (1, 1, '维修保养', 'MAINTENANCE', 'sys_task_type', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '维修保养任务');
INSERT INTO sys_dict_data VALUES (2, 2, '加油任务', 'FUEL', 'sys_task_type', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '加油任务');
INSERT INTO sys_dict_data VALUES (3, 3, '其他', 'OTHER', 'sys_task_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '其他类型任务');
-- 2. ä»»åŠ¡çŠ¶æ€å­—å…¸
-- ----------------------------
INSERT INTO sys_dict_type VALUES ('sys_task_status', '任务状态', '0', 'admin', sysdate(), '', null, '任务状态列表');
INSERT INTO sys_dict_data VALUES (4, 1, '待开始', 'PENDING', 'sys_task_status', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '任务已创建,等待开始');
INSERT INTO sys_dict_data VALUES (5, 2, '任务中', 'IN_PROGRESS', 'sys_task_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '任务已经开始');
INSERT INTO sys_dict_data VALUES (6, 3, '已完成', 'COMPLETED', 'sys_task_status', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '任务已完成');
INSERT INTO sys_dict_data VALUES (7, 4, '已取消', 'CANCELLED', 'sys_task_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '任务已取消');
-- 3. è½¦è¾†ç±»åž‹å­—å…¸
-- ----------------------------
INSERT INTO sys_dict_type VALUES ('sys_vehicle_type', '车辆类型', '0', 'admin', sysdate(), '', null, '车辆类型列表');
INSERT INTO sys_dict_data VALUES (8, 1, '救护车', 'AMBULANCE', 'sys_vehicle_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '救护车');
INSERT INTO sys_dict_data VALUES (9, 2, '转运车', 'TRANSFER', 'sys_vehicle_type', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '转运车');
INSERT INTO sys_dict_data VALUES (10, 3, '维修车', 'MAINTENANCE', 'sys_vehicle_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '维修车');
-- 4. è½¦è¾†çŠ¶æ€å­—å…¸
-- ----------------------------
INSERT INTO sys_dict_type VALUES ('sys_vehicle_status', '车辆状态', '0', 'admin', sysdate(), '', null, '车辆状态列表');
INSERT INTO sys_dict_data VALUES (11, 1, '正常', '0', 'sys_vehicle_status', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '车辆正常使用');
INSERT INTO sys_dict_data VALUES (12, 2, '停用', '1', 'sys_vehicle_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '车辆停用');
-- 5. ä»»åŠ¡è½¦è¾†å…³è”çŠ¶æ€å­—å…¸
-- ----------------------------
INSERT INTO sys_dict_type VALUES ('sys_task_vehicle_status', '任务车辆关联状态', '0', 'admin', sysdate(), '', null, '任务车辆关联状态列表');
INSERT INTO sys_dict_data VALUES (15, 1, '已分配', 'ASSIGNED', 'sys_task_vehicle_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '车辆已分配给任务');
INSERT INTO sys_dict_data VALUES (16, 2, '执行中', 'ACTIVE', 'sys_task_vehicle_status', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '车辆正在执行任务');
INSERT INTO sys_dict_data VALUES (17, 3, '已完成', 'COMPLETED', 'sys_task_vehicle_status', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '车辆任务已完成');
INSERT INTO sys_dict_data VALUES (18, 4, '已取消', 'CANCELLED', 'sys_task_vehicle_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '车辆任务已取消');
sql/task_menu.sql
New file
@@ -0,0 +1,27 @@
-- ----------------------------
-- é€šç”¨ä»»åŠ¡ç®¡ç†èœå•æƒé™é…ç½®
-- ----------------------------
-- ä»»åŠ¡ç®¡ç†èœå•
INSERT INTO sys_menu VALUES (2000, '任务管理', 0, 5, 'task', null, '', 1, 0, 'M', '0', '0', '', 'task', 'admin', sysdate(), '', null, '任务管理目录');
-- é€šç”¨ä»»åŠ¡èœå•
INSERT INTO sys_menu VALUES (2001, '通用任务', 2000, 1, 'general', 'task/general/index', '', 1, 0, 'C', '0', '0', 'task:general:view', 'list', 'admin', sysdate(), '', null, '通用任务菜单');
-- ä»»åŠ¡ç®¡ç†æŒ‰é’®æƒé™
INSERT INTO sys_menu VALUES (2002, '任务查询', 2001, 1, '', '', '', 1, 0, 'F', '0', '0', 'task:general:query', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2003, '任务新增', 2001, 2, '', '', '', 1, 0, 'F', '0', '0', 'task:general:add', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2004, '任务修改', 2001, 3, '', '', '', 1, 0, 'F', '0', '0', 'task:general:edit', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2005, '任务删除', 2001, 4, '', '', '', 1, 0, 'F', '0', '0', 'task:general:remove', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2006, '任务分配', 2001, 5, '', '', '', 1, 0, 'F', '0', '0', 'task:general:assign', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2007, '状态变更', 2001, 6, '', '', '', 1, 0, 'F', '0', '0', 'task:general:status', '#', 'admin', sysdate(), '', null, '');
-- è½¦è¾†ç®¡ç†èœå•
INSERT INTO sys_menu VALUES (2008, '车辆管理', 2000, 2, 'vehicle', 'task/vehicle/index', '', 1, 0, 'C', '0', '0', 'task:vehicle:view', 'car', 'admin', sysdate(), '', null, '车辆管理菜单');
-- è½¦è¾†ç®¡ç†æŒ‰é’®æƒé™
INSERT INTO sys_menu VALUES (2009, '车辆查询', 2008, 1, '', '', '', 1, 0, 'F', '0', '0', 'task:vehicle:query', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2010, '车辆新增', 2008, 2, '', '', '', 1, 0, 'F', '0', '0', 'task:vehicle:add', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2011, '车辆修改', 2008, 3, '', '', '', 1, 0, 'F', '0', '0', 'task:vehicle:edit', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2012, '车辆删除', 2008, 4, '', '', '', 1, 0, 'F', '0', '0', 'task:vehicle:remove', '#', 'admin', sysdate(), '', null, '');
INSERT INTO sys_menu VALUES (2013, '车辆分配', 2008, 5, '', '', '', 1, 0, 'F', '0', '0', 'task:vehicle:assign', '#', 'admin', sysdate(), '', null, '');
sql/task_tables.sql
New file
@@ -0,0 +1,113 @@
-- ----------------------------
-- é€šç”¨ä»»åŠ¡ç®¡ç†ç›¸å…³è¡¨ç»“æž„
-- ----------------------------
-- 1. ä»»åŠ¡ä¸»è¡¨
-- ----------------------------
DROP TABLE IF EXISTS sys_task;
CREATE TABLE sys_task (
    task_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '任务ID',
    task_code VARCHAR(50) NOT NULL UNIQUE COMMENT '任务编号',
    task_type VARCHAR(20) NOT NULL COMMENT '任务类型:MAINTENANCE-维修保养,FUEL-加油任务,OTHER-其他',
    task_status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态:PENDING-待开始,IN_PROGRESS-任务中,COMPLETED-已完成,CANCELLED-已取消',
    task_description VARCHAR(1000) COMMENT '任务描述',
    -- åœ°å€ä¿¡æ¯
    departure_address VARCHAR(500) COMMENT '出发地址',
    destination_address VARCHAR(500) COMMENT '目的地址',
    -- æ—¶é—´ä¿¡æ¯
    planned_start_time DATETIME COMMENT '计划开始时间',
    planned_end_time DATETIME COMMENT '计划结束时间',
    actual_start_time DATETIME COMMENT '实际开始时间',
    actual_end_time DATETIME COMMENT '实际结束时间',
    -- äººå‘˜ä¿¡æ¯
    creator_id BIGINT NOT NULL COMMENT '创建人ID',
    assignee_id BIGINT COMMENT '执行人ID',
    dept_id BIGINT NOT NULL COMMENT '归属部门ID',
    -- ç³»ç»Ÿå­—段
    create_time DATETIME NOT NULL  COMMENT '创建时间',
    update_time DATETIME NOT NULL  COMMENT '更新时间',
    create_by VARCHAR(64) NOT NULL COMMENT '创建者',
    update_by VARCHAR(64) COMMENT '更新者',
    remark VARCHAR(500) COMMENT '备注',
    del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
    INDEX idx_task_code (task_code),
    INDEX idx_task_type (task_type),
    INDEX idx_task_status (task_status),
    INDEX idx_creator_id (creator_id),
    INDEX idx_assignee_id (assignee_id),
    INDEX idx_dept_id (dept_id),
    INDEX idx_planned_start_time (planned_start_time),
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务管理表';
-- 2. ä»»åŠ¡è½¦è¾†å…³è”è¡¨
-- ----------------------------
DROP TABLE IF EXISTS sys_task_vehicle;
CREATE TABLE sys_task_vehicle (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '关联ID',
    task_id BIGINT NOT NULL COMMENT '任务ID',
    vehicle_id BIGINT NOT NULL COMMENT '车辆ID',
    assign_time DATETIME NOT NULL  COMMENT '分配时间',
    assign_by VARCHAR(64) NOT NULL COMMENT '分配人',
    status VARCHAR(20) DEFAULT 'ASSIGNED' COMMENT '关联状态:ASSIGNED-已分配,ACTIVE-执行中,COMPLETED-已完成,CANCELLED-已取消',
    remark VARCHAR(500) COMMENT '备注',
    INDEX idx_task_id (task_id),
    INDEX idx_vehicle_id (vehicle_id),
    INDEX idx_status (status),
    INDEX idx_assign_time (assign_time),
    UNIQUE KEY uk_task_vehicle (task_id, vehicle_id),
    FOREIGN KEY (task_id) REFERENCES sys_task(task_id) ON DELETE CASCADE,
    FOREIGN KEY (vehicle_id) REFERENCES tb_vehicle_info(vehicle_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务车辆关联表';
-- 3. ä»»åŠ¡é™„ä»¶è¡¨
-- ----------------------------
DROP TABLE IF EXISTS sys_task_attachment;
CREATE TABLE sys_task_attachment (
    attachment_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '附件ID',
    task_id BIGINT NOT NULL COMMENT '任务ID',
    file_name VARCHAR(255) NOT NULL COMMENT '文件名',
    file_path VARCHAR(500) NOT NULL COMMENT '文件路径',
    file_size BIGINT COMMENT '文件大小(字节)',
    file_type VARCHAR(50) COMMENT '文件类型',
    upload_time DATETIME NOT NULL COMMENT '上传时间',
    upload_by VARCHAR(64) NOT NULL COMMENT '上传者',
    INDEX idx_task_id (task_id),
    FOREIGN KEY (task_id) REFERENCES sys_task(task_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务附件表';
-- 4. ä»»åŠ¡æ“ä½œæ—¥å¿—è¡¨
-- ----------------------------
DROP TABLE IF EXISTS sys_task_log;
CREATE TABLE sys_task_log (
    log_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID',
    task_id BIGINT NOT NULL COMMENT '任务ID',
    operation_type VARCHAR(20) NOT NULL COMMENT '操作类型:CREATE-创建,UPDATE-更新,ASSIGN-分配,STATUS_CHANGE-状态变更,DELETE-删除',
    operation_desc VARCHAR(500) COMMENT '操作描述',
    old_value TEXT COMMENT '操作前值',
    new_value TEXT COMMENT '操作后值',
    operator_id BIGINT NOT NULL COMMENT '操作人ID',
    operator_name VARCHAR(64) NOT NULL COMMENT '操作人姓名',
    operation_time DATETIME NOT NULL COMMENT '操作时间',
    ip_address VARCHAR(128) COMMENT 'IP地址',
    INDEX idx_task_id (task_id),
    INDEX idx_operation_type (operation_type),
    INDEX idx_operator_id (operator_id),
    INDEX idx_operation_time (operation_time),
    FOREIGN KEY (task_id) REFERENCES sys_task(task_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务操作日志表';
-- 5. æ›´æ–°è½¦è¾†ä¿¡æ¯è¡¨ï¼Œæ·»åŠ æœºæž„å…³è”å­—æ®µ
-- ----------------------------
ALTER TABLE tb_vehicle_info ADD COLUMN dept_id BIGINT(20) DEFAULT NULL COMMENT '归属机构ID';
ALTER TABLE tb_vehicle_info ADD INDEX idx_dept_id (dept_id);
ALTER TABLE tb_vehicle_info ADD FOREIGN KEY (dept_id) REFERENCES sys_dept(dept_id) ON DELETE SET NULL;
sql/vehicle_info.sql
@@ -19,5 +19,10 @@
-- åœ¨tb_vehicle_info表中添加device_id字段
ALTER TABLE tb_vehicle_info ADD COLUMN device_id VARCHAR(50) DEFAULT NULL COMMENT '设备ID';
-- åœ¨tb_vehicle_info表中添加归属机构字段
ALTER TABLE tb_vehicle_info ADD COLUMN dept_id BIGINT(20) DEFAULT NULL COMMENT '归属机构ID';
ALTER TABLE tb_vehicle_info ADD INDEX idx_dept_id (dept_id);
ALTER TABLE tb_vehicle_info ADD CONSTRAINT fk_vehicle_dept FOREIGN KEY (dept_id) REFERENCES sys_dept(dept_id) ON DELETE SET NULL;
-- åœ¨tb_vehicle_gps表中添加device_id字段
ALTER TABLE tb_vehicle_gps ADD COLUMN device_id VARCHAR(50) DEFAULT NULL COMMENT '设备ID';