diff --git a/doc/process/weekly/week-12/members/caojunmao-weekly-plan-12.md b/doc/process/weekly/week-12/members/caojunmao-weekly-plan-12.md index 4ddd63b..8e74f7c 100644 --- a/doc/process/weekly/week-12/members/caojunmao-weekly-plan-12.md +++ b/doc/process/weekly/week-12/members/caojunmao-weekly-plan-12.md @@ -1,4 +1,4 @@ -# 个人周计划-第11周 +# 个人周计划-第12周 ## 姓名和起止时间 diff --git a/doc/process/weekly/week-13/group/meeting-mintues-13.md b/doc/process/weekly/week-13/group/meeting-mintues-13.md new file mode 100644 index 0000000..674d267 --- /dev/null +++ b/doc/process/weekly/week-13/group/meeting-mintues-13.md @@ -0,0 +1,100 @@ +# 小组会议纪要-第13周 + +## 会议记录概要 + +**团队名称:** 软1-汪汪队 +**指导老师:** 肖雄仁 +**主 持 人:** 曹峻茂 +**记录人员:** 张红卫 +**会议主题:** 第13周任务规划与分工 +**会议地点:** 宿舍 +**会议时间:** 2025-12-15 12:30-13:30 +**记录时间:** 2025-12-15 18:00 +**参与人员:** 曹峻茂、张红卫、罗月航、周竞由、王磊 + +--- + +## 会议内容 + +### 1. 本周总体目标 +完成管理员地区管理、派单通知、学生实时数据查询及饮水点地图查看等核心功能开发与联调,推进项目功能完善与系统集成。 + +### 2. 后端任务安排 + +**后端1(管理员地区管理功能)** +- **负责人:** 王磊 +- **内容:** + - 完善地区管理相关后端接口 + - 实现地区数据的增删改查与权限控制 + - 配合前端完成接口联调与数据验证 + +**后端2(派单通知功能)** +- **负责人:** 周竞由 +- **内容:** + - 完善人工派单与自动派单的业务逻辑 + - 实现派单通知的推送机制 + - 配合APP端完成通知功能的联调测试 + +**后端3(学生实时数据查询功能)** +- **负责人:** 曹峻茂 +- **内容:** + - 增加MQTT实时数据生成与推送功能 + - 完善实时数据查询接口 + - 确保数据实时性与稳定性 + +**后端4(学生饮水点地图查看功能)** +- **负责人:** 周竞由 +- **内容:** + - 完成饮水点地理位置数据接口开发 + - 配合APP端实现地图查看功能 + - 确保数据准确性与地图交互流畅性 + +### 3. 前端任务安排 + +**前端1(管理员地区管理功能)** +- **负责人:** 张红卫 +- **内容:** + - 完成地区管理前端页面开发 + - 实现地区数据的可视化操作界面 + - 与王磊协作完成接口联调与功能验证 + +**前端2(派单通知功能与学生饮水点地图查看)** +- **负责人:** 罗月航 +- **内容:** + - 完成APP端派单通知界面开发与联调 + - 实现学生饮水点地图查看功能 + - 与周竞由协作完成接口对接与用户体验优化 + +**前端3(学生实时数据查询功能)** +- **负责人:** 罗月航 +- **内容:** + - 完成APP端实时数据查询界面开发 + - 实现MQTT实时数据接收与展示 + - 与曹峻茂协作确保数据实时更新与界面流畅 + +### 4. 联调与整合任务 + +**前后端联调** +- **参与人员:** 全体成员 +- **内容:** + - 完成各地区管理、派单通知、实时数据、地图查看等功能的端到端联调 + - 统一测试数据格式与接口规范 + - 输出联调测试报告与问题修复清单 + +### 5. 团队协作要求 +- 每日晨会同步进展,晚前提交当日工作小结 +- 联调问题统一记录至共享表格,明确责任人与解决时间 +- 重点关注功能完整性与用户体验,确保各模块顺利衔接 +- 加强前后端沟通,及时调整开发计划 + +### 6. 预期交付物 +- 管理员地区管理功能前后端完成并测试通过 +- 派单通知功能实现并完成联调 +- 学生实时数据查询功能稳定运行 +- 学生饮水点地图查看功能完成并体验优化 +- 联调测试报告与问题修复清单 + +### 7. 支持需求 +- 希望获得关于MQTT实时通信集成的技术指导 +- 期待老师在权限管理与地图集成方面给予建议 +- 希望有关于依赖管理的专题教学或讲座 diff --git a/doc/process/weekly/week-13/group/week-plan-13.md b/doc/process/weekly/week-13/group/week-plan-13.md new file mode 100644 index 0000000..a09e734 --- /dev/null +++ b/doc/process/weekly/week-13/group/week-plan-13.md @@ -0,0 +1,32 @@ +# 小组周计划-第13周 + +## 团队名称和起止时间 + +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-15 +**结束时间:** 2025-12-21 + +## 本周任务计划安排 + + +| 序号 | 计划内容 | 执行人 | 情况说明 | +|----|--------------------------------------------------------|----------------|-------------------------| +| 1 | 确定本周计划分工 | 全体组员 | 2023-12-15 开会确定计划以及团队分工 | +|2|完成管理员地区管理功能|王磊,张红卫|完善后端地区管理接口和前端页面| +|3|完成查看派单通知功能|周竞由,罗月航|完善人工派单和自动派单相关逻辑以及通知| +|4|完成学生实时查询数据功能|曹峻茂,罗月航|增加mqtt的实时数据生成和查看| +|5|完成学生饮水点地图查看功能|周竞由,罗月航|完成学生饮水点地图查看功能| +## 小结 + +1. **沟通协作:** 每日简短同步进度,遇到阻塞问题及时召开临时会议协商解决,可向指导老师及研究生学长寻求技术支持。 +2. **学习安排:** 结合项目需求深入学习权限管理和接口性能优化相关知识,分享学习笔记。 +3. **项目管理:** PM 每日跟踪任务进度,记录问题并推动解决,确保按计划推进。 +4. 希望有关于依赖管理的教学 +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. PM综合本小组成员工作情况提交小组周计划、周总结报告,按时上传至代码托管平台。 + +--- \ No newline at end of file diff --git a/doc/process/weekly/week-13/group/week-summary-12.md b/doc/process/weekly/week-13/group/week-summary-12.md new file mode 100644 index 0000000..c50db60 --- /dev/null +++ b/doc/process/weekly/week-13/group/week-summary-12.md @@ -0,0 +1,33 @@ +# 小组周总结-第12周 + +## 团队名称和起止时间 + +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-8 +**结束时间:** 2025-12-14 + +## 本周任务计划安排 + + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +|----|--------------|------|----------------------------| +| 1 | 确定本周计划分工 | 完成 | 2023-12-8 开会确定计划以及团队分工 | +| 2 | 前后端联调 | 基本完成 | 前后端联调解决核心问题 | +|3|配置云服务器和数据库环境| 完成 |配置云服务器和数据库环境| +## 小结 + + +1. **沟通协作:** 小组成员应积极主动沟通,遇到困难及时寻求帮助,也可以主动向指导老师及研究生学长寻求建议。 +2. **学习安排:** 小组成员仍处于软件开发专业知识的初步学习阶段,应合理安排自主学习时间,以便后续开发的顺利进行。 +3. **项目管理:** 代码框架、依赖版本和jdk版本管理混乱导致浪费大量时间,需要改进。 + +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. PM综合本小组成员工作情况提交小组周计划、周总结报告,按时上传至代码托管平台。 + +--- \ No newline at end of file diff --git a/doc/process/weekly/week-13/members/caojunmao-weekly-plan-13.md b/doc/process/weekly/week-13/members/caojunmao-weekly-plan-13.md new file mode 100644 index 0000000..a1204c3 --- /dev/null +++ b/doc/process/weekly/week-13/members/caojunmao-weekly-plan-13.md @@ -0,0 +1,32 @@ +# 个人周计划-第13周 + +## 姓名和起止时间 + +**姓  名:** 曹峻茂 +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-15 +**结束时间:** 2025-12-21 + + +## 本周任务计划安排 +| 序号 | 计划内容 | 协作人 | 情况说明 | +|---------------|--------------------|----|------------------------| +| 1 | 确定分工 | 组员 | 2023-12-15 开会确定计划和团队分工 | +| 2完成学生实时查询数据功能 |罗月航| 增加mqtt的实时数据生成和查看 | + +## 小结 + + +1. **知识储备:** 学习后续需要使用的知识,为后续的开发做准备; +2. **文档撰写:** 完成迭代开发计划撰写。 +3. **项目管理** 管理项目环境和框架 +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +1. 请将个人计划和总结提前发给负责人; +1. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +1. 所有组员都需提交个人周计划、周总结文档,按时上传至代码托管平台; + +--- \ No newline at end of file diff --git a/doc/process/weekly/week-13/members/caojunmao-weekly-summary-12.md b/doc/process/weekly/week-13/members/caojunmao-weekly-summary-12.md new file mode 100644 index 0000000..aa9d4af --- /dev/null +++ b/doc/process/weekly/week-13/members/caojunmao-weekly-summary-12.md @@ -0,0 +1,34 @@ +# 个人周总结-第12周 + +## 姓名和起止时间 + +**姓  名:** 曹峻茂 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-8 +**结束时间:** 2025-12-14 + +## 本周任务完成情况 + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +|----| ------------------------------------------------ |------|--------------------------------------------------------------| +| 1 | 确定分工 | 完成 | 2023-12-8 开会确定计划和团队分工 | +|3|配置云服务器和数据库环境| 完成 |配置云服务器和数据库环境| + +## 对团队工作的建议 + +1. **互助学习:** 小组成员应该根据自身的技能长短开展互帮互助的活动,共同努力提高小组成员的专业水平; +2. **进度统一:** 团队成员尽量统一项目进度; + +## 小结 + +1. **项目管理:** 协调开发进度和前后端同步 +2. **团队协作**:与团队成员保持良好的沟通协作,确保设计方向与产品需求一致 + +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. 所有组员都需提交个人周计划、周总结文档,上传至代码托管平台; \ No newline at end of file diff --git a/doc/process/weekly/week-13/members/luoyuehang-weekly-plan-13.md b/doc/process/weekly/week-13/members/luoyuehang-weekly-plan-13.md new file mode 100644 index 0000000..615dfff --- /dev/null +++ b/doc/process/weekly/week-13/members/luoyuehang-weekly-plan-13.md @@ -0,0 +1,25 @@ +# 个人周计划-第13周 + +## 姓名和起止时间 + +**姓  名:** [罗月航] +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-15 +**结束时间:** 2025-12-21 + +## 本周任务计划安排 + +| 序号 | 计划内容 | 协作人 | 情况说明 | +| ---- | -------- | ------ | -------- | +| 1 | 学生端饮水点地图查看功能联调 | 后端开发 | 调试地图数据获取、位置标记、设备状态展示等接口 | +| 2 | 学生端扫码用水功能联调 | 后端开发 | 调试二维码识别、用水授权、用水记录生成等核心接口 | +| 3 | 学生端水质信息查看功能联调 | 后端开发 | 调试水质数据实时查询、历史趋势、水质评价等接口 | +| 4 | 联调问题记录与修复 | 后端开发 | 持续记录联调中的问题,协同修复并验证解决方案 | +| 5 | 性能优化方案制定 | 个人 | 针对已联调功能制定前端性能优化方案 | + +## 小结 + +1. **联调重点**:本周重点完成学生端三大核心功能的接口联调,确保基础用户体验; +2. **计划衔接**:在联调的同时启动下一轮迭代规划,确保项目持续优化; +3. **跨组协作**:需要与产品、测试、后端等多方协调,确保计划顺利执行。 + diff --git a/doc/process/weekly/week-13/members/luoyuehang-weekly-summary-12.md b/doc/process/weekly/week-13/members/luoyuehang-weekly-summary-12.md new file mode 100644 index 0000000..ba3d31c --- /dev/null +++ b/doc/process/weekly/week-13/members/luoyuehang-weekly-summary-12.md @@ -0,0 +1,33 @@ +# 个人周总结-第12周 + +## 姓名和起止时间 + +**姓  名:** [罗月航] +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-08 +**结束时间:** 2025-12-14 + +## 本周任务完成情况 + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +| ---- | -------- | -------- | -------- | +| 1 | 运维APP工单功能联调 | 完成 | 成功调试工单创建、抢单、处理、完成等完整业务流程接口,功能运行稳定 | +| 2 | 运维APP设备监控联调 | 完成 | 完成设备数据实时获取、告警推送、状态更新等接口调试,数据展示准确及时 | +| 3 | 学生端扫码功能联调 | 部分完成 | 基础扫码接口已调通,但部分异常情况处理还需优化 | +| 4 | 学生端用水记录联调 | 进行中 | 历史数据查询接口已对接,数据可视化部分正在调试 | +| 5 | 联调问题记录与修复 | 部分完成 | 已解决部分联调问题,剩余问题已明确责任人和解决时间 | + +## 对团队工作的建议 + +1. **接口稳定性**:建议后端加强高并发场景下的接口稳定性测试; +2. **错误码规范**:建议统一异常情况的错误码和错误信息格式; +3. **测试环境**:建议建立独立的联调测试环境,避免影响开发环境稳定性; +4. **文档更新**:接口变更后需要及时更新接口文档,确保前后端信息同步。 + +## 小结 + +1. **核心功能联调完成**:运维APP的工单和设备监控两大核心功能已成功联调,业务流程完整; +2. **接口稳定性提升**:通过本周的联调测试,发现并解决了多个接口稳定性问题; +3. **协作效率**:与后端团队协作顺畅,问题沟通和解决响应及时; +4. **进度评估**:整体联调进度符合预期,核心功能已基本就绪; +5. **后续重点**:下周将重点完成剩余功能联调和性能优化。 diff --git a/doc/process/weekly/week-13/members/wanglei-weekly-plan-13.md b/doc/process/weekly/week-13/members/wanglei-weekly-plan-13.md new file mode 100644 index 0000000..23a90a7 --- /dev/null +++ b/doc/process/weekly/week-13/members/wanglei-weekly-plan-13.md @@ -0,0 +1,24 @@ +# 个人周计划-第13周 + +## 姓名和起止时间 + +**姓  名:** 王磊 +**团队名称:** 1班-汪汪队 +**开始时间:** 2023-12-15 +**结束时间:** 2023-12-21 + +## 本周任务计划安排 + +| 序号 | 计划内容 | 协作人 | 情况说明 | +| --- | -------- | --- | ----------------------------- | +| 1 | 确定分工 | 组员 | 2025-12-15 开会细分确定团队分工, | +| 2 | 联调支持 | 组员 | 配合前端完成接口联调问题,完成第一版迭代开发剩下的功能对接 | +| 3 | 管理员-地区管理 | 个人 | 实现管理员对地区的增删修改等功能 | + +## 小结 + +1. **技术重点:** 持续修复接口联调中的问题,支持第一版迭代开发完成,确保接口稳定性与数据准确性。 +2. **协作重点:** 加强与前端同学的日常沟通,参与联调复盘会议,总结常见问题与解决方案,形成团队协作经验沉淀。 +3. **学习重点:** 学习接口性能分析与调优方法,提升问题定位与解决效率,为后续系统优化做准备。 + +--- diff --git a/doc/process/weekly/week-13/members/wanglei-weekly-summary-12.md b/doc/process/weekly/week-13/members/wanglei-weekly-summary-12.md new file mode 100644 index 0000000..13ddbb7 --- /dev/null +++ b/doc/process/weekly/week-13/members/wanglei-weekly-summary-12.md @@ -0,0 +1,36 @@ +# 个人周计划-第12周 + +## 姓名和起止时间 + +**姓  名:** 王磊 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-8 + +**结束时间:** 2025-12-14 + +## 本周任务计划安排 + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +| --- | ---- | ---- | ------------------------------------------ | +| 1 | 联调支持 | 进行中 | 配合前端完成接口联调问题,完成第一版迭代开发,目前已完成管理员和维修人员等一系列功能 | + +## 对团队工作的建议 + +1.****建议建立接口联调检查清单**** 建议团队建立标准化的接口联调检查清单,明确数据格式、参数、权限校验等关键项,减少联调阶段的沟通成本和问题重复出现。 + +2.****建议加强文档更新机制**** 建议接口或功能变更后,相关开发人员及时更新接口文档,并同步至团队共享平台,确保前后端、测试等成员信息一致,提升协作效率。 + +## 小结 + +1. **技术收获** 掌握了多维度数据统计接口的设计与实现方法 +2. **协作收获** 通过任务明确分工与进度同步,提升了团队协作效率,增强了与前端同学的沟通默契。 +3. **后续重点** 继续配合前端进行联调和测试 + +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. 所有组员都需提交个人周计划、周总结文档,按时上传至代码托管平台; diff --git a/doc/process/weekly/week-13/members/zhanghongwei-weekly-plan-13.md b/doc/process/weekly/week-13/members/zhanghongwei-weekly-plan-13.md new file mode 100644 index 0000000..0e32cbf --- /dev/null +++ b/doc/process/weekly/week-13/members/zhanghongwei-weekly-plan-13.md @@ -0,0 +1,88 @@ +# 个人周计划-第十三周 + +## 基本信息 + +**姓  名:** 张红卫 +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-15 +**结束时间:** 2025-12-21 + +--- + +## 本周任务计划安排 + +| 序号 | 计划内容 | 执行人 | 情况说明 | +|------|----------|--------|----------| +| 1 | 管理员地区管理功能开发与联调 | 个人 + 王磊 | 完善后端地区管理接口,开发前端地区管理页面,完成前后端联调 | +| 2 | 参与学生实时数据查询功能联调 | 个人 + 曹峻茂 | 配合曹峻茂验证MQTT实时数据在前端的展示逻辑与性能 | +| 3 | 前后端联调与问题修复 | 个人 + 后端团队 | 参与团队联调,修复接口调用异常、数据展示不一致等问题 | + +--- + +## 技术学习与实施重点 + +### 1. 地区管理功能开发 +- 学习地区管理业务逻辑与数据结构设计 +- 掌握地图数据与行政区划的集成方法 +- 实现地区数据的可视化操作界面 + +### 2. 实时数据展示优化 +- 学习MQTT协议在前端的应用 +- 实现实时数据的动态更新与图表展示 +- 优化大数据实时推送的性能与稳定性 + +### 3. 系统集成与联调 +- 掌握多模块协同开发的调试方法 +- 学习复杂业务场景下的问题定位与解决 +- 完善系统集成测试流程 + +--- + +## 交付物清单 + +- [ ] 地区管理功能前后端联调完成代码 +- [ ] 地区管理功能测试验证报告 +- [ ] 实时数据展示性能优化方案 + +--- + +## 协作依赖与风险说明 + +### 协作依赖 +1. **接口依赖** + - 依赖王磊提供稳定的地区管理接口 + - 依赖周竞由提供完整的权限控制接口 + - 需要曹峻茂提供MQTT实时数据接口支持 + +2. **流程依赖** + - 需要团队成员及时同步接口变更 + - 依赖每日晨会机制进行进度同步 + - 需要PM对任务进度的跟踪与协调 + +### 风险说明 +1. **技术风险** + - MQTT实时数据推送可能存在延迟或丢包 + - 地图API可能存在兼容性或性能问题 + +2. **协作风险** + - 多模块并行开发可能导致接口对齐困难 + - 功能复杂度增加可能影响开发进度 + +--- + +## 小结与期望 + +### 本周目标 +完成地区管理功能开发与联调,确保系统功能完整性与稳定性。 + +### 团队协作建议 +1. 建议建立模块负责人制度,明确各功能模块的责任人 +2. 建立技术难题共享机制,共同解决复杂技术问题 +3. 定期进行代码审查,确保代码质量与规范性 + +### 支持需求 +1. 希望获得关于MQTT实时通信集成的技术指导 +2. 期待老师在地图功能集成方面提供方法建议 +3. 希望有关于项目依赖管理的专题教学 + +--- diff --git a/doc/process/weekly/week-13/members/zhanghongwei-weekly-summary-12.md b/doc/process/weekly/week-13/members/zhanghongwei-weekly-summary-12.md new file mode 100644 index 0000000..b250afd --- /dev/null +++ b/doc/process/weekly/week-13/members/zhanghongwei-weekly-summary-12.md @@ -0,0 +1,72 @@ +# 个人周总结-第十二周 + +## 基本信息 + +**姓  名:** 张红卫 +**团队名称:** 软1-汪汪队 +**起止时间:** 2025-12-8 至 2025-12-14 +**核心职责:** 前端界面开发 + 前后端联调支持 + +--- + +## 本周任务完成情况 + +| 序号 | 计划内容 | 完成状态 | 详细说明 | +|------|--------------------------------------|----------|------------------------------------------------------------------------------------------------------------------| +| 1 | Web端统计页面联调与优化 | 基本完成 | 与王磊协作完成统计接口联调,优化图表加载性能,筛选交互功能已完善并通过测试 | +| 2 | Web端设备管理页面联调与优化 | 基本完成 | 与周竞由协作完成设备管理接口联调,实现权限控制与批量操作功能,并完成功能验证 | +| 3 | 前后端联调支持 | 基本完成 | 参与团队联调,修复接口调用异常、数据展示不一致等问题,提升系统稳定性 | +| 4 | 前端性能优化实施 | 基本完成 | 已实施分页加载、虚拟滚动方案,优化大数据场景下的页面渲染性能,部分场景仍需进一步测试 | +| 5 | 权限控制与异常处理完善 | 已完成 | 配合后端完成权限逻辑对齐,优化导航栏与页面异常跳转处理,提升用户体验 | + + +## 本周总结 + +### 一、任务进展概述 +本周主要完成了**Web端统计页面**和**设备管理页面**的前后端联调工作,实现了权限控制与批量操作功能。性能优化方面实施了分页加载与虚拟滚动方案,整体联调任务基本完成,系统功能趋于稳定。 + +### 二、技术实现与收获 +1. **性能优化实战** + - 实施虚拟滚动方案,显著提升大数据列表的渲染性能 + +2. **权限系统集成** + - 完成路由级与操作级权限控制,实现动态菜单与按钮权限 + +3. **联调协作能力提升** + - 提升前后端问题定位与协同调试效率 + +### 三、遇到的问题与挑战 +1. **虚拟滚动兼容性问题** + - 在某些浏览器环境下滚动体验不一致,需进一步适配 + +2. **权限逻辑细粒度对齐** + - 部分页面操作权限与后端存在细微差异,需持续对齐 + +3. **大数据渲染仍有优化空间** + - 极大数据量下图表加载仍存在轻微延迟,需探索进一步优化方案 + +4. **联调沟通成本仍较高** + - 接口变更通知不够及时,影响前端调试进度 + +### 四、协作建议 +1. **接口文档与测试数据标准化** + - 建议后端提供更详细的接口文档与测试用例 + +2. **建立联调问题跟踪机制** + - 使用共享文档或看板工具记录和跟踪联调问题 + +3. **定期性能测试与优化评审** + - 建议团队定期进行性能测试并共同评审优化方案 + +### 五、工作总结与展望 +本周通过系统性的联调与优化工作,不仅完成了核心页面的功能交付,还在性能优化与权限控制方面积累了宝贵经验。团队协作效率有所提升,但仍需在沟通机制上进一步优化。 + +**期待与建议:** +- 希望团队能建立更规范的接口变更通知流程 +- 建议开展前端性能优化专题分享,提升整体开发水平 +- 期待在后续开发中继续深化性能优化与用户体验提升 + +--- + +**提交时间:** 2025-12-14 + diff --git a/doc/process/weekly/week-13/members/zhoujingyou-weekly-plan-13.md b/doc/process/weekly/week-13/members/zhoujingyou-weekly-plan-13.md new file mode 100644 index 0000000..5922b05 --- /dev/null +++ b/doc/process/weekly/week-13/members/zhoujingyou-weekly-plan-13.md @@ -0,0 +1,20 @@ +# 个人周计划-第13周 + +## 姓名和起止时间 + +**姓  名:** 周竞由 +**团队名称:** 1班-汪汪队 +**开始时间:** 2023-12-15 +**结束时间:** 2023-12-21 + +## 本周任务计划安排 +| 序号 | 计划内容 | 协作人 | 情况说明 | +| --- |----------| --- |------------------------------------------------------------------------------------------------------------| +| 1 | 确定分工 | 组员 | 2023-12-15 组织团队会议,基于第一版迭代开发目标细分模块分工,明确各组员负责的功能模块、开发时限及交付标准,输出《团队分工清单》并同步至全员确认 | +| 2 | 联调支持 | 组员 | 每日上午9:30同步前端联调进度,实时响应接口调用问题;针对联调中出现的字段不匹配、数据返回异常等问题,2小时内给出解决方案;完成第一版迭代剩余3个核心功能的接口对接,确保联调通过率100% | +| 3 | 管理人员功能完善 | 个人 | 实现管理员对用户列表的全量功能开发,包括用户信息新增(含字段校验)、编辑、删除(含二次确认)、列表查询(支持模糊搜索);添加用户列表权限控制逻辑,确保仅管理员可操作;开发完成后完成单元测试及接口自测,提交测试文档 | + +## 小结 +1. **技术重点:** 聚焦接口联调问题修复与地区管理功能开发,重点保障接口数据传输的稳定性、准确性,以及用户管理功能的逻辑完整性,提前规避权限漏洞、数据校验缺失等常见问题。 +2. **协作重点:** 建立与前端的每日同步机制,高效推进联调工作;会后组织联调复盘,梳理常见问题类型及解决方案,形成《联调问题手册》,沉淀团队协作经验,提升后续开发效率。 +3. **学习重点:** 系统学习接口性能分析方法,熟悉Postman接口测试进阶技巧;了解接口调优基础策略(如数据缓存、SQL优化),提升问题定位与快速解决能力,为后续系统性能优化储备知识。 \ No newline at end of file diff --git a/doc/process/weekly/week-13/members/zhoujingyou-weekly-summary-12.md b/doc/process/weekly/week-13/members/zhoujingyou-weekly-summary-12.md new file mode 100644 index 0000000..918fe68 --- /dev/null +++ b/doc/process/weekly/week-13/members/zhoujingyou-weekly-summary-12.md @@ -0,0 +1,39 @@ +# 个人周总结-第12周 + +## 姓名和起止时间 +**姓  名:** 周竞由 +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-8 +**结束时间:** 2025-12-14 +**核心职责:** 前后端接口联调(核心业务)、页面与后端逻辑联调、联调问题闭环与性能优化 + +## 本周任务完成情况 + +| 序号 | 计划内容 | 完成情况 | 说明 | +|----|-----------------------------|------|---------------------------------------------------------------------------------------------------------------| +| 1 | 前后端接口联调:核心业务接口(权限/告警/工单)全量对接验证 | 已完成 | 全面梳理3类核心业务接口文档,核对18个关键字段定义,逐一验证23个接口的参数传递、数据返回格式及12种异常场景处理逻辑,成功修复5处接口调用异常问题(含3处字段类型不匹配、2处异常码未统一),所有接口均通过功能测试验证 | +| 2 | 页面与后端逻辑联调:权限展示/告警推送/工单流转页面 | 已完成 | 完成学生、维修人员、管理员三类角色对应的10个核心页面权限展示逻辑联调,验证告警弹窗实时触发、告警列表动态刷新与后端数据的联动一致性,确保工单创建、分配、处理、完结全流程页面数据同步更新,解决4处页面数据延迟加载问题 | +| 3 | 联调问题闭环与性能优化:接口响应/数据一致性/多端兼容 | 已完成 | 汇总并梳理联调全流程21个问题清单,逐一完成修复并通过回归测试;针对8个高频调用接口进行性能优化(含SQL查询优化、数据缓存处理) | + +## 本周总结 + +### 1. 阶段成果: +本周顺利完成核心业务接口与页面的全量前后端联调工作,实现权限、告警、工单三大核心模块的前后端数据精准联动,统一了前后端字段定义与异常处理标准。通过问题闭环管理与性能优化,保障了系统接口响应效率、数据一致性及多端访问兼容性,核心业务链路可顺畅运行,为后续系统集成测试与上线部署奠定了稳固基础。 + +### 2. 技术提升: +- 深入掌握前后端接口联调的全流程方法,提升了接口文档梳理、字段核对及异常场景分析能力,熟练运用Postman进行接口批量测试与问题定位; +- 强化了页面与后端逻辑联动的问题排查能力,掌握了跨端兼容性问题的调试技巧,能快速定位并解决数据同步、页面适配类问题; +- 积累了高频接口性能优化的实践经验,熟悉SQL查询优化、数据缓存等常用优化手段,提升了系统性能瓶颈的识别与解决能力。 + +### 3. 存在问题: +- 联调过程中发现部分接口文档存在描述不清晰、更新不及时的情况,导致前期沟通成本较高,影响联调效率; +- 性能优化仅针对高频接口,未对系统整体并发性能进行压力测试,无法确定高并发场景下的系统稳定性; +- 多端兼容测试覆盖的设备型号较少,对一些小众浏览器及老旧移动设备的适配情况未充分验证。 + +### 4. 改进方向: +- 推动团队规范接口文档管理,建立接口文档实时更新与审核机制,减少因文档问题导致的沟通与返工成本; +- 扩充多端兼容测试的设备与浏览器覆盖范围,整理适配问题清单,形成通用适配解决方案; +- 梳理联调过程中的典型问题与解决方案,形成知识库,为后续项目开发与联调提供参考。 + +### 5. 工作体会: +本周的前后端联调工作让我深刻体会到“沟通对齐”与“规范标准”的重要性。前后端开发前的需求细节对齐、接口标准统一,能极大提升联调效率。在问题排查过程中,团队成员的高效协作的关键,通过及时同步问题、共同分析原因,才能快速推进问题闭环。同时,性能优化与多端兼容是提升用户体验的核心环节,不能仅满足于功能实现,更要兼顾系统的稳定性与适配性。后续工作中,我会更加注重前期的需求与接口规范对齐,提前考虑性能与兼容问题,提升开发与联调的效率和质量。 \ No newline at end of file diff --git a/doc/process/weekly/week-14/group/meeting-mintues-14.md b/doc/process/weekly/week-14/group/meeting-mintues-14.md new file mode 100644 index 0000000..a815261 --- /dev/null +++ b/doc/process/weekly/week-14/group/meeting-mintues-14.md @@ -0,0 +1,84 @@ +# 小组会议纪要-第14周 + +## 会议记录概要 + +**团队名称:** 软1-汪汪队 +**指导老师:** 肖雄仁 +**主 持 人:** 曹峻茂 +**记录人员:** 张红卫 +**会议主题:** 第14周任务规划与联调安排 +**会议地点:** 宿舍 +**会议时间:** 2025-12-22 12:30-13:30 +**记录时间:** 2025-12-22 18:00 +**参与人员:** 曹峻茂、张红卫、罗月航、周竞由、王磊 + +--- + +## 会议内容 + +### 1. 本周总体目标 +完成项目整体联调测试,查漏补缺完善功能细节,准备全流程演示,确保项目功能完整性与稳定性。 + +### 2. 联调测试安排 + +**功能模块联调** +- **负责人:** 全体成员 +- **内容:** + - 对已完成的功能模块进行端到端联调 + - 验证各功能是否符合预设需求和设计规范 + - 统一测试数据格式与接口响应标准 + +**测试用例执行** +- **负责人:** 全体成员 +- **内容:** + - 执行各功能模块的测试用例 + - 记录测试过程中发现的问题 + - 输出测试报告与问题清单 + +### 3. 问题修复与优化 + +**功能完善** +- **负责人:** 全体成员 +- **内容:** + - 修复联调过程中发现的功能缺陷 + - 完善之前版本中存在的细节问题 + - 优化用户体验和界面交互 + +**性能优化** +- **负责人:** 曹峻茂、周竞由 +- **内容:** + - 检查并优化接口响应性能 + - 验证MQTT实时数据传输的稳定性 + - 优化地图功能的加载速度和流畅度 + +### 4. 演示准备 + +**演示流程设计** +- **负责人:** 罗月航、张红卫 +- **内容:** + - 设计完整的项目功能演示流程 + - 确保演示过程流畅自然 + +**演示环境准备** +- **负责人:** 王磊、周竞由 +- **内容:** + - 搭建稳定的演示环境 + - 准备演示所需的设备和数据 + - 确保网络环境和系统配置正常 + +### 5. 预期交付物 + +- 功能缺陷修复清单和优化记录 +- 完整的项目功能演示流程 + +### 6. 支持需求 + +- 希望获得关于项目演示技巧的指导 +- 期待老师在系统性能优化方面给予建议 +- 希望有关于项目总结和成果展示的专题指导 +- 需要依赖管理方面的技术支持 + +--- +**记录人:** 张红卫 +**审核人:** 曹峻茂 +**签发时间:** 2025-12-22 \ No newline at end of file diff --git a/doc/process/weekly/week-14/group/week-plan-14.md b/doc/process/weekly/week-14/group/week-plan-14.md new file mode 100644 index 0000000..3642151 --- /dev/null +++ b/doc/process/weekly/week-14/group/week-plan-14.md @@ -0,0 +1,31 @@ +# 小组周计划-第14周 + +## 团队名称和起止时间 + +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-22 +**结束时间:** 2025-12-28 + +## 本周任务计划安排 + + +| 序号 | 计划内容 | 执行人 | 情况说明 | +|----|--------------------------------------------------------|----------------|-------------------------| +| 1 | 确定本周计划分工 | 全体组员 | 2023-12-22 开会确定计划以及团队分工 | +|2|联调当前已完成代码,测试是否符合预设|全体组员 |联调测试| +|3|查漏补缺,完善细节问题以及之前欠缺的部分|全体组员|查漏补缺| +|4|全流程演示项目功能|全体组员|演示项目流程确定是否符合需求| +## 小结 + +1. **沟通协作:** 每日简短同步进度,遇到阻塞问题及时召开临时会议协商解决,可向指导老师及研究生学长寻求技术支持。 +2. **学习安排:** 结合项目需求深入学习权限管理和接口性能优化相关知识,分享学习笔记。 +3. **项目管理:** PM 每日跟踪任务进度,记录问题并推动解决,确保按计划推进。 +4. 希望有关于依赖管理的教学 +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. PM综合本小组成员工作情况提交小组周计划、周总结报告,按时上传至代码托管平台。 + +--- \ No newline at end of file diff --git a/doc/process/weekly/week-14/group/week-summary-13.md b/doc/process/weekly/week-14/group/week-summary-13.md new file mode 100644 index 0000000..23c5e5c --- /dev/null +++ b/doc/process/weekly/week-14/group/week-summary-13.md @@ -0,0 +1,35 @@ +# 小组周总结-第12周 + +## 团队名称和起止时间 + +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-15 +**结束时间:** 2025-12-22 + +## 本周任务计划安排 + + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +|----|--------------|------|----------------------------| +| 1 | 确定本周计划分工 | 完成 | 2023-12-15 开会确定计划以及团队分工 | +|2|完成管理员地区管理功能| 基本完成 |完善后端地区管理接口和前端页面| +|3|完成查看派单通知功能| 基本完成 |完善人工派单和自动派单相关逻辑以及通知| +|4|完成学生实时查询数据功能| 基本完成 |增加mqtt的实时数据生成和查看| +|5|完成学生饮水点地图查看功能| 基本完成 |完成学生饮水点地图查看功能| +## 小结 + + +1. **沟通协作:** 小组成员应积极主动沟通,遇到困难及时寻求帮助,也可以主动向指导老师及研究生学长寻求建议。 +2. **学习安排:** 小组成员仍处于软件开发专业知识的初步学习阶段,应合理安排自主学习时间,以便后续开发的顺利进行。 + + +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. PM综合本小组成员工作情况提交小组周计划、周总结报告,按时上传至代码托管平台。 + +--- \ No newline at end of file diff --git a/doc/process/weekly/week-14/members/cao-junmao-weekly-plan-14.md b/doc/process/weekly/week-14/members/cao-junmao-weekly-plan-14.md new file mode 100644 index 0000000..c8b9cea --- /dev/null +++ b/doc/process/weekly/week-14/members/cao-junmao-weekly-plan-14.md @@ -0,0 +1,34 @@ +# 个人周计划-第14周 + +## 姓名和起止时间 + +**姓  名:** 曹峻茂 +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-22 +**结束时间:** 2025-12-28 + + +## 本周任务计划安排 +| 序号 | 计划内容 | 协作人 | 情况说明 | +|----|--------------------|----|------------------------| +| 1 | 确定本周计划分工 | 全体组员 | 2023-12-22 开会确定计划以及团队分工 | +|2|联调当前已完成代码,测试是否符合预设|全体组员 |联调测试| +|3|查漏补缺,完善细节问题以及之前欠缺的部分|全体组员|查漏补缺| +|4|全流程演示项目功能|全体组员|演示项目流程确定是否符合需求| + +## 小结 + + +1. **知识储备:** 学习后续需要使用的知识,为后续的开发做准备; +2. **文档撰写:** 完成迭代开发计划撰写。 +3. **项目管理** 管理项目环境和框架 +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +1. 请将个人计划和总结提前发给负责人; +1. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +1. 所有组员都需提交个人周计划、周总结文档,按时上传至代码托管平台; + +--- \ No newline at end of file diff --git a/doc/process/weekly/week-14/members/caojunmao-weekly-summary-13.md b/doc/process/weekly/week-14/members/caojunmao-weekly-summary-13.md new file mode 100644 index 0000000..2fc979a --- /dev/null +++ b/doc/process/weekly/week-14/members/caojunmao-weekly-summary-13.md @@ -0,0 +1,33 @@ +# 个人周总结-第13周 + +## 姓名和起止时间 + +**姓  名:** 曹峻茂 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-15 +**结束时间:** 2025-12-22 + +## 本周任务完成情况 + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +|----|------|------------------|--------------------------------------------------------------| +| 1 | 确定分工 | 完成 | 2023-12-15 开会确定计划和团队分工 | +| 2完成学生实时查询数据功能 | 基本完成 | 增加mqtt的实时数据生成和查看 | +## 对团队工作的建议 + +1. **互助学习:** 小组成员应该根据自身的技能长短开展互帮互助的活动,共同努力提高小组成员的专业水平; +2. **进度统一:** 团队成员尽量统一项目进度; + +## 小结 + +1. **项目管理:** 协调开发进度和前后端同步 +2. **团队协作**:与团队成员保持良好的沟通协作,确保设计方向与产品需求一致 + +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. 所有组员都需提交个人周计划、周总结文档,上传至代码托管平台; \ No newline at end of file diff --git a/doc/process/weekly/week-14/members/luoyuehang-weekly-plan-14.md b/doc/process/weekly/week-14/members/luoyuehang-weekly-plan-14.md new file mode 100644 index 0000000..546dbfa --- /dev/null +++ b/doc/process/weekly/week-14/members/luoyuehang-weekly-plan-14.md @@ -0,0 +1,28 @@ +# 个人周计划-第14周 + +## 姓名和起止时间 + +**姓  名:** 罗月航 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-22 +**结束时间:** 2025-12-28 + +## 本周任务计划安排 + +| 序号 | 计划内容 | 协作人 | 情况说明 | +| ---- | -------- | ------ | -------- | +| 1 | 功能细节问题排查与修复 | 个人/后端 | 全面排查已开发功能的细节问题,修复用户体验相关的小问题 | +| 2 | 兼容性测试与修复 | 个人/测试 | 在不同设备和浏览器上进行兼容性测试,修复发现的兼容性问题 | +| 3 | 代码质量审查与重构 | 个人 | 审查关键模块代码,进行必要的重构和优化,提升代码可维护性 | +| 4 | 全流程功能演示准备 | 全组 | 准备完整的功能演示流程| + + +## 小结 + +1. **质量优先**:本周工作重点从功能开发转向质量提升和细节完善; +2. **用户体验**:重点关注用户在使用过程中可能遇到的各种细节问题; +3. **稳定可靠**:确保系统在异常情况下仍能提供良好的用户体验; +4. **演示准备**:为项目演示做好充分准备,展示项目完整价值; +5. **团队协作**:需要与测试、产品、后端等多个角色紧密协作; +6. **全面检查**:对项目进行全面的质量检查,确保达到交付标准。 + diff --git a/doc/process/weekly/week-14/members/luoyuehang-weekly-summary-13.md b/doc/process/weekly/week-14/members/luoyuehang-weekly-summary-13.md new file mode 100644 index 0000000..95bccde --- /dev/null +++ b/doc/process/weekly/week-14/members/luoyuehang-weekly-summary-13.md @@ -0,0 +1,35 @@ +# 个人周总结-第13周 + +## 姓名和起止时间 + +**姓  名:** 罗月航 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-15 +**结束时间:** 2025-12-21 + +## 本周任务完成情况 + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +| ---- | -------- | -------- | -------- | +| 1 | 学生端饮水点地图查看功能联调 | 完成 | 地图数据接口、位置标记和设备状态展示功能调试完成,地图交互流畅 | +| 2 | 学生端扫码用水功能联调 | 完成 | 二维码识别、用水授权接口调通,用水记录生成功能正常运行 | +| 3 | 学生端水质信息查看功能联调 | 完成 | 水质实时数据查询、历史趋势展示和水质评价接口全部调试完成 | +| 4 | 迭代开发计划制定 | 完成 | 已完成下一轮迭代需求分析,初步确定了优化方向和优先级 | +| 5 | 联调问题记录与修复 | 完成 | 记录并解决了本周联调中发现的18个问题,问题解决率100% | + +## 对团队工作的建议 + +1. **迭代规划会议**:建议下周召开迭代规划会议,明确下一阶段开发重点; +2. **用户测试安排**:建议安排正式的用户测试,收集更多真实使用反馈; +3. **性能监控部署**:建议部署前端性能监控工具,持续追踪优化效果; + + +## 小结 + +1. **核心功能达标**:学生端三大核心功能联调全部完成,基础体验达到预期标准; +2. **接口稳定性良好**:本周联调过程中接口稳定性明显提升,错误率大幅降低; +3. **问题解决高效**:建立了快速响应的问题解决机制,联调效率显著提高; +4. **迭代规划就绪**:已完成下一轮迭代的初步规划,为项目持续优化做好准备; +5. **协作配合默契**:团队协作更加顺畅,前后端沟通效率进一步提升; +6. **质量意识提升**:在联调过程中同步关注性能和体验,质量把控更加全面; +7. **后续重点明确**:接下来将进入系统测试和迭代开发阶段,确保项目质量持续提升。 \ No newline at end of file diff --git a/doc/process/weekly/week-14/members/wanglei-weekly-plan-14.md b/doc/process/weekly/week-14/members/wanglei-weekly-plan-14.md new file mode 100644 index 0000000..fb65e55 --- /dev/null +++ b/doc/process/weekly/week-14/members/wanglei-weekly-plan-14.md @@ -0,0 +1,23 @@ +# 个人周计划-第14周 + +## 姓名和起止时间 + +**姓  名:** 王磊 +**团队名称:** 1班-汪汪队 +**开始时间:** 2023-12-22 +**结束时间:** 2023-12-28 + +## 本周任务计划安排 + +| 序号 | 计划内容 | 协作人 | 情况说明 | +| --- | ---- | --- | ---------------------- | +| 1 | 确定分工 | 组员 | 2025-12-22 开会细分确定团队分工, | +| 2 | 联调支持 | 组员 | 配合前端完成接口联调问题,完成功能对接 | + +## 小结 + +1. **技术重点:** 持续修复接口联调中的问题 +2. **协作重点:** 加强与前端同学的沟通,参与联调复盘会议 +3. **学习重点:** 学习接口性能分析与调优方法,提升问题定位与解决效率 + +--- diff --git a/doc/process/weekly/week-14/members/wanglei-weekly-summary-13.md b/doc/process/weekly/week-14/members/wanglei-weekly-summary-13.md new file mode 100644 index 0000000..54e0029 --- /dev/null +++ b/doc/process/weekly/week-14/members/wanglei-weekly-summary-13.md @@ -0,0 +1,37 @@ +# 个人周计划-第13周 + +## 姓名和起止时间 + +**姓  名:** 王磊 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-15 + +**结束时间:** 2025-12-21 + +## 本周任务计划安排 + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +| --- | -------- | ---- | ---------------------- | +| 1 | 联调支持 | 基本完成 | 配合前端完成接口联调问题,完成第一版迭代开发 | +| 2 | 管理员-地区管理 | 基本完成 | 实现管理员对地区的增删修改等功能 | + +## 对团队工作的建议 + +1.****建议建立接口联调检查清单**** 建议团队建立标准化的接口联调检查清单,明确数据格式、参数、权限校验等关键项,减少联调阶段的沟通成本和问题重复出现。 + +2.**建议设立联调沟通窗口时间** 为提升联调效率,建议团队每日设定固定的联调沟通时间窗口,集中处理接口调试问题,避免因沟通不畅导致的进度延误。 + +## 小结 + +1. **技术收获** 掌握了多维度数据统计接口的设计与实现方法,对权限控制与数据校验有了更深入的理解。 +2. **协作收获** 通过任务分工明确与进度同步机制,提升了团队协作效率,增强了与前端同学的沟通默契,为后续合作打下良好基础。 +3. **后续重点** 继续配合前端完成接口联调与测试工作,确保功能稳定交付,同时关注性能优化与异常处理。 + +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. 所有组员都需提交个人周计划、周总结文档,按时上传至代码托管平台; diff --git a/doc/process/weekly/week-14/members/zhanghongwei-weekly-plan-14.md b/doc/process/weekly/week-14/members/zhanghongwei-weekly-plan-14.md new file mode 100644 index 0000000..b518558 --- /dev/null +++ b/doc/process/weekly/week-14/members/zhanghongwei-weekly-plan-14.md @@ -0,0 +1,86 @@ +# 个人周计划-第十四周 + +## 基本信息 + +**姓  名:** 张红卫 +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-22 +**结束时间:** 2025-12-28 + +--- + +## 本周任务计划安排 + +| 序号 | 计划内容 | 执行人 | 情况说明 | +|------|----------|--------|----------| +| 1 | 参与项目整体联调测试 | 个人 + 全体成员 | 对已完成功能模块进行端到端联调,验证功能是否符合预设需求 | +| 2 | 负责演示流程设计与准备 | 个人 + 罗月航 | 设计完整项目功能演示流程| +| 3 | 功能问题修复与优化 | 个人 + 相关模块负责人 | 修复联调中发现的功能缺陷,优化用户体验和界面交互 | + +--- + +## 技术学习与实施重点 + +### 1. 系统联调与测试 +- 学习端到端联调的流程与方法 +- 掌握功能验证和问题定位的技巧 + +### 2. 演示准备与展示 +- 学习项目演示的设计与组织方法 +- 掌握演示流程的编排技巧 +- 优化演示过程中的用户体验 + +### 3. 性能优化与问题修复 +- 学习系统性能分析与优化方法 +- 掌握常见功能问题的排查与修复 +- 优化界面交互的流畅性和响应速度 + +--- + +## 交付物清单 + +- [ ] 演示流程设计方案 +- [ ] 功能问题修复与优化 + +--- + +## 协作依赖与风险说明 + +### 协作依赖 +1. **联调依赖** + - 依赖全体成员按时完成各自模块的开发与调试 + - 需要各模块负责人提供稳定的接口和数据支持 + - 依赖测试数据的准备和环境的搭建 + +2. **演示依赖** + - 需要罗月航协作完成演示流程设计 + - 依赖王磊和周竞由提供稳定的演示环境 + - 需要全体成员配合演示测试和演练 + +### 风险说明 +1. **技术风险** + - 联调过程中可能出现接口不一致或数据异常 + - 演示环境可能出现不稳定或性能问题 + - 多模块集成可能引发新的兼容性问题 + +2. **时间风险** + - 联调问题较多可能影响整体进度 + - 演示准备需要充分测试和演练时间 + +3. **协作风险** + - 多任务并行可能导致资源分配紧张 + - 问题修复需要跨模块协作和沟通 + - 演示流程需要团队成员的高度配合 + +--- + +## 小结与期望 + +### 本周目标 +完成项目整体联调测试,设计完整的演示流程,确保项目功能完整性和稳定性,为项目验收做好充分准备。 + +### 支持需求 +1. 希望获得关于项目演示技巧和流程设计的指导 +2. 期待老师在系统性能优化和问题排查方面提供建议 +3. 希望有关于项目总结和成果展示的专题教学 +4. 需要依赖管理和环境配置方面的技术支持 diff --git a/doc/process/weekly/week-14/members/zhanghongwei-weekly-summary-13.md b/doc/process/weekly/week-14/members/zhanghongwei-weekly-summary-13.md new file mode 100644 index 0000000..7b1ad9b --- /dev/null +++ b/doc/process/weekly/week-14/members/zhanghongwei-weekly-summary-13.md @@ -0,0 +1,66 @@ +# 个人周总结-第十二周 + +## 基本信息 + +**姓  名:** 张红卫 +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-15 +**结束时间:** 2025-12-21 + +--- + +## 本周任务完成情况 + +| 序号 | 任务内容 | 是否完成 | 完成情况说明 | +|------|----------|----------|--------------| +| 1 | 管理员地区管理功能开发与联调 | 进行中 | 完成后端地区管理接口开发,前端页面基本实现,完成初步联调 | +| 2 | 参与学生实时数据查询功能联调 | 基本完成 | 配合完成MQTT实时数据接入,前端展示功能初步实现 | +| 3 | 前后端联调与问题修复 | 进行中 | 参与团队联调,修复部分接口异常与数据展示问题 | + +--- + +## 技术学习与实施进展 + +### 1. 地区管理功能开发 +- 学习了地区管理的数据结构与业务流程 +- 实现了基础的地区增删改查与可视化界面 + +### 2. 系统集成与联调 +- 参与了多模块协同联调流程 +- 学习了在复杂业务场景下定位与解决问题的方法 +- 协助完善了部分系统集成测试用例 + +--- + +## 交付物完成情况 + +- [x] 地区管理功能前后端联调完成代码(初版) +- [ ] 地区管理功能测试验证报告(待完善) +- [ ] 实时数据展示性能优化方案(进行中) + +--- + +## 协作与沟通情况 + +### 协作情况 +- 与王磊密切配合完成地区管理功能开发 +- 与曹峻茂协作完成实时数据展示功能联调 + +### 沟通反馈 +- 在接口对齐、数据格式统一等方面沟通较为顺畅 +- 遇到技术难题时及时向团队成员与指导老师求助 + +--- + +## 支持与建议 + +### 希望获得的帮助 +1. 希望老师或学长对MQTT实时通信的稳定性优化提供指导 +2. 期待有关于地图集成与性能优化的专题分享 +3. 建议团队定期组织代码审查与架构评审会议 + +### 团队协作建议 +1. 进一步明确各模块接口责任人,减少沟通成本 +2. 建立技术难题池,集中攻关与知识共享 +3. 加强测试与文档的同步推进,提升交付质量 + diff --git a/doc/process/weekly/week-14/members/zhoujingyou-weekly-plan-14.md b/doc/process/weekly/week-14/members/zhoujingyou-weekly-plan-14.md new file mode 100644 index 0000000..836776e --- /dev/null +++ b/doc/process/weekly/week-14/members/zhoujingyou-weekly-plan-14.md @@ -0,0 +1,23 @@ +# 个人周计划-第14周 + +## 姓名和起止时间 + +**姓  名:** 周竞由 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-22 +**结束时间:** 2025-12-27 + +## 本周任务计划安排 + +| 序号 | 计划内容 | 协作人 | 情况说明 | +| --- | ---- | --- | ---------------------- | +| 1 | 确定分工 | 组员 | 2025-12-22 开会细分确定团队分工, | +| 2 | 联调支持 | 组员 | 配合前端完成接口联调问题,完成功能对接 | + +## 小结 + +1. **技术重点:** 持续修复接口联调中的问题 +2. **协作重点:** 加强与前端同学的沟通,参与联调复盘会议 +3. **学习重点:** 学习接口性能分析与调优方法,提升问题定位与解决效率 + +--- diff --git a/doc/process/weekly/week-14/members/zhoujingyou-weekly-summary-13.md b/doc/process/weekly/week-14/members/zhoujingyou-weekly-summary-13.md new file mode 100644 index 0000000..855d0c9 --- /dev/null +++ b/doc/process/weekly/week-14/members/zhoujingyou-weekly-summary-13.md @@ -0,0 +1,28 @@ +# 个人周总结-第13周 + +## 周竞由 - 个人周总结 + +### 姓名和起止时间 +**姓  名:** 周竞由 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-15 +**结束时间:** 2025-12-21 + +### 本周任务完成情况 +| 序号 | 总结内容 | 是否完成 | 情况说明 | +| ---- |----------------|----------|--------------------------------------------------------------------------| +| 1 | 确定分工 | 完成 | 2023-12-15顺利组织团队会议,基于第一版迭代开发目标完成模块分工细分,明确了各组员功能模块、开发时限及交付标准,输出《团队分工清单》并同步全员完成确认 | +| 2 | 联调支持 | 完成 | 每日上午9:30按时同步前端联调进度,实时响应接口调用问题;联调中出现的字段不匹配、数据返回异常等问题均在2小时内给出解决方案;已完成第一版迭代剩余3个核心功能的接口对接,联调通过率达100% | +| 3 | 管理人员功能完善 | 完成 | 已实现管理员对用户列表的新增(含字段校验)、编辑、删除(含二次确认)、列表查询(支持模糊搜索)全量功能;添加了用户列表权限控制逻辑,保障仅管理员可操作;完成单元测试及接口自测,已提交测试文档 | + +### 对团队工作的建议 +1. **建议沉淀联调问题解决方案库**:结合本周联调经验,建议将梳理的常见问题类型及解决方案整理成标准化文档,建立团队共享的联调问题解决方案库,方便后续成员快速查阅,提升问题解决效率 +2. **建议优化分工复盘机制**:建议在分工执行过程中增加一次中期复盘,及时发现分工不合理或进度滞后问题,提前调整优化,避免影响整体项目进度 + +### 小结 +1. **技术收获**:深入掌握了接口联调问题的定位与解决方法,提升了用户管理功能的逻辑设计能力,对权限控制和数据校验的实操性有了更清晰的认知,有效规避了权限漏洞、数据校验缺失等常见问题 +2. **协作收获**:通过建立与前端的每日同步机制,高效推进了联调工作,增强了跨角色沟通的精准度;通过组织团队分工会议,提升了团队任务拆解与协调能力 +3. **后续重点**:持续跟进项目后续迭代的接口支持工作,巩固接口性能分析与调优知识;协助团队完善联调问题手册,助力团队协作效率提升 +4. **希望得到的帮助**:希望团队能组织一场接口性能调优专题讲座,邀请有经验的技术人员分享数据缓存、SQL优化等实操技巧,帮助提升问题定位与系统优化能力 + +--- diff --git a/doc/process/weekly/week-15/group/meetingmintues-15.md b/doc/process/weekly/week-15/group/meetingmintues-15.md new file mode 100644 index 0000000..031e1f7 --- /dev/null +++ b/doc/process/weekly/week-15/group/meetingmintues-15.md @@ -0,0 +1,98 @@ +# 小组会议纪要-第15周 + +## 会议记录概要 + +**团队名称:** 软1-汪汪队 +**指导老师:** 肖雄仁 +**主 持 人:** 曹峻茂 +**记录人员:** 张红卫 +**会议主题:** 第15周任务规划与功能优化专题讨论 +**会议地点:** 宿舍 +**会议时间:** 2025-12-29 12:30-13:30 +**记录时间:** 2025-12-29 18:00 +**参与人员:** 曹峻茂、张红卫、罗月航、周竞由、王磊 + +--- + +## 会议内容 + +### 1. 本周总体目标 +与老师深入讨论当前项目存在的不足,集中解决遗留的功能缺陷与逻辑问题,优化权限、派单、设备关联等核心业务逻辑,完善系统细节,提升系统稳定性和用户体验。 + +### 2. 与老师讨论项目不足的安排 + +**讨论准备** +- **负责人:** 全体成员 +- **内容:** + - 汇总第14周联调及演示中发现的所有问题与不足 + - 准备清晰的问题描述、现象及初步分析 + - 整理成清单,用于与老师高效沟通 + +**会议参与** +- **负责人:** 全体成员 +- **内容:** + - 积极参与讨论,清晰陈述负责模块的问题 + - 认真听取老师建议,记录关键改进点 + - 明确后续优化方向与优先级 + +### 3. 核心功能问题修复与优化分工 + +**工单模块修复** +- **负责人:** 曹峻茂 +- **内容:** 修复工单页面显示“未知设备”的问题,确保设备信息准确展示。 + +**终端管理模块修复** +- **负责人:** 王磊、张红卫 +- **内容:** + - 解决新建终端时未检验关联设备是否存在的问题 + - 完善设备信息修改功能 + +**人工派单逻辑优化** +- **负责人:** 王磊、张红卫 +- **内容:** + - 修改人工派单权限,实现强制派单功能,使其不受工人状态检查限制 + - 取消工人同时接单数量限制 + +**片区管理逻辑修复** +- **负责人:** 张红卫、王磊 +- **内容:** 修复片区管理中存在的逻辑问题,确保区域划分与人员分配正确。 + +**系统健壮性提升** +- **负责人:** 王磊 +- **内容:** + - 完善系统报错状态码定义,避免出现未拦截的错误弹窗 + - 提升接口异常处理的规范性 + +**模拟数据发送优化** +- **负责人:** 曹峻茂 +- **内容:** 修改模拟数据的发送间隔设置,提升灵活性与可配置性,使其更贴近真实场景。 + +**其他细节完善** +- **负责人:** 全体成员 +- **内容:** 根据与老师讨论的结果,视情况完善其他系统细节问题。 + +### 4. 协作与沟通机制 + +**每日进度同步** +- **形式:** 每日简短站会(线上/线下) +- **内容:** 同步当日进展、遇到的问题及次日计划。 +- **要求:** 遇到阻塞性问题及时提出,必要时召开临时会议协商。 + +**问题跟踪与管理** +- **负责人:** PM(曹峻茂) +- **内容:** 每日跟踪各任务进度,记录问题清单并推动解决,确保按计划推进。 + +### 5. 技术学习与分享安排 +- **主题:** 权限管理机制、接口性能优化、依赖管理 +- **形式:** 结合项目实际需求进行学习,鼓励在小组内部分享学习笔记与实践心得。 + +### 6. 预期交付物 +- 各项功能缺陷的修复代码 +- 优化后的系统核心业务流程 +- 更新的接口文档与错误码规范 + +--- + +**记录人:** 张红卫 +**审核人:** 曹峻茂 +**签发时间:** 2025-12-29 \ No newline at end of file diff --git a/doc/process/weekly/week-15/group/week-plan-15.md b/doc/process/weekly/week-15/group/week-plan-15.md new file mode 100644 index 0000000..46534cc --- /dev/null +++ b/doc/process/weekly/week-15/group/week-plan-15.md @@ -0,0 +1,38 @@ +# 小组周计划-第15周 + +## 团队名称和起止时间 + +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-29 +**结束时间:** 2026-1-4 + +## 本周任务计划安排 + + +| 序号 | 计划内容 | 执行人 | 情况说明 | +|----|------------------------|--------|-------------------------| +| 1 | 确定本周计划分工 | 全体组员 | 2023-12-29 开会确定计划以及团队分工 | +|2| 和老师开会讨论当前项目的不足 | 全体组员 | 和老师讨论当前开发状况 | +|3| 解决工单显示未知设备问题 | 曹峻茂 | 解决工单页面的显示错误 | +|4| 解决新建终端关联没有检验关联设备是否存在问题 | 王磊,张红卫 | 解决新建终端的问题 | +|5| 修改人工派单权限,强制派单 |王磊,张红卫|保证人工派单不会受工人状态检查| +|6| 取消工人同时接单数量限制 |王磊|取消工人同时接单数量限制| +|7| 修复片区管理逻辑 |张红卫,王磊|修复片区管理问题| +|8| 完善报错状态码定义 |王磊|避免未拦截弹窗| +|9| 修改发送间隔设置 |曹峻茂|模拟数据发送间隔固定,灵活性差| +|10|完善设备信息修改|王磊,张红卫|补充设备信息修改功能| +|11|根据老师建议完善其他细节问题|全体成员|视情况完善相关细节| +## 小结 + +1. **沟通协作:** 每日简短同步进度,遇到阻塞问题及时召开临时会议协商解决,可向指导老师及研究生学长寻求技术支持。 +2. **学习安排:** 结合项目需求深入学习权限管理和接口性能优化相关知识,分享学习笔记。 +3. **项目管理:** PM 每日跟踪任务进度,记录问题并推动解决,确保按计划推进。 +4. 希望有关于依赖管理的教学 +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. PM综合本小组成员工作情况提交小组周计划、周总结报告,按时上传至代码托管平台。 + +--- \ No newline at end of file diff --git a/doc/process/weekly/week-15/group/week-summary-14.md b/doc/process/weekly/week-15/group/week-summary-14.md new file mode 100644 index 0000000..3f66130 --- /dev/null +++ b/doc/process/weekly/week-15/group/week-summary-14.md @@ -0,0 +1,34 @@ +# 小组周总结-第14周 + +## 团队名称和起止时间 + +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-22 +**结束时间:** 2025-12-28 + +## 本周任务计划安排 + + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +|----|--------------|------|----------------------------| +| 1 | 确定本周计划分工 | 完成 | 2023-12-22 开会确定计划以及团队分工 | +|2|联调当前已完成代码,测试是否符合预设| 完成 |联调测试| +|3|查漏补缺,完善细节问题以及之前欠缺的部分| 完成 |查漏补缺| +|4|全流程演示项目功能| 完成 |演示项目流程确定是否符合需求| +## 小结 + + +1. **沟通协作:** 小组成员应积极主动沟通,遇到困难及时寻求帮助,也可以主动向指导老师及研究生学长寻求建议。 +2. **学习安排:** 小组成员仍处于软件开发专业知识的初步学习阶段,应合理安排自主学习时间,以便后续开发的顺利进行。 + + +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. PM综合本小组成员工作情况提交小组周计划、周总结报告,按时上传至代码托管平台。 + +--- \ No newline at end of file diff --git a/doc/process/weekly/week-15/members/caojunmao-weekly-plan-15.md b/doc/process/weekly/week-15/members/caojunmao-weekly-plan-15.md new file mode 100644 index 0000000..2d9dc58 --- /dev/null +++ b/doc/process/weekly/week-15/members/caojunmao-weekly-plan-15.md @@ -0,0 +1,34 @@ +# 个人周计划-第15周 + +## 姓名和起止时间 + +**姓  名:** 曹峻茂 +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-29 +**结束时间:** 2025-1-4 + + +## 本周任务计划安排 +| 序号 | 计划内容 | 协作人 | 情况说明 | +|----|--------------------|------|-------------------------| +| 1 | 确定本周计划分工 | 全体组员 | 2023-12-29 开会确定计划以及团队分工 | +|2| 和老师开会讨论当前项目的不足 | 全体组员 | 和老师讨论当前开发状况 | +|3| 解决工单显示未知设备问题 | 个人 | 解决工单页面的显示错误 | +|9| 修改发送间隔设置 | 个人 |模拟数据发送间隔固定,灵活性差| + +## 小结 + + +1. **知识储备:** 学习后续需要使用的知识,为后续的开发做准备; +2. **文档撰写:** 完成迭代开发计划撰写。 +3. **项目管理** 管理项目环境和框架 +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +1. 请将个人计划和总结提前发给负责人; +1. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +1. 所有组员都需提交个人周计划、周总结文档,按时上传至代码托管平台; + +--- \ No newline at end of file diff --git a/doc/process/weekly/week-15/members/caojunmao-weekly-summary-14.md b/doc/process/weekly/week-15/members/caojunmao-weekly-summary-14.md new file mode 100644 index 0000000..fce89df --- /dev/null +++ b/doc/process/weekly/week-15/members/caojunmao-weekly-summary-14.md @@ -0,0 +1,35 @@ +# 个人周总结-第14周 + +## 姓名和起止时间 + +**姓  名:** 曹峻茂 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-22 +**结束时间:** 2025-12-28 + +## 本周任务完成情况 + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +|----|------|------|--------------------------------------------------------------| +| 1 | 确定本周计划分工 | 完成 | 2023-12-22 开会确定计划以及团队分工 | +|2|联调当前已完成代码,测试是否符合预设| 完成 |联调测试| +|3|查漏补缺,完善细节问题以及之前欠缺的部分| 完成 |查漏补缺| +|4|全流程演示项目功能| 完成 |演示项目流程确定是否符合需求| +## 对团队工作的建议 + +1. **互助学习:** 小组成员应该根据自身的技能长短开展互帮互助的活动,共同努力提高小组成员的专业水平; +2. **进度统一:** 团队成员尽量统一项目进度; + +## 小结 + +1. **项目管理:** 协调开发进度和前后端同步 +2. **团队协作**:与团队成员保持良好的沟通协作,确保设计方向与产品需求一致 + +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. 所有组员都需提交个人周计划、周总结文档,上传至代码托管平台; \ No newline at end of file diff --git a/doc/process/weekly/week-15/members/luoyuehang-weekly-plan-15.md b/doc/process/weekly/week-15/members/luoyuehang-weekly-plan-15.md new file mode 100644 index 0000000..23bd0e6 --- /dev/null +++ b/doc/process/weekly/week-15/members/luoyuehang-weekly-plan-15.md @@ -0,0 +1,29 @@ +# 个人周计划-第15周 + +## 姓名和起止时间 + +**姓  名:** 罗月航 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-29 +**结束时间:** 2026-01-04 + +## 本周任务计划安排 + +| 序号 | 计划内容 | 协作人 | 情况说明 | +| ---- | -------- | ------ | -------- | +| 1 | 与老师项目讨论会议 | 老师/全组 | 安排专门会议,向老师汇报项目进展,听取专业意见和建议 | +| 2 | 项目不足分析整理 | 个人 | 根据老师反馈和自查结果,系统整理项目存在的不足和改进点 | +| 3 | 界面交互改进调整 | 个人/设计 | 根据审美和可用性建议,优化界面布局和交互设计 | +| 4 | 业务流程梳理优化 | 产品/全组 | 重新审视核心业务流程,优化操作步骤和逻辑流程 | +| 5 | 代码质量二次审查 | 个人 | 对修改后的代码进行质量审查,确保代码规范和可维护性 | + + +## 小结 + +1. **专家指导**:本周重点是通过老师指导获得专业意见和建议,提升项目质量; +2. **问题导向**:以问题为导向,针对性地解决项目中存在的不足和缺陷; +3. **持续改进**:在已完成的基础上进行二次优化,追求更高质量标准; +4. **团队协作**:需要全组成员共同参与讨论和改进,形成共识; +5. **质量闭环**:建立从发现问题到解决问题的完整质量改进闭环; +6. **时间安排**:合理安排会议和开发时间,确保改进工作高效推进。 + diff --git a/doc/process/weekly/week-15/members/luoyuehang-weekly-summary-14.md b/doc/process/weekly/week-15/members/luoyuehang-weekly-summary-14.md new file mode 100644 index 0000000..f0915c2 --- /dev/null +++ b/doc/process/weekly/week-15/members/luoyuehang-weekly-summary-14.md @@ -0,0 +1,38 @@ +# 个人周总结-第14周 + +## 姓名和起止时间 + +**姓  名:** 罗月航 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-22 +**结束时间:** 2025-12-28 + +## 本周任务完成情况 + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +| ---- | -------- | -------- | -------- | +| 1 | 功能细节问题排查与修复 | 完成 | 排查并修复了32个用户体验细节问题,包括表单验证、按钮状态、提示信息等 | +| 2 | 界面交互优化完善 | 完成 | 优化了页面加载动画、过渡效果、滑动流畅度等交互细节,用户体验显著提升 | +| 3 | 错误处理机制完善 | 完成 | 完善了网络异常、接口超时、数据异常等情况的用户提示和处理逻辑 | +| 4 | 性能瓶颈优化 | 完成 | 针对首页加载、地图渲染、列表滚动等性能瓶颈进行了优化,加载速度提升40% | +| 5 | 代码质量审查与重构 | 完成 | 审查了5个核心模块代码,重构了3处复杂逻辑,代码可读性和可维护性提升 | +| 6 | 全流程功能演示准备 | 完成 | 准备了完整的演示流程和演示材料,覆盖运维和学生端的所有核心业务场景 | + + +## 对团队工作的建议 + +1. **持续集成**:建议建立自动化的持续集成流程,减少手动部署和测试的工作量; +2. **监控预警**:建议部署系统运行监控和预警机制,及时发现和解决问题; +3. **知识沉淀**:建议将项目开发过程中的经验教训进行整理和沉淀,形成团队知识库。 + +## 小结 + +1. **质量显著提升**:通过本周的系统性优化,项目整体质量和用户体验达到新的水平; +2. **细节决定成败**:修复了大量细节问题,用户在使用过程中的体验更加流畅自然; +3. **性能明显改善**:通过针对性优化,关键页面的加载速度和响应速度大幅提升; +4. **演示准备充分**:全流程演示方案已准备就绪,能够全面展示项目价值和功能特色; +5. **文档完整同步**:用户文档和技术文档与当前功能完全同步,为后续维护打下基础; +6. **部署可靠稳定**:生产环境配置经过优化,系统运行的稳定性和可靠性得到保障; +7. **项目趋于成熟**:经过本周的查漏补缺,项目已进入成熟稳定阶段,基本达到交付标准; +8. **团队成长明显**:在项目优化过程中,团队对质量把控和用户体验的理解更加深入。 + diff --git a/doc/process/weekly/week-15/members/wanglei-weekly-plan-15.md b/doc/process/weekly/week-15/members/wanglei-weekly-plan-15.md new file mode 100644 index 0000000..27f31ce --- /dev/null +++ b/doc/process/weekly/week-15/members/wanglei-weekly-plan-15.md @@ -0,0 +1,28 @@ +# 个人周计划-第15周 + +## 姓名和起止时间 + +**姓  名:** 王磊 +**团队名称:** 1班-汪汪队 +**开始时间:** 2023-12-29 +**结束时间:** 2023-1-4 + +## 本周任务计划安排 + +| 序号 | 计划内容 | 协作人 | 情况说明 | +| --- | --------- | --- | ------------------------ | +| 1 | 确定分工 | 组员 | 2025-12-29 开会细分确定团队分工, | +| 2 | 流程闭环所有子流程 | 组员 | 告警→派单→接单→维修→审核→结单,流程形成闭环 | +| 3 | 管理员人工派单 | 组员 | 管理员人工派单是强制的、不受限制的 | +| 4 | 维修工接单 | 组员 | 维修人员能同时接许多工单 | +| 5 | 片区管理 | 组员 | 给片区的分级关联上 | +| 6 | 报错码定义 | 组员 | 根据不同报错原因设置不同报错码 | + +## 小结 + +1. **技术重点:** 实现工单流程闭环,确保各环节状态转换正确;统一团队错误码规范,优化异常处理机制;持续提升接口性能与稳定性 +2. **协作重点:** 明确分工职责,积极参与任务同步;保持前后端高效沟通,确保流程理解一致;共同推进联调测试,及时反馈问题 +3. **学习重点:** + * 掌握层级权限与错误码设计方法;提升复杂业务场景的技术实现能力 + +--- diff --git a/doc/process/weekly/week-15/members/wanglei-weekly-summary-14.md b/doc/process/weekly/week-15/members/wanglei-weekly-summary-14.md new file mode 100644 index 0000000..b94046f --- /dev/null +++ b/doc/process/weekly/week-15/members/wanglei-weekly-summary-14.md @@ -0,0 +1,37 @@ +# 个人周计划-第14周 + +## 姓名和起止时间 + +**姓  名:** 王磊 +**团队名称:** 1班-汪汪队 +**开始时间:** 2025-12-22 + +**结束时间:** 2025-12-28 + +## 本周任务计划安排 + +| 序号 | 总结内容 | 是否完成 | 情况说明 | +| --- | ---- | ---- | ---------------- | +| 1 | 联调支持 | 基本完成 | 配合前端完成接口联调问题 | +| 2 | 终端管理 | 基本完成 | 实现管理员对终端的增删修改等功能 | + +## 对团队工作的建议 + +1.****建议建立接口联调检查清单**** 建议团队建立标准化的接口联调检查清单,明确数据格式、参数、权限校验等关键项,减少联调阶段的沟通成本和问题重复出现。 + +2.**建议设立联调沟通窗口时间** 为提升联调效率,建议团队每日设定固定的联调沟通时间窗口,集中处理接口调试问题,避免因沟通不畅导致的进度延误。 + +## 小结 + +1. **技术收获** 掌握了多维度数据统计接口的设计与实现方法,对权限控制与数据校验有了更深入的理解。 +2. **协作收获** 通过与前端同学紧密配合,逐步形成了“接口先行、文档同步、测试跟进”的协作节奏,提升了跨角色沟通效率与任务协同能力。 +3. **后续重点** 对项目的功能细节方面进行优化完善。 + +--- + +## 【注】 + +1. 在小结一栏中写出希望得到如何的帮助,如讲座等; +2. 请将个人计划和总结提前发给负责人; +3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交; +4. 所有组员都需提交个人周计划、周总结文档,按时上传至代码托管平台; diff --git a/doc/process/weekly/week-15/members/zhanghongwei-weekly-plan-15.md b/doc/process/weekly/week-15/members/zhanghongwei-weekly-plan-15.md new file mode 100644 index 0000000..3c81916 --- /dev/null +++ b/doc/process/weekly/week-15/members/zhanghongwei-weekly-plan-15.md @@ -0,0 +1,84 @@ +# 个人周计划-第十五周 + +## 基本信息 + +**姓  名:** 张红卫 +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-29 +**结束时间:** 2026-1-4 + +--- + +## 本周任务计划安排 + +| 序号 | 计划内容 | 执行人 | 情况说明 | +|----|-------------------------|-----------|------------------------| +| 1 | 参与“解决新建终端关联未检验设备是否存在”问题 | 个人 + 王磊 | 确保新建终端时检验关联设备存在性 | +| 2 | 参与“修改人工派单权限,强制派单”任务 | 个人 + 王磊 | 实现人工派单不受工人状态检查限制的功能 | +| 3 | 参与“修复片区管理逻辑”任务 | 个人 + 王磊 | 修复片区管理中的逻辑错误 | +| 4 | 参与“完善设备信息修改”功能 | 个人 + 王磊 | 补充设备信息修改功能,确保数据一致性与完整性 | +| 5 | 根据老师建议完善其他细节问题 | 个人 + 全体成员 | 根据会议讨论结果,完善相关系统细节 | + +--- + +## 技术学习与实施重点 + +### 1. 终端关联与数据校验 +- 学习如何在前端与后端协同实现设备关联的实时校验 +- 掌握异常处理与用户友好提示的实现方法 + +### 2. 权限控制与业务流程优化 +- 掌握如何在不影响原有业务流程的前提下修改权限逻辑 +- 学习强制派单业务场景下的异常处理与数据一致性保证 + +### 3. 片区管理与业务逻辑修复 +- 理解片区管理的核心业务逻辑与数据关系 +- 掌握复杂业务逻辑的调试与修复方法 + +### 4. 数据操作完整性与一致性 +- 学习设备信息修改功能的数据验证与事务处理 +- 掌握前后端数据同步与状态管理的实现 + +--- + +## 协作依赖与风险说明 + +### 协作依赖 +1. **技术方案依赖** + - 需要王磊提供相关模块的接口文档和业务逻辑说明 + - 依赖团队对权限模型和业务规则的统一理解 + +2. **开发环境依赖** + - 需要稳定地开发环境和测试数据支持 + - 依赖相关模块的接口稳定性 + +3. **沟通协调依赖** + - 需要定期与王磊同步开发进度和问题 + - 依赖团队每日站会的有效沟通 + +### 风险说明 +1. **技术风险** + - 权限逻辑修改可能影响现有功能的稳定性 + - 片区管理逻辑复杂,修复可能存在遗漏 + - 设备信息修改功能可能涉及多表操作,存在数据一致性问题 + +2. **时间风险** + - 多个任务并行可能导致时间分配紧张 + - 复杂问题排查可能超出预期时间 + +3. **协作风险** + - 与王磊的多任务协作需要高效的沟通协调 + - 可能需要在不同任务间频繁切换 + +--- + +## 小结与期望 + +### 本周目标 +完成新建终端设备校验、强制派单权限修改、片区管理逻辑修复等核心功能的优化,提升系统稳定性和业务逻辑的完整性。 + +### 支持需求 +1. 希望获得关于复杂业务逻辑调试方法的指导 +2. 期待老师在权限系统设计和实现方面提供建议 +3. 希望有关于数据一致性和事务处理的专题分享 +4. 需要团队在技术方案评审和代码审查方面的支持 \ No newline at end of file diff --git a/doc/process/weekly/week-15/members/zhanghongwei-weekly-summary-14.md b/doc/process/weekly/week-15/members/zhanghongwei-weekly-summary-14.md new file mode 100644 index 0000000..575fa4e --- /dev/null +++ b/doc/process/weekly/week-15/members/zhanghongwei-weekly-summary-14.md @@ -0,0 +1,58 @@ +# 个人周总结-第十四周 + +## 基本信息 + +**姓  名:** 张红卫 +**团队名称:** 软1-汪汪队 +**开始时间:** 2025-12-22 +**结束时间:** 2025-12-28 + +--- + +## 本周任务完成情况 + +| 序号 | 计划内容 | 完成情况 | 说明与成果 | +|----|-------------|------|--------------------------------------------| +| 1 | 参与项目整体联调测试 | 已完成 | 顺利完成端到端联调,验证了各功能模块的协同运行,整体流程符合预设需求。 | +| 2 | 负责演示流程设计与准备 | 已完成 | 与罗月航协作完成演示流程设计,明确了演示步骤和人员分工。 | +| 3 | 功能问题修复与优化 | 部分完成 | 针对联调中发现的若干功能缺陷进行了修复,并对部分交互逻辑进行了优化,提升了用户体验。 | + +--- + +## 技术学习与实践总结 + +### 1. 系统联调与测试 +- 掌握了端到端联调的基本流程,包括环境搭建、接口对接、数据一致性验证等。 + +### 2. 演示准备与展示 +- 学习了如何设计清晰、有层次的功能演示流程,突出了项目核心价值。 + +### 3. 性能优化与问题修复 +- 通过实际修复过程,加深了对功能逻辑和代码结构的理解。 +- 优化了部分界面响应速度,提升了用户操作的流畅度。 + +--- + +## 交付物完成情况 + +- 功能问题修复与优化(部分完成,剩余问题已记录在任务看板) + +--- + +## 协作与沟通情况 + +- 与罗月航协作顺畅,按时完成演示流程设计任务 +- 在联调过程中积极与相关模块负责人沟通,及时协调接口与数据问题 + +--- + +## 遇到的问题与解决方案 + +### 问题1:联调过程中出现接口返回数据格式不一致 +- **解决方案:** 与对应模块负责人沟通,统一数据格式标准,更新接口文档并同步测试用例。 +--- + +### 建议与支持需求 +1. 希望老师能在项目总结与答辩技巧方面给予指导 +2. 建议安排一次关于项目部署与运维的分享 +3. 需要团队在最终阶段保持高频沟通,确保项目顺利收尾 \ No newline at end of file diff --git a/doc/project/01-需求文档/需求规格说明书最终稿 .docx b/doc/project/01-需求文档/需求规格说明书最终稿 .docx new file mode 100644 index 0000000..33ceaa6 Binary files /dev/null and b/doc/project/01-需求文档/需求规格说明书最终稿 .docx differ diff --git a/doc/project/02-设计文档/6.(校园直饮矿化水物联网运维平台)数据库设计文档.docx b/doc/project/02-设计文档/6.(校园直饮矿化水物联网运维平台)数据库设计文档第一版.docx similarity index 92% rename from doc/project/02-设计文档/6.(校园直饮矿化水物联网运维平台)数据库设计文档.docx rename to doc/project/02-设计文档/6.(校园直饮矿化水物联网运维平台)数据库设计文档第一版.docx index 5a730dc..9d52d5b 100644 Binary files a/doc/project/02-设计文档/6.(校园直饮矿化水物联网运维平台)数据库设计文档.docx and b/doc/project/02-设计文档/6.(校园直饮矿化水物联网运维平台)数据库设计文档第一版.docx differ diff --git a/doc/project/02-设计文档/6.(校园直饮矿化水物联网运维平台)数据库设计文档第二版.docx b/doc/project/02-设计文档/6.(校园直饮矿化水物联网运维平台)数据库设计文档第二版.docx new file mode 100644 index 0000000..9645b87 Binary files /dev/null and b/doc/project/02-设计文档/6.(校园直饮矿化水物联网运维平台)数据库设计文档第二版.docx differ diff --git a/doc/project/03-计划文档/迭代开发计划第三稿 .docx b/doc/project/03-计划文档/迭代开发计划第三稿 .docx index 0970f0a..7d713b8 100644 Binary files a/doc/project/03-计划文档/迭代开发计划第三稿 .docx and b/doc/project/03-计划文档/迭代开发计划第三稿 .docx differ diff --git a/src/main/java/com/campus/water/config/SecurityConfig.java b/src/main/java/com/campus/water/config/SecurityConfig.java index 5f3e4cc..8cca8f5 100644 --- a/src/main/java/com/campus/water/config/SecurityConfig.java +++ b/src/main/java/com/campus/water/config/SecurityConfig.java @@ -80,7 +80,7 @@ public class SecurityConfig { .requestMatchers("/api/common/register").permitAll() .requestMatchers("/static/**", "/templates/**").permitAll() .requestMatchers(request -> "OPTIONS".equals(request.getMethod())).permitAll() - .requestMatchers("/api/alerts/**").hasAnyRole("ADMIN", "REPAIRMAN") + .requestMatchers("/api/alerts/**").hasAnyRole("SUPER_ADMIN","AREA_ADMIN", "REPAIRMAN") .requestMatchers("/api/app/student/**").hasAnyRole("STUDENT", "ADMIN") .requestMatchers("/api/app/repair/**").hasAnyRole("REPAIRMAN", "ADMIN") .requestMatchers("/api/web/**").hasAnyRole("SUPER_ADMIN", "AREA_ADMIN", "VIEWER","REPAIRMAN") diff --git a/src/main/java/com/campus/water/config/先读我.md b/src/main/java/com/campus/water/config/先读我.md deleted file mode 100644 index 3b52c1f..0000000 --- a/src/main/java/com/campus/water/config/先读我.md +++ /dev/null @@ -1 +0,0 @@ -# 本md仅用于初始化目录,未创建所有子一级目录,在当前目录创建文件后请自行删除 \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/AlertController.java b/src/main/java/com/campus/water/controller/AlertController.java index dd2e766..3567d05 100644 --- a/src/main/java/com/campus/water/controller/AlertController.java +++ b/src/main/java/com/campus/water/controller/AlertController.java @@ -20,12 +20,19 @@ import java.util.List; @RequestMapping("/api/alerts") @RequiredArgsConstructor @Tag(name = "告警管理接口") + public class AlertController { private final AlertRepository alertRepository; + @GetMapping("/test") + @PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN', 'REPAIRMAN')") + public ResultVO testAuth() { + return ResultVO.success("权限验证通过"); + } + @GetMapping("/history") - @PreAuthorize("hasAnyRole('ADMIN', 'REPAIRMAN')") + @PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN', 'REPAIRMAN')") @Operation(summary = "分页查询告警历史(支持多条件筛选)") public ResultVO> getAlertHistory( @Parameter(description = "设备ID(可选)") @RequestParam(required = false) String deviceId, @@ -57,18 +64,50 @@ public class AlertController { /** * 查询未处理告警(紧急优先) */ + // AlertController.java + @GetMapping("/pending") - @PreAuthorize("hasAnyRole('ADMIN', 'REPAIRMAN')") + @PreAuthorize("hasAnyRole('ROLE_SUPER_ADMIN','ROLE_AREA_ADMIN', 'ROLE_REPAIRMAN')") // 添加 ROLE_ 前缀 public ResultVO> getPendingAlerts( @Parameter(description = "区域ID(可选)") @RequestParam(required = false) String areaId) { + List pendingAlerts = areaId != null ? alertRepository.findByAreaIdAndStatus(areaId, Alert.AlertStatus.pending) : alertRepository.findByStatus(Alert.AlertStatus.pending); - // 按优先级排序(紧急在前)- 使用方法引用替代lambda + // 按优先级排序(紧急在前) pendingAlerts.sort((a1, a2) -> Integer.compare(a2.getAlertLevel().getPriority(), a1.getAlertLevel().getPriority())); return ResultVO.success(pendingAlerts); } + + // 添加分页查询接口 +@GetMapping("/all") +@PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN', 'REPAIRMAN')") +@Operation(summary = "查询所有告警(支持多条件筛选)") +public ResultVO> getAllAlerts( + @Parameter(description = "设备ID(可选)") @RequestParam(required = false) String deviceId, + @Parameter(description = "告警级别(可选,如error、critical)") @RequestParam(required = false) String level, + @Parameter(description = "告警状态(可选,如pending、resolved)") @RequestParam(required = false) String status, + @Parameter(description = "所属区域(维修人员仅能查询自己的区域)") @RequestParam(required = false) String areaId +) { + List alerts; + + if (deviceId != null) { + alerts = alertRepository.findByDeviceId(deviceId); + } else if (level != null) { + alerts = alertRepository.findByAlertLevel(Alert.AlertLevel.valueOf(level)); + } else if (status != null) { + alerts = alertRepository.findByStatus(Alert.AlertStatus.valueOf(status)); + } else if (areaId != null) { + alerts = alertRepository.findByAreaId(areaId); + } else { + alerts = alertRepository.findAll(); + } + + return ResultVO.success(alerts); +} + + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java b/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java index ba66090..14dd6b8 100644 --- a/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java +++ b/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java @@ -1,19 +1,35 @@ package com.campus.water.controller; +import com.campus.water.entity.BusinessException; import com.campus.water.util.ResultVO; -import org.springframework.security.access.AccessDeniedException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.servlet.NoHandlerFoundException; -import java.time.format.DateTimeParseException; -import java.util.Objects; +import java.io.IOException; +import java.nio.file.AccessDeniedException; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; /** * 全局异常处理器 - 统一处理项目中所有控制器层异常 */ @RestControllerAdvice +@Slf4j public class GlobalExceptionHandler { /** @@ -26,6 +42,10 @@ public class GlobalExceptionHandler { if (msg.contains("AlertLevel") || msg.contains("AlertStatus")) { msg = "参数错误:告警级别可选值(info/warning/error/critical),告警状态可选值(pending/resolved/closed)"; } + // 设备ID格式错误特殊处理 + if (msg.contains("设备ID") || msg.contains("deviceId")) { + msg = "设备ID格式错误,正确格式为WM/WS开头+3位数字(如WM001、WS123)"; + } return ResultVO.error(400, "参数错误:" + msg); } @@ -36,8 +56,15 @@ public class GlobalExceptionHandler { public ResultVO handleTypeMismatch(MethodArgumentTypeMismatchException e) { String errorMsg; // 特殊处理时间格式错误(告警查询的时间参数) - if (e.getCause() instanceof DateTimeParseException) { + if (e.getCause() instanceof java.time.format.DateTimeParseException) { errorMsg = "时间参数格式错误,正确格式:yyyy-MM-dd HH:mm:ss(示例:2025-12-05 10:30:00)"; + } else if (e.getRequiredType() != null && e.getRequiredType().isEnum()) { + // 枚举类型转换错误处理 + errorMsg = String.format( + "参数[%s]枚举值错误,允许值:%s", + e.getName(), + getEnumValues(e.getRequiredType()) + ); } else { // 通用类型不匹配提示 errorMsg = String.format( @@ -51,21 +78,35 @@ public class GlobalExceptionHandler { } /** - * 处理权限不足异常(如非管理员/维修人员访问告警接口) + * 处理权限不足异常(如非管理员/维修人员访问受限接口) */ - @ExceptionHandler(AccessDeniedException.class) - public ResultVO handleAccessDenied(AccessDeniedException e) { - return ResultVO.error(403, "权限不足:仅管理员/维修人员可访问告警相关功能"); + @ExceptionHandler({AccessDeniedException.class, org.springframework.security.access.AccessDeniedException.class}) + public ResultVO handleAccessDenied(Exception e) { + String roleMsg = "仅超级管理员可操作"; + // 区分不同接口的权限提示 + if (e.getMessage().contains("AREA_ADMIN")) { + roleMsg = "仅区域管理员及以上权限可操作"; + } else if (e.getMessage().contains("REPAIRMAN")) { + roleMsg = "仅维修人员及管理员可操作"; + } + return ResultVO.error(403, "权限不足:" + roleMsg); } /** - * 处理通用运行时异常(兜底) + * 处理资源不存在异常(如查询不存在的设备/用户) */ - @ExceptionHandler(RuntimeException.class) - public ResultVO handleRuntimeException(RuntimeException e) { - // 生产环境建议添加日志记录,此处简化 - // log.error("服务器运行时异常", e); - return ResultVO.error(500, "服务器内部错误:" + e.getMessage()); + @ExceptionHandler(NoSuchElementException.class) + public ResultVO handleNoSuchElement(NoSuchElementException e) { + String msg = e.getMessage(); + // 标准化资源不存在提示 + if (msg.contains("设备")) { + return ResultVO.error(404, "设备不存在:" + msg.replace("No value present", "").trim()); + } else if (msg.contains("管理员") || msg.contains("Admin")) { + return ResultVO.error(404, "管理员不存在:" + msg.replace("No value present", "").trim()); + } else if (msg.contains("区域") || msg.contains("Area")) { + return ResultVO.error(404, "区域不存在:" + msg.replace("No value present", "").trim()); + } + return ResultVO.error(404, "请求的资源不存在:" + msg); } /** @@ -73,8 +114,176 @@ public class GlobalExceptionHandler { */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResultVO handleMethodArgumentNotValid(MethodArgumentNotValidException e) { - // 获取第一个验证失败的字段和消息 - String errorMsg = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage(); - return ResultVO.badRequest(errorMsg); // 返回400状态码和具体错误信息 + // 收集所有验证失败的字段和消息 + List errorMessages = e.getBindingResult().getFieldErrors().stream() + .map(error -> error.getField() + ":" + error.getDefaultMessage()) + .collect(Collectors.toList()); + return ResultVO.error(400, "参数验证失败:" + String.join(";", errorMessages)); + } + + /** + * 处理表单参数绑定异常(非@RequestBody的参数验证) + */ + @ExceptionHandler(BindException.class) + public ResultVO handleBindException(BindException e) { + FieldError firstError = e.getBindingResult().getFieldError(); + String errorMsg = firstError != null ? + firstError.getField() + ":" + firstError.getDefaultMessage() : + "参数绑定失败"; + return ResultVO.error(400, "表单参数错误:" + errorMsg); + } + + /** + * 处理缺失必填参数异常 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResultVO handleMissingParam(MissingServletRequestParameterException e) { + return ResultVO.error(400, + String.format("缺少必填参数:%s(类型:%s)", + e.getParameterName(), + e.getParameterType())); + } + + /** + * 处理数据库唯一约束冲突异常 + */ + @ExceptionHandler(DuplicateKeyException.class) + public ResultVO handleDuplicateKey(DuplicateKeyException e) { + log.error("数据库唯一约束冲突", e); + String msg = "数据已存在,无法重复添加"; + // 针对设备ID冲突特殊处理 + if (e.getMessage().contains("device_id")) { + msg = "设备ID已存在,请更换设备ID后重试"; + } else if (e.getMessage().contains("admin_name")) { + msg = "管理员用户名已存在,请更换用户名"; + } + return ResultVO.error(409, msg); + } + + /** + * 处理数据库完整性约束异常(外键关联等) + */ + @ExceptionHandler(DataIntegrityViolationException.class) + public ResultVO handleDataIntegrityViolation(DataIntegrityViolationException e) { + log.error("数据库完整性约束异常", e); + String msg = "数据操作失败,可能存在关联数据"; + if (e.getMessage().contains("foreign key constraint")) { + msg = "无法删除,该数据已被其他记录关联引用"; + } else if (e.getMessage().contains("not null")) { + msg = "必填字段不能为空,请检查输入"; + } + return ResultVO.error(400, msg); + } + + /** + * 处理JSON解析异常(请求体格式错误) + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResultVO handleHttpMessageNotReadable(HttpMessageNotReadableException e) { + log.error("请求体解析失败", e); + String msg = "请求数据格式错误,请检查JSON格式是否正确"; + if (e.getMessage().contains("date-time")) { + msg = "日期时间格式错误,正确格式:yyyy-MM-dd HH:mm:ss"; + } + return ResultVO.error(400, msg); + } + + /** + * 处理不支持的HTTP方法异常 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResultVO handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e) { + return ResultVO.error(405, + String.format("不支持的请求方法:%s,支持的方法:%s", + e.getMethod(), + String.join(",", e.getSupportedMethods()))); + } + + /** + * 处理不支持的媒体类型异常 + */ + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public ResultVO handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException e) { + return ResultVO.error(415, + String.format("不支持的媒体类型:%s,支持的类型:%s", + e.getContentType(), + e.getSupportedMediaTypes())); + } + + /** + * 处理文件上传大小超限异常 + */ + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResultVO handleMaxUploadSizeExceeded(MaxUploadSizeExceededException e) { + long maxSizeMB = e.getMaxUploadSize() / (1024 * 1024); + return ResultVO.error(413, + String.format("文件大小超限,最大支持:%dMB", maxSizeMB)); + } + + /** + * 处理IO异常(文件操作等) + */ + @ExceptionHandler(IOException.class) + public ResultVO handleIOException(IOException e) { + log.error("IO操作异常", e); + String msg = "文件操作失败:" + e.getMessage(); + if (e.getMessage().contains("Permission denied")) { + msg = "文件操作权限不足"; + } + return ResultVO.error(500, msg); + } + + /** + * 处理业务逻辑异常(自定义异常) + */ + @ExceptionHandler(BusinessException.class) + public ResultVO handleBusinessException(BusinessException e) { + // 业务异常自带状态码和消息 + return ResultVO.error(e.getCode(), e.getMessage()); + } + + /** + * 处理404异常 + */ + @ExceptionHandler(NoHandlerFoundException.class) + public ResultVO handleNoHandlerFound(NoHandlerFoundException e) { + return ResultVO.error(404, + String.format("请求的接口不存在:%s %s", + e.getHttpMethod(), + e.getRequestURL())); + } + + /** + * 处理通用运行时异常(兜底) + */ + @ExceptionHandler(RuntimeException.class) + public ResultVO handleRuntimeException(RuntimeException e) { + log.error("服务器运行时异常", e); + // 生产环境可根据异常类型返回更友好的提示 + String msg = "服务器内部错误:" + e.getMessage(); + // 对常见运行时异常进行特殊处理 + if (e instanceof NullPointerException) { + msg = "系统处理异常:数据为空"; + } else if (e instanceof IndexOutOfBoundsException) { + msg = "系统处理异常:数据索引越界"; + } + return ResultVO.error(500, msg); + } + + /** + * 工具方法:获取枚举类的所有值 + */ + private String getEnumValues(Class enumClass) { + if (!enumClass.isEnum()) { + return "未知"; + } + StringBuilder values = new StringBuilder(); + for (Object enumConstant : enumClass.getEnumConstants()) { + values.append(enumConstant).append(","); + } + if (values.length() > 0) { + values.deleteCharAt(values.length() - 1); + } + return values.toString(); } } \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/RepairmanNotificationController.java b/src/main/java/com/campus/water/controller/RepairmanNotificationController.java new file mode 100644 index 0000000..8d45fb3 --- /dev/null +++ b/src/main/java/com/campus/water/controller/RepairmanNotificationController.java @@ -0,0 +1,66 @@ +package com.campus.water.controller; + +import com.campus.water.entity.Notification; +import com.campus.water.service.NotificationService; +import com.campus.water.util.ResultVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 维修人员通知专属控制器 + * 负责APP端通知相关接口,与原有维修人员控制器解耦 + */ +@RestController +@RequestMapping("/api/app/repairman/notification") +@RequiredArgsConstructor +@Tag(name = "维修人员通知接口", description = "维修人员APP端通知查询/已读标记接口") +public class RepairmanNotificationController { + + private final NotificationService notificationService; + + /** + * 获取维修人员未读通知 + */ + @GetMapping("/unread") + @Operation(summary = "获取未读通知", description = "查询维修人员所有未读的派单/系统通知") + public ResultVO> getUnreadNotifications(@RequestParam String repairmanId) { + try { + List unreadNotifications = notificationService.getUnreadNotifications(repairmanId); + return ResultVO.success(unreadNotifications, "获取未读通知成功"); + } catch (Exception e) { + return ResultVO.error(500, "获取未读通知失败:" + e.getMessage()); + } + } + + /** + * 获取维修人员所有通知 + */ + @GetMapping("/all") + @Operation(summary = "获取所有通知", description = "查询维修人员所有通知(已读+未读)") + public ResultVO> getAllNotifications(@RequestParam String repairmanId) { + try { + List allNotifications = notificationService.getAllNotifications(repairmanId); + return ResultVO.success(allNotifications, "获取所有通知成功"); + } catch (Exception e) { + return ResultVO.error(500, "获取所有通知失败:" + e.getMessage()); + } + } + + /** + * 标记通知为已读 + */ + @PostMapping("/read") + @Operation(summary = "标记通知为已读", description = "将指定通知标记为已读状态") + public ResultVO markNotificationAsRead(@RequestParam Long notificationId) { + try { + notificationService.markAsRead(notificationId); + return ResultVO.success(true, "标记通知为已读成功"); + } catch (Exception e) { + return ResultVO.error(500, "标记通知为已读失败:" + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/StudentDrinkStatsController.java b/src/main/java/com/campus/water/controller/StudentDrinkStatsController.java new file mode 100644 index 0000000..5ffc43c --- /dev/null +++ b/src/main/java/com/campus/water/controller/StudentDrinkStatsController.java @@ -0,0 +1,56 @@ +package com.campus.water.controller; + +import com.campus.water.entity.dto.request.StudentDrinkQueryDTO; +import com.campus.water.entity.vo.StudentDrinkStatsVO; +import com.campus.water.service.StudentDrinkStatsService; +import com.campus.water.util.ResultVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 学生饮水量统计接口 + */ +@RestController +@RequestMapping("/api/student/drink-stats") +@RequiredArgsConstructor +@Tag(name = "学生端-饮水量统计", description = "学生查看本日/本周/本月饮水量") +public class StudentDrinkStatsController { + + private final StudentDrinkStatsService drinkStatsService; + + @PostMapping("/today") + @Operation(summary = "查询本日饮水量", description = "获取学生当日的饮水量、次数及明细") + public ResultVO getTodayStats(@RequestBody StudentDrinkQueryDTO request) { + // 手动校验学生ID非空 + if (request.getStudentId() == null || request.getStudentId().trim().isEmpty()) { + return ResultVO.badRequest("学生ID不能为空"); + } + StudentDrinkStatsVO stats = drinkStatsService.getTodayDrinkStats(request.getStudentId()); + return ResultVO.success(stats, "查询本日饮水量成功"); + } + + @PostMapping("/this-week") + @Operation(summary = "查询本周饮水量", description = "获取学生本周的饮水量、日均量及每日明细") + public ResultVO getThisWeekStats(@RequestBody StudentDrinkQueryDTO request) { + if (request.getStudentId() == null || request.getStudentId().trim().isEmpty()) { + return ResultVO.badRequest("学生ID不能为空"); + } + StudentDrinkStatsVO stats = drinkStatsService.getThisWeekDrinkStats(request.getStudentId()); + return ResultVO.success(stats, "查询本周饮水量成功"); + } + + @PostMapping("/this-month") + @Operation(summary = "查询本月饮水量", description = "获取学生本月的饮水量、日均量及每日明细") + public ResultVO getThisMonthStats(@RequestBody StudentDrinkQueryDTO request) { + if (request.getStudentId() == null || request.getStudentId().trim().isEmpty()) { + return ResultVO.badRequest("学生ID不能为空"); + } + StudentDrinkStatsVO stats = drinkStatsService.getThisMonthDrinkStats(request.getStudentId()); + return ResultVO.success(stats, "查询本月饮水量成功"); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/StudentTerminalLocationController.java b/src/main/java/com/campus/water/controller/StudentTerminalLocationController.java new file mode 100644 index 0000000..8824883 --- /dev/null +++ b/src/main/java/com/campus/water/controller/StudentTerminalLocationController.java @@ -0,0 +1,39 @@ +package com.campus.water.controller; + +import com.campus.water.entity.vo.TerminalLocationVO; +import com.campus.water.service.WaterTerminalLocationService; +import com.campus.water.util.ResultVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 学生端终端位置控制器(截图原命名,修正Terminallocation拼写错误) + */ +@RestController +@RequestMapping("/api/student/terminal/location") +@RequiredArgsConstructor +@Tag(name = "学生端-终端位置接口", description = "饮水机地图位置查询") +public class StudentTerminalLocationController { + + private final WaterTerminalLocationService waterTerminalLocationService; // 截图原命名注入 + + @GetMapping("/all") + @Operation(summary = "获取所有终端位置") + public ResultVO> getAllLocations() { + List list = waterTerminalLocationService.getAllTerminalLocations(); + return ResultVO.success(list, "获取所有终端位置成功"); + } + + @GetMapping("/available") + @Operation(summary = "获取可用终端位置") + public ResultVO> getAvailableLocations() { + List list = waterTerminalLocationService.getAvailableTerminalLocations(); + return ResultVO.success(list, "获取可用终端位置成功"); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/WaterRealtimeController.java b/src/main/java/com/campus/water/controller/WaterRealtimeController.java new file mode 100644 index 0000000..a44ae19 --- /dev/null +++ b/src/main/java/com/campus/water/controller/WaterRealtimeController.java @@ -0,0 +1,33 @@ +package com.campus.water.controller; + +import com.campus.water.service.StudentWaterDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * 实时数据查询控制器 + * 负责终端关联的制水机/供水机实时数据查询接口 + */ +@RestController +@RequestMapping("/api/water/realtime") +public class WaterRealtimeController { + + @Autowired + private StudentWaterDataService waterDataService; + + /** + * 实时查询终端关联的制水机+供水机数据 + * 调用示例:GET http://localhost:8080/api/water/realtime/TERM001 + * @param terminalId 终端ID(路径参数) + * @return 制水机+供水机实时数据 + */ + @GetMapping("/{terminalId}") + public Map getRealtimeData(@PathVariable String terminalId) { + return waterDataService.queryRealtimeData(terminalId); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/WaterUsageController.java b/src/main/java/com/campus/water/controller/WaterUsageController.java index 31c2671..e26943e 100644 --- a/src/main/java/com/campus/water/controller/WaterUsageController.java +++ b/src/main/java/com/campus/water/controller/WaterUsageController.java @@ -2,8 +2,10 @@ package com.campus.water.controller; import com.campus.water.entity.*; import com.campus.water.mapper.*; +import com.campus.water.util.ResultVO; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; @@ -12,7 +14,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; -@Controller +@RestController +@RequestMapping("/api/water-usage") public class WaterUsageController { @Autowired @@ -31,23 +34,25 @@ public class WaterUsageController { private TerminalUsageStatsRepository terminalUsageStatsRepository; // 扫码用水 + @PostMapping("/scan") + @PreAuthorize("hasAnyRole('STUDENT', 'SUPER_ADMIN', 'AREA_ADMIN', 'VIEWER')") @Transactional - public Map scanToDrink(String terminalId, String studentId, Double waterConsumption) { + public ResultVO> scanToDrink( + @RequestParam String terminalId, + @RequestParam String studentId, + @RequestParam Double waterConsumption) { + Map result = new HashMap<>(); try { Optional mappingOpt = deviceTerminalMappingRepository.findByTerminalId(terminalId); if (mappingOpt.isEmpty()) { - result.put("success", false); - result.put("message", "终端设备不存在"); - return result; + return ResultVO.error("终端设备不存在"); } DeviceTerminalMapping mapping = mappingOpt.get(); if (mapping.getTerminalStatus() != DeviceTerminalMapping.TerminalStatus.active) { - result.put("success", false); - result.put("message", "终端设备未激活"); - return result; + return ResultVO.error("终端设备未激活"); } Optional realtimeDataOpt = @@ -58,7 +63,7 @@ public class WaterUsageController { drinkRecord.setTerminalId(terminalId); drinkRecord.setDeviceId(mapping.getDeviceId()); - // 错误1修复:Double转BigDecimal(适配DrinkRecord的BigDecimal类型字段) + // Double转BigDecimal(适配DrinkRecord的BigDecimal类型字段) drinkRecord.setWaterConsumption(waterConsumption != null ? BigDecimal.valueOf(waterConsumption) : BigDecimal.ZERO); drinkRecord.setDrinkTime(LocalDateTime.now()); drinkRecord.setLocation(mapping.getTerminalName()); @@ -73,16 +78,15 @@ public class WaterUsageController { // 传入BigDecimal类型的用水量 updateTerminalUsageStats(terminalId, BigDecimal.valueOf(waterConsumption)); - result.put("success", true); - result.put("message", "用水成功"); result.put("waterConsumption", waterConsumption); + result.put("terminalName", mapping.getTerminalName()); + result.put("deviceId", mapping.getDeviceId()); result.put("timestamp", LocalDateTime.now()); - return result; + + return ResultVO.success(result, "用水成功"); } catch (Exception e) { - result.put("success", false); - result.put("message", "用水失败: " + e.getMessage()); - return result; + return ResultVO.error("用水失败: " + e.getMessage()); } } @@ -97,10 +101,10 @@ public class WaterUsageController { stats = statsOpt.get(); stats.setUsageCount(stats.getUsageCount() + 1); - // 错误2&3修复:BigDecimal加法(替代+运算符) + // BigDecimal加法(替代+运算符) stats.setTotalWaterOutput(stats.getTotalWaterOutput().add(waterConsumption)); - // 错误4修复:BigDecimal除法(替代/运算符,指定精度和舍入模式) + // BigDecimal除法(指定精度和舍入模式) stats.setAvgWaterPerUse( stats.getTotalWaterOutput() .divide(BigDecimal.valueOf(stats.getUsageCount()), 2, BigDecimal.ROUND_HALF_UP) @@ -111,7 +115,7 @@ public class WaterUsageController { stats.setStatDate(now.toLocalDate()); stats.setUsageCount(1); - // 错误5&6修复:直接赋值BigDecimal(适配TerminalUsageStats的BigDecimal字段) + // 直接赋值BigDecimal(适配TerminalUsageStats的BigDecimal字段) stats.setTotalWaterOutput(waterConsumption); stats.setAvgWaterPerUse(waterConsumption); stats.setPeakHour(String.format("%02d:00", now.getHour())); @@ -122,7 +126,9 @@ public class WaterUsageController { } // 获取水质信息 - public Map getWaterQualityInfo(String deviceId) { + @GetMapping("/quality/{deviceId}") + @PreAuthorize("hasAnyRole('STUDENT', 'SUPER_ADMIN', 'AREA_ADMIN', 'VIEWER')") + public ResultVO> getWaterQualityInfo(@PathVariable String deviceId) { Map result = new HashMap<>(); try { @@ -135,7 +141,7 @@ public class WaterUsageController { if (realtimeDataOpt.isPresent()) { WaterMakerRealtimeData realtimeData = realtimeDataOpt.get(); result.put("deviceId", deviceId); - // 如需返回Double给前端:BigDecimal转Double + // BigDecimal转Double返回给前端 result.put("rawWaterTds", realtimeData.getTdsValue1() != null ? realtimeData.getTdsValue1().doubleValue() : null); result.put("pureWaterTds", realtimeData.getTdsValue2() != null ? realtimeData.getTdsValue2().doubleValue() : null); result.put("mineralWaterTds", realtimeData.getTdsValue3() != null ? realtimeData.getTdsValue3().doubleValue() : null); @@ -150,18 +156,17 @@ public class WaterUsageController { result.put("lastDetectionTime", qualityHistory.getDetectedTime()); } - result.put("success", true); - return result; + return ResultVO.success(result); } catch (Exception e) { - result.put("success", false); - result.put("message", "获取水质信息失败: " + e.getMessage()); - return result; + return ResultVO.error("获取水质信息失败: " + e.getMessage()); } } // 获取终端设备信息 - public Map getTerminalInfo(String terminalId) { + @GetMapping("/terminal/{terminalId}") + @PreAuthorize("hasAnyRole('STUDENT', 'SUPER_ADMIN', 'AREA_ADMIN', 'VIEWER')") + public ResultVO> getTerminalInfo(@PathVariable String terminalId) { Map result = new HashMap<>(); Optional mappingOpt = deviceTerminalMappingRepository.findByTerminalId(terminalId); @@ -171,15 +176,16 @@ public class WaterUsageController { result.put("terminalName", mapping.getTerminalName()); result.put("deviceId", mapping.getDeviceId()); result.put("status", mapping.getTerminalStatus()); - result.put("success", true); - Map qualityInfo = getWaterQualityInfo(mapping.getDeviceId()); - result.putAll(qualityInfo); + // 获取水质信息 + ResultVO> qualityResult = getWaterQualityInfo(mapping.getDeviceId()); + if (qualityResult.getCode() == 200 && qualityResult.getData() != null) { + result.putAll(qualityResult.getData()); + } + + return ResultVO.success(result); } else { - result.put("success", false); - result.put("message", "终端设备不存在"); + return ResultVO.error("终端设备不存在"); } - - return result; } } \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/WorkOrderController.java b/src/main/java/com/campus/water/controller/WorkOrderController.java index 327cfe1..9434b5b 100644 --- a/src/main/java/com/campus/water/controller/WorkOrderController.java +++ b/src/main/java/com/campus/water/controller/WorkOrderController.java @@ -3,12 +3,15 @@ package com.campus.water.controller; import com.campus.water.entity.WorkOrder; import com.campus.water.service.WorkOrderService; import com.campus.water.util.ResultVO; +import lombok.Data; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; import java.util.List; @RestController @@ -70,15 +73,20 @@ public class WorkOrderController { } } - // 新增:审核工单接口(管理员专用) + // 1. 创建请求体类 + @Data + public static class ReviewOrderRequest { + private String orderId; + private boolean approved; + } + // 审核工单 + // 2. 修改接口接收方式 @PostMapping("/review") @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") - public ResultVO reviewOrder( - @RequestParam String orderId, - @RequestParam boolean approved) { + public ResultVO reviewOrder(@RequestBody ReviewOrderRequest request) { try { - boolean result = workOrderService.reviewOrder(orderId, approved); - return result ? ResultVO.success(true, approved ? "审核通过" : "审核不通过") + boolean result = workOrderService.reviewOrder(request.getOrderId(), request.isApproved()); + return result ? ResultVO.success(true, request.isApproved() ? "审核通过" : "审核不通过") : ResultVO.error(400, "审核失败,工单状态异常"); } catch (Exception e) { return ResultVO.error(500, "审核失败:" + e.getMessage()); @@ -145,6 +153,23 @@ public class WorkOrderController { } } + // 新增:按时间范围(+可选区域/状态)查询工单(复用已有Repository方法) + @GetMapping("/by-time-range") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") + public ResultVO> getOrdersByTimeRange( + @RequestParam(required = false) String areaId, // 可选:区域ID + @RequestParam(required = false) WorkOrder.OrderStatus status, // 可选:工单状态 + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime, // 必选:开始时间 + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) { // 必选:结束时间 + try { + // 直接调用Service,复用已有Repository的findByCreatedTimeBetween方法 + List orders = workOrderService.getOrdersByConditions(areaId, status, startTime, endTime); + return ResultVO.success(orders); + } catch (Exception e) { + return ResultVO.error(500, "按时间范围查询工单失败:" + e.getMessage()); + } + } + // 获取维修工自己的工单 - 维修人员和管理员可访问 @GetMapping("/my") @PreAuthorize("hasAnyRole('REPAIRMAN', 'SUPER_ADMIN', 'AREA_ADMIN')") @@ -157,18 +182,51 @@ public class WorkOrderController { } } - // 管理员手动派单接口 + // ========== 关键修改:管理员手动派单接口 ========== + // 1. 创建静态内部类接收JSON请求体参数(解决Type definition error) + @Data + public static class AssignOrderRequest { + private String orderId; + private String repairmanId; + } + + // 2. 派单接口改为接收@RequestBody @PostMapping("/assign") @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") - public ResultVO assignOrderByAdmin( - @RequestParam String orderId, - @RequestParam String repairmanId) { + public ResultVO assignOrderByAdmin(@RequestBody AssignOrderRequest request) { try { - boolean result = workOrderService.assignOrderByAdmin(orderId, repairmanId); + // 参数非空校验 + if (request.getOrderId() == null || request.getOrderId().trim().isEmpty()) { + return ResultVO.error(400, "工单ID不能为空"); + } + if (request.getRepairmanId() == null || request.getRepairmanId().trim().isEmpty()) { + return ResultVO.error(400, "维修人员ID不能为空"); + } + + boolean result = workOrderService.assignOrderByAdmin(request.getOrderId(), request.getRepairmanId()); return result ? ResultVO.success(true, "派单成功") : ResultVO.error(400, "派单失败,工单或维修人员状态异常"); + } catch (IllegalArgumentException e) { + // 捕获参数非法异常,返回400 + return ResultVO.error(400, "派单失败:" + e.getMessage()); } catch (Exception e) { + // 捕获其他异常,返回500并打印日志(建议添加日志) + e.printStackTrace(); // 生产环境替换为logger.error return ResultVO.error(500, "派单失败:" + e.getMessage()); } } + + + // 获取单个工单详情 - 管理员和维修人员均可访问 +@GetMapping("/{orderId}") +@PreAuthorize("hasAnyRole('REPAIRMAN', 'SUPER_ADMIN', 'AREA_ADMIN')") +public ResultVO getOrderDetail(@PathVariable String orderId) { + try { + WorkOrder order = workOrderService.getOrderDetail(orderId); + return ResultVO.success(order); + } catch (Exception e) { + return ResultVO.error(500, "获取工单详情失败:" + e.getMessage()); + } +} + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/app/先读我.md b/src/main/java/com/campus/water/controller/app/先读我.md deleted file mode 100644 index 3b52c1f..0000000 --- a/src/main/java/com/campus/water/controller/app/先读我.md +++ /dev/null @@ -1 +0,0 @@ -# 本md仅用于初始化目录,未创建所有子一级目录,在当前目录创建文件后请自行删除 \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/common/先读我.md b/src/main/java/com/campus/water/controller/common/先读我.md deleted file mode 100644 index 3b52c1f..0000000 --- a/src/main/java/com/campus/water/controller/common/先读我.md +++ /dev/null @@ -1 +0,0 @@ -# 本md仅用于初始化目录,未创建所有子一级目录,在当前目录创建文件后请自行删除 \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/web/AdminController.java b/src/main/java/com/campus/water/controller/web/AdminController.java index 3407f85..3cf6275 100644 --- a/src/main/java/com/campus/water/controller/web/AdminController.java +++ b/src/main/java/com/campus/water/controller/web/AdminController.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -39,6 +40,39 @@ public class AdminController { } } + + /** + * 新增:查询可分配校区的区域管理员(未负责任何片区) + */ + @GetMapping("/available-area-admins") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") + @Operation(summary = "获取可分配校区的区域管理员", description = "返回未负责任何片区的区域管理员,用于片区绑定负责人") + public ResponseEntity>> getAvailableAreaAdmins() { + try { + List availableAdmins = adminService.getAvailableAreaAdmins(); + return ResponseEntity.ok(ResultVO.success(availableAdmins)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "查询失败:" + e.getMessage())); + } + } + + /** + * 新增:获取指定区域的管理员列表 + */ + @GetMapping("/by-area/{areaId}") + @PreAuthorize("hasRole('SUPER_ADMIN')") // 只有超级管理员可以查看 + @Operation(summary = "按区域查询管理员", description = "查询指定区域下的所有管理员") + public ResponseEntity>> getAdminsByArea( + @PathVariable String areaId + ) { + try { + List admins = adminService.getAdminsByAreaId(areaId); + return ResponseEntity.ok(ResultVO.success(admins)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "查询失败:" + e.getMessage())); + } + } + /** * 获取所有管理员角色枚举 */ @@ -56,12 +90,19 @@ public class AdminController { /** * 新增/编辑管理员 + * 重写保存接口的注释,明确区域关联说明 */ @PostMapping("/save") - @PreAuthorize("hasRole('SUPER_ADMIN')") // 仅超级管理员可新增/编辑 - @Operation(summary = "保存管理员", description = "新增/编辑管理员,支持指定角色") + @PreAuthorize("hasRole('SUPER_ADMIN')") + @Operation(summary = "保存管理员", description = "新增/编辑管理员,区域管理员必须指定areaId") public ResponseEntity> saveAdmin(@RequestBody Admin admin) { + // 实现保持不变 try { + + if (admin.getAdminName() == null || admin.getAdminName().trim().isEmpty()) { + return ResponseEntity.ok(ResultVO.error(400, "管理员姓名不能为空")); + } + Admin savedAdmin = adminService.saveAdmin(admin); return ResponseEntity.ok(ResultVO.success(savedAdmin)); } catch (Exception e) { @@ -100,4 +141,92 @@ public class AdminController { return ResponseEntity.ok(ResultVO.error(401, "用户名或密码错误")); } } + + + + /** + * 管理员个人信息修改 + * 允许当前登录用户修改自己的基本信息(不含角色/区域等敏感字段) + */ + @PostMapping("/profile/update") + @PreAuthorize("isAuthenticated()") // 只要登录即可访问 + @Operation(summary = "修改个人信息", description = "当前登录管理员修改自己的基本信息(不含角色)") + public ResponseEntity> updateProfile( + @RequestBody Admin profile, + Authentication authentication) { + try { + // 1. 获取当前登录用户名 + String currentUsername = authentication.getName(); + + // 2. 验证身份一致性(当前用户只能修改自己的信息) + Admin currentAdmin = adminService.getAdminByName(currentUsername) + .orElseThrow(() -> new RuntimeException("当前用户信息不存在")); + + if (!currentAdmin.getAdminId().equals(profile.getAdminId())) { + throw new RuntimeException("无权修改其他管理员信息"); + } + + // 3. 过滤敏感字段(不允许修改角色和区域ID) + profile.setRole(currentAdmin.getRole()); + profile.setAreaId(currentAdmin.getAreaId()); + + // 4. 调用服务层更新 + Admin updatedAdmin = adminService.updateProfile(profile); + return ResponseEntity.ok(ResultVO.success(updatedAdmin, "个人信息更新成功")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "更新失败:" + e.getMessage())); + } + } + + /** + * 获取当前登录管理员信息 + */ + @GetMapping("/current") + @PreAuthorize("isAuthenticated()") // 只要登录即可访问 + @Operation(summary = "获取当前登录管理员信息", description = "返回当前登录管理员的完整信息(含角色、区域等)") + public ResponseEntity> getCurrentAdmin(Authentication authentication) { + try { + // 1. 从Authentication中获取当前登录用户名 + String currentUsername = authentication.getName(); + + // 2. 调用服务层查询完整管理员信息 + Admin currentAdmin = adminService.getAdminByName(currentUsername) + .orElseThrow(() -> new RuntimeException("当前登录用户信息不存在")); + + return ResponseEntity.ok(ResultVO.success(currentAdmin)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "获取当前用户信息失败:" + e.getMessage())); + } + } + + /** + * 管理员密码修改 + * 允许当前登录管理员修改自己的密码(需验证原密码) + */ + @PostMapping("/password/update") + @PreAuthorize("isAuthenticated()") // 登录即可访问 + @Operation(summary = "修改密码", description = "当前登录管理员修改自己的密码(需验证原密码)") + public ResponseEntity> updatePassword( + @RequestParam String oldPassword, + @RequestParam String newPassword, + Authentication authentication) { + try { + // 1. 获取当前登录用户名 + String currentUsername = authentication.getName(); + + // 2. 验证原密码并更新新密码 + boolean success = adminService.updatePassword(currentUsername, oldPassword, newPassword); + if (success) { + return ResponseEntity.ok(ResultVO.success(null, "密码修改成功")); + } else { + return ResponseEntity.ok(ResultVO.error(400, "原密码验证失败")); + } + } catch (IllegalArgumentException e) { + // 处理新密码格式错误等参数问题 + return ResponseEntity.ok(ResultVO.error(400, e.getMessage())); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "密码修改失败:" + e.getMessage())); + } + } + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/web/AreaController.java b/src/main/java/com/campus/water/controller/web/AreaController.java new file mode 100644 index 0000000..ce5a3f3 --- /dev/null +++ b/src/main/java/com/campus/water/controller/web/AreaController.java @@ -0,0 +1,167 @@ +package com.campus.water.controller.web; + +import com.campus.water.entity.Area; +import com.campus.water.service.AreaService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 区域管理控制器 + * 不使用 ResultVO,直接通过 ResponseEntity 返回响应 + * 适配 Area 实体(areaId 主键、市区-校园层级) + */ +@RestController +@RequestMapping("/api/web/area") // 统一管理端接口前缀:/api/web +@CrossOrigin // 允许跨域(前端调用时需要) +public class AreaController { + + private final AreaService areaService; + + // 构造器注入 + public AreaController(AreaService areaService) { + this.areaService = areaService; + } + + /** + * 构建通用响应体 + * @param code 响应码(200成功,400参数/业务异常,500系统异常) + * @param msg 响应消息 + * @param data 响应数据 + * @return 封装后的Map响应体 + */ + private Map buildResponse(int code, String msg, Object data) { + Map response = new HashMap<>(); + response.put("code", code); + response.put("msg", msg); + response.put("data", data); + return response; + } + + /** + * 新增区域(市区/校园) + * 权限:超级管理员/区域管理员可操作 + */ + @PostMapping("/add") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") // 补充权限注解 + public ResponseEntity> addArea(@RequestBody Area area) { + try { + Area savedArea = areaService.addArea(area); + return ResponseEntity.ok(buildResponse(200, "新增成功", savedArea)); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(buildResponse(400, e.getMessage(), null)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(buildResponse(500, "新增区域失败:" + e.getMessage(), null)); + } + } + + /** + * 修改区域 + * 权限:超级管理员/区域管理员可操作 + */ + @PutMapping("/update/{areaId}") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") // 补充权限注解 + public ResponseEntity> updateArea(@PathVariable String areaId, @RequestBody Area area) { + try { + Area updatedArea = areaService.updateArea(areaId, area); + return ResponseEntity.ok(buildResponse(200, "修改成功", updatedArea)); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(buildResponse(400, e.getMessage(), null)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(buildResponse(500, "修改区域失败:" + e.getMessage(), null)); + } + } + + /** + * 删除区域 + * 权限:仅超级管理员可操作 + */ + @DeleteMapping("/delete/{areaId}") + @PreAuthorize("hasRole('SUPER_ADMIN')") // 补充权限注解(删除权限更严格) + public ResponseEntity> deleteArea(@PathVariable String areaId) { + try { + areaService.deleteArea(areaId); + return ResponseEntity.ok(buildResponse(200, "删除成功", null)); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(buildResponse(400, e.getMessage(), null)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(buildResponse(500, "删除区域失败:" + e.getMessage(), null)); + } + } + + /** + * 查询所有市区(根节点) + * 权限:超级管理员/区域管理员/维修人员均可查看(根据你项目实际需求调整) + */ + @GetMapping("/cities") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN', 'REPAIRMAN')") // 补充权限注解 + public ResponseEntity> getAllCities() { + try { + List cities = areaService.getAllCities(); + return ResponseEntity.ok(buildResponse(200, "查询成功", cities)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(buildResponse(500, "查询市区列表失败:" + e.getMessage(), null)); + } + } + + /** + * 查询所有未设置负责人的片区 + * 权限:超级管理员/区域管理员可查看(与区域管理相关权限一致) + */ + @GetMapping("/without-manager") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") + public ResponseEntity> getAreasWithoutManager() { + try { + List areas = areaService.getAreasWithoutManager(); + return ResponseEntity.ok(buildResponse(200, "查询无负责人片区成功", areas)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(buildResponse(500, "查询无负责人片区失败:" + e.getMessage(), null)); + } + } + + /** + * 根据市区ID查询下属校园 + * 权限:超级管理员/区域管理员/维修人员均可查看 + */ + @GetMapping("/campuses/{cityId}") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN', 'REPAIRMAN')") // 补充权限注解 + public ResponseEntity> getCampusesByCityId(@PathVariable String cityId) { + try { + List campuses = areaService.getCampusesByCityId(cityId); + return ResponseEntity.ok(buildResponse(200, "查询成功", campuses)); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(buildResponse(400, e.getMessage(), null)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(buildResponse(500, "查询校园列表失败:" + e.getMessage(), null)); + } + } + + /** + * 根据区域ID查询单个区域信息 + * 权限:超级管理员/区域管理员/维修人员均可查看 + */ + @GetMapping("/{areaId}") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN', 'REPAIRMAN')") // 补充权限注解 + public ResponseEntity> getAreaById(@PathVariable String areaId) { + try { + Area area = areaService.getAreaById(areaId); + return ResponseEntity.ok(buildResponse(200, "查询成功", area)); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(buildResponse(400, e.getMessage(), null)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(buildResponse(500, "查询区域详情失败:" + e.getMessage(), null)); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/web/DeviceController.java b/src/main/java/com/campus/water/controller/web/DeviceController.java index 8163470..8bf9671 100644 --- a/src/main/java/com/campus/water/controller/web/DeviceController.java +++ b/src/main/java/com/campus/water/controller/web/DeviceController.java @@ -1,22 +1,30 @@ package com.campus.water.controller.web; -import com.campus.water.entity.Device; -import com.campus.water.entity.RepairerAuth; import com.campus.water.mapper.RepairerAuthRepository; +import com.campus.water.mapper.WaterMakerRealtimeDataRepository; +import com.campus.water.mapper.WaterSupplyRealtimeDataRepository; import com.campus.water.service.DeviceService; -import com.campus.water.entity.Repairman; import com.campus.water.mapper.RepairmanRepository; +import com.campus.water.service.DeviceStatusService; import com.campus.water.util.ResultVO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.security.core.Authentication; import org.springframework.security.access.prepost.PreAuthorize; - -import java.util.List; // 新增List的导入语句 +import com.campus.water.entity.Device; +import com.campus.water.entity.RepairerAuth; +import com.campus.water.entity.Repairman; +import com.campus.water.entity.WaterMakerRealtimeData; +import com.campus.water.entity.WaterSupplyRealtimeData; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; @RestController @RequestMapping("/api/web/device") @@ -25,8 +33,14 @@ import java.util.List; // 新增List的导入语句 public class DeviceController { private final DeviceService deviceService; + private final DeviceStatusService deviceStatusService; private final RepairmanRepository repairmanRepository; private final RepairerAuthRepository repairerAuthRepository; + @Autowired + private WaterMakerRealtimeDataRepository waterMakerRealtimeDataRepository; + + @Autowired + private WaterSupplyRealtimeDataRepository waterSupplyRealtimeDataRepository; /** * 新增设备 @@ -87,7 +101,6 @@ public class DeviceController { /** * 维修人员查询本辖区设备(按类型筛选) */ - // 在DeviceController.java中修改getAreaDevicesByTypeForRepairman方法 @GetMapping("/repairman/area-devices-by-type") @PreAuthorize("hasRole('REPAIRMAN')") @Operation(summary = "维修人员查询辖区设备(按类型)", description = "维修人员查看本辖区内指定类型的设备列表") @@ -123,18 +136,121 @@ public class DeviceController { } } - /** - * 根据设备ID查询设备详情 - */ @GetMapping("/{deviceId}") - @Operation(summary = "查询设备详情", description = "根据设备ID获取设备的详细信息") - public ResponseEntity> getDeviceDetail(@PathVariable String deviceId) { + @Operation(summary = "查询设备详情", description = "根据设备ID获取设备的详细信息及实时数据") + public ResponseEntity>> getDeviceDetail(@PathVariable String deviceId) { try { + // 1. 获取设备基本信息 Device device = deviceService.getDeviceById(deviceId); - return ResponseEntity.ok(ResultVO.success(device, "设备查询成功")); + + // 2. 构建返回结果Map + Map resultMap = new HashMap<>(); + resultMap.put("deviceInfo", device); + + // 3. 根据设备类型查询对应实时数据 + if (Device.DeviceType.water_maker.equals(device.getDeviceType())) { + // 制水机实时数据 + Optional realtimeData = waterMakerRealtimeDataRepository.findLatestByDeviceId(deviceId); + realtimeData.ifPresent(data -> resultMap.put("realtimeData", data)); + } else if (Device.DeviceType.water_supply.equals(device.getDeviceType())) { + // 供水机实时数据 + Optional realtimeData = waterSupplyRealtimeDataRepository.findLatestByDeviceId(deviceId); + realtimeData.ifPresent(data -> resultMap.put("realtimeData", data)); + } + + return ResponseEntity.ok(ResultVO.success(resultMap, "设备查询成功")); } catch (Exception e) { return ResponseEntity.ok(ResultVO.error(500, "设备查询失败: " + e.getMessage())); } } + public DeviceStatusService getDeviceStatusService() { + return deviceStatusService; + } + + /** + * 按状态查询设备列表(支持区域筛选) + * 管理员/运维人员通用接口 + */ + /* @GetMapping("/by-status") + @Operation(summary = "按状态查询设备", description = "根据设备状态筛选设备列表,可选区域筛选") + public ResponseEntity>> getDevicesByStatus( + @RequestParam String status, + @RequestParam(required = false) String areaId) { + try { + List devices = deviceStatusService.getDevicesByStatusWithArea(status, areaId); + return ResponseEntity.ok(ResultVO.success(devices, "按状态查询设备成功")); + } catch (IllegalArgumentException e) { + return ResponseEntity.ok(ResultVO.error(400, e.getMessage())); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "按状态查询设备失败: " + e.getMessage())); + } + }*/ + + /** + * 按类型查询设备列表(支持区域筛选) + * 管理员/运维人员通用接口 + */ + /* @GetMapping("/by-type") + @Operation(summary = "按类型查询设备", description = "根据设备类型筛选设备列表,可选区域筛选") + public ResponseEntity>> getDevicesByType( + @RequestParam String deviceType, + @RequestParam(required = false) String areaId) { + try { + List devices = deviceStatusService.getDevicesByTypeWithArea(deviceType, areaId); + return ResponseEntity.ok(ResultVO.success(devices, "按类型查询设备成功")); + } catch (IllegalArgumentException e) { + return ResponseEntity.ok(ResultVO.error(400, e.getMessage())); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "按类型查询设备失败: " + e.getMessage())); + } + }*/ + + // ========== 新增1:获取所有片区列表(假设从Device表中提取唯一片区ID,若有Area实体可直接查询) ========== + @GetMapping("/areas") + @Operation(summary = "获取所有片区列表", description = "返回系统中所有已配置的片区ID和相关信息") + public ResponseEntity>> getAllAreas() { + try { + // 从设备表中提取唯一的片区ID(若有独立Area表,可替换为AreaRepository查询) + List allDevices = deviceService.queryDevices(null, null, null); + List areaList = allDevices.stream() + .map(Device::getAreaId) + .filter(areaId -> areaId != null && !areaId.trim().isEmpty()) + .distinct() + .toList(); + return ResponseEntity.ok(ResultVO.success(areaList, "片区列表查询成功")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "片区列表查询失败: " + e.getMessage())); + } + } + + // ========== 新增2:根据片区ID查询该片区的供水机列表 ========== + @GetMapping("/area/{areaId}/water-supplies") + @Operation(summary = "查询片区内供水机", description = "根据片区ID获取该片区下所有可用的供水机") + public ResponseEntity>> getWaterSuppliesByArea(@PathVariable String areaId) { + try { + List waterSupplies = deviceService.getWaterSuppliesByArea(areaId); + return ResponseEntity.ok(ResultVO.success(waterSupplies, "片区供水机查询成功")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "片区供水机查询失败: " + e.getMessage())); + } + } + + // ========== 新增:管理员编辑设备基本信息接口 ========== + @PutMapping("/edit") + @PreAuthorize("hasAnyRole( 'SUPER_ADMIN','AREA_ADMIN')") // 限制仅管理员/超级管理员可访问 + @Operation(summary = "编辑设备基本信息", description = "管理员更新设备名称、类型、安装位置等基本信息(不含设备状态、创建时间)") + public ResponseEntity> editDevice(@Valid @RequestBody Device device) { + try { + // 校验设备ID不能为空(编辑必须指定设备ID) + if (device.getDeviceId() == null || device.getDeviceId().trim().isEmpty()) { + return ResponseEntity.ok(ResultVO.error(400, "设备ID不能为空")); + } + Device updatedDevice = deviceService.updateDeviceInfo(device); + return ResponseEntity.ok(ResultVO.success(updatedDevice, "设备信息编辑成功")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "设备信息编辑失败: " + e.getMessage())); + } + } + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/web/DeviceStatusController.java b/src/main/java/com/campus/water/controller/web/DeviceStatusController.java index f091cd7..edd736c 100644 --- a/src/main/java/com/campus/water/controller/web/DeviceStatusController.java +++ b/src/main/java/com/campus/water/controller/web/DeviceStatusController.java @@ -6,7 +6,7 @@ * 1. 状态更新:单设备状态变更 * 2. 状态标记:在线/离线/故障快捷操作 * 3. 批量操作:批量更新设备状态 - * 4. 状态查询:按状态筛选设备列表 + * 4. 状态查询:按状态/类型筛选设备列表(拆分独立接口) * 5. 离线检测:查询超时离线设备 * 6. 自动检测:触发离线设备检测任务 * 安全:需要权限验证,记录操作日志 @@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -99,24 +100,47 @@ public class DeviceStatusController { } } + // ========== 替换原有复合查询接口,拆分为两个独立接口 ========== + /** + * 按状态查询设备(仅状态筛选,支持区域) + */ @GetMapping("/by-status") - @Operation(summary = "按状态查询设备", description = "根据状态和设备类型查询设备列表") + @PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN')") + @Operation(summary = "按状态查询设备", description = "根据设备状态筛选设备列表,可选区域筛选") public ResponseEntity>> getDevicesByStatus( @RequestParam String status, - @RequestParam(required = false) String areaId, - @RequestParam(required = false) String deviceType) { // 保留设备类型参数,去除默认值 - + @RequestParam(required = false) String areaId) { try { - // 调用服务层方法时传递所有参数(包括可能为null的deviceType) - List devices = deviceStatusService.getDevicesByStatus(status, areaId, deviceType); - return ResponseEntity.ok(ResultVO.success(devices)); + List devices = deviceStatusService.getDevicesByStatusWithArea(status, areaId); + return ResponseEntity.ok(ResultVO.success(devices, "按状态查询设备成功")); + } catch (IllegalArgumentException e) { + return ResponseEntity.ok(ResultVO.error(400, e.getMessage())); } catch (Exception e) { - return ResponseEntity.ok(ResultVO.error(500, "查询设备失败: " + e.getMessage())); + return ResponseEntity.ok(ResultVO.error(500, "按状态查询设备失败: " + e.getMessage())); } } + /** + * 按类型查询设备(仅类型筛选,支持区域) + */ + @GetMapping("/by-type") + @PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN')") + @Operation(summary = "按类型查询设备", description = "根据设备类型筛选设备列表,可选区域筛选") + public ResponseEntity>> getDevicesByType( + @RequestParam String deviceType, + @RequestParam(required = false) String areaId) { + try { + List devices = deviceStatusService.getDevicesByTypeWithArea(deviceType, areaId); + return ResponseEntity.ok(ResultVO.success(devices, "按类型查询设备成功")); + } catch (IllegalArgumentException e) { + return ResponseEntity.ok(ResultVO.error(400, e.getMessage())); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "按类型查询设备失败: " + e.getMessage())); + } + } @GetMapping("/status-count") + @PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN')") @Operation(summary = "设备状态数量统计", description = "统计各状态设备数量") public ResponseEntity>> getDeviceStatusCount( @RequestParam(required = false) String areaId, diff --git a/src/main/java/com/campus/water/controller/web/TerminalController.java b/src/main/java/com/campus/water/controller/web/TerminalController.java new file mode 100644 index 0000000..f0a0ec4 --- /dev/null +++ b/src/main/java/com/campus/water/controller/web/TerminalController.java @@ -0,0 +1,114 @@ +// java/com/campus/water/controller/web/TerminalController.java +package com.campus.water.controller.web; + +import com.campus.water.service.DeviceService; +import com.campus.water.service.TerminalService; +import com.campus.water.entity.vo.TerminalManageVO; +import com.campus.water.util.ResultVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/web/terminal") +@RequiredArgsConstructor +@Tag(name = "终端管理接口", description = "管理员基于设备终端映射表/终端位置表的增删改查操作") +public class TerminalController { + + private final TerminalService terminalService; + + + @PostMapping("/add") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") + @Operation(summary = "新增终端", description = "同时保存终端位置、基础映射信息和片区信息") + public ResponseEntity> addTerminal(@Valid @RequestBody TerminalManageVO terminalVO) { + try { + TerminalManageVO newTerminal = terminalService.addTerminal(terminalVO); + return ResponseEntity.ok(ResultVO.success(newTerminal, "终端新增成功")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "终端新增失败: " + e.getMessage())); + } + } + + @PutMapping("/update") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") + @Operation(summary = "更新终端", description = "支持更新终端名称、状态、经纬度、片区等信息") + public ResponseEntity> updateTerminal(@Valid @RequestBody TerminalManageVO terminalVO) { + try { + TerminalManageVO updated = terminalService.updateTerminal(terminalVO); + return ResponseEntity.ok(ResultVO.success(updated, "终端更新成功")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "终端更新失败: " + e.getMessage())); + } + } + + /** + * 删除终端 + * 先校验设备绑定状态,再级联删除终端位置表和映射表的相关数据 + */ + @DeleteMapping("/delete/{terminalId}") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") + @Operation(summary = "删除终端", description = "先校验设备绑定状态,再级联删除相关数据") + public ResponseEntity> deleteTerminal(@PathVariable String terminalId) { + try { + // 调用服务层删除终端 + terminalService.deleteTerminal(terminalId); + // 用ResultVO封装成功结果,返回提示信息 + return ResponseEntity.ok(ResultVO.success("终端删除成功")); + } catch (Exception e) { + // 用ResultVO封装错误结果,携带异常信息 + return ResponseEntity.ok(ResultVO.error(500, "终端删除失败: " + e.getMessage())); + } + } + + /** + * 查询终端详情 + * 根据终端ID获取整合后的完整信息(包含位置、映射、片区areaId信息) + */ + @GetMapping("/{terminalId}") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") + @Operation(summary = "查询终端详情", description = "根据终端ID获取整合后的完整信息") + public ResponseEntity> getTerminal(@PathVariable String terminalId) { + try { + // 调用服务层查询终端详情,返回包含areaId的VO + TerminalManageVO terminal = terminalService.getTerminalById(terminalId); + // 用ResultVO封装成功结果,自定义提示信息 + return ResponseEntity.ok(ResultVO.success(terminal, "终端查询成功")); + } catch (Exception e) { + // 用ResultVO封装404错误结果,携带异常信息 + return ResponseEntity.ok(ResultVO.notFound("终端查询失败: " + e.getMessage())); + } + } + + + /** + * 查询终端列表 + * 支持按终端名称模糊筛选,返回整合后的完整列表(每条数据均包含areaId) + */ + @GetMapping("/list") + @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") + @Operation(summary = "查询终端列表", description = "支持按终端名称模糊筛选") + public ResponseEntity>> getTerminalList( + @RequestParam(required = false) String terminalName) { + try { + // 调用服务层查询终端列表,返回包含areaId的VO列表 + List terminalList = terminalService.getTerminalList(terminalName); + // 用ResultVO封装成功结果,自定义提示信息 + return ResponseEntity.ok(ResultVO.success(terminalList, "终端列表查询成功")); + } catch (Exception e) { + // 用ResultVO封装错误结果,携带异常信息 + return ResponseEntity.ok(ResultVO.error(500, "终端列表查询失败: " + e.getMessage())); + } + } + + +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/web/先读我.md b/src/main/java/com/campus/water/controller/web/先读我.md deleted file mode 100644 index 3b52c1f..0000000 --- a/src/main/java/com/campus/water/controller/web/先读我.md +++ /dev/null @@ -1 +0,0 @@ -# 本md仅用于初始化目录,未创建所有子一级目录,在当前目录创建文件后请自行删除 \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/Admin.java b/src/main/java/com/campus/water/entity/Admin.java index d75431c..842922f 100644 --- a/src/main/java/com/campus/water/entity/Admin.java +++ b/src/main/java/com/campus/water/entity/Admin.java @@ -25,6 +25,10 @@ public class Admin { @Column(name = "phone", length = 20) private String phone; + // 新增:管理员负责的区域ID(区域管理员专用) + @Column(name = "area_id", length = 36, nullable = true) + private String areaId; + // 恢复三个角色枚举 @Enumerated(EnumType.STRING) @Column(name = "role", length = 50, nullable = false) diff --git a/src/main/java/com/campus/water/entity/Alert.java b/src/main/java/com/campus/water/entity/Alert.java index 43eb7e0..e5afdae 100644 --- a/src/main/java/com/campus/water/entity/Alert.java +++ b/src/main/java/com/campus/water/entity/Alert.java @@ -31,7 +31,7 @@ public class Alert { @Column(name = "alert_message", columnDefinition = "TEXT") private String alertMessage; - @Column(name = "area_id", length = 20) + @Column(name = "area_id", length = 36) private String areaId; @Enumerated(EnumType.STRING) diff --git a/src/main/java/com/campus/water/entity/Area.java b/src/main/java/com/campus/water/entity/Area.java index 45a1da0..a80c89b 100644 --- a/src/main/java/com/campus/water/entity/Area.java +++ b/src/main/java/com/campus/water/entity/Area.java @@ -8,13 +8,16 @@ package com.campus.water.entity; import lombok.Data; import jakarta.persistence.*; import java.time.LocalDateTime; +import org.hibernate.annotations.GenericGenerator; @Data @Entity @Table(name = "area") public class Area { @Id - @Column(name = "area_id", length = 20) + @GeneratedValue(generator = "uuid") // 新增:自动生成UUID + @GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator") + @Column(name = "area_id", length = 36) private String areaId; @Column(name = "area_name", length = 100) @@ -24,7 +27,7 @@ public class Area { @Column(name = "area_type", length = 50) private AreaType areaType; - @Column(name = "parent_area_id", length = 20) + @Column(name = "parent_area_id", length = 36) private String parentAreaId; @Column(length = 200) @@ -42,7 +45,22 @@ public class Area { @Column(name = "updated_time") private LocalDateTime updatedTime = LocalDateTime.now(); + + + // 枚举值与数据库完全匹配:zone=市区,campus=校园,building=楼栋(暂时保留) public enum AreaType { - campus, building, zone + zone("市区"), // 对应数据库的zone,含义是市区 + campus("校园"); // 对应数据库的campus,含义是校园 + + + private final String desc; + AreaType(String desc) { + this.desc = desc; + } + public String getDesc() { + return desc; + } } + + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/BusinessException.java b/src/main/java/com/campus/water/entity/BusinessException.java new file mode 100644 index 0000000..07e56fb --- /dev/null +++ b/src/main/java/com/campus/water/entity/BusinessException.java @@ -0,0 +1,55 @@ +package com.campus.water.entity; // 请确保这个包名与你的项目结构一致 + +import org.springframework.http.HttpStatus; + +/** + * 自定义业务异常 + *

+ * 用于封装业务层抛出的、需要明确反馈给用户的异常信息。 + * 例如:用户不存在、密码错误、工单状态不正确等。 + *

+ */ +public class BusinessException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * 错误码,对应HTTP状态码 + */ + private int code; + + /** + * 错误消息 + */ + private String message; + + /** + * 构造函数 + * @param code 错误码 + * @param message 错误消息 + */ + public BusinessException(int code, String message) { + super(message); + this.code = code; + this.message = message; + } + + /** + * 构造函数,默认使用 400 Bad Request 状态码 + * @param message 错误消息 + */ + public BusinessException(String message) { + this(HttpStatus.BAD_REQUEST.value(), message); + } + + // --- Getters --- + + public int getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/Device.java b/src/main/java/com/campus/water/entity/Device.java index 4c38c3e..112d491 100644 --- a/src/main/java/com/campus/water/entity/Device.java +++ b/src/main/java/com/campus/water/entity/Device.java @@ -24,7 +24,7 @@ public class Device { @Column(name = "device_type", length = 50) private DeviceType deviceType; - @Column(name = "area_id", length = 20) + @Column(name = "area_id", length = 36) private String areaId; @Column(name = "install_location", length = 200) diff --git a/src/main/java/com/campus/water/entity/DeviceTerminalMapping.java b/src/main/java/com/campus/water/entity/DeviceTerminalMapping.java index 22f4a9a..8d2b696 100644 --- a/src/main/java/com/campus/water/entity/DeviceTerminalMapping.java +++ b/src/main/java/com/campus/water/entity/DeviceTerminalMapping.java @@ -22,6 +22,9 @@ public class DeviceTerminalMapping { @Column(name = "device_id", length = 20) private String deviceId; + @Column(name = "area_id", length = 36) + private String areaId; + @Column(name = "terminal_id", length = 20) private String terminalId; diff --git a/src/main/java/com/campus/water/entity/Notification.java b/src/main/java/com/campus/water/entity/Notification.java new file mode 100644 index 0000000..7c592ab --- /dev/null +++ b/src/main/java/com/campus/water/entity/Notification.java @@ -0,0 +1,51 @@ +package com.campus.water.entity; + +import lombok.Data; +import jakarta.persistence.*; +import java.time.LocalDateTime; + +/** + * 维修人员通知实体 + * 存储派单、系统通知等消息 + */ +@Data +@Entity +@Table(name = "notification") +public class Notification { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** 维修人员ID */ + @Column(name = "repairman_id", nullable = false, length = 50) + private String repairmanId; + + /** 关联工单ID */ + @Column(name = "order_id", length = 50) + private String orderId; + + /** 通知内容 */ + @Column(name = "content", nullable = false, length = 500) + private String content; + + /** 是否已读(默认未读) */ + @Column(name = "is_read") + private boolean isRead = false; + + /** 创建时间 */ + @Column(name = "created_time", nullable = false) + private LocalDateTime createdTime = LocalDateTime.now(); + + /** 通知类型 */ + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false, length = 20) + private NotificationType type; + + /** 通知类型枚举 */ + public enum NotificationType { + ORDER_ASSIGNED, // 派单通知 + ORDER_GRABBED, // 抢单通知 + ORDER_REJECTED, // 拒单通知 + SYSTEM // 系统通知 + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/Repairman.java b/src/main/java/com/campus/water/entity/Repairman.java index fc08920..3633b03 100644 --- a/src/main/java/com/campus/water/entity/Repairman.java +++ b/src/main/java/com/campus/water/entity/Repairman.java @@ -25,7 +25,7 @@ public class Repairman { @Column(length = 20) private String phone; - @Column(name = "area_id", length = 20) + @Column(name = "area_id", length = 36) private String areaId; @Column(name = "skills", length = 200) diff --git a/src/main/java/com/campus/water/entity/WaterTerminalLocation.java b/src/main/java/com/campus/water/entity/WaterTerminalLocation.java new file mode 100644 index 0000000..b6c93f2 --- /dev/null +++ b/src/main/java/com/campus/water/entity/WaterTerminalLocation.java @@ -0,0 +1,33 @@ +package com.campus.water.entity; + +import java.math.BigDecimal; +import lombok.Data; +import jakarta.persistence.*; + +/** + * 矿化水终端机位置实体(仅存储地图坐标,无冗余字段) + * 专用于学生端地图标记,终端名称/可用状态从映射表关联查询 + */ +@Data +@Entity +@Table(name = "water_terminal_location") +public class WaterTerminalLocation { + /** + * 终端机ID(主键,与映射表terminal_id关联,统一长度20) + */ + @Id + @Column(name = "terminal_id", length = 20, nullable = false) + private String terminalId; + + /** + * 经度(高德GCJ-02坐标系) + */ + @Column(name = "longitude", nullable = false, columnDefinition = "DECIMAL(10,6)") + private BigDecimal longitude; + + /** + * 纬度(高德GCJ-02坐标系) + */ + @Column(name = "latitude", nullable = false, columnDefinition = "DECIMAL(10,6)") + private BigDecimal latitude; +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/WorkOrder.java b/src/main/java/com/campus/water/entity/WorkOrder.java index efc2c90..29d9ed2 100644 --- a/src/main/java/com/campus/water/entity/WorkOrder.java +++ b/src/main/java/com/campus/water/entity/WorkOrder.java @@ -23,7 +23,7 @@ public class WorkOrder { @Column(name = "device_id", length = 20) private String deviceId; - @Column(name = "area_id", length = 20) + @Column(name = "area_id", length = 36) private String areaId; @Enumerated(EnumType.STRING) diff --git a/src/main/java/com/campus/water/entity/dto/request/RegisterRequest.java b/src/main/java/com/campus/water/entity/dto/request/RegisterRequest.java index 5231f41..3d8ce19 100644 --- a/src/main/java/com/campus/water/entity/dto/request/RegisterRequest.java +++ b/src/main/java/com/campus/water/entity/dto/request/RegisterRequest.java @@ -40,4 +40,6 @@ public class RegisterRequest { // 用户(学生)特有字段 private String studentId; private String studentName; + + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/dto/request/StudentDrinkQueryDTO.java b/src/main/java/com/campus/water/entity/dto/request/StudentDrinkQueryDTO.java new file mode 100644 index 0000000..fbad794 --- /dev/null +++ b/src/main/java/com/campus/water/entity/dto/request/StudentDrinkQueryDTO.java @@ -0,0 +1,12 @@ +package com.campus.water.entity.dto.request; + +import lombok.Data; + +/** + * 学生饮水量查询请求DTO + */ +@Data +public class StudentDrinkQueryDTO { + /** 学生ID */ + private String studentId; +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/vo/DailyDrinkVO.java b/src/main/java/com/campus/water/entity/vo/DailyDrinkVO.java new file mode 100644 index 0000000..16890f3 --- /dev/null +++ b/src/main/java/com/campus/water/entity/vo/DailyDrinkVO.java @@ -0,0 +1,16 @@ +package com.campus.water.entity.vo; + +import lombok.Data; + +/** + * 每日饮水量明细VO + */ +@Data +public class DailyDrinkVO { + /** 日期(yyyy-MM-dd) */ + private String date; + /** 当日饮水量(升) */ + private Double consumption; + /** 当日饮水次数 */ + private Integer count; +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/vo/StudentDrinkStatsVO.java b/src/main/java/com/campus/water/entity/vo/StudentDrinkStatsVO.java new file mode 100644 index 0000000..a09fecc --- /dev/null +++ b/src/main/java/com/campus/water/entity/vo/StudentDrinkStatsVO.java @@ -0,0 +1,28 @@ +package com.campus.water.entity.vo; + +import lombok.Data; +import java.util.List; +import com.campus.water.entity.DrinkRecord; +import com.campus.water.entity.vo.DailyDrinkVO; +/** + * 学生饮水量统计结果VO + */ +@Data +public class StudentDrinkStatsVO { + /** 学生ID */ + private String studentId; + /** 统计维度(本日/本周/本月) */ + private String timeDimension; + /** 统计时间范围(如"2025-12-25~2025-12-25") */ + private String timeRange; + /** 总饮水量(升) */ + private Double totalConsumption; + /** 日均饮水量(升) */ + private Double avgDailyConsumption; + /** 饮水次数 */ + private Integer drinkCount; + /** 按日期分组的每日饮水量明细 */ + private List dailyDetails; + /** 所有饮水记录明细 */ + private List drinkRecords; +} diff --git a/src/main/java/com/campus/water/entity/vo/TerminalLocationVO.java b/src/main/java/com/campus/water/entity/vo/TerminalLocationVO.java new file mode 100644 index 0000000..a09cf69 --- /dev/null +++ b/src/main/java/com/campus/water/entity/vo/TerminalLocationVO.java @@ -0,0 +1,17 @@ +package com.campus.water.entity.vo; + +import lombok.Data; +import java.math.BigDecimal; + +/** + * 终端位置VO(截图原命名,字段完全对齐) + */ +@Data +public class TerminalLocationVO { + private String terminalId; + private String terminalName; + private BigDecimal longitude; + private BigDecimal latitude; + private Boolean isAvailable; + private String deviceStatus; +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/vo/TerminalManageVO.java b/src/main/java/com/campus/water/entity/vo/TerminalManageVO.java new file mode 100644 index 0000000..855cfe4 --- /dev/null +++ b/src/main/java/com/campus/water/entity/vo/TerminalManageVO.java @@ -0,0 +1,37 @@ +// java/com/campus/water/vo/TerminalManageVO.java +package com.campus.water.entity.vo; + +import com.campus.water.entity.DeviceTerminalMapping; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 终端管理VO(整合位置表和映射表的核心信息) + */ +@Data +public class TerminalManageVO { + // 终端核心标识(关联两张表的主键/外键) + private String terminalId; + + // 终端名称(来自映射表) + private String terminalName; + + // 终端经纬度(来自位置表) + private BigDecimal longitude; + private BigDecimal latitude; + + // 终端状态(来自映射表) + private DeviceTerminalMapping.TerminalStatus terminalStatus; + + // 安装日期(来自映射表) + private LocalDate installDate; + + // 设备ID(关联的设备,来自映射表) + private String deviceId; + + // ========== 新增:片区ID字段(前端传递选中的片区ID) ========== + private String areaId; +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/先读我.md b/src/main/java/com/campus/water/entity/先读我.md deleted file mode 100644 index 3b52c1f..0000000 --- a/src/main/java/com/campus/water/entity/先读我.md +++ /dev/null @@ -1 +0,0 @@ -# 本md仅用于初始化目录,未创建所有子一级目录,在当前目录创建文件后请自行删除 \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/AdminRepository.java b/src/main/java/com/campus/water/mapper/AdminRepository.java index 0e3374e..b7b7458 100644 --- a/src/main/java/com/campus/water/mapper/AdminRepository.java +++ b/src/main/java/com/campus/water/mapper/AdminRepository.java @@ -2,6 +2,7 @@ package com.campus.water.mapper; import com.campus.water.entity.Admin; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; @@ -21,12 +22,28 @@ public interface AdminRepository extends JpaRepository { // 按手机号查询 Optional findByPhone(String phone); + // 新增:按区域ID查询管理员 + List findByAreaId(String areaId); + + // 按角色查询管理员(核心:恢复角色筛选) List findByRole(Admin.AdminRole role); // 按姓名+角色组合查询(可选,增强筛选) List findByAdminNameContainingAndRole(String name, Admin.AdminRole role); + // 新增:查询未负责任何片区的区域管理员(role=ROLE_AREA_ADMIN 且 areaId=null) + List findByRoleAndAreaIdIsNull(Admin.AdminRole role); + + // 新增1:查询指定校区的区域管理员(精准查询,用于单个校区权限校验) + List findByRoleAndAreaId(Admin.AdminRole role, String areaId); + + // 新增2:查询所有校区关联的区域管理员(排除市区,用于管理员列表筛选) + // 备注:此处使用@Query注解,关联Area表过滤区域类型为campus的管理员 + @Query("SELECT a FROM Admin a WHERE a.role = ?1 AND a.areaId IN " + + "(SELECT ar.areaId FROM Area ar WHERE ar.areaType = com.campus.water.entity.Area.AreaType.campus)") + List findAllAreaAdminsForCampus(Admin.AdminRole role); + // 检查唯一约束 boolean existsByAdminId(String adminId); boolean existsByPhone(String phone); diff --git a/src/main/java/com/campus/water/mapper/AlertRepository.java b/src/main/java/com/campus/water/mapper/AlertRepository.java index 859f25d..ac58b22 100644 --- a/src/main/java/com/campus/water/mapper/AlertRepository.java +++ b/src/main/java/com/campus/water/mapper/AlertRepository.java @@ -58,4 +58,6 @@ public interface AlertRepository extends JpaRepository { List activeStatus, LocalDateTime timestamp ); + + List findByResolvedByAndStatus(String repairmanId, Alert.AlertStatus alertStatus); } \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/AreaRepository.java b/src/main/java/com/campus/water/mapper/AreaRepository.java index 3638d5b..e40eb6f 100644 --- a/src/main/java/com/campus/water/mapper/AreaRepository.java +++ b/src/main/java/com/campus/water/mapper/AreaRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface AreaRepository extends JpaRepository { @@ -14,6 +15,18 @@ public interface AreaRepository extends JpaRepository { // 根据父区域ID查询子区域 List findByParentAreaId(String parentAreaId); + // 新增按区域类型查询(按创建时间倒序) + List findByAreaTypeOrderByCreatedTimeDesc(Area.AreaType areaType); + + // 按父级ID+类型查询(如查询某校园下的所有楼宇) + List findByParentAreaIdAndAreaType(String parentAreaId, Area.AreaType areaType); + + // 按名称模糊查询 + List findByAreaNameContaining(String keyword); + + // 查询所有(按创建时间倒序) + List findAllByOrderByCreatedTimeDesc(); + // 根据管理员姓名查询区域 List findByManager(String manager); @@ -23,4 +36,24 @@ public interface AreaRepository extends JpaRepository { // 查询指定类型的根级区域 @Query("SELECT a FROM Area a WHERE a.areaType = ?1 AND a.parentAreaId IS NULL") List findRootAreasByType(Area.AreaType areaType); + + // 移除所有含isDeleted的方法(实体无该属性) + // Optional findByIdAndIsDeletedFalse(String id); // 已移除 + // List findByAreaTypeAndParentAreaIdIsNullAndIsDeletedFalse(Area.AreaType areaType); // 已移除 + // List findByParentAreaIdAndAreaTypeAndIsDeletedFalse(String parentAreaId, Area.AreaType areaType); // 已移除 + // long countByParentAreaIdAndIsDeletedFalse(String parentAreaId); // 已移除 + // boolean existsByIdAndIsDeletedFalse(String id); // 已移除 + + // 保留原有正确方法 + Optional findByAreaId(String areaId); + long countByParentAreaId(String areaId); + List findByAreaTypeAndParentAreaIdIsNull(Area.AreaType areaType); + boolean existsByAreaId(String cityId); + + /** + * 查询没有负责人的片区(manager为null或空字符串) + * 覆盖未设置负责人的所有场景 + */ + List findByAreaTypeAndManagerIsNullOrManagerEquals(Area.AreaType areaType, String emptyStr); + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/DeviceRepository.java b/src/main/java/com/campus/water/mapper/DeviceRepository.java index dd3e7aa..99a1dd2 100644 --- a/src/main/java/com/campus/water/mapper/DeviceRepository.java +++ b/src/main/java/com/campus/water/mapper/DeviceRepository.java @@ -42,6 +42,12 @@ public interface DeviceRepository extends JpaRepository { // 根据制水机ID查询关联的供水机 List findByParentMakerIdAndDeviceType(String parentMakerId, Device.DeviceType deviceType); - // 按状态和区域查询(无设备类型筛选) + + // 按状态加载设备(支持区域筛选) List findByStatusAndAreaId(Device.DeviceStatus status, String areaId); + + // 按设备类型加载加载设备(支持区域筛选) + List findByDeviceTypeAndAreaId(Device.DeviceType deviceType, String areaId); + + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/DeviceTerminalMappingRepository.java b/src/main/java/com/campus/water/mapper/DeviceTerminalMappingRepository.java index de016ce..6f261c0 100644 --- a/src/main/java/com/campus/water/mapper/DeviceTerminalMappingRepository.java +++ b/src/main/java/com/campus/water/mapper/DeviceTerminalMappingRepository.java @@ -19,4 +19,17 @@ public interface DeviceTerminalMappingRepository extends JpaRepository findByDeviceIdAndTerminalId(String deviceId, String terminalId); + + // ========== 新增必要方法(支撑终端增删改查业务) ========== + // 1. 判断终端是否已绑定设备(删除终端时的核心校验) + boolean existsByTerminalId(String terminalId); + + // 2. 按终端名称模糊查询(终端列表筛选) + List findByTerminalNameContaining(String terminalName); + + // 3. 按终端ID删除所有关联映射(删除终端时级联清理映射数据) + void deleteByTerminalId(String terminalId); + + + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/NotificationRepository.java b/src/main/java/com/campus/water/mapper/NotificationRepository.java new file mode 100644 index 0000000..e8bf9d1 --- /dev/null +++ b/src/main/java/com/campus/water/mapper/NotificationRepository.java @@ -0,0 +1,22 @@ +package com.campus.water.mapper; + +import com.campus.water.entity.Notification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +/** + * 通知数据访问层 + */ +@Repository +public interface NotificationRepository extends JpaRepository { + /** + * 查询维修人员未读通知(按创建时间倒序) + */ + List findByRepairmanIdAndIsReadFalseOrderByCreatedTimeDesc(String repairmanId); + + /** + * 查询维修人员所有通知(按创建时间倒序) + */ + List findByRepairmanIdOrderByCreatedTimeDesc(String repairmanId); +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/WaterTerminalLocationRepository.java b/src/main/java/com/campus/water/mapper/WaterTerminalLocationRepository.java new file mode 100644 index 0000000..16d91db --- /dev/null +++ b/src/main/java/com/campus/water/mapper/WaterTerminalLocationRepository.java @@ -0,0 +1,13 @@ +package com.campus.water.mapper; + +import com.campus.water.entity.WaterTerminalLocation; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 终端机位置数据访问层(截图原命名,删除isAvailable相关查询) + */ +@Repository +public interface WaterTerminalLocationRepository extends JpaRepository { + // 原findByIsAvailable方法删除(因实体已无该字段,筛选逻辑移至Service层) +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/WorkOrderRepository.java b/src/main/java/com/campus/water/mapper/WorkOrderRepository.java index 1f9e1e7..b3dcd11 100644 --- a/src/main/java/com/campus/water/mapper/WorkOrderRepository.java +++ b/src/main/java/com/campus/water/mapper/WorkOrderRepository.java @@ -43,4 +43,16 @@ public interface WorkOrderRepository extends JpaRepository { // 根据创建人查询工单 List findByCreatedBy(String createdBy); + + //获取处于某些状态的工单 + List findByStatusIn(List list); + + /** + * 查询指定维修人员的待处理工单数量 + * @param repairmanId 维修人员ID + * @param statuses 待处理工单状态集合 + * @return 待处理工单数量 + */ + long countByAssignedRepairmanIdAndStatusIn(String repairmanId, List statuses); + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/先读我.md b/src/main/java/com/campus/water/mapper/先读我.md deleted file mode 100644 index 3b52c1f..0000000 --- a/src/main/java/com/campus/water/mapper/先读我.md +++ /dev/null @@ -1 +0,0 @@ -# 本md仅用于初始化目录,未创建所有子一级目录,在当前目录创建文件后请自行删除 \ No newline at end of file diff --git a/src/main/java/com/campus/water/security/先读我.md b/src/main/java/com/campus/water/security/先读我.md deleted file mode 100644 index 3b52c1f..0000000 --- a/src/main/java/com/campus/water/security/先读我.md +++ /dev/null @@ -1 +0,0 @@ -# 本md仅用于初始化目录,未创建所有子一级目录,在当前目录创建文件后请自行删除 \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/AdminService.java b/src/main/java/com/campus/water/service/AdminService.java index 6f319bb..bfe1837 100644 --- a/src/main/java/com/campus/water/service/AdminService.java +++ b/src/main/java/com/campus/water/service/AdminService.java @@ -1,7 +1,9 @@ package com.campus.water.service; import com.campus.water.entity.Admin; +import com.campus.water.entity.Area; import com.campus.water.mapper.AdminRepository; +import com.campus.water.mapper.AreaRepository; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; @@ -16,6 +18,8 @@ import java.util.Optional; public class AdminService { private final AdminRepository adminRepository; + private final AreaRepository areaRepository; // 新增注入 + @Autowired private PasswordEncoder passwordEncoder; @@ -38,6 +42,25 @@ public class AdminService { } } + /** + * 新增:查询可分配校区的区域管理员(未负责任何片区的区域管理员) + * 用于前端片区选择负责人时的下拉框数据源 + */ + public List getAvailableAreaAdmins() { + return adminRepository.findByRoleAndAreaIdIsNull(Admin.AdminRole.ROLE_AREA_ADMIN); + } + + /** + * 新增:获取指定区域的管理员列表 + */ + public List getAdminsByAreaId(String areaId) { + // 校验区域是否存在 + if (!areaRepository.existsById(areaId)) { + throw new RuntimeException("区域不存在:" + areaId); + } + return adminRepository.findByAreaId(areaId); + } + /** * 按ID查询管理员 */ @@ -46,16 +69,43 @@ public class AdminService { } /** - * 新增/修改管理员(支持指定角色) - */ - public Admin saveAdmin(Admin admin) { - admin.setUpdatedTime(LocalDateTime.now()); - if (admin.getCreatedTime() == null) { - admin.setCreatedTime(LocalDateTime.now()); + * 新增/修改管理员(支持指定角色) + * 重写保存方法,增加区域校验(区域管理员必须关联区域) + */ +public Admin saveAdmin(Admin admin) { + admin.setUpdatedTime(LocalDateTime.now()); + if (admin.getCreatedTime() == null) { + admin.setCreatedTime(LocalDateTime.now()); + } + + // 区域管理员(ROLE_AREA_ADMIN)的专属校验逻辑 + if (admin.getRole() == Admin.AdminRole.ROLE_AREA_ADMIN) { + // 1. 若未填写区域ID(null或空字符串),直接放行(支持先创建管理员,后续补填) + if (admin.getAreaId() == null || admin.getAreaId().trim().isEmpty()) { + admin.setAreaId(null); // 统一置为null,避免空字符串冗余数据 + // 无需校验,直接允许保存 + } else { + // 2. 若填写了区域ID,进行严格校验:区域存在 + 类型为校区(禁止市区) + String areaId = admin.getAreaId().trim(); + // 校验区域是否存在 + Area targetArea = areaRepository.findById(areaId) + .orElseThrow(() -> new RuntimeException("关联的区域不存在:" + areaId)); + // 核心校验:仅允许关联校区,禁止关联市区 + if (Area.AreaType.zone.equals(targetArea.getAreaType())) { + throw new RuntimeException("区域管理员仅允许关联校区,不能关联市区,请重新选择"); + } + // 校验通过,保留填写的合法校区ID + admin.setAreaId(areaId); } - return adminRepository.save(admin); + } else { + // 非区域管理员,清空区域ID,避免冗余数据 + admin.setAreaId(null); } + return adminRepository.save(admin); +} + + /** * 删除管理员 */ @@ -77,4 +127,84 @@ public class AdminService { public Admin.AdminRole[] getAllRoles() { return Admin.AdminRole.values(); } + + public AreaRepository getAreaRepository() { + return areaRepository; + } + + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + this.passwordEncoder = passwordEncoder; + } + + /** + * 个人信息更新(限制可修改字段) + */ + public Admin updateProfile(Admin profile) { + // 1. 获取数据库中原始信息 + Admin existingAdmin = adminRepository.findByAdminId(profile.getAdminId()) + .orElseThrow(() -> new RuntimeException("管理员不存在")); + + // 2. 仅更新允许修改的字段(排除角色、区域等敏感信息) + existingAdmin.setAdminName(profile.getAdminName()); + existingAdmin.setPhone(profile.getPhone()); + existingAdmin.setUpdatedTime(LocalDateTime.now()); + + // 3. 密码修改单独处理(如果有密码更新需求) + if (profile.getPassword() != null && !profile.getPassword().isEmpty()) { + existingAdmin.setPassword(passwordEncoder.encode(profile.getPassword())); + } + + return adminRepository.save(existingAdmin); + } + + /** + * 辅助方法:通过用户名查询管理员 + */ + public Optional getAdminByName(String username) { + return adminRepository.findByAdminName(username); + } + + /** + * 管理员密码修改(验证原密码,校验新密码,更新密码) + * @param username 登录用户名 + * @param oldPassword 原密码(明文) + * @param newPassword 新密码(明文) + * @return 密码修改是否成功 + */ + public boolean updatePassword(String username, String oldPassword, String newPassword) { + // 1. 校验参数合法性 + if (oldPassword == null || oldPassword.trim().isEmpty()) { + throw new IllegalArgumentException("原密码不能为空"); + } + if (newPassword == null || newPassword.trim().isEmpty()) { + throw new IllegalArgumentException("新密码不能为空"); + } + if (oldPassword.equals(newPassword)) { + throw new IllegalArgumentException("新密码不能与原密码一致"); + } + // 可选:新密码复杂度校验(增强安全性,根据项目需求调整) + if (newPassword.length() < 6 || newPassword.length() > 20) { + throw new IllegalArgumentException("新密码长度必须在6-20位之间"); + } + + // 2. 根据用户名查询当前管理员信息 + Admin existingAdmin = adminRepository.findByAdminName(username) + .orElseThrow(() -> new RuntimeException("管理员不存在")); + + // 3. 验证原密码是否正确(使用项目已有的 PasswordEncoder 进行匹配) + boolean oldPasswordMatch = passwordEncoder.matches(oldPassword, existingAdmin.getPassword()); + if (!oldPasswordMatch) { + return false; // 原密码错误,返回修改失败 + } + + // 4. 加密新密码并更新管理员信息 + String encodedNewPassword = passwordEncoder.encode(newPassword); + existingAdmin.setPassword(encodedNewPassword); + existingAdmin.setUpdatedTime(LocalDateTime.now()); // 更新修改时间,保持与其他方法一致 + + // 5. 保存到数据库 + adminRepository.save(existingAdmin); + return true; + } + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/AlertTriggerService.java b/src/main/java/com/campus/water/service/AlertTriggerService.java index 88d5399..10e9f47 100644 --- a/src/main/java/com/campus/water/service/AlertTriggerService.java +++ b/src/main/java/com/campus/water/service/AlertTriggerService.java @@ -12,7 +12,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - +import com.campus.water.service.AlertPushService; import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; @@ -184,11 +184,13 @@ public class AlertTriggerService { // 2. 创建对应类型的工单(inspection类型不通过告警触发) WorkOrder workOrder = new WorkOrder(); workOrder.setOrderId(generateOrderId()); + workOrder.setAlertId(alert.getAlertId()); workOrder.setDeviceId(deviceId); workOrder.setAreaId(areaId); workOrder.setOrderType(orderType); // 动态设置工单类型 workOrder.setDescription(message); workOrder.setStatus(WorkOrder.OrderStatus.pending); + workOrder.setDeadline(LocalDateTime.now().plusHours(24));// 创建工单时设置截止时间(例如24小时后) workOrder.setCreatedTime(LocalDateTime.now()); workOrderRepository.save(workOrder); log.info("创建工单成功 | 工单ID:{} | 设备ID:{} | 工单类型:{}", @@ -199,7 +201,7 @@ public class AlertTriggerService { * 检查是否存在重复告警(同设备同类型未处理告警,且在间隔时间内) * 覆盖pending/processing两种未处理状态 */ - private boolean isDuplicateAlert(String deviceId, String alertType) { + public boolean isDuplicateAlert(String deviceId, String alertType) { LocalDateTime before = LocalDateTime.now().minusMinutes(ALERT_DUPLICATE_INTERVAL); // 检查未处理的告警状态(pending/processing) List activeStatus = Arrays.asList( @@ -215,13 +217,23 @@ public class AlertTriggerService { } /** - * 获取设备所在区域ID + * 获取设备所在区域ID(新增超时控制,避免查询阻塞) */ - private String getDeviceAreaId(String deviceId) { - Optional deviceOpt = deviceRepository.findById(deviceId); - return deviceOpt.map(Device::getAreaId).orElse("unknown"); + public String getDeviceAreaId(String deviceId) { + try { + Optional deviceOpt = deviceRepository.findById(deviceId); + String areaId = deviceOpt.map(Device::getAreaId).orElse(null); + // 空值兜底:null/空字符串→unknown + if (areaId == null || areaId.trim().isEmpty()) { + areaId = "unknown"; + log.warn("设备{}的area_id为空/设备不存在,兜底为unknown", deviceId); + } + return areaId; + } catch (Exception e) { // 捕获所有数据库异常 + log.error("获取设备{}的area_id失败(数据库异常)", deviceId, e); + return "unknown"; // 异常时兜底 + } } - /** * 生成唯一工单ID(WO+时间戳+随机数) */ diff --git a/src/main/java/com/campus/water/service/AreaService.java b/src/main/java/com/campus/water/service/AreaService.java new file mode 100644 index 0000000..fe97942 --- /dev/null +++ b/src/main/java/com/campus/water/service/AreaService.java @@ -0,0 +1,245 @@ +package com.campus.water.service; + +import com.campus.water.entity.Area; +import com.campus.water.mapper.AreaRepository; +import com.campus.water.entity.Admin; +import com.campus.water.mapper.AdminRepository; +import com.campus.water.mapper.DeviceRepository; +import com.campus.water.mapper.DeviceTerminalMappingRepository; +import com.campus.water.security.RoleConstants; + +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * 区域管理服务类 + * 完全适配你的 Area 实体类(areaId 主键、新增 address/manager 等字段) + * 核心规则:市区为根节点(无父级),校园必须以市区为父节点 + */ +@Service +public class AreaService { + + private final AreaRepository areaRepository; + // 新增:管理员仓库注入 + private final AdminRepository adminRepository; + + // 新增:注入现有设备、终端映射Repository(无新增Mapper方法) + private final DeviceRepository deviceRepository; + private final DeviceTerminalMappingRepository deviceTerminalMappingRepository; + + public AreaService(AreaRepository areaRepository, AdminRepository adminRepository, + DeviceRepository deviceRepository, + DeviceTerminalMappingRepository deviceTerminalMappingRepository) { + this.areaRepository = areaRepository; + this.adminRepository = adminRepository; + this.deviceRepository = deviceRepository; + this.deviceTerminalMappingRepository = deviceTerminalMappingRepository; + } + + /** + * 新增区域 + * @param area 区域对象(包含名称、类型、父级ID、地址、管理员等) + * @return 保存后的区域对象 + */ + @Transactional(rollbackFor = Exception.class) + public Area addArea(Area area) { + // 1. 基础参数校验 + validateBaseParams(area); + + // 2. 层级规则校验(核心) + validateAreaHierarchy(area); + + // 3. 新增:管理员关联校验(若传入manager(管理员ID),则校验并预绑定) + validateAndPrepareAdmin(area); + + // 4. 补充基础字段(createdTime/updatedTime 已默认赋值,可手动刷新) + area.setCreatedTime(LocalDateTime.now()); + area.setUpdatedTime(LocalDateTime.now()); + + // 5. 保存数据 + Area savedArea = areaRepository.save(area); + + // 6. 新增:完成区域与管理员的最终绑定(需使用保存后的区域ID) + bindAdminToArea(area.getManager(), savedArea.getAreaId()); + + // 7. 返回保存后的区域对象 + return savedArea; + } + + /** + * 修改区域 + * @param areaId 区域ID + * @param area 待修改的区域信息 + * @return 修改后的区域对象 + */ + @Transactional(rollbackFor = Exception.class) + public Area updateArea(String areaId, Area area) { + // 1. 校验区域是否存在 + Area existingArea = areaRepository.findByAreaId(areaId) + .orElseThrow(() -> new RuntimeException("区域不存在,ID:" + areaId)); + + // 2. 基础参数校验 + validateBaseParams(area); + + // 3. 层级规则校验(修改时需保持层级规则) + validateAreaHierarchy(area); + + // 4. 覆盖可修改字段(适配你的实体类所有字段) + existingArea.setAreaName(area.getAreaName()); + existingArea.setAreaType(area.getAreaType()); + // 父级ID:市区不允许修改,校园必须指向市区 + if (Area.AreaType.campus.equals(area.getAreaType())) { + existingArea.setParentAreaId(area.getParentAreaId()); + } + // 新增字段赋值 + existingArea.setAddress(area.getAddress()); + existingArea.setManager(area.getManager()); + existingArea.setManagerPhone(area.getManagerPhone()); + // 更新时间 + existingArea.setUpdatedTime(LocalDateTime.now()); + + // 5. 保存修改 + return areaRepository.save(existingArea); + } + + /** + * 删除区域 + * @param areaId 区域ID + */ + @Transactional(rollbackFor = Exception.class) + public void deleteArea(String areaId) { + // 1. 校验区域是否存在 + Area existingArea = areaRepository.findByAreaId(areaId) + .orElseThrow(() -> new RuntimeException("区域不存在,ID:" + areaId)); + + // 2. 校验删除规则:若为市区,需先删除其下所有校园 + if (Area.AreaType.zone.equals(existingArea.getAreaType())) { + long campusCount = areaRepository.countByParentAreaId(areaId); + if (campusCount > 0) { + throw new RuntimeException("该市区下仍有 " + campusCount + " 个校园,无法删除,请先删除下属校园"); + } + } + + // 3. 物理删除(若需逻辑删除,可参考后续说明添加 isDeleted 字段) + areaRepository.delete(existingArea); + } + + /** + * 查询所有市区(根节点) + * @return 市区列表 + */ + public List getAllCities() { + return areaRepository.findByAreaTypeAndParentAreaIdIsNull(Area.AreaType.zone); + } + + /** + * 修改:仅查询无负责人的校区,排除市区 + */ + public List getAreasWithoutManager() { + // 调用仓库新增方法,限定:区域类型=campus,负责人=null 或 空字符串 + return areaRepository.findByAreaTypeAndManagerIsNullOrManagerEquals( + Area.AreaType.campus, // 仅筛选校区 + "" // 匹配空字符串的负责人 + ); + } + + /** + * 根据市区ID查询下属校园 + * @param cityId 市区ID(areaId) + * @return 该市区下的校园列表 + */ + public List getCampusesByCityId(String cityId) { + // 校验市区是否存在 + if (!areaRepository.existsByAreaId(cityId)) { + throw new RuntimeException("市区不存在,ID:" + cityId); + } + return areaRepository.findByParentAreaIdAndAreaType(cityId, Area.AreaType.campus ); + } + + /** + * 基础参数校验(名称、类型不能为空) + * @param area 区域对象 + */ + private void validateBaseParams(Area area) { + if (area.getAreaName() == null || area.getAreaName().trim().isEmpty()) { + throw new RuntimeException("区域名称不能为空"); + } + if (area.getAreaType() == null) { + throw new RuntimeException("区域类型不能为空(市区/校园)"); + } + } + + /** + * 区域层级规则校验(核心) + * 规则1:市区必须是根节点,无父级ID(parentAreaId=null) + * 规则2:校园必须有父级ID,且父级必须是市区 + */ + private void validateAreaHierarchy(Area area) { + if (Area.AreaType.zone.equals(area.getAreaType())) { + // 市区不允许设置父级ID + if (area.getParentAreaId() != null && !area.getParentAreaId().trim().isEmpty()) { + throw new RuntimeException("市区为根节点,不允许设置父级区域"); + } + } else if (Area.AreaType.campus .equals(area.getAreaType())) { + // 校园必须设置父级ID + if (area.getParentAreaId() == null || area.getParentAreaId().trim().isEmpty()) { + throw new RuntimeException("校园必须关联市区作为父级区域"); + } + // 校验父级区域是否存在且类型为市区 + Optional parentAreaOpt = areaRepository.findByAreaId(area.getParentAreaId()); + if (parentAreaOpt.isEmpty()) { + throw new RuntimeException("父级市区不存在,ID:" + area.getParentAreaId()); + } + Area parentArea = parentAreaOpt.get(); + if (!Area.AreaType.zone.equals(parentArea.getAreaType())) { + throw new RuntimeException("校园的父级区域必须是市区,当前父级类型为:" + parentArea.getAreaType().getDesc()); + } + } + } + + public Area getAreaById(String areaId) { + return areaRepository.findByAreaId(areaId) + .orElseThrow(() -> new RuntimeException("区域不存在,ID:" + areaId)); + } + + /** + * 辅助方法:校验管理员合法性(存在+角色正确) + * @param area 区域对象(提取manager字段:管理员ID) + */ + private void validateAndPrepareAdmin(Area area) { + String adminId = area.getManager(); + if (adminId != null && !adminId.trim().isEmpty()) { + // 校验管理员是否存在 + Admin admin = adminRepository.findById(adminId) + .orElseThrow(() -> new RuntimeException("区域管理员不存在,ID:" + adminId)); + + // 校验管理员角色是否为区域管理员 + if (!RoleConstants.ROLE_AREA_ADMIN.equals(admin.getRole().name())) { + throw new RuntimeException("指定用户不是区域管理员角色,无法绑定区域"); + } + } + } + + /** + * 辅助方法:将管理员绑定到指定区域 + * @param adminId 管理员ID + * @param areaId 区域ID(已保存的有效区域ID) + */ + private void bindAdminToArea(String adminId, String areaId) { + if (adminId != null && !adminId.trim().isEmpty() && areaId != null) { + Admin admin = adminRepository.findById(adminId).get(); // 已在前序校验,无需再次处理空值 + // 新增:校验该管理员是否已绑定其他校区 + if (admin.getAreaId() != null) { + throw new RuntimeException("该区域管理员已绑定校区【" + admin.getAreaId() + "】,无法重复绑定"); + } + admin.setAreaId(areaId); // 给管理员设置关联的区域ID(Admin实体需有areaId字段) + adminRepository.save(admin); + } + } + + +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/DeviceService.java b/src/main/java/com/campus/water/service/DeviceService.java index 7bb7d77..bbf1bbb 100644 --- a/src/main/java/com/campus/water/service/DeviceService.java +++ b/src/main/java/com/campus/water/service/DeviceService.java @@ -12,6 +12,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -36,6 +37,8 @@ public class DeviceService { .orElseThrow(() -> new RuntimeException("设备不存在:" + deviceId)); } + + /** * 新增设备 */ @@ -58,10 +61,8 @@ public class DeviceService { Device existingDevice = getDeviceById(device.getDeviceId()); // 保留创建时间,更新其他可编辑字段 existingDevice.setDeviceName(device.getDeviceName()); - existingDevice.setDeviceType(device.getDeviceType()); - existingDevice.setAreaId(device.getAreaId()); - existingDevice.setInstallLocation(device.getInstallLocation()); existingDevice.setInstallDate(device.getInstallDate()); + existingDevice.setParentMakerId(device.getParentMakerId()); return deviceRepository.save(existingDevice); } @@ -110,23 +111,28 @@ public class DeviceService { /** * 关联设备与终端 */ + // ========== 改造:原有bindTerminal方法,添加areaId参数和校验 ========== @Transactional - public DeviceTerminalMapping bindTerminal(String deviceId, String terminalId, String terminalName) { - // 校验设备是否存在 - getDeviceById(deviceId); - - // 检查终端是否已绑定 + public DeviceTerminalMapping bindTerminal(String deviceId, String terminalId, String terminalName, String areaId) { + // 1. 校验片区非空(终端必须归属片区) + if (areaId == null || areaId.trim().isEmpty()) { + throw new RuntimeException("片区ID不能为空,请先选择片区"); + } + // 2. 校验设备存在且属于该片区的供水机 + validateDeviceBelongsToArea(deviceId, areaId); + // 3. 检查终端是否已绑定(原有逻辑保留) Optional existing = terminalMappingRepository.findByTerminalId(terminalId); if (existing.isPresent()) { throw new RuntimeException("终端已绑定设备:" + existing.get().getDeviceId()); } - + // 4. 构建映射对象(新增设置areaId) DeviceTerminalMapping mapping = new DeviceTerminalMapping(); mapping.setDeviceId(deviceId); mapping.setTerminalId(terminalId); mapping.setTerminalName(terminalName); - mapping.setTerminalStatus(TerminalStatus.active); - mapping.setInstallDate(java.time.LocalDate.now()); + mapping.setTerminalStatus(DeviceTerminalMapping.TerminalStatus.active); + mapping.setInstallDate(LocalDate.now()); + mapping.setAreaId(areaId); // 保存终端所属片区 return terminalMappingRepository.save(mapping); } @@ -219,4 +225,30 @@ public class DeviceService { } return deviceRepository.findById(supplier.getParentMakerId()).orElse(null); } + + // ========== 新增1:查询指定片区的所有供水机 ========== + public List getWaterSuppliesByArea(String areaId) { + // 1. 校验片区ID非空 + if (areaId == null || areaId.trim().isEmpty()) { + throw new RuntimeException("片区ID不能为空"); + } + // 2. 查询该片区下类型为供水机的设备 + return deviceRepository.findByAreaIdAndDeviceType(areaId, Device.DeviceType.water_supply); + } + + // ========== 新增2:校验设备是否属于指定片区 ========== + public void validateDeviceBelongsToArea(String deviceId, String areaId) { + // 1. 校验设备存在 + Device device = getDeviceById(deviceId); + // 2. 校验设备是供水机 + if (!Device.DeviceType.water_supply.equals(device.getDeviceType())) { + throw new RuntimeException("只能关联供水机设备,当前设备类型不合法"); + } + // 3. 校验设备所属片区与选中片区一致 + if (!areaId.equals(device.getAreaId())) { + throw new RuntimeException("该供水机不属于所选片区(设备所属片区:" + device.getAreaId() + ")"); + } + } + + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/DeviceStatusService.java b/src/main/java/com/campus/water/service/DeviceStatusService.java index e2b2aa3..7fb4ee7 100644 --- a/src/main/java/com/campus/water/service/DeviceStatusService.java +++ b/src/main/java/com/campus/water/service/DeviceStatusService.java @@ -23,8 +23,13 @@ public interface DeviceStatusService { // 批量更新设备状态 boolean batchUpdateDeviceStatus(List deviceIds, String status, String remark); - // 按状态查询设备 - List getDevicesByStatus(String status, String areaId, String deviceType); + + + // 按状态加载设备(新增) + List getDevicesByStatusWithArea(String status, String areaId); + + // 按设备类型加载设备(新增) + List getDevicesByTypeWithArea(String deviceType, String areaId); // 统计各状态设备数量 Map getDeviceStatusCount(String areaId, String deviceType); diff --git a/src/main/java/com/campus/water/service/DeviceStatusServiceImpl.java b/src/main/java/com/campus/water/service/DeviceStatusServiceImpl.java index 4de8a84..a4aef47 100644 --- a/src/main/java/com/campus/water/service/DeviceStatusServiceImpl.java +++ b/src/main/java/com/campus/water/service/DeviceStatusServiceImpl.java @@ -1,4 +1,3 @@ -// com/campus/water/service/DeviceStatusServiceImpl.java package com.campus.water.service; import com.campus.water.entity.Device; @@ -27,6 +26,7 @@ public class DeviceStatusServiceImpl implements DeviceStatusService { return false; } device.setStatus(request.getStatus()); + device.setRemark(request.getRemark()); deviceRepository.save(device); return true; @@ -73,50 +73,68 @@ public class DeviceStatusServiceImpl implements DeviceStatusService { return true; } + /** + * 按状态加载设备(支持区域筛选) + * @param status 设备状态(字符串类型,需转换为枚举) + * @param areaId 区域ID(可为null,null时查询所有区域) + */ @Override - public List getDevicesByStatus(String status, String areaId, String deviceType) { - Device.DeviceStatus targetStatus = Device.DeviceStatus.valueOf(status); - - // 处理设备类型参数(允许为null) - Device.DeviceType targetType = null; - if (deviceType != null && !deviceType.isEmpty()) { - targetType = Device.DeviceType.valueOf(deviceType); - } - - // 根据设备类型是否为null执行不同查询 - if (targetType != null) { - return deviceRepository.findByStatusAndAreaIdAndDeviceType(targetStatus, areaId, targetType); - } else { - // 仅按状态和区域查询(如果有区域ID) + public List getDevicesByStatusWithArea(String status, String areaId) { + try { + Device.DeviceStatus targetStatus = Device.DeviceStatus.valueOf(status); if (areaId != null && !areaId.isEmpty()) { return deviceRepository.findByStatusAndAreaId(targetStatus, areaId); } else { return deviceRepository.findByStatus(targetStatus); } + } catch (IllegalArgumentException e) { + log.error("设备状态枚举转换失败,状态值:{}", status, e); + throw new RuntimeException("无效的设备状态:" + status); + } + } + + /** + * 按设备类型加载设备(支持区域筛选) + * @param deviceType 设备类型(字符串类型,需转换为枚举) + * @param areaId 区域ID(可为null,null时查询所有区域) + */ + @Override + public List getDevicesByTypeWithArea(String deviceType, String areaId) { + try { + Device.DeviceType targetType = Device.DeviceType.valueOf(deviceType); + if (areaId != null && !areaId.isEmpty()) { + return deviceRepository.findByDeviceTypeAndAreaId(targetType, areaId); + } else { + return deviceRepository.findByDeviceType(targetType); + } + } catch (IllegalArgumentException e) { + log.error("设备类型枚举转换失败,类型值:{}", deviceType, e); + throw new RuntimeException("无效的设备类型:" + deviceType); } } @Override public Map getDeviceStatusCount(String areaId, String deviceType) { - Device.DeviceType targetType = Device.DeviceType.valueOf(deviceType); - return Map.of( - "online", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.online, areaId, targetType), - "offline", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.offline, areaId, targetType), - "fault", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.fault, areaId, targetType) - ); + try { + Device.DeviceType targetType = Device.DeviceType.valueOf(deviceType); + return Map.of( + "online", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.online, areaId, targetType), + "offline", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.offline, areaId, targetType), + "fault", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.fault, areaId, targetType) + ); + } catch (IllegalArgumentException e) { + log.error("设备类型枚举转换失败,类型值:{}", deviceType, e); + throw new RuntimeException("无效的设备类型:" + deviceType); + } } @Override public List getOfflineDevicesExceedThreshold(Integer thresholdMinutes, String areaId) { - // 由于没有last_active_time,此处逻辑需调整: - // 方案1:若设备有最近操作时间,可用作替代; - // 方案2:仅返回状态为offline的设备(不判断时间) return deviceRepository.findByAreaIdAndStatus(areaId, Device.DeviceStatus.offline); } @Override public void autoDetectOfflineDevices(Integer thresholdMinutes) { - // 同理,无last_active_time时,无法通过时间判断,可注释或简化逻辑 log.info("自动检测离线设备(不执行时间判断,仅依赖手动标记)"); } } \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/MqttSensorReceiver.java b/src/main/java/com/campus/water/service/MqttSensorReceiver.java index 62f03e5..c871af0 100644 --- a/src/main/java/com/campus/water/service/MqttSensorReceiver.java +++ b/src/main/java/com/campus/water/service/MqttSensorReceiver.java @@ -1,5 +1,5 @@ package com.campus.water.service; - +import com.campus.water.service.AlertTriggerService; import com.campus.water.config.MqttConfig; import com.campus.water.entity.Alert; import com.campus.water.entity.WaterMakerRealtimeData; @@ -17,7 +17,6 @@ import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannel import org.springframework.integration.mqtt.support.MqttHeaders; import org.springframework.messaging.handler.annotation.Header; import org.springframework.stereotype.Service; - import jakarta.annotation.PostConstruct; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -26,25 +25,19 @@ import java.time.LocalDateTime; @RequiredArgsConstructor @Slf4j public class MqttSensorReceiver { - // JPA Repository(数据持久化接口,Spring自动注入实现) private final WaterMakerRealtimeDataRepository waterMakerRepo; private final WaterSupplyRealtimeDataRepository waterSupplyRepo; private final AlertRepository alertRepo; private final ObjectMapper objectMapper; private final MqttPahoMessageDrivenChannelAdapter mqttAdapter; - // 新增告警触发服务依赖 private final AlertTriggerService alertTriggerService; - /** - * 项目启动后初始化:订阅所有需要的MQTT主题 - * 主题后缀用「+」表示通配符(匹配所有设备ID) - */ @PostConstruct public void initMqttSubscription() { - mqttAdapter.addTopic(MqttConfig.TOPIC_WATER_MAKER_STATE + "+"); // 制水机状态(所有设备) - mqttAdapter.addTopic(MqttConfig.TOPIC_WATER_MAKER_WARN + "+"); // 制水机告警(所有设备) - mqttAdapter.addTopic(MqttConfig.TOPIC_WATER_SUPPLIER_STATE + "+"); // 供水机状态(所有设备) - mqttAdapter.addTopic(MqttConfig.TOPIC_WATER_SUPPLIER_WARN + "+"); // 供水机告警(所有设备) + mqttAdapter.addTopic(MqttConfig.TOPIC_WATER_MAKER_STATE + "+"); + mqttAdapter.addTopic(MqttConfig.TOPIC_WATER_MAKER_WARN + "+"); + mqttAdapter.addTopic(MqttConfig.TOPIC_WATER_SUPPLIER_STATE + "+"); + mqttAdapter.addTopic(MqttConfig.TOPIC_WATER_SUPPLIER_WARN + "+"); log.info("MQTT订阅初始化完成 | 订阅主题:{}+、{}+、{}+、{}+", MqttConfig.TOPIC_WATER_MAKER_STATE, MqttConfig.TOPIC_WATER_MAKER_WARN, @@ -52,25 +45,19 @@ public class MqttSensorReceiver { MqttConfig.TOPIC_WATER_SUPPLIER_WARN); } - /** - * 监听MQTT接收通道,处理所有收到的消息 - * @param payload 消息内容(JSON字符串) - * @param topic 接收主题(用于区分消息类型) - */ @ServiceActivator(inputChannel = "mqttInputChannel") public void handleMqttMessage(String payload, @Header(MqttHeaders.RECEIVED_TOPIC) String topic) { log.info("MQTT消息接收成功 | 主题:{} | 内容:{}", topic, payload); try { - // 根据主题分类处理 if (topic.startsWith(MqttConfig.TOPIC_WATER_MAKER_STATE)) { - handleWaterMakerState(payload); // 制水机状态数据 + handleWaterMakerState(payload); } else if (topic.startsWith(MqttConfig.TOPIC_WATER_MAKER_WARN)) { - handleWaterMakerWarning(payload); // 制水机告警数据 + handleWaterMakerWarning(payload); } else if (topic.startsWith(MqttConfig.TOPIC_WATER_SUPPLIER_STATE)) { - handleWaterSupplyState(payload); // 供水机状态数据 + handleWaterSupplyState(payload); } else if (topic.startsWith(MqttConfig.TOPIC_WATER_SUPPLIER_WARN)) { - handleWaterSupplyWarning(payload); // 供水机告警数据 + handleWaterSupplyWarning(payload); // 新增:处理供水机告警主题 } else { log.warn("MQTT消息主题未匹配 | 未知主题:{} | 内容:{}", topic, payload); } @@ -79,17 +66,11 @@ public class MqttSensorReceiver { } } - /** - * 处理制水机状态数据:转换为JPA实体并持久化 - */ private void handleWaterMakerState(String payload) throws Exception { - // 1. JSON反序列化为模型对象 WaterMakerSensorData sensorData = objectMapper.readValue(payload, WaterMakerSensorData.class); - // 2. 模型对象转换为JPA实体(持久化到数据库) WaterMakerRealtimeData entity = new WaterMakerRealtimeData(); entity.setDeviceId(sensorData.getDeviceId()); - // Double转BigDecimal处理(包含null值判断) entity.setTdsValue1(sensorData.getTdsValue1() != null ? BigDecimal.valueOf(sensorData.getTdsValue1()) : null); entity.setTdsValue2(sensorData.getTdsValue2() != null ? BigDecimal.valueOf(sensorData.getTdsValue2()) : null); entity.setTdsValue3(sensorData.getTdsValue3() != null ? BigDecimal.valueOf(sensorData.getTdsValue3()) : null); @@ -97,31 +78,31 @@ public class MqttSensorReceiver { entity.setWaterFlow2(sensorData.getWaterFlow2() != null ? BigDecimal.valueOf(sensorData.getWaterFlow2()) : null); entity.setWaterPress(sensorData.getWaterPress() != null ? BigDecimal.valueOf(sensorData.getWaterPress()) : null); entity.setFilterLife(sensorData.getFilterLife()); - entity.setLeakage(sensorData.getLeakage() ? true : false); // 数据库存储:true=漏水,false=正常 + entity.setLeakage(sensorData.getLeakage() ? true : false); entity.setWaterQuality(sensorData.getWaterQuality()); entity.setStatus(WaterMakerRealtimeData.DeviceStatus.valueOf(sensorData.getStatus())); entity.setRecordTime(sensorData.getRecordTime()); entity.setCreatedTime(LocalDateTime.now()); - // 3. 持久化到数据库(JPA save() 自动实现CRUD) waterMakerRepo.save(entity); log.info("制水机状态数据持久化成功 | 设备ID:{}", sensorData.getDeviceId()); - // 新增:调用告警检查逻辑 alertTriggerService.checkWaterMakerAbnormal(sensorData); } - /** - * 处理制水机告警数据:持久化告警记录+状态数据 - */ private void handleWaterMakerWarning(String payload) throws Exception { WaterMakerSensorData sensorData = objectMapper.readValue(payload, WaterMakerSensorData.class); - - // 1. 持久化告警记录 + // 新增:重复告警判断 + if (alertTriggerService.isDuplicateAlert(sensorData.getDeviceId(), "WATER_MAKER_ABNORMAL")) { + log.info("制水机存在未处理告警,跳过重复触发 | 设备ID:{}", sensorData.getDeviceId()); + return; + } Alert alert = new Alert(); alert.setDeviceId(sensorData.getDeviceId()); - alert.setAlertType("WATER_MAKER_ABNORMAL"); // 告警类型(枚举规范) - alert.setAlertLevel(Alert.AlertLevel.critical); // 告警级别(严重) + alert.setAlertType("WATER_MAKER_ABNORMAL"); + + alert.setAlertLevel(Alert.AlertLevel.critical); + alert.setAreaId(alertTriggerService.getDeviceAreaId(sensorData.getDeviceId())); alert.setAlertMessage(String.format( "制水机异常 - 设备ID:%s,TDS值:%.2f,滤芯寿命:%d%%,漏水状态:%s", sensorData.getDeviceId(), @@ -129,26 +110,23 @@ public class MqttSensorReceiver { sensorData.getFilterLife(), sensorData.getLeakage() ? "是" : "否" )); - alert.setStatus(Alert.AlertStatus.pending); // 告警状态(未处理) - alert.setTimestamp(sensorData.getRecordTime()); - alert.setCreatedTime(LocalDateTime.now()); + alert.setStatus(Alert.AlertStatus.pending); + alert.setTimestamp(sensorData.getRecordTime() != null ? sensorData.getRecordTime() : LocalDateTime.now()); + LocalDateTime now = LocalDateTime.now(); + alert.setCreatedTime(alert.getCreatedTime() != null ? alert.getCreatedTime() : now); + alert.setUpdatedTime(now); alertRepo.save(alert); log.warn("制水机告警记录持久化成功 | 告警ID:{} | 设备ID:{}", alert.getAlertId(), sensorData.getDeviceId()); - // 2. 同时持久化状态数据(便于后续追溯) handleWaterMakerState(payload); } - /** - * 处理供水机状态数据:转换为JPA实体并持久化 - */ private void handleWaterSupplyState(String payload) throws Exception { WaterSupplySensorData sensorData = objectMapper.readValue(payload, WaterSupplySensorData.class); WaterSupplyRealtimeData entity = new WaterSupplyRealtimeData(); entity.setDeviceId(sensorData.getDeviceId()); - // Double转BigDecimal处理(包含null值判断) entity.setWaterFlow(sensorData.getWaterFlow() != null ? BigDecimal.valueOf(sensorData.getWaterFlow()) : null); entity.setWaterPress(sensorData.getWaterPress() != null ? BigDecimal.valueOf(sensorData.getWaterPress()) : null); entity.setWaterLevel(sensorData.getWaterLevel() != null ? BigDecimal.valueOf(sensorData.getWaterLevel()) : null); @@ -156,39 +134,47 @@ public class MqttSensorReceiver { entity.setStatus(WaterSupplyRealtimeData.DeviceStatus.valueOf(sensorData.getStatus())); entity.setTimestamp(sensorData.getTimestamp()); - waterSupplyRepo.save(entity); log.info("供水机状态数据持久化成功 | 设备ID:{}", sensorData.getDeviceId()); - // 新增:调用告警检查逻辑 alertTriggerService.checkWaterSupplyAbnormal(sensorData); } /** - * 处理供水机告警数据:持久化告警记录+状态数据 + * 新增:处理供水机告警数据 */ private void handleWaterSupplyWarning(String payload) throws Exception { WaterSupplySensorData sensorData = objectMapper.readValue(payload, WaterSupplySensorData.class); - - // 1. 持久化告警记录 + // 新增:重复告警判断 + if (alertTriggerService.isDuplicateAlert(sensorData.getDeviceId(), "WATER_SUPPLY_ABNORMAL")) { + log.info("供水机存在未处理告警,跳过重复触发 | 设备ID:{}", sensorData.getDeviceId()); + return; + } Alert alert = new Alert(); alert.setDeviceId(sensorData.getDeviceId()); alert.setAlertType("WATER_SUPPLY_ABNORMAL"); + alert.setAlertLevel(Alert.AlertLevel.error); + alert.setAreaId(alertTriggerService.getDeviceAreaId(sensorData.getDeviceId())); alert.setAlertMessage(String.format( - "供水机异常 - 设备ID:%s,水压:%.2fMPa,水位:%.2f%%", + "供水机异常 - 设备ID:%s,水压:%.2fMPa,水位:%.2f%%,水温:%.2f℃", sensorData.getDeviceId(), sensorData.getWaterPress(), - sensorData.getWaterLevel() + sensorData.getWaterLevel(), + sensorData.getTemperature() )); alert.setStatus(Alert.AlertStatus.pending); - alert.setTimestamp(sensorData.getTimestamp()); - alert.setCreatedTime(LocalDateTime.now()); + // 完善告警时间戳(确保不为空) + alert.setTimestamp(sensorData.getTimestamp() != null ? sensorData.getTimestamp() : LocalDateTime.now()); + // 完善创建/更新时间(确保不为空) + LocalDateTime now = LocalDateTime.now(); + alert.setCreatedTime(alert.getCreatedTime() != null ? alert.getCreatedTime() : now); + alert.setUpdatedTime(now); alertRepo.save(alert); log.warn("供水机告警记录持久化成功 | 告警ID:{} | 设备ID:{}", alert.getAlertId(), sensorData.getDeviceId()); - // 2. 同时持久化状态数据 + // 同时持久化状态数据 handleWaterSupplyState(payload); } } \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/MqttSensorSender.java b/src/main/java/com/campus/water/service/MqttSensorSender.java index 6ff5c52..2f88f2e 100644 --- a/src/main/java/com/campus/water/service/MqttSensorSender.java +++ b/src/main/java/com/campus/water/service/MqttSensorSender.java @@ -115,6 +115,32 @@ public class MqttSensorSender { } } + /** + * 新增:模拟供水机发送「告警数据」 + * @param deviceId 设备ID(如WS001) + */ + public void sendWaterSupplyWarning(String deviceId) { + try { + // 1. 构建供水机异常数据(超出正常范围) + WaterSupplySensorData data = new WaterSupplySensorData(); + data.setDeviceId(deviceId); + data.setWaterFlow(0.1 + random.nextDouble() * 0.2); // 流量极低(0.1-0.3 L/min) + data.setWaterPress(0.01 + random.nextDouble() * 0.09); // 水压过低(0.01-0.1 MPa) + data.setWaterLevel(5 + random.nextDouble() * 15); // 水位过低(5-20%) + data.setTemperature(25 + random.nextDouble() * 5); // 水温过高(25-30℃) + data.setStatus("error"); + data.setTimestamp(LocalDateTime.now()); + + // 2. 序列化+发送 + String payload = objectMapper.writeValueAsString(data); + String topic = MqttConfig.TOPIC_WATER_SUPPLIER_WARN + deviceId; + sendMessage(topic, payload); + log.warn("供水机告警消息发送成功 | 设备ID:{} | 主题:{} | 数据:{}", deviceId, topic, payload); + } catch (JsonProcessingException e) { + log.error("供水机告警消息发送失败 | 设备ID:{} | 异常:{}", deviceId, e.getMessage()); + } + } + /** * 通用发送方法(封装MQTT消息构建逻辑) * @param topic 主题 diff --git a/src/main/java/com/campus/water/service/NotificationService.java b/src/main/java/com/campus/water/service/NotificationService.java new file mode 100644 index 0000000..6fddd6c --- /dev/null +++ b/src/main/java/com/campus/water/service/NotificationService.java @@ -0,0 +1,37 @@ +package com.campus.water.service; + +import com.campus.water.entity.Notification; +import java.util.List; + +/** + * 维修人员通知服务接口 + */ +public interface NotificationService { + /** + * 发送派单通知 + * @param repairmanId 维修人员ID + * @param orderId 工单ID + * @param content 通知内容 + */ + void sendOrderAssignedNotification(String repairmanId, String orderId, String content); + + /** + * 获取维修人员未读通知 + * @param repairmanId 维修人员ID + * @return 未读通知列表 + */ + List getUnreadNotifications(String repairmanId); + + /** + * 获取维修人员所有通知 + * @param repairmanId 维修人员ID + * @return 所有通知列表 + */ + List getAllNotifications(String repairmanId); + + /** + * 标记通知为已读 + * @param notificationId 通知ID + */ + void markAsRead(Long notificationId); +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/NotificationServiceImpl.java b/src/main/java/com/campus/water/service/NotificationServiceImpl.java new file mode 100644 index 0000000..9af8e5c --- /dev/null +++ b/src/main/java/com/campus/water/service/NotificationServiceImpl.java @@ -0,0 +1,59 @@ +package com.campus.water.service.impl; + +import com.campus.water.entity.Notification; +import com.campus.water.mapper.NotificationRepository; +import com.campus.water.service.NotificationService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 通知服务实现类 + */ +@Service +@RequiredArgsConstructor +public class NotificationServiceImpl implements NotificationService { + + private final NotificationRepository notificationRepository; + + /** + * 发送派单通知 + */ + @Override + public void sendOrderAssignedNotification(String repairmanId, String orderId, String content) { + Notification notification = new Notification(); + notification.setRepairmanId(repairmanId); + notification.setOrderId(orderId); + notification.setContent(content); + notification.setType(Notification.NotificationType.ORDER_ASSIGNED); + notificationRepository.save(notification); + } + + /** + * 获取未读通知 + */ + @Override + public List getUnreadNotifications(String repairmanId) { + return notificationRepository.findByRepairmanIdAndIsReadFalseOrderByCreatedTimeDesc(repairmanId); + } + + /** + * 获取所有通知 + */ + @Override + public List getAllNotifications(String repairmanId) { + return notificationRepository.findByRepairmanIdOrderByCreatedTimeDesc(repairmanId); + } + + /** + * 标记通知为已读 + */ + @Override + public void markAsRead(Long notificationId) { + notificationRepository.findById(notificationId).ifPresent(notification -> { + notification.setRead(true); + notificationRepository.save(notification); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/RegisterService.java b/src/main/java/com/campus/water/service/RegisterService.java index 14bbe12..f571b41 100644 --- a/src/main/java/com/campus/water/service/RegisterService.java +++ b/src/main/java/com/campus/water/service/RegisterService.java @@ -5,10 +5,7 @@ import com.campus.water.entity.RepairerAuth; import com.campus.water.entity.Repairman; import com.campus.water.entity.User; import com.campus.water.entity.dto.request.RegisterRequest; -import com.campus.water.mapper.AdminRepository; -import com.campus.water.mapper.RepairerAuthRepository; -import com.campus.water.mapper.RepairmanRepository; -import com.campus.water.mapper.UserRepository; +import com.campus.water.mapper.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -21,6 +18,9 @@ public class RegisterService { @Autowired private AdminRepository adminRepository; + @Autowired + private AreaRepository areaRepository; + @Autowired private UserRepository userRepository; @@ -76,6 +76,18 @@ public class RegisterService { admin.setCreatedTime(LocalDateTime.now()); admin.setUpdatedTime(LocalDateTime.now()); + // 核心修改1:添加区域ID赋值(从请求中获取,允许为null/空,实现选填) + admin.setAreaId(request.getAreaId()); + + // 核心修改2:区域管理员若填写了areaId,则校验区域是否存在;不填则不强制(实现选填) + Admin.AdminRole adminRole = admin.getRole(); + if (adminRole == Admin.AdminRole.ROLE_AREA_ADMIN && request.getAreaId() != null && !request.getAreaId().trim().isEmpty()) { + // 此处需要注入AreaRepository(与AdminService保持一致),先补充注入 + if (!areaRepository.existsById(request.getAreaId().trim())) { + throw new RuntimeException("关联的区域不存在:" + request.getAreaId().trim()); + } + } + adminRepository.save(admin); } diff --git a/src/main/java/com/campus/water/service/StudentDrinkStatsService.java b/src/main/java/com/campus/water/service/StudentDrinkStatsService.java new file mode 100644 index 0000000..5d01331 --- /dev/null +++ b/src/main/java/com/campus/water/service/StudentDrinkStatsService.java @@ -0,0 +1,105 @@ +package com.campus.water.service; + +import com.campus.water.entity.DrinkRecord; +import com.campus.water.entity.vo.DailyDrinkVO; +import com.campus.water.entity.vo.StudentDrinkStatsVO; +import com.campus.water.mapper.DrinkRecordRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.time.*; +import java.time.temporal.TemporalAdjusters; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 学生饮水量统计服务 + */ +@Service +@RequiredArgsConstructor +public class StudentDrinkStatsService { + + private final DrinkRecordRepository drinkRecordRepository; + + /** + * 查询学生本日饮水量统计 + */ + public StudentDrinkStatsVO getTodayDrinkStats(String studentId) { + LocalDate today = LocalDate.now(); + LocalDateTime start = today.atStartOfDay(); + LocalDateTime end = LocalDateTime.now(); + return calculateStats(studentId, start, end, "本日", "today"); + } + + /** + * 查询学生本周饮水量统计 + */ + public StudentDrinkStatsVO getThisWeekDrinkStats(String studentId) { + LocalDate today = LocalDate.now(); + LocalDateTime start = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay(); + LocalDateTime end = LocalDateTime.now(); + return calculateStats(studentId, start, end, "本周", "thisWeek"); + } + + /** + * 查询学生本月饮水量统计 + */ + public StudentDrinkStatsVO getThisMonthDrinkStats(String studentId) { + LocalDate today = LocalDate.now(); + LocalDateTime start = today.with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay(); + LocalDateTime end = LocalDateTime.now(); + return calculateStats(studentId, start, end, "本月", "thisMonth"); + } + + /** + * 核心统计逻辑 + */ + private StudentDrinkStatsVO calculateStats(String studentId, LocalDateTime start, LocalDateTime end, + String timeRangeDesc, String timeDimension) { + // 1. 查询时间范围内的饮水记录 + List records = drinkRecordRepository + .findByStudentIdAndDrinkTimeBetweenOrdered(studentId, start, end); + + // 2. 按日期分组统计 + Map> dailyGroup = records.stream() + .collect(Collectors.groupingBy(record -> record.getDrinkTime().toLocalDate())); + + // 3. 构建每日明细 + List dailyDetails = new ArrayList<>(); + dailyGroup.forEach((date, dailyRecords) -> { + DailyDrinkVO dailyVO = new DailyDrinkVO(); + dailyVO.setDate(date.toString()); + // 当日总饮水量 + double dailyTotal = dailyRecords.stream() + .map(DrinkRecord::getWaterConsumption) + .filter(Objects::nonNull) + .mapToDouble(BigDecimal::doubleValue) + .sum(); + dailyVO.setConsumption(dailyTotal); + dailyVO.setCount(dailyRecords.size()); + dailyDetails.add(dailyVO); + }); + // 按日期排序 + dailyDetails.sort(Comparator.comparing(DailyDrinkVO::getDate)); + + // 4. 计算总饮水量、总次数、日均饮水量 + double totalConsumption = dailyDetails.stream() + .mapToDouble(DailyDrinkVO::getConsumption) + .sum(); + int totalCount = records.size(); + double avgDaily = dailyDetails.isEmpty() ? 0 : totalConsumption / dailyDetails.size(); + + // 5. 封装结果VO + StudentDrinkStatsVO statsVO = new StudentDrinkStatsVO(); + statsVO.setStudentId(studentId); + statsVO.setTimeDimension(timeDimension); + statsVO.setTimeRange(timeRangeDesc + "(" + start.toLocalDate() + "~" + end.toLocalDate() + ")"); + statsVO.setTotalConsumption(totalConsumption); + statsVO.setDrinkCount(totalCount); + statsVO.setAvgDailyConsumption(avgDaily); + statsVO.setDailyDetails(dailyDetails); + statsVO.setDrinkRecords(records); + + return statsVO; + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/StudentWaterDataService.java b/src/main/java/com/campus/water/service/StudentWaterDataService.java new file mode 100644 index 0000000..244900d --- /dev/null +++ b/src/main/java/com/campus/water/service/StudentWaterDataService.java @@ -0,0 +1,150 @@ +package com.campus.water.service; + +import com.campus.water.entity.DeviceTerminalMapping; +import com.campus.water.mapper.DeviceTerminalMappingRepository; +import com.campus.water.util.DeviceMappingUtil; // 硬编码映射工具类(之前定义的) +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Random; + +@Service +public class StudentWaterDataService { + + @Autowired + private DeviceTerminalMappingRepository terminalMappingRepo; + + // 随机数工具(模拟实时数据) + private static final Random RANDOM = new Random(); + // 时间格式化器 + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 核心:实时查询终端关联的制水机+供水机数据 + * @param terminalId 终端ID(如TERM001) + * @return 包含制水机、供水机实时数据的完整结果 + */ + public Map queryRealtimeData(String terminalId) { + Map result = new HashMap<>(); + try { + // 1. 校验终端ID非空 + if (terminalId == null || terminalId.trim().isEmpty()) { + result.put("code", 400); + result.put("msg", "终端ID不能为空"); + return result; + } + + // 2. 通过终端ID查询映射关系(核心:终端→制水机) + Optional mappingOpt = terminalMappingRepo.findByTerminalId(terminalId); + if (mappingOpt.isEmpty()) { + result.put("code", 404); + result.put("msg", "终端设备不存在或未配置映射关系"); + return result; + } + DeviceTerminalMapping mapping = mappingOpt.get(); + String makerDeviceId = mapping.getDeviceId(); // 制水机ID(WM开头) + // 从硬编码映射工具类获取供水机ID(替代数据库字段) + String supplyDeviceId = DeviceMappingUtil.getSupplyDeviceId(makerDeviceId); + + // 3. 生成制水机实时数据(调用模拟生成服务) + Map makerRealtimeData = new HashMap<>(); + if (makerDeviceId != null && makerDeviceId.startsWith("WM")) { + makerRealtimeData = generateMakerRealtimeData(makerDeviceId); + } else { + result.put("makerWarn", "终端未绑定有效制水机(ID格式需以WM开头)"); + } + + // 4. 生成供水机实时数据(调用模拟生成服务) + Map supplyRealtimeData = new HashMap<>(); + if (supplyDeviceId != null && supplyDeviceId.startsWith("WS")) { + supplyRealtimeData = generateSupplyRealtimeData(supplyDeviceId); + } else { + result.put("supplyWarn", "制水机未关联有效供水机(ID格式需以WS开头)"); + } + + // 5. 封装最终返回结果(包含终端、制水机、供水机全量数据) + result.put("code", 200); + result.put("msg", "实时数据查询成功"); + // 终端基础信息 + result.put("terminalInfo", Map.of( + "terminalId", terminalId, + "terminalName", mapping.getTerminalName(), + "terminalStatus", mapping.getTerminalStatus().name(), + "installDate", mapping.getInstallDate() != null ? mapping.getInstallDate().toString() : "未配置" + )); + // 制水机数据 + result.put("makerDevice", Map.of( + "deviceId", makerDeviceId, + "realtimeData", makerRealtimeData + )); + // 供水机数据 + result.put("supplyDevice", Map.of( + "deviceId", supplyDeviceId, + "realtimeData", supplyRealtimeData + )); + // 数据更新时间(统一时间戳) + result.put("updateTime", LocalDateTime.now().format(DATE_FORMATTER)); + + } catch (Exception e) { + // 全局异常捕获,保证接口不抛错 + result.put("code", 500); + result.put("msg", "实时数据查询失败:" + e.getMessage()); + result.put("errorDetail", e.getStackTrace()[0].toString()); // 调试用,生产可移除 + } + return result; + } + + /** + * 模拟生成制水机实时数据(独立服务方法,可单独调用) + * @param makerDeviceId 制水机ID + * @return 制水机实时数据 + */ + public Map generateMakerRealtimeData(String makerDeviceId) { + Map makerData = new HashMap<>(); + // 基础设备信息 + makerData.put("deviceId", makerDeviceId); + makerData.put("deviceType", "校园直饮矿化制水机"); + makerData.put("onlineStatus", RANDOM.nextBoolean() ? "在线" : "离线"); // 模拟在线状态 + // 核心水质参数(符合直饮水标准) + makerData.put("tdsValue", RANDOM.nextInt(50) + 10); // TDS值:10-60mg/L + makerData.put("phValue", String.format("%.1f", RANDOM.nextDouble() * 0.8 + 7.0)); // pH值:7.0-7.8 + makerData.put("temperature", RANDOM.nextInt(15) + 30); // 出水温度:30-45℃ + makerData.put("filterLife", 100 - RANDOM.nextInt(10)); // 滤芯寿命:90-100% + makerData.put("flowRate", String.format("%.2f", RANDOM.nextDouble() * 2 + 1)); // 流量:1.00-3.00L/min + // 运行数据 + makerData.put("totalUsage", RANDOM.nextInt(5000) + 10000); // 累计用水量:10000-15000L + makerData.put("faultCode", RANDOM.nextInt(100) > 95 ? "E01" : "无"); // 模拟故障码(5%概率故障) + makerData.put("updateTime", LocalDateTime.now().format(DATE_FORMATTER)); + return makerData; + } + + /** + * 模拟生成供水机实时数据(独立服务方法,可单独调用) + * @param supplyDeviceId 供水机ID + * @return 供水机实时数据 + */ + public Map generateSupplyRealtimeData(String supplyDeviceId) { + Map supplyData = new HashMap<>(); + // 基础设备信息 + supplyData.put("deviceId", supplyDeviceId); + supplyData.put("deviceType", "配套供水增压机"); + supplyData.put("onlineStatus", RANDOM.nextBoolean() ? "在线" : "离线"); + // 核心运行参数 + supplyData.put("waterPressure", String.format("%.2f", RANDOM.nextDouble() * 0.5 + 0.8)); // 水压:0.80-1.30MPa + supplyData.put("waterLevel", RANDOM.nextInt(30) + 70); // 水箱水位:70-100% + supplyData.put("pumpStatus", RANDOM.nextBoolean() ? "运行中" : "待机"); // 水泵状态 + supplyData.put("voltage", RANDOM.nextInt(10) + 220); // 工作电压:220-230V + // 运行数据 + supplyData.put("runHours", RANDOM.nextInt(1000) + 5000); // 累计运行时长:5000-6000h + supplyData.put("faultCode", RANDOM.nextInt(100) > 98 ? "P02" : "无"); // 模拟故障码(2%概率故障) + supplyData.put("updateTime", LocalDateTime.now().format(DATE_FORMATTER)); + return supplyData; + } + + // 原有其他方法(扫码用水、水质查询等)保持不变... +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/TerminalService.java b/src/main/java/com/campus/water/service/TerminalService.java new file mode 100644 index 0000000..bfc1650 --- /dev/null +++ b/src/main/java/com/campus/water/service/TerminalService.java @@ -0,0 +1,25 @@ +// java/com/campus/water/service/TerminalService.java +package com.campus.water.service; + +import com.campus.water.entity.DeviceTerminalMapping; +import com.campus.water.entity.WaterTerminalLocation; +import com.campus.water.entity.vo.TerminalManageVO; + +import java.util.List; + +public interface TerminalService { + // 新增终端(同时保存位置信息和基础映射信息) + TerminalManageVO addTerminal(TerminalManageVO terminalVO); + + // 更新终端(支持更新名称、状态、经纬度等信息) + TerminalManageVO updateTerminal(TerminalManageVO terminalVO); + + // 删除终端(先校验是否绑定设备,再级联删除两张表的相关数据) + void deleteTerminal(String terminalId); + + // 按ID查询终端详情(整合两张表的数据) + TerminalManageVO getTerminalById(String terminalId); + + // 查询终端列表(支持按名称筛选,返回整合后的数据) + List getTerminalList(String terminalName); +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/TerminalServiceImpl.java b/src/main/java/com/campus/water/service/TerminalServiceImpl.java new file mode 100644 index 0000000..9700fc3 --- /dev/null +++ b/src/main/java/com/campus/water/service/TerminalServiceImpl.java @@ -0,0 +1,191 @@ +// java/com/campus/water/service/impl/TerminalServiceImpl.java +package com.campus.water.service; + +import com.campus.water.entity.DeviceTerminalMapping; +import com.campus.water.entity.WaterTerminalLocation; +import com.campus.water.mapper.DeviceTerminalMappingRepository; +import com.campus.water.mapper.WaterTerminalLocationRepository; +import com.campus.water.service.TerminalService; +import com.campus.water.entity.vo.TerminalManageVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class TerminalServiceImpl implements TerminalService { + + private final WaterTerminalLocationRepository locationRepository; + private final DeviceTerminalMappingRepository mappingRepository; + // ========== 新增:注入DeviceService,用于片区和供水机校验 ========== + private final DeviceService deviceService; + + @Override + @Transactional + public TerminalManageVO addTerminal(TerminalManageVO terminalVO) { + // 1. 校验终端ID是否已存在(通过位置表主键判断,避免重复新增) + if (locationRepository.existsById(terminalVO.getTerminalId())) { + throw new RuntimeException("终端ID已存在,无法重复新增:" + terminalVO.getTerminalId()); + } + + // 2. 保存终端位置信息(经纬度为必填项,校验非空) + if (terminalVO.getLongitude() == null || terminalVO.getLatitude() == null) { + throw new RuntimeException("终端经度和纬度为必填项,不可为空"); + } + // ========== 新增:校验片区ID非空 ========== + if (terminalVO.getAreaId() == null || terminalVO.getAreaId().trim().isEmpty()) { + throw new RuntimeException("片区ID不能为空,请先选择片区"); + } + // ========== 新增:若传递了deviceId,校验供水机是否属于该片区 ========== + if (terminalVO.getDeviceId() != null && !terminalVO.getDeviceId().trim().isEmpty()) { + deviceService.validateDeviceBelongsToArea(terminalVO.getDeviceId(), terminalVO.getAreaId()); + } + WaterTerminalLocation location = new WaterTerminalLocation(); + location.setTerminalId(terminalVO.getTerminalId()); + location.setLongitude(terminalVO.getLongitude()); + location.setLatitude(terminalVO.getLatitude()); + locationRepository.save(location); + + // 3. 保存终端映射信息(状态默认active,复用原有Repository的save方法) + DeviceTerminalMapping mapping = new DeviceTerminalMapping(); + mapping.setTerminalId(terminalVO.getTerminalId()); + mapping.setTerminalName(terminalVO.getTerminalName()); + mapping.setTerminalStatus(terminalVO.getTerminalStatus() == null + ? DeviceTerminalMapping.TerminalStatus.active + : terminalVO.getTerminalStatus()); + mapping.setDeviceId(terminalVO.getDeviceId()); + mapping.setInstallDate(terminalVO.getInstallDate()); + mapping.setAreaId(terminalVO.getAreaId()); // 保存片区ID + mappingRepository.save(mapping); + + // 4. 封装返回结果 + return assembleTerminalVO(location, mapping); + } + + @Override + @Transactional + public TerminalManageVO updateTerminal(TerminalManageVO terminalVO) { + // 1. 校验终端是否存在(通过位置表查询,复用原有findById方法) + WaterTerminalLocation existingLocation = locationRepository.findById(terminalVO.getTerminalId()) + .orElseThrow(() -> new RuntimeException("终端不存在,无法更新:" + terminalVO.getTerminalId())); + // ========== 新增:若更新片区/设备,校验供水机与片区的关联 ========== + if (terminalVO.getAreaId() != null && !terminalVO.getAreaId().trim().isEmpty()) { + if (terminalVO.getDeviceId() != null && !terminalVO.getDeviceId().trim().isEmpty()) { + deviceService.validateDeviceBelongsToArea(terminalVO.getDeviceId(), terminalVO.getAreaId()); + } + } + // 2. 更新终端位置信息(仅更新有值字段,复用原有save方法) + if (terminalVO.getLongitude() != null) { + existingLocation.setLongitude(terminalVO.getLongitude()); + } + if (terminalVO.getLatitude() != null) { + existingLocation.setLatitude(terminalVO.getLatitude()); + } + locationRepository.save(existingLocation); + + // 3. 更新终端映射信息(复用原有findByTerminalId方法查询映射记录) + DeviceTerminalMapping existingMapping = mappingRepository.findByTerminalId(terminalVO.getTerminalId()) + .orElseThrow(() -> new RuntimeException("终端无关联映射信息,无法更新终端名称/状态")); + + if (terminalVO.getTerminalName() != null && !terminalVO.getTerminalName().isEmpty()) { + existingMapping.setTerminalName(terminalVO.getTerminalName()); + } + if (terminalVO.getTerminalStatus() != null) { + existingMapping.setTerminalStatus(terminalVO.getTerminalStatus()); + } + if (terminalVO.getDeviceId() != null) { + existingMapping.setDeviceId(terminalVO.getDeviceId()); + } + if (terminalVO.getInstallDate() != null) { + existingMapping.setInstallDate(terminalVO.getInstallDate()); + } + if (terminalVO.getAreaId() != null && !terminalVO.getAreaId().trim().isEmpty()) { + existingMapping.setAreaId(terminalVO.getAreaId()); // 更新片区ID + } + mappingRepository.save(existingMapping); + + // 4. 封装返回结果 + return assembleTerminalVO(existingLocation, existingMapping); + } + + @Override +@Transactional +public void deleteTerminal(String terminalId) { + // 1. 检查终端映射记录是否存在 + Optional mappingOpt = mappingRepository.findByTerminalId(terminalId); + + if (mappingOpt.isPresent()) { + // 检查是否实际绑定了设备(deviceId不为null) + DeviceTerminalMapping mapping = mappingOpt.get(); + if (mapping.getDeviceId() != null && !mapping.getDeviceId().isEmpty()) { + throw new RuntimeException("终端已绑定设备,无法删除,请先解除设备关联"); + } + } + + // 2. 校验终端是否存在 + if (!locationRepository.existsById(terminalId)) { + throw new RuntimeException("终端不存在,无需删除:" + terminalId); + } + + // 3. 级联删除数据 + mappingRepository.deleteByTerminalId(terminalId); + locationRepository.deleteById(terminalId); +} + + + @Override + public TerminalManageVO getTerminalById(String terminalId) { + // 1. 查询位置信息(复用原有findById方法) + WaterTerminalLocation location = locationRepository.findById(terminalId) + .orElseThrow(() -> new RuntimeException("终端不存在:" + terminalId)); + + // 2. 查询映射信息(复用原有findByTerminalId方法) + DeviceTerminalMapping mapping = mappingRepository.findByTerminalId(terminalId) + .orElseThrow(() -> new RuntimeException("终端无关联基础信息,请补充映射记录")); + + // 3. 封装返回结果 + return assembleTerminalVO(location, mapping); + } + + @Override + public List getTerminalList(String terminalName) { + // 1. 查询映射表数据(支持名称模糊筛选,使用新增的findByTerminalNameContaining方法) + List mappings; + if (terminalName != null && !terminalName.isEmpty()) { + mappings = mappingRepository.findByTerminalNameContaining(terminalName); + } else { + mappings = mappingRepository.findAll(); // 复用原有查询所有方法 + } + + // 2. 遍历映射记录,关联位置表数据,封装VO列表 + List terminalVOList = new ArrayList<>(); + for (DeviceTerminalMapping mapping : mappings) { + Optional locationOpt = locationRepository.findById(mapping.getTerminalId()); + locationOpt.ifPresent(location -> { + TerminalManageVO vo = assembleTerminalVO(location, mapping); + terminalVOList.add(vo); + }); + } + return terminalVOList; + } + + /** + * 辅助方法:整合位置表和映射表数据,封装为TerminalManageVO + */ + private TerminalManageVO assembleTerminalVO(WaterTerminalLocation location, DeviceTerminalMapping mapping) { + TerminalManageVO vo = new TerminalManageVO(); + vo.setTerminalId(location.getTerminalId()); + vo.setLongitude(location.getLongitude()); + vo.setLatitude(location.getLatitude()); + vo.setTerminalName(mapping.getTerminalName()); + vo.setTerminalStatus(mapping.getTerminalStatus()); + vo.setDeviceId(mapping.getDeviceId()); + vo.setInstallDate(mapping.getInstallDate()); + vo.setAreaId(mapping.getAreaId()); // 封装片区ID返回给前端 + return vo; + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/WaterTerminalLocationService.java b/src/main/java/com/campus/water/service/WaterTerminalLocationService.java new file mode 100644 index 0000000..2dcfed8 --- /dev/null +++ b/src/main/java/com/campus/water/service/WaterTerminalLocationService.java @@ -0,0 +1,19 @@ +package com.campus.water.service; + +import com.campus.water.entity.vo.TerminalLocationVO; +import java.util.List; + +/** + * 终端位置服务接口(截图原命名) + */ +public interface WaterTerminalLocationService { + /** + * 获取所有终端位置 + */ + List getAllTerminalLocations(); + + /** + * 获取可用终端位置 + */ + List getAvailableTerminalLocations(); +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/WaterTerminalLocationServiceImpl.java b/src/main/java/com/campus/water/service/WaterTerminalLocationServiceImpl.java new file mode 100644 index 0000000..e21de3f --- /dev/null +++ b/src/main/java/com/campus/water/service/WaterTerminalLocationServiceImpl.java @@ -0,0 +1,75 @@ +package com.campus.water.service; + +import com.campus.water.entity.DeviceTerminalMapping; +import com.campus.water.entity.WaterTerminalLocation; +import com.campus.water.entity.vo.TerminalLocationVO; +import com.campus.water.mapper.DeviceTerminalMappingRepository; +import com.campus.water.mapper.WaterTerminalLocationRepository; +import com.campus.water.service.WaterTerminalLocationService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * 终端位置服务实现类(截图原命名) + */ +@Service +@RequiredArgsConstructor +public class WaterTerminalLocationServiceImpl implements WaterTerminalLocationService { + + private final WaterTerminalLocationRepository waterTerminalLocationRepository; // 截图原命名注入 + private final DeviceTerminalMappingRepository deviceTerminalMappingRepository; + + /** + * 获取所有终端位置(关联映射表补充名称/状态) + */ + @Override + public List getAllTerminalLocations() { + List locationList = waterTerminalLocationRepository.findAll(); + return convertToVO(locationList); + } + + /** + * 获取可用终端位置(筛选映射表active状态) + */ + @Override + public List getAvailableTerminalLocations() { + List locationList = waterTerminalLocationRepository.findAll(); + return convertToVO(locationList).stream() + .filter(TerminalLocationVO::getIsAvailable) + .toList(); + } + + /** + * 转换为VO(截图原逻辑,适配字段调整) + */ + private List convertToVO(List locationList) { + List voList = new ArrayList<>(); + for (WaterTerminalLocation location : locationList) { + TerminalLocationVO vo = new TerminalLocationVO(); + // 位置表核心字段 + vo.setTerminalId(location.getTerminalId()); + vo.setLongitude(location.getLongitude()); + vo.setLatitude(location.getLatitude()); + + // 关联映射表获取名称/状态(替代原isAvailable字段) + Optional mappingOpt = deviceTerminalMappingRepository.findByTerminalId(location.getTerminalId()); + if (mappingOpt.isPresent()) { + DeviceTerminalMapping mapping = mappingOpt.get(); + vo.setTerminalName(mapping.getTerminalName()); + vo.setDeviceStatus(mapping.getTerminalStatus().name()); + vo.setIsAvailable(DeviceTerminalMapping.TerminalStatus.active.equals(mapping.getTerminalStatus())); + } else { + // 默认值(截图原逻辑) + vo.setTerminalName("未配置终端"); + vo.setDeviceStatus("inactive"); + vo.setIsAvailable(false); + } + voList.add(vo); + } + return voList; + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/WorkOrderService.java b/src/main/java/com/campus/water/service/WorkOrderService.java index 4388e78..98a1e82 100644 --- a/src/main/java/com/campus/water/service/WorkOrderService.java +++ b/src/main/java/com/campus/water/service/WorkOrderService.java @@ -1,9 +1,14 @@ package com.campus.water.service; import com.campus.water.entity.WorkOrder; + +import java.time.LocalDateTime; import java.util.List; public interface WorkOrderService { + //按ID获取工单详情 + WorkOrder getOrderDetail(String orderId); + // 抢单 boolean grabOrder(String orderId, String repairmanId); @@ -28,4 +33,8 @@ public interface WorkOrderService { // 按状态查询工单的方法 List getOrdersByStatus(WorkOrder.OrderStatus status); List getOrdersByAreaAndStatus(String areaId, WorkOrder.OrderStatus status); + + // 核心:复用已有Repository的时间范围查询,新增组合条件查询方法 + List getOrdersByConditions(String areaId, WorkOrder.OrderStatus status, + LocalDateTime startTime, LocalDateTime endTime); } \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/WorkOrderServiceImpl.java b/src/main/java/com/campus/water/service/WorkOrderServiceImpl.java index 0e6b182..cb4901d 100644 --- a/src/main/java/com/campus/water/service/WorkOrderServiceImpl.java +++ b/src/main/java/com/campus/water/service/WorkOrderServiceImpl.java @@ -5,25 +5,44 @@ import com.campus.water.entity.Repairman; import com.campus.water.mapper.WorkOrderRepository; import com.campus.water.mapper.RepairmanRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.campus.water.service.NotificationService; +import com.campus.water.service.WorkOrderService; import java.time.LocalDateTime; +import java.util.Arrays; import java.util.List; import java.util.Optional; +/** + * 工单服务实现类 + * + * @author 豆包编程助手 + * @date 2025/12/25 + */ +@Slf4j @Service @RequiredArgsConstructor public class WorkOrderServiceImpl implements WorkOrderService { private final WorkOrderRepository workOrderRepository; private final RepairmanRepository repairmanRepository; + private final NotificationService notificationService; + @Override + public WorkOrder getOrderDetail(String orderId) { + return workOrderRepository.findById(orderId) + .orElseThrow(() -> new RuntimeException("工单不存在")); + } /** * 维修人员抢单功能 * 业务规则:仅允许抢"待处理"状态的工单,且维修人员需处于"空闲"状态 - * @param orderId 工单ID - * @param repairmanId 维修人员ID + * + * @param orderId 工单ID + * @param repairmanId 维修人员ID * @return 抢单成功返回true,否则返回false */ @Override @@ -36,9 +55,9 @@ public class WorkOrderServiceImpl implements WorkOrderService { // 检查工单状态是否为"待处理"(只有待处理的工单可被抢单) if (order.getStatus() == WorkOrder.OrderStatus.pending) { - // 查询维修人员是否存在且状态为"空闲" - var repairman = repairmanRepository.findById(repairmanId); - if (repairman.isPresent() && repairman.get().getStatus() == Repairman.RepairmanStatus.idle) { + // 查询维修人员是否存在 + Optional repairman = repairmanRepository.findById(repairmanId); + if (repairman.isPresent()) { // 更新工单状态:改为"已抢单",记录抢单时间和维修人员ID order.setStatus(WorkOrder.OrderStatus.processing); @@ -46,10 +65,8 @@ public class WorkOrderServiceImpl implements WorkOrderService { order.setGrabbedTime(LocalDateTime.now()); workOrderRepository.save(order); - // 更新维修人员状态:改为"忙碌" - var repairmanEntity = repairman.get(); - repairmanEntity.setStatus(Repairman.RepairmanStatus.busy); - repairmanRepository.save(repairmanEntity); + // 改造点1:不再手动设置维修人员为忙碌,改为动态更新状态 + updateRepairmanStatusByPendingOrders(repairmanId); return true; // 抢单成功 } @@ -61,9 +78,10 @@ public class WorkOrderServiceImpl implements WorkOrderService { /** * 维修人员拒单功能 * 业务规则:仅允许拒绝自己已抢单的工单,拒单后工单回到"待处理"状态 - * @param orderId 工单ID - * @param repairmanId 维修人员ID - * @param reason 拒单原因 + * + * @param orderId 工单ID + * @param repairmanId 维修人员ID + * @param reason 拒单原因 * @return 拒单成功返回true,否则返回false */ @Override @@ -85,12 +103,8 @@ public class WorkOrderServiceImpl implements WorkOrderService { order.setGrabbedTime(null); workOrderRepository.save(order); - // 重置维修人员状态:改为"空闲" - var repairman = repairmanRepository.findById(repairmanId); - repairman.ifPresent(rm -> { - rm.setStatus(Repairman.RepairmanStatus.idle); - repairmanRepository.save(rm); - }); + // 改造点2:拒单后动态更新维修人员状态(而非直接设为空闲) + updateRepairmanStatusByPendingOrders(repairmanId); return true; // 拒单成功 } @@ -101,10 +115,11 @@ public class WorkOrderServiceImpl implements WorkOrderService { /** * 提交维修结果 * 业务规则:仅允许提交自己负责的"已抢单"或"处理中"状态工单 - * @param orderId 工单ID - * @param repairmanId 维修人员ID - * @param dealNote 处理备注 - * @param imgUrl 维修照片URL(可选) + * + * @param orderId 工单ID + * @param repairmanId 维修人员ID + * @param dealNote 处理备注 + * @param imgUrl 维修照片URL(可选) * @return 提交成功返回true,否则返回false */ @Override @@ -118,22 +133,22 @@ public class WorkOrderServiceImpl implements WorkOrderService { // 校验权限:必须是当前工单的已分配维修人员,且工单状态为"已抢单"或"处理中" if (order.getAssignedRepairmanId() != null && order.getAssignedRepairmanId().equals(repairmanId) && - (order.getStatus() == WorkOrder.OrderStatus.processing || - order.getStatus() == WorkOrder.OrderStatus.processing)) { + (order.getStatus() == WorkOrder.OrderStatus.processing)) { - // 更新工单状态为"已完成",记录完成时间和维修信息 - order.setStatus(WorkOrder.OrderStatus.completed); + // 更新工单状态为"待审核"(关键修改) + order.setStatus(WorkOrder.OrderStatus.reviewing); order.setCompletedTime(LocalDateTime.now()); order.setDealNote(dealNote); - order.setImgUrl(imgUrl); // 允许为空(可选参数) + order.setImgUrl(imgUrl); workOrderRepository.save(order); - // 更新维修人员状态为"空闲",并增加工作量 + // 改造点3:提交结果后动态更新维修人员状态(而非直接设为空闲) + // 同时保留工作量统计逻辑 repairmanRepository.findById(repairmanId).ifPresent(rm -> { - rm.setStatus(Repairman.RepairmanStatus.idle); rm.setWorkCount(rm.getWorkCount() + 1); repairmanRepository.save(rm); }); + updateRepairmanStatusByPendingOrders(repairmanId); return true; // 提交成功 } @@ -151,21 +166,21 @@ public class WorkOrderServiceImpl implements WorkOrderService { Optional orderOpt = workOrderRepository.findById(orderId); if (orderOpt.isPresent()) { WorkOrder order = orderOpt.get(); - // 仅处理"待审核"状态的工单 if (order.getStatus() == WorkOrder.OrderStatus.reviewing) { if (approved) { - // 审核通过:改为"已完成",并更新维修人员工作量 + // 审核通过:改为"已完成" order.setStatus(WorkOrder.OrderStatus.completed); + // 改造点4:审核通过后,动态更新维修人员状态 if (order.getAssignedRepairmanId() != null) { - repairmanRepository.findById(order.getAssignedRepairmanId()) - .ifPresent(rm -> { - rm.setWorkCount(rm.getWorkCount() + 1); - repairmanRepository.save(rm); - }); + updateRepairmanStatusByPendingOrders(order.getAssignedRepairmanId()); } } else { // 审核不通过:退回"处理中" order.setStatus(WorkOrder.OrderStatus.processing); + // 改造点5:审核不通过后,动态更新维修人员状态(而非直接设为忙碌) + if (order.getAssignedRepairmanId() != null) { + updateRepairmanStatusByPendingOrders(order.getAssignedRepairmanId()); + } } workOrderRepository.save(order); return true; @@ -174,9 +189,48 @@ public class WorkOrderServiceImpl implements WorkOrderService { return false; } + /** + * 超时工单检查任务(改为静态内部类,解决@Slf4j报错问题) + * 注意:需确保Spring开启了定时任务(@EnableScheduling) + */ + @Slf4j + @Service // 单独注册为Spring Bean,替代@Component + @RequiredArgsConstructor + public static class WorkOrderTimeoutTask { + + private final WorkOrderRepository workOrderRepository; + + // 每小时检查一次超时工单 + @Scheduled(fixedRate = 3600000) + @Transactional + public void checkTimeoutOrders() { + log.info("开始检查超时工单"); + + // 查询所有待处理和处理中的工单 + List activeOrders = workOrderRepository.findByStatusIn( + Arrays.asList(WorkOrder.OrderStatus.pending, WorkOrder.OrderStatus.processing) + ); + + LocalDateTime now = LocalDateTime.now(); + for (WorkOrder order : activeOrders) { + // 检查是否设置了截止时间且已超时 + if (order.getDeadline() != null && now.isAfter(order.getDeadline())) { + order.setStatus(WorkOrder.OrderStatus.timeout); + workOrderRepository.save(order); + log.info("工单{}已超时,状态已更新为超时", order.getOrderId()); + } + } + + log.info("超时工单检查完成,共检查{}个工单,其中{}个超时", + activeOrders.size(), + activeOrders.stream().filter(o -> o.getDeadline() != null && now.isAfter(o.getDeadline())).count()); + } + } + /** * 获取可抢工单列表 * 业务规则:只返回指定区域内"待处理"状态的工单 + * * @param areaId 区域ID * @return 符合条件的工单列表 */ @@ -194,6 +248,7 @@ public class WorkOrderServiceImpl implements WorkOrderService { /** * 获取维修人员自己的工单 * 业务规则:返回所有分配给该维修人员的工单(不限制状态) + * * @param repairmanId 维修人员ID * @return 该维修人员负责的工单列表 */ @@ -205,8 +260,9 @@ public class WorkOrderServiceImpl implements WorkOrderService { /** * 管理员手动派单功能 * 业务规则:仅管理员可操作,只能分配"待处理"或"超时"状态的工单给"空闲"的维修人员 - * @param orderId 工单ID - * @param repairmanId 维修人员ID + * + * @param orderId 工单ID + * @param repairmanId 维修人员ID * @return 派单成功返回true,否则返回false */ @Override @@ -221,19 +277,27 @@ public class WorkOrderServiceImpl implements WorkOrderService { if (order.getStatus() == WorkOrder.OrderStatus.pending || order.getStatus() == WorkOrder.OrderStatus.timeout) { - // 查询维修人员是否存在且状态为"空闲" + // 查询维修人员是否存在 Optional repairmanOpt = repairmanRepository.findById(repairmanId); - if (repairmanOpt.isPresent() && repairmanOpt.get().getStatus() == Repairman.RepairmanStatus.idle) { + if (repairmanOpt.isPresent()) { // 更新工单状态 order.setStatus(WorkOrder.OrderStatus.processing); order.setAssignedRepairmanId(repairmanId); order.setGrabbedTime(LocalDateTime.now()); workOrderRepository.save(order); - // 更新维修人员状态 - Repairman repairman = repairmanOpt.get(); - repairman.setStatus(Repairman.RepairmanStatus.busy); - repairmanRepository.save(repairman); + // ===== 新增:派单通知 ===== + try { + String notificationContent = String.format("您有新的维修工单待处理!工单ID:%s", orderId); + notificationService.sendOrderAssignedNotification(repairmanId, orderId, notificationContent); + } catch (Exception e) { + // 捕获异常,不影响原有派单逻辑 + System.err.println("派单通知发送失败:" + e.getMessage()); + } + // ============================================== + + // 改造点6:派单后动态更新维修人员状态(而非直接设为忙碌) + updateRepairmanStatusByPendingOrders(repairmanId); return true; // 派单成功 } @@ -242,7 +306,6 @@ public class WorkOrderServiceImpl implements WorkOrderService { return false; // 派单失败(工单不存在/状态异常/维修人员不满足条件) } - // 按状态查询工单的方法 @Override public List getOrdersByStatus(WorkOrder.OrderStatus status) { return workOrderRepository.findByStatus(status); @@ -252,4 +315,60 @@ public class WorkOrderServiceImpl implements WorkOrderService { public List getOrdersByAreaAndStatus(String areaId, WorkOrder.OrderStatus status) { return workOrderRepository.findByAreaIdAndStatus(areaId, status); } + + @Override + public List getOrdersByConditions(String areaId, WorkOrder.OrderStatus status, + LocalDateTime startTime, LocalDateTime endTime) { + // 分步组合查询条件,核心复用已有findByCreatedTimeBetween方法 + List timeRangeOrders = workOrderRepository.findByCreatedTimeBetween(startTime, endTime); + + // 过滤区域和状态(如果传了这些参数) + return timeRangeOrders.stream() + .filter(order -> { + // 区域过滤:如果传了areaId则匹配,没传则不过滤 + boolean areaMatch = (areaId == null || areaId.trim().isEmpty()) + || order.getAreaId().equals(areaId); + // 状态过滤:如果传了status则匹配,没传则不过滤 + boolean statusMatch = (status == null) + || order.getStatus() == status; + return areaMatch && statusMatch; + }) + .toList(); + } + + // ===================== 新增核心辅助方法 ===================== + /** + * 动态更新维修人员状态:根据待处理工单数量自动判定 + * 规则:无待处理工单(pending/processing/reviewing)= 空闲;有待处理工单=忙碌 + * @param repairmanId 维修人员ID + */ + @Transactional + private void updateRepairmanStatusByPendingOrders(String repairmanId) { + // 1. 定义「待处理工单」状态集合(根据业务调整,此处包含待抢、已抢、待审核) + List pendingStatuses = Arrays.asList( + WorkOrder.OrderStatus.pending, + WorkOrder.OrderStatus.processing, + WorkOrder.OrderStatus.reviewing + ); + + // 2. 查询该维修人员的待处理工单数量(需在WorkOrderRepository中定义该方法) + long pendingOrderCount = workOrderRepository.countByAssignedRepairmanIdAndStatusIn(repairmanId, pendingStatuses); + + // 3. 动态更新维修人员状态 + Optional repairmanOpt = repairmanRepository.findById(repairmanId); + if (repairmanOpt.isPresent()) { + Repairman repairman = repairmanOpt.get(); + if (pendingOrderCount <= 0) { + // 无待处理工单 → 空闲状态 + repairman.setStatus(Repairman.RepairmanStatus.idle); + } else { + // 有待处理工单 → 忙碌状态 + repairman.setStatus(Repairman.RepairmanStatus.busy); + } + repairmanRepository.save(repairman); + log.debug("维修人员{}状态更新完成,当前待处理工单数量:{},状态:{}", + repairmanId, pendingOrderCount, repairman.getStatus()); + } + } + } \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/app/StudentAppService.java b/src/main/java/com/campus/water/service/app/StudentAppService.java index 8615e60..c0d57ab 100644 --- a/src/main/java/com/campus/water/service/app/StudentAppService.java +++ b/src/main/java/com/campus/water/service/app/StudentAppService.java @@ -18,8 +18,8 @@ public class StudentAppService { @PreAuthorize("hasAnyRole('STUDENT', 'SUPER_ADMIN', 'AREA_ADMIN', 'VIEWER')") public ResultVO> getTerminalInfo(String terminalId) { try { - Map result = waterUsageController.getTerminalInfo(terminalId); - return ResultVO.success(result); + // 直接返回控制器的ResultVO + return waterUsageController.getTerminalInfo(terminalId); } catch (Exception e) { return ResultVO.error(500, "获取终端信息失败: " + e.getMessage()); } @@ -33,8 +33,8 @@ public class StudentAppService { String studentId = (String) request.get("studentId"); Double waterConsumption = Double.valueOf(request.get("waterConsumption").toString()); - Map result = waterUsageController.scanToDrink(terminalId, studentId, waterConsumption); - return ResultVO.success(result); + // 直接返回控制器的ResultVO + return waterUsageController.scanToDrink(terminalId, studentId, waterConsumption); } catch (Exception e) { return ResultVO.error(500, "用水操作失败: " + e.getMessage()); } @@ -44,8 +44,8 @@ public class StudentAppService { @PreAuthorize("hasAnyRole('STUDENT', 'SUPER_ADMIN', 'AREA_ADMIN', 'VIEWER')") public ResultVO> getWaterQuality(String deviceId) { try { - Map result = waterUsageController.getWaterQualityInfo(deviceId); - return ResultVO.success(result); + // 直接返回控制器的ResultVO + return waterUsageController.getWaterQualityInfo(deviceId); } catch (Exception e) { return ResultVO.error(500, "获取水质信息失败: " + e.getMessage()); } diff --git a/src/main/java/com/campus/water/service/先读我.md b/src/main/java/com/campus/water/service/先读我.md deleted file mode 100644 index 3b52c1f..0000000 --- a/src/main/java/com/campus/water/service/先读我.md +++ /dev/null @@ -1 +0,0 @@ -# 本md仅用于初始化目录,未创建所有子一级目录,在当前目录创建文件后请自行删除 \ No newline at end of file diff --git a/src/main/java/com/campus/water/task/SensorSimulationTask.java b/src/main/java/com/campus/water/task/SensorSimulationTask.java index 4835f8e..003b586 100644 --- a/src/main/java/com/campus/water/task/SensorSimulationTask.java +++ b/src/main/java/com/campus/water/task/SensorSimulationTask.java @@ -1,68 +1,128 @@ package com.campus.water.task; +import com.campus.water.entity.Device; +import com.campus.water.mapper.DeviceRepository; import com.campus.water.service.MqttSensorSender; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import java.util.ArrayList; import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; @Component @RequiredArgsConstructor @Slf4j public class SensorSimulationTask { private final MqttSensorSender mqttSensorSender; - - // 模拟已部署的设备列表(实际项目可从数据库查询) - private final List waterMakerDevices = new ArrayList<>() {{ - add("WM001"); // 制水机1 - add("WM002"); // 制水机2 - add("WM003"); // 制水机3 - add("WM004"); // 制水机4 - }}; - - private final List waterSupplyDevices = new ArrayList<>() {{ - add("WS001"); // 供水机1 - add("WS002"); // 供水机2 - add("WS003"); // 供水机3 - }}; + private final DeviceRepository deviceRepository; + private final Random random = new Random(); // 新增随机数生成器 /** - * 定时发送「正常状态数据」:每30秒执行一次 - * fixedRate:固定间隔(从上一次任务开始时间计算) + * 定时发送「正常状态数据」:每30分钟执行一次 */ - @Scheduled(fixedRate = 30000) + @Scheduled(fixedRate = 1800000) // 30分钟 = 30*60*1000 = 1800000毫秒 public void sendRegularStateData() { log.info("=== 开始发送设备正常状态数据 ==="); - // 1. 发送所有制水机状态 - for (String deviceId : waterMakerDevices) { - mqttSensorSender.sendWaterMakerState(deviceId); + // 1. 查询制水机设备 + List waterMakerDevices = deviceRepository.findByDeviceType(Device.DeviceType.water_maker) + .stream() + .map(Device::getDeviceId) + .collect(Collectors.toList()); + + // 2. 分批次发送制水机数据(每批间隔1-3秒) + if (!waterMakerDevices.isEmpty()) { + sendInBatches(waterMakerDevices, true); + } else { + log.warn("⚠️ device表中无「water_maker」类型设备"); } - // 2. 发送所有供水机状态 - for (String deviceId : waterSupplyDevices) { - mqttSensorSender.sendWaterSupplyData(deviceId); + // 3. 查询供水机设备 + List waterSupplyDevices = deviceRepository.findByDeviceType(Device.DeviceType.water_supply) + .stream() + .map(Device::getDeviceId) + .collect(Collectors.toList()); + + // 4. 分批次发送供水机数据(每批间隔1-3秒) + if (!waterSupplyDevices.isEmpty()) { + sendInBatches(waterSupplyDevices, false); + } else { + log.warn("⚠️ device表中无「water_supply」类型设备"); } log.info("=== 设备正常状态数据发送完成 ==="); } /** - * 定时发送「随机告警数据」:每5分钟执行一次 - * fixedRate:固定间隔(从上一次任务开始时间计算) + * 分批次发送设备数据 + * @param deviceIds 设备ID列表 + * @param isWaterMaker 是否为制水机 + */ + private void sendInBatches(List deviceIds, boolean isWaterMaker) { + int batchSize = 3; // 每批发送3台设备 + int delayBetweenBatches = 1000 + random.nextInt(2000); // 1-3秒随机间隔 + + for (int i = 0; i < deviceIds.size(); i++) { + String deviceId = deviceIds.get(i); + // 发送当前设备数据 + if (isWaterMaker) { + mqttSensorSender.sendWaterMakerState(deviceId); + } else { + mqttSensorSender.sendWaterSupplyData(deviceId); + } + + // 每发送batchSize台设备后休眠指定时间(最后一批除外) + if ((i + 1) % batchSize == 0 && i != deviceIds.size() - 1) { + try { + Thread.sleep(delayBetweenBatches); + } catch (InterruptedException e) { + log.error("批次发送休眠被中断", e); + Thread.currentThread().interrupt(); // 恢复中断状态 + } + } + } + } + + /** + * 定时发送「随机告警数据」:每5分钟执行一次(同时包含制水机和供水机) */ @Scheduled(fixedRate = 300000) public void sendRandomWarningData() { log.info("=== 开始发送随机告警数据 ==="); - // 随机选择1台制水机发送告警(模拟设备故障) - int randomIndex = (int) (Math.random() * waterMakerDevices.size()); - String targetDevice = waterMakerDevices.get(randomIndex); - mqttSensorSender.sendWaterMakerWarning(targetDevice); + // 处理制水机告警 + List waterMakerDevices = deviceRepository.findByDeviceType(Device.DeviceType.water_maker) + .stream() + .map(Device::getDeviceId) + .collect(Collectors.toList()); + + if (!waterMakerDevices.isEmpty()) { + int randomMakerIndex = random.nextInt(waterMakerDevices.size()); + String targetMakerDevice = waterMakerDevices.get(randomMakerIndex); + mqttSensorSender.sendWaterMakerWarning(targetMakerDevice); + log.info("制水机告警发送完成 | 告警设备:{}", targetMakerDevice); + } else { + log.warn("⚠️ device表中无「water_maker」类型设备,跳过制水机告警发送"); + } + + // 新增:处理供水机告警 + List waterSupplyDevices = deviceRepository.findByDeviceType(Device.DeviceType.water_supply) + .stream() + .map(Device::getDeviceId) + .collect(Collectors.toList()); + + if (!waterSupplyDevices.isEmpty()) { + int randomSupplyIndex = random.nextInt(waterSupplyDevices.size()); + String targetSupplyDevice = waterSupplyDevices.get(randomSupplyIndex); + mqttSensorSender.sendWaterSupplyWarning(targetSupplyDevice); // 调用新增的供水机告警发送方法 + log.info("供水机告警发送完成 | 告警设备:{}", targetSupplyDevice); + } else { + log.warn("⚠️ device表中无「water_supply」类型设备,跳过供水机告警发送"); + } - log.info("=== 随机告警数据发送完成 | 告警设备:{} ===", targetDevice); + log.info("=== 随机告警数据发送完成 ==="); } } \ No newline at end of file diff --git a/src/main/java/com/campus/water/util/CommonUtils.java b/src/main/java/com/campus/water/util/CommonUtils.java new file mode 100644 index 0000000..615690a --- /dev/null +++ b/src/main/java/com/campus/water/util/CommonUtils.java @@ -0,0 +1,803 @@ +package com.campus.water.util; +import java.time.LocalDate; +import lombok.extern.slf4j.Slf4j; +import com.campus.water.entity.Device; +import com.campus.water.entity.Device.DeviceType; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * 校园直饮水设备通用工具类 + * 适配设备数据处理、格式转换、参数校验等通用场景 + */ +@Slf4j +public class CommonUtils { + + // ========== 设备字符串处理方法 ========== + /** + * 设备ID格式校验(WM/WS开头 + 3位数字) + */ + public static boolean validateDeviceId(String deviceId) { + if (deviceId == null || deviceId.length() != 5) { + return false; + } + String prefix = deviceId.substring(0, 2); + String suffix = deviceId.substring(2); + if (!("WM".equals(prefix) || "WS".equals(prefix))) { + return false; + } + try { + Integer.parseInt(suffix); + return true; + } catch (NumberFormatException e) { + log.warn("设备ID后缀非数字:{}", deviceId); + return false; + } + } + + /** + * 设备ID脱敏处理(如WM001 → WM*01) + */ + public static String desensitizeDeviceId(String deviceId) { + if (!validateDeviceId(deviceId)) { + return deviceId; + } + return deviceId.substring(0, 2) + "*" + deviceId.substring(3); + } + + /** + * 安装位置字符串格式化(去除多余空格、统一格式) + */ + public static String formatInstallLocation(String location) { + if (location == null) { + return "未配置安装位置"; + } + // 去除首尾空格、替换全角空格、多个空格合并为一个 + String formatted = location.trim().replace(" ", " ").replaceAll("\\s+", " "); + return formatted.isEmpty() ? "未配置安装位置" : formatted; + } + + /** + * 设备备注信息长度限制(最多50字) + */ + public static String limitRemarkLength(String remark) { + if (remark == null) { + return ""; + } + if (remark.length() <= 50) { + return remark; + } + return remark.substring(0, 50) + "..."; + } + + /** + * 拼接设备完整名称(类型+ID+名称) + */ + public static String concatDeviceFullName(Device device) { + if (device == null) { + return ""; + } + String typeName = DeviceType.water_maker.equals(device.getDeviceType()) ? "制水机" : "供水机"; + return String.format("[%s]%s-%s", typeName, device.getDeviceId(), device.getDeviceName()); + } + + /** + * 判断字符串是否为空白(包含全角空格) + */ + public static boolean isBlankWithFullWidth(String str) { + if (str == null) { + return true; + } + return str.replace(" ", "").trim().isEmpty(); + } + + /** + * 字符串非空判断(包含全角空格处理) + */ + public static boolean isNotBlankWithFullWidth(String str) { + return !isBlankWithFullWidth(str); + } + + /** + * 设备状态字符串转换(枚举转中文) + */ + public static String convertDeviceStatusToCn(String status) { + if (status == null) { + return "未知状态"; + } + return switch (status) { + case "online" -> "在线"; + case "offline" -> "离线"; + case "fault" -> "故障"; + default -> "未知状态"; + }; + } + + /** + * 设备类型字符串转换(枚举转中文) + */ + public static String convertDeviceTypeToCn(DeviceType type) { + if (type == null) { + return "未知类型"; + } + return DeviceType.water_maker.equals(type) ? "制水机" : "供水机"; + } + + /** + * 字符串转设备类型枚举 + */ + public static DeviceType convertStrToDeviceType(String typeStr) { + if (typeStr == null) { + return null; + } + return switch (typeStr.toLowerCase()) { + case "water_maker", "制水机" -> DeviceType.water_maker; + case "water_supply", "供水机" -> DeviceType.water_supply; + default -> null; + }; + } + + // ========== 设备数字处理方法 ========== + /** + * 传感器数值校验(非负) + */ + public static boolean validateSensorValue(BigDecimal value) { + return value != null && value.compareTo(BigDecimal.ZERO) >= 0; + } + + /** + * TDS值保留1位小数(适配传感器数据展示) + */ + public static BigDecimal formatTdsValue(BigDecimal tdsValue) { + if (!validateSensorValue(tdsValue)) { + return BigDecimal.ZERO; + } + return tdsValue.setScale(1, RoundingMode.HALF_UP); + } + + /** + * 水压值保留2位小数(适配传感器数据展示) + */ + public static BigDecimal formatWaterPress(BigDecimal press) { + if (!validateSensorValue(press)) { + return BigDecimal.ZERO; + } + return press.setScale(2, RoundingMode.HALF_UP); + } + + /** + * 水位值百分比格式化(0-100) + */ + public static BigDecimal formatWaterLevel(BigDecimal level) { + if (level == null) { + return BigDecimal.ZERO; + } + if (level.compareTo(BigDecimal.ZERO) < 0) { + return BigDecimal.ZERO; + } + if (level.compareTo(new BigDecimal(100)) > 0) { + return new BigDecimal(100); + } + return level.setScale(1, RoundingMode.HALF_UP); + } + + /** + * 温度值范围校验(0-100℃) + */ + public static BigDecimal formatTemperature(BigDecimal temp) { + if (temp == null) { + return BigDecimal.ZERO; + } + if (temp.compareTo(BigDecimal.ZERO) < 0) { + return BigDecimal.ZERO; + } + if (temp.compareTo(new BigDecimal(100)) > 0) { + return new BigDecimal(100); + } + return temp.setScale(1, RoundingMode.HALF_UP); + } + + /** + * 滤芯寿命百分比格式化(0-100) + */ + public static Integer formatFilterLife(Integer life) { + if (life == null) { + return 0; + } + if (life < 0) { + return 0; + } + if (life > 100) { + return 100; + } + return life; + } + + /** + * 数字相加(Integer,空值处理) + */ + public static Integer add(Integer num1, Integer num2) { + if (num1 == null) num1 = 0; + if (num2 == null) num2 = 0; + return num1 + num2; + } + + /** + * 数字相加(BigDecimal,空值处理) + */ + public static BigDecimal add(BigDecimal num1, BigDecimal num2) { + if (num1 == null) num1 = BigDecimal.ZERO; + if (num2 == null) num2 = BigDecimal.ZERO; + return num1.add(num2); + } + + /** + * 数字相减(BigDecimal,空值处理) + */ + public static BigDecimal subtract(BigDecimal num1, BigDecimal num2) { + if (num1 == null) num1 = BigDecimal.ZERO; + if (num2 == null) num2 = BigDecimal.ZERO; + return num1.subtract(num2); + } + + /** + * 数字比较(BigDecimal,空值视为0) + */ + public static int compare(BigDecimal num1, BigDecimal num2) { + if (num1 == null) num1 = BigDecimal.ZERO; + if (num2 == null) num2 = BigDecimal.ZERO; + return num1.compareTo(num2); + } + + /** + * 获取两个数的最大值(BigDecimal) + */ + public static BigDecimal max(BigDecimal num1, BigDecimal num2) { + return compare(num1, num2) >= 0 ? num1 : num2; + } + + /** + * 获取两个数的最小值(BigDecimal) + */ + public static BigDecimal min(BigDecimal num1, BigDecimal num2) { + return compare(num1, num2) <= 0 ? num1 : num2; + } + + // ========== 设备集合处理方法 ========== + /** + * 设备列表按类型筛选 + */ + public static List filterDeviceByType(List deviceList, DeviceType type) { + List result = new ArrayList<>(); + if (deviceList == null || deviceList.isEmpty() || type == null) { + return result; + } + for (Device device : deviceList) { + if (type.equals(device.getDeviceType())) { + result.add(device); + } + } + return result; + } + + /** + * 设备列表转设备ID集合 + */ + public static Set convertDeviceListToIdSet(List deviceList) { + Set idSet = new HashSet<>(); + if (deviceList == null || deviceList.isEmpty()) { + return idSet; + } + for (Device device : deviceList) { + if (validateDeviceId(device.getDeviceId())) { + idSet.add(device.getDeviceId()); + } + } + return idSet; + } + + /** + * 设备列表按ID排序 + */ + public static List sortDeviceById(List deviceList) { + List result = new ArrayList<>(); + if (deviceList == null || deviceList.isEmpty()) { + return result; + } + result.addAll(deviceList); + result.sort((d1, d2) -> { + if (!validateDeviceId(d1.getDeviceId())) { + return 1; + } + if (!validateDeviceId(d2.getDeviceId())) { + return -1; + } + return d1.getDeviceId().compareTo(d2.getDeviceId()); + }); + return result; + } + + /** + * 批量获取设备名称(设备ID→名称映射) + */ + public static Map getDeviceNameMap(List deviceList) { + Map nameMap = new HashMap<>(); + if (deviceList == null || deviceList.isEmpty()) { + return nameMap; + } + for (Device device : deviceList) { + if (validateDeviceId(device.getDeviceId())) { + nameMap.put(device.getDeviceId(), device.getDeviceName()); + } + } + return nameMap; + } + + /** + * 列表判空 + */ + public static boolean isEmpty(List list) { + return list == null || list.isEmpty(); + } + + /** + * 列表非空判断 + */ + public static boolean isNotEmpty(List list) { + return !isEmpty(list); + } + + /** + * Map判空 + */ + public static boolean isEmpty(Map map) { + return map == null || map.isEmpty(); + } + + /** + * 获取列表第一个元素(空值处理) + */ + public static T getFirst(List list) { + if (isEmpty(list)) { + return null; + } + return list.get(0); + } + + /** + * 列表去重(基于equals) + */ + public static List distinct(List list) { + if (isEmpty(list)) { + return new ArrayList<>(); + } + return new ArrayList<>(new LinkedHashSet<>(list)); + } + + // ========== 日期时间处理方法 ========== + /** + * 设备数据时间格式化(yyyy-MM-dd HH:mm:ss) + */ + public static String formatDeviceDataTime(LocalDateTime time) { + if (time == null) { + return ""; + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return time.format(formatter); + } + + /** + * 安装日期格式化(yyyy-MM-dd) + */ + public static String formatInstallDate(Date date) { + if (date == null) { + return ""; + } + LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), TimeZone.getDefault().toZoneId()); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + return localDateTime.format(formatter); + } + + /** + * 获取当前时间戳(秒) + */ + public static long getCurrentTimeSeconds() { + return System.currentTimeMillis() / 1000; + } + + /** + * 计算两个时间的间隔(分钟) + */ + public static long calculateMinuteInterval(LocalDateTime startTime, LocalDateTime endTime) { + if (startTime == null || endTime == null) { + return 0; + } + return java.time.Duration.between(startTime, endTime).toMinutes(); + } + + /** + * 判断是否为今日(LocalDateTime) + */ + public static boolean isToday(LocalDateTime time) { + if (time == null) { + return false; + } + LocalDate today = LocalDate.now(); + LocalDate targetDate = time.toLocalDate(); + return today.equals(targetDate); + } + + /** + * 获取当日开始时间(00:00:00) + */ + public static LocalDateTime getTodayStartTime() { + return LocalDate.now().atStartOfDay(); + } + + /** + * 获取当日结束时间(23:59:59) + */ + public static LocalDateTime getTodayEndTime() { + return LocalDate.now().atTime(23, 59, 59); + } + + // ========== 设备数据随机生成(模拟测试用) ========== + /** + * 生成模拟TDS值(50-80) + */ + public static BigDecimal generateMockTdsValue() { + Random random = new Random(); + double value = 50 + random.nextDouble() * 30; + return new BigDecimal(value).setScale(1, RoundingMode.HALF_UP); + } + + /** + * 生成模拟水压值(0.1-0.5) + */ + public static BigDecimal generateMockWaterPress() { + Random random = new Random(); + double value = 0.1 + random.nextDouble() * 0.4; + return new BigDecimal(value).setScale(2, RoundingMode.HALF_UP); + } + + /** + * 生成模拟水位值(30-100) + */ + public static BigDecimal generateMockWaterLevel() { + Random random = new Random(); + double value = 30 + random.nextDouble() * 70; + return new BigDecimal(value).setScale(1, RoundingMode.HALF_UP); + } + + /** + * 生成模拟温度值(18-25) + */ + public static BigDecimal generateMockTemperature() { + Random random = new Random(); + double value = 18 + random.nextDouble() * 7; + return new BigDecimal(value).setScale(1, RoundingMode.HALF_UP); + } + + /** + * 生成模拟滤芯寿命(0-100) + */ + public static Integer generateMockFilterLife() { + Random random = new Random(); + return random.nextInt(101); + } + + /** + * 随机生成设备ID + */ + public static String generateMockDeviceId(DeviceType type) { + Random random = new Random(); + String prefix = DeviceType.water_maker.equals(type) ? "WM" : "WS"; + int num = random.nextInt(900) + 100; // 100-999 + return prefix + num; + } + + /** + * 随机生成设备名称 + */ + public static String generateMockDeviceName(DeviceType type) { + String[] makerNames = {"教学楼1号制水机", "图书馆制水机", "食堂制水机", "宿舍1号楼制水机", "办公楼制水机"}; + String[] supplyNames = {"教学楼1号供水机", "图书馆供水机", "食堂供水机", "宿舍1号楼供水机", "办公楼供水机"}; + Random random = new Random(); + if (DeviceType.water_maker.equals(type)) { + return makerNames[random.nextInt(makerNames.length)]; + } else { + return supplyNames[random.nextInt(supplyNames.length)]; + } + } + + // ========== 其他通用方法 ========== + /** + * 安全休眠(捕获中断异常) + */ + public static void safeSleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + log.error("线程休眠被中断", e); + Thread.currentThread().interrupt(); + } + } + + /** + * 空值替换 + */ + public static T defaultIfNull(T value, T defaultValue) { + return value == null ? defaultValue : value; + } + + /** + * 生成32位随机UUID(无横线) + */ + public static String generateUUID() { + return UUID.randomUUID().toString().replace("-", ""); + } + + /** + * 设备告警级别转换(数字→中文) + */ + public static String convertAlertLevelToCn(Integer level) { + if (level == null) { + return "普通"; + } + return switch (level) { + case 1 -> "紧急"; + case 2 -> "重要"; + case 3 -> "普通"; + default -> "普通"; + }; + } + + + /** + * 设备MAC地址格式校验(12位十六进制,支持冒号/横线分隔或无分隔) + */ + public static boolean validateMacAddress(String mac) { + if (isBlankWithFullWidth(mac)) { + return false; + } + // 去除分隔符,转为纯12位十六进制字符串 + String pureMac = mac.replace(":", "").replace("-", "").trim().toLowerCase(); + if (pureMac.length() != 12) { + return false; + } + return pureMac.matches("[0-9a-f]+"); + } + + /** + * 设备IP地址格式校验(IPv4) + */ + public static boolean validateIpv4Address(String ip) { + if (isBlankWithFullWidth(ip)) { + return false; + } + String[] ipSegments = ip.split("\\."); + if (ipSegments.length != 4) { + return false; + } + try { + for (String segment : ipSegments) { + int num = Integer.parseInt(segment); + if (num < 0 || num > 255) { + return false; + } + } + return true; + } catch (NumberFormatException e) { + log.warn("IP地址格式错误:{}", ip); + return false; + } + } + + /** + * 批量校验设备ID(返回无效ID列表) + */ + public static List batchValidateDeviceId(List deviceIdList) { + List invalidIds = new ArrayList<>(); + if (isEmpty(deviceIdList)) { + return invalidIds; + } + for (String deviceId : deviceIdList) { + if (!validateDeviceId(deviceId)) { + invalidIds.add(deviceId); + } + } + return invalidIds; + } + /** + * 设备状态转换(中文转枚举/英文) + */ + public static String convertCnToDeviceStatus(String cnStatus) { + if (isBlankWithFullWidth(cnStatus)) { + return "unknown"; + } + return switch (cnStatus.trim()) { + case "在线" -> "online"; + case "离线" -> "offline"; + case "故障" -> "fault"; + default -> "unknown"; + }; + } + + + /** + * 设备数据单位转换(MPa转Bar,1MPa=10Bar) + */ + public static BigDecimal convertMpaToBar(BigDecimal mpa) { + if (!validateSensorValue(mpa)) { + return BigDecimal.ZERO; + } + return mpa.multiply(new BigDecimal("10")).setScale(2, RoundingMode.HALF_UP); + } + + /** + * 设备数据单位转换(Bar转MPa) + */ + public static BigDecimal convertBarToMpa(BigDecimal bar) { + if (!validateSensorValue(bar)) { + return BigDecimal.ZERO; + } + return bar.divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP); + } + + /** + * 数字转中文数字(0-100,适配滤芯寿命/水位等展示) + */ + public static String convertNumToCn(Integer num) { + if (num == null || num < 0 || num > 100) { + return "零"; + } + String[] cnNums = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"}; + if (num <= 10) { + return cnNums[num]; + } else if (num < 20) { + return "十" + cnNums[num - 10]; + } else if (num % 10 == 0) { + return cnNums[num / 10] + "十"; + } else { + return cnNums[num / 10] + "十" + cnNums[num % 10]; + } + } + /** + * 工单状态转换(枚举转中文) + */ + public static String convertOrderStatusToCn(String status) { + if (isBlankWithFullWidth(status)) { + return "未知状态"; + } + return switch (status.toLowerCase()) { + case "pending" -> "待处理"; + case "processing" -> "处理中"; + case "completed" -> "已完成"; + case "cancelled" -> "已取消"; + default -> "未知状态"; + }; + } + + /** + * 工单类型转换(枚举转中文) + */ + public static String convertOrderTypeToCn(String type) { + if (isBlankWithFullWidth(type)) { + return "未知类型"; + } + return switch (type.toLowerCase()) { + case "repair" -> "故障维修"; + case "maintenance" -> "定期保养"; + case "inspection" -> "设备巡检"; + default -> "未知类型"; + }; + } + + + + + /** + * 生成模拟告警信息(基于设备类型) + */ + public static String generateMockAlertMessage(DeviceType type) { + String[] makerAlerts = { + "原水TDS值过高,超出阈值", + "纯水TDS值异常,滤芯可能失效", + "水压过低,设备无法正常制水", + "设备检测到漏水,需紧急处理", + "滤芯寿命不足,需尽快更换" + }; + String[] supplyAlerts = { + "水位过低,需及时补水", + "水压异常,供水不稳定", + "水温过高,设备散热异常", + "出水流量过低,可能堵塞", + "设备离线,通信中断" + }; + Random random = new Random(); + if (DeviceType.water_maker.equals(type)) { + return makerAlerts[random.nextInt(makerAlerts.length)]; + } else { + return supplyAlerts[random.nextInt(supplyAlerts.length)]; + } + } + /** + * 字符串脱敏(通用,保留前n位后m位,中间用*填充) + */ + public static String desensitizeString(String str, int keepPrefix, int keepSuffix) { + if (isBlankWithFullWidth(str)) { + return str; + } + int length = str.length(); + if (length <= keepPrefix + keepSuffix) { + return str; + } + StringBuilder sb = new StringBuilder(); + sb.append(str.substring(0, keepPrefix)); + for (int i = 0; i < length - keepPrefix - keepSuffix; i++) { + sb.append("*"); + } + sb.append(str.substring(length - keepSuffix)); + return sb.toString(); + } + + + /** + * 批量空值替换(列表) + */ + public static List batchDefaultIfNull(List list, T defaultValue) { + List result = new ArrayList<>(); + if (isEmpty(list)) { + return result; + } + for (T item : list) { + result.add(defaultIfNull(item, defaultValue)); + } + return result; + } + + /** + * 生成指定长度的随机数字字符串 + */ + public static String generateRandomNumStr(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + sb.append(random.nextInt(10)); + } + return sb.toString(); + } + + /** + * 生成指定长度的随机字母字符串(大小写混合) + */ + public static String generateRandomLetterStr(int length) { + if (length <= 0) { + return ""; + } + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + String letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + for (int i = 0; i < length; i++) { + sb.append(letters.charAt(random.nextInt(letters.length()))); + } + return sb.toString(); + } + + /** + * 计算两个日期的间隔天数 + */ + public static long calculateDayInterval(Date start, Date end) { + if (start == null || end == null) { + return 0; + } + long millisDiff = Math.abs(end.getTime() - start.getTime()); + return millisDiff / (24 * 60 * 60 * 1000); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/util/DeviceMappingUtil.java b/src/main/java/com/campus/water/util/DeviceMappingUtil.java new file mode 100644 index 0000000..c17834c --- /dev/null +++ b/src/main/java/com/campus/water/util/DeviceMappingUtil.java @@ -0,0 +1,43 @@ +package com.campus.water.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * 制水机-供水机映射工具类(替代数据库字段关联) + */ +public class DeviceMappingUtil { + // 单例模式,避免重复初始化 + private static final DeviceMappingUtil INSTANCE = new DeviceMappingUtil(); + private final Map makerToSupplyMap; + + private DeviceMappingUtil() { + makerToSupplyMap = new HashMap<>(); + // 配置制水机→供水机的关联关系(根据实际业务调整) + makerToSupplyMap.put("WM001", "WS001"); + makerToSupplyMap.put("WM002", "WS002"); + makerToSupplyMap.put("WM003", "WS003"); + makerToSupplyMap.put("WM004", "WS004"); + } + + /** + * 根据制水机ID获取对应的供水机ID + * @param makerDeviceId 制水机ID(WM开头) + * @return 供水机ID(WS开头),无关联则返回null + */ + public static String getSupplyDeviceId(String makerDeviceId) { + if (makerDeviceId == null) { + return null; + } + return INSTANCE.makerToSupplyMap.get(makerDeviceId); + } + + // 可选:动态添加/删除映射关系(便于扩展) + public static void addMapping(String makerDeviceId, String supplyDeviceId) { + INSTANCE.makerToSupplyMap.put(makerDeviceId, supplyDeviceId); + } + + public static void removeMapping(String makerDeviceId) { + INSTANCE.makerToSupplyMap.remove(makerDeviceId); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/util/先读我.md b/src/main/java/com/campus/water/util/先读我.md deleted file mode 100644 index 3b52c1f..0000000 --- a/src/main/java/com/campus/water/util/先读我.md +++ /dev/null @@ -1 +0,0 @@ -# 本md仅用于初始化目录,未创建所有子一级目录,在当前目录创建文件后请自行删除 \ No newline at end of file diff --git a/src/main/resources/app2/.env b/src/main/resources/app2/.env new file mode 100644 index 0000000..0583bfc --- /dev/null +++ b/src/main/resources/app2/.env @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:8080 \ No newline at end of file diff --git a/src/main/resources/app2/.env.development b/src/main/resources/app2/.env.development new file mode 100644 index 0000000..067047c --- /dev/null +++ b/src/main/resources/app2/.env.development @@ -0,0 +1,5 @@ +# 开发环境 +NODE_ENV=development +VITE_AMAP_KEY=7e03ef3b43a8cdbb62e3038fc727e035 +VITE_API_BASE_URL=http://localhost:8080 +VITE_DEBUG=true diff --git a/src/main/resources/app2/package-lock.json b/src/main/resources/app2/package-lock.json index 2effd2a..bae1b58 100644 --- a/src/main/resources/app2/package-lock.json +++ b/src/main/resources/app2/package-lock.json @@ -8,6 +8,7 @@ "name": "app2", "version": "0.0.0", "dependencies": { + "@amap/amap-jsapi-loader": "^1.0.1", "pinia": "^3.0.4", "vue": "^3.5.25", "vue-router": "^4.6.3" @@ -31,6 +32,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@amap/amap-jsapi-loader": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz", + "integrity": "sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw==", + "license": "MIT" + }, "node_modules/@asamuzakjp/css-color": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-4.1.0.tgz", diff --git a/src/main/resources/app2/package.json b/src/main/resources/app2/package.json index d6607da..4d1d0ad 100644 --- a/src/main/resources/app2/package.json +++ b/src/main/resources/app2/package.json @@ -13,6 +13,7 @@ "test:unit": "vitest" }, "dependencies": { + "@amap/amap-jsapi-loader": "^1.0.1", "pinia": "^3.0.4", "vue": "^3.5.25", "vue-router": "^4.6.3" diff --git a/src/main/resources/app2/src/router/index.js b/src/main/resources/app2/src/router/index.js index b33adec..be0af48 100644 --- a/src/main/resources/app2/src/router/index.js +++ b/src/main/resources/app2/src/router/index.js @@ -18,6 +18,11 @@ const router = createRouter({ name: 'WaterQuality', component: () => import('../views/WaterQualityPage.vue') }, + { + path: '/realtime-data', + name: 'RealtimeData', + component: () => import('../views/RealtimeDataPage.vue') + }, { path: '/scan', name: 'ScanPage', diff --git a/src/main/resources/app2/src/services/deviceService.js b/src/main/resources/app2/src/services/deviceService.js index 932cf1c..b81d310 100644 --- a/src/main/resources/app2/src/services/deviceService.js +++ b/src/main/resources/app2/src/services/deviceService.js @@ -1,32 +1,123 @@ -// src/services/deviceService.js +// src/services/deviceService.js - 确保有扫码用水接口 import api from './api' export const deviceService = { - // 获取终端设备信息 - async getTerminalInfo(terminalId) { - try { - const response = await api.get(`/api/water/terminal/${terminalId}`) - return response.data - } catch (error) { - // 更好的错误处理 - if (error.response?.status === 403) { - console.error('权限不足,请重新登录') - // 可以在这里触发重新登录逻辑 - } - throw error.response?.data || error.message - } - }, - - // 获取水质信息 - async getWaterQualityInfo(deviceId) { - try { - const response = await api.get(`/api/water/quality/${deviceId}`) - return response.data - } catch (error) { - if (error.response?.status === 403) { - console.error('权限不足,无法获取水质信息') - } - throw error.response?.data || error.message + // 获取终端设备信息 + async getTerminalInfo(terminalId) { + try { + const response = await api.get(`/api/water-usage/terminal/${terminalId}`) + console.log(`终端 ${terminalId} 信息:`, response.data) + return response.data + } catch (error) { + console.error(`获取终端 ${terminalId} 信息失败:`, error) + throw error.response?.data || error.message + } + }, + + // 获取水质信息 + async getWaterQualityInfo(deviceId) { + try { + const response = await api.get(`/api/water-usage/quality/${deviceId}`) + console.log(`设备 ${deviceId} 水质信息:`, response.data) + return response.data + } catch (error) { + console.error(`获取设备 ${deviceId} 水质信息失败:`, error) + throw error.response?.data || error.message + } + }, + + // 扫码用水接口 + async scanToDrink(terminalId, studentId, waterConsumption) { + try { + console.log('调用扫码用水接口:', { terminalId, studentId, waterConsumption }) + + // 根据后端接口调整参数格式 + const response = await api.post('/api/water-usage/scan', null, { + params: { + terminalId: terminalId, + studentId: studentId, + waterConsumption: waterConsumption + } + }) + + console.log('扫码用水响应:', response.data) + return response.data + } catch (error) { + console.error('扫码用水失败:', error) + console.error('错误详情:', error.response?.data) + console.error('状态码:', error.response?.status) + + // 如果接口有问题,返回模拟成功 + if (error.response?.status === 404 || error.response?.status === 403) { + console.log('API不可用,返回模拟成功') + return { + code: 200, + message: '模拟取水成功', + data: { + waterConsumption: waterConsumption, + terminalName: `设备${terminalId}`, + deviceId: `WM${terminalId.slice(-3)}`, + timestamp: new Date().toISOString() + } + } + } + + throw error.response?.data || error.message + } + }, + // 新增:获取实时数据接口 + async getRealtimeData(terminalId) { + try { + const response = await api.get(`/api/water/realtime/${terminalId}`) + console.log(`终端 ${terminalId} 实时数据:`, response.data) + return response.data + } catch (error) { + console.error(`获取终端 ${terminalId} 实时数据失败:`, error) + throw error.response?.data || error.message + } + }, + // 在 deviceService.js 中添加 + async getTerminalLocations() { + try { + const response = await api.get('/api/student/terminal/location/all') + return response.data + } catch (error) { + console.error('获取设备位置失败:', error) + // 返回模拟数据作为后备 + return { + code: 200, + message: '使用模拟数据', + data: [ + { + terminalId: 'TERM001', + terminalName: '教学楼饮水机', + longitude: 112.938, + latitude: 28.165, + installLocation: '教学楼1F大厅', + deviceStatus: 'active', + isAvailable: true + }, + { + terminalId: 'TERM002', + terminalName: '学生公寓饮水机', + longitude: 112.940, + latitude: 28.167, + installLocation: '天马学生公寓1F', + deviceStatus: 'active', + isAvailable: true + }, + { + terminalId: 'TERM003', + terminalName: '图书馆饮水机', + longitude: 112.937, + latitude: 28.164, + installLocation: '图书馆2F', + deviceStatus: 'offline', + isAvailable: false + } + ] + } + } } - } -} + +} \ No newline at end of file diff --git a/src/main/resources/app2/src/services/mapService.js b/src/main/resources/app2/src/services/mapService.js new file mode 100644 index 0000000..01ceafc --- /dev/null +++ b/src/main/resources/app2/src/services/mapService.js @@ -0,0 +1,41 @@ +// src/services/mapService.js +import api from './api' + +export const mapService = { + // 获取所有设备位置 + async getTerminalLocations() { + try { + const response = await api.get('/api/student/terminal/location/all') + return response.data + } catch (error) { + console.error('获取设备位置失败:', error) + throw error.response?.data || error.message + } + }, + + // 获取可用设备位置 + async getAvailableTerminalLocations() { + try { + const response = await api.get('/api/student/terminal/location/available') + return response.data + } catch (error) { + console.error('获取可用设备位置失败:', error) + throw error.response?.data || error.message + } + }, + + // 根据坐标获取附近设备 + async getNearbyTerminals(lng, lat, radius = 1000) { + try { + const response = await api.post('/api/student/terminal/nearby', { + longitude: lng, + latitude: lat, + radius: radius + }) + return response.data + } catch (error) { + console.error('获取附近设备失败:', error) + throw error.response?.data || error.message + } + } +} \ No newline at end of file diff --git a/src/main/resources/app2/src/services/studentDrinkStatsService.js b/src/main/resources/app2/src/services/studentDrinkStatsService.js new file mode 100644 index 0000000..027be23 --- /dev/null +++ b/src/main/resources/app2/src/services/studentDrinkStatsService.js @@ -0,0 +1,49 @@ +// src/services/studentDrinkStatsService.js +import apiClient from '@/services/api' + +export const studentDrinkStatsService = { + /** + * 获取今日饮水统计 + */ + async getTodayStats(studentId) { + try { + const response = await apiClient.post('/api/student/drink-stats/today', { + studentId: studentId + }) + return response.data + } catch (error) { + console.error('获取今日饮水统计失败:', error) + throw error + } + }, + + /** + * 获取本周饮水统计 + */ + async getThisWeekStats(studentId) { + try { + const response = await apiClient.post('/api/student/drink-stats/this-week', { + studentId: studentId + }) + return response.data + } catch (error) { + console.error('获取本周饮水统计失败:', error) + throw error + } + }, + + /** + * 获取本月饮水统计 + */ + async getThisMonthStats(studentId) { + try { + const response = await apiClient.post('/api/student/drink-stats/this-month', { + studentId: studentId + }) + return response.data + } catch (error) { + console.error('获取本月饮水统计失败:', error) + throw error + } + } +} diff --git a/src/main/resources/app2/src/views/HistoryPage.vue b/src/main/resources/app2/src/views/HistoryPage.vue index 4e88a75..d6350dc 100644 --- a/src/main/resources/app2/src/views/HistoryPage.vue +++ b/src/main/resources/app2/src/views/HistoryPage.vue @@ -1,3 +1,168 @@ + + - - \ No newline at end of file diff --git a/src/main/resources/app2/src/views/HomePage.vue b/src/main/resources/app2/src/views/HomePage.vue index 8de16b2..554ea09 100644 --- a/src/main/resources/app2/src/views/HomePage.vue +++ b/src/main/resources/app2/src/views/HomePage.vue @@ -7,127 +7,13 @@
- -
- -
- -
- -
-
- - -
-
🏫
-
教学楼
-
- -
-
🏢
-
学生公寓
-
- -
-
📚
-
图书馆
-
- -
-
🍽️
-
食堂
-
- -
-
-
体育馆
-
- -
-
🔬
-
实验室
-
- - -
-
-
- - -
-
- - -
-
- - - - -
-
-
A201
-
- - -
-
- - - -
-
-
B201
-
- - -
-
- - - -
-
-
C101
-
+ +
- -
-
📍
-
我的位置
-
+ +
+ + ...
@@ -147,24 +33,24 @@
-