8 天以前 f0efc10af370936b4db47b8428eb1ca4686431f9
feat(home): 添加报表日期范围筛选功能并完善设备维护管理

- 在HomeController中为多个报表接口添加开始和结束日期参数支持
- 实现HomeService中各类报表方法的日期范围查询逻辑
- 添加设备维护完成和批量完成功能接口及实现
- 完善DeviceMaintenanceDto中添加日期分组字段
- 优化DeviceMaintenanceMapper查询结果按创建时间倒序排列
- 新增增值税明细按发票/订单展示接口
- 实现巡检任务完成和批量完成功能
- 更新相关实体类和映射文件以支持新功能
- 修改开发环境端口配置从7006调整为7005
已修改27个文件
已添加15个文件
1763 ■■■■■ 文件已修改
.claude/commands/database-migration.md 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/commands/feature-development.md 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/identity.json 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/rules/frontend-docs.md 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/rules/security-guardrails.md 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/settings.local.json 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/skills/project-conventions.md 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260806_inspection_task_add_status.sql 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/旭宝峰系统功能优化需求整理.md 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/dashboard_optimization.md 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/inspection_task_complete.md 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/maintenance_task_complete.md 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/spare_parts_outbound.md 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/vat_comparison_optimization.md 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/SparePartsRequisitionRecordSourceTypeEnum.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/controller/DeviceMaintenanceController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/dto/DeviceMaintenanceDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/IDeviceMaintenanceService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceServiceImpl.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/controller/HomeController.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/mapper/HomeMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/HomeService.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java 253 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/controller/InspectionTaskController.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/pojo/InspectionTask.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/InspectionTaskService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/controller/SparePartsRequisitionRecordController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/dto/SparePartsOutboundDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/service/SparePartsRequisitionRecordService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/SparePartsRequisitionRecordServiceImpl.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/VatDto.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/InvoicePurchaseMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/IInvoicePurchaseService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/SalesLedgerProductMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceMaintenanceMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/home/HomeMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/InvoicePurchaseMapper.xml 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/commands/database-migration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
# /数据库迁移
在进行**数据库迁移**时使用此工作流。
## ç›®æ ‡
安全的数据库迁移工作流
## å¸¸ç”¨æ–‡ä»¶
- `migrations/*`
- `schemas/*`
- `**/models/**`
- `**/db/**`
## å»ºè®®æ­¥éª¤
1. æŸ¥çœ‹çŽ°æœ‰è¿ç§»ä»¥äº†è§£æ¨¡å¼ã€‚
2. åˆ›å»ºå¸¦æœ‰æ—¶é—´æˆ³å‰ç¼€çš„æ–°è¿ç§»æ–‡ä»¶ã€‚
3. ç¼–写支持回滚的迁移。
4. åœ¨æœ¬åœ°çŽ¯å¢ƒä¸­æµ‹è¯•è¿ç§»ã€‚
5. è®°å½•任何破坏性变更。
## å…¸åž‹æäº¤ä¿¡æ¯
- ä¸º[功能]添加迁移
- ä¿®å¤è¿ç§»[描述]
- æ›´æ–°æž¶æž„文档
## æ³¨æ„äº‹é¡¹
- å§‹ç»ˆåœ¨å‰¯æœ¬ä¸Šæµ‹è¯•迁移。
- ç¡®ä¿å›žæ»šè„šæœ¬ä¹Ÿç»è¿‡æµ‹è¯•ã€‚
- æ²¡æœ‰å¤‡ä»½çš„æƒ…况下避免破坏性操作。
.claude/commands/feature-development.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
# /功能开发
在进行**功能开发**时使用此工作流。
## ç›®æ ‡
标准化功能实现工作流
## å¸¸ç”¨æ–‡ä»¶
- `manifests/*`
- `schemas/*`
- `**/*.test.*`
- `**/api/**`
## å»ºè®®æ­¥éª¤
1. åœ¨ç¼–辑前了解当前状态和失败模式。
2. è¿›è¡Œæœ€å°ä¸”连贯的更改以满足工作流目标。
3. å¯¹ä¿®æ”¹çš„æ–‡ä»¶è¿è¡Œæœ€ç›¸å…³çš„验证。
4. æ€»ç»“更改内容以及仍需审查的部分。
## å…¸åž‹æäº¤ä¿¡æ¯
- æ·»åŠ åŠŸèƒ½å®žçŽ°
- ä¸ºåŠŸèƒ½æ·»åŠ æµ‹è¯•
- æ›´æ–°æ–‡æ¡£
## æ³¨æ„äº‹é¡¹
- å°†æ­¤è§†ä¸ºè„šæ‰‹æž¶ï¼Œè€Œéžç¡¬ç¼–ç è„šæœ¬ã€‚
- å¦‚果工作流有重大变化,更新此命令。
.claude/identity.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
{
  "name": "product-inventory-management",
  "description": "Product inventory management system with JDK 25+",
  "language": "Java",
  "framework": "Spring Boot",
  "database": "MySQL",
  "orm": "MyBatis",
  "javaVersion": "25"
}
.claude/rules/frontend-docs.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
# æŽ¥å£å˜æ›´å‰ç«¯æ–‡æ¡£è§„则
## è§„则
- æ¯æ¬¡æ¶‰åŠæŽ¥å£å˜æ›´ï¼ˆæ–°å¢žæŽ¥å£ã€ä¿®æ”¹è¯·æ±‚参数、修改响应结构)时,必须输出一份前端修改文档到 `docs/` ç›®å½•。
- æ–‡æ¡£ä»¥ Markdown æ ¼å¼ç¼–写,文件名使用小写字母和下划线,如 `docs/customer_handover.md`。
- æ–‡æ¡£å†…容至少包含:API è¯´æ˜Žï¼ˆè·¯å¾„、方法、请求参数、响应格式)、涉及的前端页面、具体的修改代码示例。
## æ–‡æ¡£æ¨¡æ¿
```markdown
# åŠŸèƒ½åç§°
## æ¶‰åŠé¡µé¢
- é¡µé¢1
- é¡µé¢2
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /xxx/xxx | è¯´æ˜Ž |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| xxx | String | æ˜¯ | xxx |
**响应:** `{ "code": 200, "msg": "操作成功" }`
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æŒ‰é’®/弹窗/表格等
\`\`\`html
<el-button>xxx</el-button>
\`\`\`
### 2. data æ•°æ®
\`\`\`js
data() {
  return {
    xxx: '',
  }
}
\`\`\`
### 3. æ–¹æ³•
\`\`\`js
handleXxx() {
  // ...
}
\`\`\`
## æ³¨æ„äº‹é¡¹
- æ³¨æ„äº‹é¡¹1
- æ³¨æ„äº‹é¡¹2
```
.claude/rules/security-guardrails.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
# å®‰å…¨æŠ¤æ 
## SQL æ³¨å…¥é˜²æŠ¤
- æ°¸è¿œä¸è¦å°†ç”¨æˆ·è¾“入直接拼接到 SQL æŸ¥è¯¢ä¸­ã€‚
- å§‹ç»ˆä½¿ç”¨å‚数化查询或 ORM æ–¹æ³•。
- åœ¨æ•°æ®åº“操作前验证和清理所有输入。
## XSS é˜²æŠ¤
- åœ¨ HTML ä¸­æ¸²æŸ“前转义所有用户生成的内容。
- ä½¿ç”¨æ¨¡æ¿è‡ªåŠ¨è½¬ä¹‰åŠŸèƒ½ã€‚
- åœ¨å®¢æˆ·ç«¯å’ŒæœåŠ¡å™¨ç«¯éƒ½è¿›è¡Œè¾“å…¥éªŒè¯å’Œæ¸…ç†ã€‚
## å¯†é’¥ç®¡ç†
- æ°¸è¿œä¸è¦åœ¨æºä»£ç ä¸­ç¡¬ç¼–码密钥、API å¯†é’¥æˆ–密码。
- ä½¿ç”¨çŽ¯å¢ƒå˜é‡æˆ–å®‰å…¨ä¿é™©åº“å­˜å‚¨æ•æ„Ÿæ•°æ®ã€‚
- å®šæœŸè½®æ¢å¯†é’¥ï¼Œé¿å…åœ¨æ—¥å¿—中记录它们。
## è¾“入验证
- åœ¨ç³»ç»Ÿè¾¹ç•Œå¤„验证所有输入。
- ä½¿ç”¨ç™½åå•而非黑名单进行验证。
- åœ¨å¤„理上传前检查文件类型和大小。
## é”™è¯¯å¤„理
- ä¸è¦åœ¨é”™è¯¯æ¶ˆæ¯ä¸­æš´éœ²æ•æ„Ÿä¿¡æ¯ã€‚
- åœ¨å†…部记录详细错误,向用户返回通用消息。
- å¤„理所有异常以防止信息泄露。
## ä¾èµ–管理
- ä¿æŒä¾èµ–项更新。
- å®¡è®¡ä¾èµ–项中的已知漏洞。
- å›ºå®šç‰ˆæœ¬ä»¥é¿å…æ„å¤–的破坏性变更。
.claude/settings.local.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
{
  "permissions": {
    "allow": [
      "Read",
      "Glob",
      "Grep",
      "Write",
      "Edit",
      "Bash(git status)",
      "Bash(git diff)",
      "Bash(npm test)",
      "Bash(npm run build)",
      "Bash(pytest *)",
      "mcp__ide__getDiagnostics",
      "Bash(mvn compile *)",
      "Bash(sed -i '269,271d' src/main/resources/mapper/system/SysUserMapper.xml)",
      "Bash(sed -i '268a\\\\\\\\t\\\\t<update id=\"resetAllUserPwd\">\\\\n\\\\t\\\\t\\\\tupdate sys_user set password = #{password} where del_flag = '\\\\''0'\\\\''\\\\n\\\\t\\\\t</update>' src/main/resources/mapper/system/SysUserMapper.xml)",
      "Bash(sed -i '269s/^.*$/\\\\t\\\\t<update id=\"resetAllUserPwd\">/' src/main/resources/mapper/system/SysUserMapper.xml)",
      "Bash(sed -i '270s/^.*$/\\\\t\\\\t\\\\tupdate sys_user set password = #{password} where del_flag = '\\\\''0'\\\\''/' src/main/resources/mapper/system/SysUserMapper.xml)",
      "Bash(sed -i '271s/^.*$/\\\\t\\\\t<\\\\/update>/' src/main/resources/mapper/system/SysUserMapper.xml)"
    ],
    "deny": [
      "Bash(rm -rf *)",
      "Bash(curl * | bash)",
      "Bash(git push --force *)",
      "Bash(sudo *)"
    ]
  }
}
.claude/skills/project-conventions.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
# é¡¹ç›®å¼€å‘约定
## æ¦‚è¿°
此技能定义了本项目的开发模式和约定。
## æŠ€æœ¯æ ˆ
- **主要语言**: Java (JDK 25+)
- **架构**: Spring Boot + MyBatis
- **测试位置**: `src/test/java`
## æäº¤çº¦å®š
遵循约定式提交格式:
### ä½¿ç”¨çš„前缀
- `feat` - æ–°åŠŸèƒ½
- `fix` - é”™è¯¯ä¿®å¤
- `docs` - æ–‡æ¡£å˜æ›´
- `test` - æµ‹è¯•添加或更新
- `refactor` - ä»£ç é‡æž„
- `style` - ä»£ç é£Žæ ¼å˜æ›´ï¼ˆæ ¼å¼åŒ–、分号等)
- `chore` - æž„建过程或辅助工具变更
### æ¶ˆæ¯è§„范
- ä¿æŒç¬¬ä¸€è¡Œç®€æ´ä¸”有描述性(少于 70 ä¸ªå­—符)
- ä½¿ç”¨ç¥ˆä½¿è¯­æ°”("添加功能"而非"添加了功能")
- ç›¸å…³æ—¶åŒ…含范围:`feat(范围): æè¿°`
## ä»£ç é£Žæ ¼
- éµå¾ª Java å‘½åçº¦å®š
- ä½¿ç”¨æœ‰æ„ä¹‰çš„变量和方法名
- ä¸ºå…¬å…± API æ·»åŠ  Javadoc
- ä¿æŒæ–¹æ³•专注且单一职责
## æµ‹è¯•
- ä¸ºä¸šåŠ¡é€»è¾‘ç¼–å†™å•å…ƒæµ‹è¯•
- ä¸ºæ•°æ®åº“操作使用集成测试
- ä¿æŒæµ‹è¯•覆盖率在 80% ä»¥ä¸Š
## åˆå¹¶è¯·æ±‚规范
- ä¿æŒåˆå¹¶è¯·æ±‚专注且小
- ä¸ºæ–°åŠŸèƒ½åŒ…å«æµ‹è¯•
- æ ¹æ®éœ€è¦æ›´æ–°æ–‡æ¡£
- åˆå¹¶å‰å®¡æŸ¥ä»£ç 
doc/20260806_inspection_task_add_status.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
-- inspection_task è¡¨æ–°å¢ž status å­—段
ALTER TABLE inspection_task ADD COLUMN status TINYINT DEFAULT 0 COMMENT '状态 0待巡检 1已完成';
doc/Ðñ±¦·åϵͳ¹¦ÄÜÓÅ»¯ÐèÇóÕûÀí.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
# ä¸€ã€è®¾å¤‡ç®¡ç†æ¨¡å—
## 1\. å¤‡ä»¶ç®¡ç† - å¤‡ä»¶é¢†ç”¨è®°å½•
现有备件出库仅能依托设备检修场景,出库渠道存在局限,需新增独立备件出库功能入口,支持直接完成备件领用出库操作。
## å¤‡ä»¶é¢†ç”¨è®°å½•页面2. è®¾å¤‡ä¿å…» - ä¿å…»ä»»åŠ¡
保养任务下发后,页面参照销售 / é‡‡è´­è®¢å•样式,支持按日期分组折叠展示,分组条目可展开查看明细;单条明细行末尾增加【保养完成】操作按钮,可单独办结单条保养任务;折叠汇总状态下,新增【一键保养完成】批量操作按钮,快速办结当前分组全部保养项。
## ä¿å…»ä»»åŠ¡é¡µé¢3. è®¾å¤‡å·¡æ£€ - å·¡æ£€ä»»åŠ¡
巡检任务下发完成后,页面参照销售 / é‡‡è´­è®¢å•布局,按日期分组折叠展示,分组支持展开查看明细;每条巡检明细后增设【巡检完成】按钮,支持单独办结单条巡检记录;分组折叠未展开状态下,提供【一键巡检完成】批量操作,统一办结该分组下所有巡检任务。
# äºŒã€å†³ç­–分析模块
## 1\. å¢žå€¼ç¨Žå¯¹æ¯”页面
当前页面查询无数据,按监理要求优化数据展示逻辑,列表展示各订单增值税明细;页面右侧新增柱状可视化图表,直观对比进销项增值税数据。
## å¢žå€¼ç¨Žå¯¹æ¯”页面2. ç®¡ç†é©¾é©¶èˆ±ï¼ˆPSI æ•°æ®åˆ†æžå¤§å±ï¼‰
指标修改:将顶部「销售产品数」调整为「销售订单数」;时间筛选:页面左上角新增日期范围下拉筛选控件,所有统计指标标注当前筛选时段;数据修复:解决「采购品分布」环形图无数据展示问题。
docs/dashboard_optimization.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,177 @@
# ç®¡ç†é©¾é©¶èˆ±ï¼ˆPSI æ•°æ®åˆ†æžå¤§å±ï¼‰ä¼˜åŒ–
## æ¶‰åŠé¡µé¢
- ç®¡ç†é©¾é©¶èˆ± / PSI æ•°æ®åˆ†æžå¤§å±
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /home/salesPurchaseStorageProductCount | é”€å”®-采购-储存统计(新增日期筛选) |
| GET | /home/productCategoryDistribution | äº§å“å¤§ç±»åˆ†å¸ƒï¼ˆä¿®å¤æ•°æ®ï¼‰ |
### 1. é”€å”®-采购-储存统计接口
**请求参数(新增):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| startDate | String | å¦ | å¼€å§‹æ—¥æœŸï¼ˆyyyy-MM-dd) |
| endDate | String | å¦ | ç»“束日期(yyyy-MM-dd) |
不传日期参数时默认统计当前月 vs ä¸Šæœˆã€‚
**响应格式变化:**
- ç¬¬ä¸€ä¸ªæŒ‡æ ‡åç§°ä»Ž `"销售产品数"` æ”¹ä¸º `"销售订单数"`
- æ•°å€¼ç»Ÿè®¡å£å¾„从"销售产品记录数"改为"销售订单数(sales_ledger è¡¨è®°å½•数)"
```json
{
  "code": 200,
  "data": [
    { "name": "销售订单数", "value": "156", "rate": "12.50" },
    { "name": "采购产品数", "value": "89", "rate": "-5.30" },
    { "name": "储存产品数", "value": "234", "rate": "8.20" }
  ]
}
```
### 2. äº§å“å¤§ç±»åˆ†å¸ƒæŽ¥å£
修复了小类 value/rate æ•°æ®ä¸º 0 çš„问题,现在返回实际的采购数据。
**响应格式:**
```json
{
  "code": 200,
  "data": {
    "items": [
      {
        "name": "原材料",
        "value": "45",
        "rate": "40.50",
        "children": [
          { "name": "钢材", "value": "20", "rate": "44.44", "children": [] },
          { "name": "塑料", "value": "15", "rate": "33.33", "children": [] },
          { "name": "电子元件", "value": "10", "rate": "22.22", "children": [] }
        ]
      }
    ]
  }
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. é¡¶éƒ¨æŒ‡æ ‡ä¿®æ”¹ - "销售产品数" â†’ "销售订单数"
```html
<div class="stat-card">
  <div class="stat-label">销售订单数</div>
  <div class="stat-value">{{ salesOrderCount.value }}</div>
  <div class="stat-rate" :class="{ up: salesOrderCount.rate > 0, down: salesOrderCount.rate < 0 }">
    çŽ¯æ¯” {{ salesOrderCount.rate }}%
  </div>
</div>
```
### 2. æ—¥æœŸèŒƒå›´ç­›é€‰æŽ§ä»¶
在页面左上角新增日期范围下拉筛选。
```html
<el-date-picker
  v-model="dateRange"
  type="daterange"
  range-separator="至"
  start-placeholder="开始日期"
  end-placeholder="结束日期"
  value-format="yyyy-MM-dd"
  @change="handleDateChange"
  style="margin-right: 16px;" />
```
### 3. data æ•°æ®
```js
data() {
  return {
    dateRange: [],
    salesOrderCount: { value: 0, rate: 0 },
    purchaseProductCount: { value: 0, rate: 0 },
    storageProductCount: { value: 0, rate: 0 },
    productCategoryData: [],
  }
}
```
### 4. æ–¹æ³•
```js
async loadSalesPurchaseStorageCount() {
  const params = {};
  if (this.dateRange && this.dateRange.length === 2) {
    params.startDate = this.dateRange[0];
    params.endDate = this.dateRange[1];
  }
  const res = await this.$http.get('/home/salesPurchaseStorageProductCount', { params });
  const data = res.data || [];
  this.salesOrderCount = this.extractStatData(data[0]);
  this.purchaseProductCount = this.extractStatData(data[1]);
  this.storageProductCount = this.extractStatData(data[2]);
},
extractStatData(item) {
  return item ? {
    value: parseInt(item.value) || 0,
    rate: parseFloat(item.rate) || 0,
  } : { value: 0, rate: 0 };
},
handleDateChange(val) {
  this.loadSalesPurchaseStorageCount();
},
async loadProductCategoryDistribution() {
  const res = await this.$http.get('/home/productCategoryDistribution');
  this.productCategoryData = res.data.items || [];
  this.renderCategoryChart();
},
renderCategoryChart() {
  const chart = this.$echarts.init(this.$refs.categoryChart);
  const seriesData = [];
  const legendData = [];
  this.productCategoryData.forEach(major => {
    legendData.push(major.name);
    seriesData.push({
      name: major.name,
      value: parseInt(major.value) || 0,
      children: (major.children || []).map(child => ({
        name: child.name,
        value: parseInt(child.value) || 0,
      }))
    });
  });
  chart.setOption({
    tooltip: { trigger: 'item' },
    legend: { data: legendData, orient: 'vertical', right: 10, top: 20 },
    series: [{
      type: 'sunburst',
      data: seriesData,
      radius: [0, '90%'],
      label: { rotate: 'radial' }
    }]
  });
},
```
## æ³¨æ„äº‹é¡¹
- "销售订单数"统计的是 sales_ledger è¡¨ä¸­çš„订单记录数(按 entry_date),而非 sales_ledger_product è¡¨ä¸­çš„产品记录数
- æ—¥æœŸèŒƒå›´ç­›é€‰ä¸ºç©ºæ—¶ï¼Œé»˜è®¤å±•示当前月 vs ä¸Šæœˆçš„æ•°æ®
- é‡‡è´­å“åˆ†å¸ƒçŽ¯å½¢å›¾ä¸­å„å°ç±»çš„ value çŽ°åœ¨æ¥è‡ªå®žé™…çš„é‡‡è´­æ•°æ®ï¼ˆsales_ledger_product type=2),而非写死的 0
- æ‰€æœ‰ç»Ÿè®¡æŒ‡æ ‡ä¸Šæ ‡æ³¨å½“前筛选时段(前端用 dateRange å±•示)
docs/inspection_task_complete.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,155 @@
# å·¡æ£€ä»»åŠ¡å®ŒæˆåŠŸèƒ½
## æ¶‰åŠé¡µé¢
- è®¾å¤‡å·¡æ£€ - å·¡æ£€ä»»åŠ¡é¡µé¢
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| PUT | /inspectionTask/complete/{id} | å•条巡检完成 |
| PUT | /inspectionTask/batchComplete | ä¸€é”®å·¡æ£€å®Œæˆ |
### 1. å·¡æ£€å®Œæˆ
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | å·¡æ£€ä»»åŠ¡ID(路径参数) |
**响应:**
```json
{ "code": 200, "msg": "操作成功" }
```
### 2. ä¸€é”®å·¡æ£€å®Œæˆ
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| dateStr | String | æ˜¯ | æ—¥æœŸï¼ˆæ ¼å¼ï¼šyyyy-MM-dd) |
**响应:**
```json
{ "code": 200, "msg": "操作成功" }
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. å·¡æ£€ä»»åŠ¡åˆ—è¡¨æ”¹é€  - æŒ‰æ—¥æœŸåˆ†ç»„折叠
参照销售/采购订单布局,将巡检记录按日期分组折叠展示。
```html
<el-collapse v-model="activeDateGroups" accordion>
  <el-collapse-item v-for="group in groupedInspectionList" :key="group.dateStr" :name="group.dateStr">
    <template slot="title">
      <div class="group-header">
        <span>{{ group.dateStr }}</span>
        <span>共 {{ group.items.length }} æ¡</span>
        <el-button type="success" size="small" @click.stop="handleBatchComplete(group.dateStr)">一键巡检完成</el-button>
      </div>
    </template>
    <el-table :data="group.items" border>
      <el-table-column prop="taskName" label="巡检任务名称" />
      <el-table-column prop="inspectionProject" label="巡检项目" />
      <el-table-column prop="inspector" label="巡检人" />
      <el-table-column prop="inspectionResult" label="巡检结果">
        <template slot-scope="scope">
          <el-tag v-if="scope.row.inspectionResult === '1'" type="success">正常</el-tag>
          <el-tag v-else-if="scope.row.inspectionResult === '0'" type="danger">异常</el-tag>
          <el-tag v-else type="info">未完成</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="status" label="状态">
        <template slot-scope="scope">
          <el-tag :type="scope.row.status === 0 ? 'warning' : 'success'">
            {{ scope.row.status === 0 ? '待巡检' : '已完成' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="120">
        <template slot-scope="scope">
          <el-button v-if="scope.row.status === 0" type="primary" size="small" @click="handleComplete(scope.row.id)">
            å·¡æ£€å®Œæˆ
          </el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-collapse-item>
</el-collapse>
```
### 2. data æ•°æ®
```js
data() {
  return {
    activeDateGroups: [],
    inspectionList: [],
  }
}
```
### 3. è®¡ç®—属性
```js
computed: {
  groupedInspectionList() {
    const groups = {};
    this.inspectionList.forEach(item => {
      const dateStr = item.dateStr; // åŽç«¯å·²è¿”回 dateStr å­—段
      if (!groups[dateStr]) {
        groups[dateStr] = { dateStr, items: [] };
      }
      groups[dateStr].items.push(item);
    });
    return Object.values(groups).sort((a, b) => b.dateStr.localeCompare(a.dateStr));
  }
}
```
### 4. æ–¹æ³•
```js
async handleComplete(id) {
  try {
    await this.$confirm('确认该巡检任务已完成?', '提示', { type: 'warning' });
    await this.$http.put(`/inspectionTask/complete/${id}`);
    this.$message.success('巡检完成');
    this.loadList();
  } catch (e) {
    if (e !== 'cancel') {
      this.$message.error(e.msg || '操作失败');
    }
  }
},
async handleBatchComplete(dateStr) {
  try {
    await this.$confirm(`确认将 ${dateStr} çš„æ‰€æœ‰å¾…巡检任务一键完成?`, '提示', { type: 'warning' });
    await this.$http.put('/inspectionTask/batchComplete', null, { params: { dateStr } });
    this.$message.success('批量巡检完成');
    this.loadList();
  } catch (e) {
    if (e !== 'cancel') {
      this.$message.error(e.msg || '操作失败');
    }
  }
},
async loadList() {
  const res = await this.$http.get('/inspectionTask/list', { params: this.queryParams });
  this.inspectionList = res.data.records || [];
}
```
## æ³¨æ„äº‹é¡¹
- å·²å®Œæˆï¼ˆstatus=1)的巡检记录不显示"巡检完成"按钮
- ä¸€é”®å·¡æ£€å®Œæˆä»…处理当天所有待巡检(status=0)的记录
- å·¡æ£€å®Œæˆæ—¶è‹¥æœªè®¾ç½®å·¡æ£€ç»“果,默认设为"正常"
- æ•°æ®åº“新增了 status å­—段,部署前需执行迁移 SQL
docs/maintenance_task_complete.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
# ä¿å…»ä»»åŠ¡å®ŒæˆåŠŸèƒ½
## æ¶‰åŠé¡µé¢
- è®¾å¤‡ä¿å…» - ä¿å…»ä»»åŠ¡é¡µé¢
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| PUT | /device/maintenance/complete/{id} | å•条保养完成 |
| PUT | /device/maintenance/batchComplete | ä¸€é”®ä¿å…»å®Œæˆ |
### 1. ä¿å…»å®Œæˆ
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | ä¿å…»è®°å½•ID(路径参数) |
**响应:** `{ "code": 200, "msg": "操作成功" }`
### 2. ä¸€é”®ä¿å…»å®Œæˆ
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| dateStr | String | æ˜¯ | æ—¥æœŸï¼ˆæ ¼å¼ï¼šyyyy-MM-dd) |
**响应:** `{ "code": 200, "msg": "已完成 N æ¡ä¿å…»è®°å½•" }`
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. ä¿å…»ä»»åŠ¡åˆ—è¡¨æ”¹é€  - æŒ‰æ—¥æœŸåˆ†ç»„折叠
参照销售/采购订单样式,将保养记录按日期分组折叠展示。
```html
<el-collapse v-model="activeDateGroups" accordion>
  <el-collapse-item v-for="group in groupedMaintenanceList" :key="group.dateStr" :name="group.dateStr">
    <template slot="title">
      <div class="group-header">
        <span>{{ group.dateStr }}</span>
        <span>共 {{ group.items.length }} æ¡</span>
        <el-button type="success" size="small" @click.stop="handleBatchComplete(group.dateStr)">一键保养完成</el-button>
      </div>
    </template>
    <el-table :data="group.items" border>
      <el-table-column prop="deviceName" label="设备名称" />
      <el-table-column prop="deviceModel" label="规格型号" />
      <el-table-column prop="machineryCategory" label="设备项目" />
      <el-table-column prop="maintenanceActuallyName" label="保养人" />
      <el-table-column prop="status" label="状态">
        <template slot-scope="scope">
          <el-tag :type="scope.row.status === 0 ? 'warning' : 'success'">
            {{ scope.row.status === 0 ? '待保养' : '已完结' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="120">
        <template slot-scope="scope">
          <el-button v-if="scope.row.status === 0" type="primary" size="small" @click="handleComplete(scope.row.id)">
            ä¿å…»å®Œæˆ
          </el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-collapse-item>
</el-collapse>
```
### 2. data æ•°æ®
```js
data() {
  return {
    activeDateGroups: [],
    maintenanceList: [],
  }
}
```
### 3. è®¡ç®—属性
```js
computed: {
  groupedMaintenanceList() {
    const groups = {};
    this.maintenanceList.forEach(item => {
      const dateStr = item.createTime.substring(0, 10); // å–日期部分 yyyy-MM-dd
      if (!groups[dateStr]) {
        groups[dateStr] = { dateStr, items: [] };
      }
      groups[dateStr].items.push(item);
    });
    return Object.values(groups).sort((a, b) => b.dateStr.localeCompare(a.dateStr));
  }
}
```
### 4. æ–¹æ³•
```js
async handleComplete(id) {
  try {
    await this.$confirm('确认该保养任务已完成?', '提示', { type: 'warning' });
    await this.$http.put(`/device/maintenance/complete/${id}`);
    this.$message.success('保养完成');
    this.loadList();
  } catch (e) {
    if (e !== 'cancel') {
      this.$message.error(e.msg || '操作失败');
    }
  }
},
async handleBatchComplete(dateStr) {
  try {
    await this.$confirm(`确认将 ${dateStr} çš„æ‰€æœ‰å¾…保养任务一键完成?`, '提示', { type: 'warning' });
    await this.$http.put('/device/maintenance/batchComplete', null, { params: { dateStr } });
    this.$message.success('批量保养完成');
    this.loadList();
  } catch (e) {
    if (e !== 'cancel') {
      this.$message.error(e.msg || '操作失败');
    }
  }
},
async loadList() {
  const res = await this.$http.get('/device/maintenance/page', { params: this.queryParams });
  this.maintenanceList = res.data.records || [];
}
```
## æ³¨æ„äº‹é¡¹
- å·²å®Œæˆï¼ˆstatus=1)的保养记录不显示"保养完成"按钮
- ä¸€é”®ä¿å…»å®Œæˆä»…处理当天所有待保养(status=0)的记录
- ä¿å…»å®ŒæˆåŽ status å˜ä¸º 1,同时记录当前操作人为实际保养人
docs/spare_parts_outbound.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
# å¤‡ä»¶ç‹¬ç«‹å‡ºåº“
## æ¶‰åŠé¡µé¢
- å¤‡ä»¶é¢†ç”¨è®°å½•页面
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /sparePartsRequisitionRecord/outbound | å¤‡ä»¶ç‹¬ç«‹å‡ºåº“ |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| sparePartsId | Long | æ˜¯ | å¤‡ä»¶ID |
| quantity | Integer | æ˜¯ | å‡ºåº“数量 |
| deviceLedgerId | Long | å¦ | è®¾å¤‡ID |
| remark | String | å¦ | å¤‡æ³¨ |
**请求示例:**
```json
{
  "sparePartsId": 1,
  "quantity": 5,
  "deviceLedgerId": 10,
  "remark": "直接领用"
}
```
**响应:** `{ "code": 200, "msg": "操作成功" }`
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æ–°å¢žå‡ºåº“按钮/弹窗
在备件领用记录页面新增【备件出库】按钮,点击后弹出出库表单。
```html
<el-button type="primary" @click="handleOpenOutbound">备件出库</el-button>
<el-dialog title="备件出库" :visible.sync="outboundDialogVisible" width="500px">
  <el-form :model="outboundForm" :rules="outboundRules" ref="outboundFormRef" label-width="100px">
    <el-form-item label="选择备件" prop="sparePartsId">
      <el-select v-model="outboundForm.sparePartsId" placeholder="请选择备件" filterable>
        <el-option v-for="item in sparePartsList" :key="item.id" :label="item.name" :value="item.id" />
      </el-select>
    </el-form-item>
    <el-form-item label="出库数量" prop="quantity">
      <el-input-number v-model="outboundForm.quantity" :min="1" />
    </el-form-item>
    <el-form-item label="关联设备" prop="deviceLedgerId">
      <el-select v-model="outboundForm.deviceLedgerId" placeholder="请选择设备(可选)" filterable clearable>
        <el-option v-for="item in deviceList" :key="item.id" :label="item.deviceName" :value="item.id" />
      </el-select>
    </el-form-item>
    <el-form-item label="备注" prop="remark">
      <el-input v-model="outboundForm.remark" type="textarea" />
    </el-form-item>
  </el-form>
  <span slot="footer">
    <el-button @click="outboundDialogVisible = false">取消</el-button>
    <el-button type="primary" @click="handleOutboundSubmit">确认出库</el-button>
  </span>
</el-dialog>
```
### 2. data æ•°æ®
```js
data() {
  return {
    outboundDialogVisible: false,
    outboundForm: {
      sparePartsId: null,
      quantity: 1,
      deviceLedgerId: null,
      remark: '',
    },
    outboundRules: {
      sparePartsId: [{ required: true, message: '请选择备件', trigger: 'change' }],
      quantity: [{ required: true, message: '请输入出库数量', trigger: 'blur' }],
    },
    sparePartsList: [],
    deviceList: [],
  }
}
```
### 3. æ–¹æ³•
```js
handleOpenOutbound() {
  this.outboundForm = { sparePartsId: null, quantity: 1, deviceLedgerId: null, remark: '' };
  this.outboundDialogVisible = true;
  this.loadSparePartsList();
  this.loadDeviceList();
},
handleOutboundSubmit() {
  this.$refs.outboundFormRef.validate(async (valid) => {
    if (!valid) return;
    try {
      await this.$http.post('/sparePartsRequisitionRecord/outbound', this.outboundForm);
      this.$message.success('出库成功');
      this.outboundDialogVisible = false;
      this.loadList(); // åˆ·æ–°é¢†ç”¨è®°å½•列表
    } catch (e) {
      this.$message.error(e.msg || '出库失败');
    }
  });
},
async loadSparePartsList() {
  const res = await this.$http.get('/spareParts/listPage');
  this.sparePartsList = res.data.records || [];
},
async loadDeviceList() {
  const res = await this.$http.get('/device/ledger/page');
  this.deviceList = res.data.records || [];
},
```
## æ³¨æ„äº‹é¡¹
- å‡ºåº“会扣减备件库存,请确认数量准确
- é¢†ç”¨è®°å½•来源类型会标记为"直接出库"
- å¤‡ä»¶åº“存不足时接口会返回错误提示
docs/vat_comparison_optimization.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,189 @@
# å¢žå€¼ç¨Žå¯¹æ¯”页面优化
## æ¶‰åŠé¡µé¢
- å†³ç­–分析 - å¢žå€¼ç¨Žå¯¹æ¯”页面
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /purchase/report/listVat | å¢žå€¼ç¨ŽæŒ‰æœˆæ±‡æ€»ï¼ˆåŽŸæœ‰ï¼‰ |
| GET | /purchase/report/listVatDetail | å¢žå€¼ç¨Žæ˜Žç»†-按发票/订单展示(新增) |
### å¢žå€¼ç¨Žæ˜Žç»†æŽ¥å£
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| page | Page | å¦ | åˆ†é¡µå‚æ•° |
| month | String | å¦ | æœˆä»½ç­›é€‰ï¼ˆyyyy-MM),为空则查询全部 |
**响应格式:**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "invoiceNo": "FP20260801001",
        "salesContractNo": "HT20260801",
        "supplierName": "供应商A",
        "customerName": null,
        "orderType": "进项",
        "invoiceDate": "2026-08-01",
        "taxRate": 13.00,
        "jTaxAmount": 1300.00,
        "xTaxAmount": 0.00,
        "taxAmount": 1300.00
      },
      {
        "invoiceNo": "XS20260801001",
        "salesContractNo": "XS-HT20260801",
        "supplierName": null,
        "customerName": "客户B",
        "orderType": "销项",
        "invoiceDate": "2026-08-03",
        "taxRate": 13.00,
        "jTaxAmount": 0.00,
        "xTaxAmount": 2600.00,
        "taxAmount": 2600.00
      }
    ],
    "total": 100
  }
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. é¡µé¢å¸ƒå±€æ”¹é€  - å·¦ä¾§åˆ—表 + å³ä¾§æŸ±çж图
```html
<el-row :gutter="20">
  <!-- å·¦ä¾§ï¼šå¢žå€¼ç¨Žæ˜Žç»†åˆ—表 -->
  <el-col :span="14">
    <el-card>
      <div slot="header">
        <span>增值税明细</span>
        <el-select v-model="queryMonth" placeholder="选择月份" clearable style="float: right; width: 150px;">
          <el-option v-for="m in monthOptions" :key="m" :label="m" :value="m" />
        </el-select>
      </div>
      <el-table :data="vatDetailList" border v-loading="loading">
        <el-table-column prop="orderType" label="类型" width="60">
          <template slot-scope="scope">
            <el-tag :type="scope.row.orderType === '进项' ? 'info' : 'warning'" size="small">
              {{ scope.row.orderType }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="invoiceNo" label="发票号" width="140" />
        <el-table-column prop="salesContractNo" label="合同号" width="140" />
        <el-table-column prop="supplierName" label="供应商" />
        <el-table-column prop="customerName" label="客户" />
        <el-table-column prop="invoiceDate" label="开票日期" width="110" />
        <el-table-column prop="taxRate" label="税率(%)" width="80" />
        <el-table-column prop="taxAmount" label="税额" width="120">
          <template slot-scope="scope">
            <span :style="{ color: scope.row.orderType === '进项' ? '#409EFF' : '#E6A23C' }">
              {{ scope.row.taxAmount }}
            </span>
          </template>
        </el-table-column>
      </el-table>
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="page.current"
        :page-sizes="[10, 20, 50]"
        :page-size="page.size"
        :total="page.total"
        layout="total, sizes, prev, pager, next" />
    </el-card>
  </el-col>
  <!-- å³ä¾§ï¼šæŸ±çŠ¶å¯¹æ¯”å›¾ -->
  <el-col :span="10">
    <el-card>
      <div slot="header"><span>进销项增值税对比</span></div>
      <div ref="vatChart" style="height: 400px;"></div>
    </el-card>
  </el-col>
</el-row>
```
### 2. data æ•°æ®
```js
data() {
  return {
    queryMonth: '',
    monthOptions: [],
    vatDetailList: [],
    loading: false,
    page: { current: 1, size: 20, total: 0 },
  }
}
```
### 3. æ–¹æ³•
```js
async loadVatDetail() {
  this.loading = true;
  try {
    const res = await this.$http.get('/purchase/report/listVatDetail', {
      params: {
        current: this.page.current,
        size: this.page.size,
        month: this.queryMonth || null,
      }
    });
    this.vatDetailList = res.data.records || [];
    this.page.total = res.data.total || 0;
    this.renderChart();
  } finally {
    this.loading = false;
  }
},
renderChart() {
  const chart = this.$echarts.init(this.$refs.vatChart);
  // æŒ‰è®¢å•类型分组统计
  const inputItems = this.vatDetailList.filter(i => i.orderType === '进项');
  const outputItems = this.vatDetailList.filter(i => i.orderType === '销项');
  chart.setOption({
    tooltip: { trigger: 'axis' },
    legend: { data: ['进项税额', '销项税额'] },
    xAxis: { type: 'category', data: this.vatDetailList.map(i => i.invoiceNo) },
    yAxis: { type: 'value', name: '税额(元)' },
    series: [
      {
        name: '进项税额',
        type: 'bar',
        data: this.vatDetailList.map(i => i.orderType === '进项' ? i.taxAmount : 0),
        itemStyle: { color: '#409EFF' }
      },
      {
        name: '销项税额',
        type: 'bar',
        data: this.vatDetailList.map(i => i.orderType === '销项' ? i.taxAmount : 0),
        itemStyle: { color: '#E6A23C' }
      }
    ]
  });
},
handleSizeChange(val) { this.page.size = val; this.loadVatDetail(); },
handleCurrentChange(val) { this.page.current = val; this.loadVatDetail(); },
```
## æ³¨æ„äº‹é¡¹
- åˆ—表展示每条发票/订单级别的税额明细(而非以前只按月汇总)
- è¿›é¡¹ç¨Žæ¥è‡ªé‡‡è´­å‘票(invoice_ledger),销项税来自销售开票(ticket_registration)
- å³ä¾§æŸ±çŠ¶å›¾ä½¿ç”¨ ECharts å®žçŽ°ï¼ŒæŒ‰å‘ç¥¨å·åˆ†ç»„å±•ç¤ºè¿›é”€å¯¹æ¯”
- month ç­›é€‰å‚数为空时查询全部数据
src/main/java/com/ruoyi/common/enums/SparePartsRequisitionRecordSourceTypeEnum.java
@@ -5,7 +5,8 @@
@Getter
public enum SparePartsRequisitionRecordSourceTypeEnum implements BaseEnum<Integer> {
    SparePartsRequisitionRecordSourceTypeRepair(0, "ç»´ä¿®"),
    SparePartsRequisitionRecordSourceTypeMaintenance(1, "保养");
    SparePartsRequisitionRecordSourceTypeMaintenance(1, "保养"),
    SparePartsRequisitionRecordSourceTypeDirectOutbound(2, "直接出库");
    private final Integer code;
    private final String value;
src/main/java/com/ruoyi/device/controller/DeviceMaintenanceController.java
@@ -79,5 +79,16 @@
        deviceMaintenanceService.export(response, ids);
    }
    @PutMapping("/complete/{id}")
    @Operation(summary = "保养完成")
    public AjaxResult complete(@PathVariable Long id) {
        return deviceMaintenanceService.complete(id);
    }
    @PutMapping("/batchComplete")
    @Operation(summary = "一键保养完成")
    public AjaxResult batchComplete(String dateStr) {
        return deviceMaintenanceService.batchComplete(dateStr);
    }
}
src/main/java/com/ruoyi/device/dto/DeviceMaintenanceDto.java
@@ -53,5 +53,8 @@
    @Schema(description = "保养图片")
    private List<StorageBlobDTO> storageBlobDTOs;
    @Schema(description = "日期分组字段(yyyy-MM-dd)")
    private String dateStr;
}
src/main/java/com/ruoyi/device/service/IDeviceMaintenanceService.java
@@ -21,4 +21,8 @@
    void export(HttpServletResponse response, Long[] ids);
    DeviceMaintenanceVo detailById(Long id);
    AjaxResult complete(Long id);
    AjaxResult batchComplete(String dateStr);
}
src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceServiceImpl.java
@@ -6,6 +6,7 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.device.dto.DeviceMaintenanceDto;
@@ -27,6 +28,9 @@
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
@@ -124,4 +128,49 @@
        vo.setStorageBlobVOs(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.DEVICE_MAINTENANCE, id));
        return vo;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult complete(Long id) {
        DeviceMaintenance record = this.getById(id);
        if (record == null) {
            return AjaxResult.error("保养记录不存在");
        }
        if (record.getStatus() != null && record.getStatus() == 1) {
            return AjaxResult.error("该保养记录已完成");
        }
        record.setStatus(1);
        record.setMaintenanceActuallyName(SecurityUtils.getLoginUser().getNickName());
        record.setMaintenanceActuallyTime(LocalDateTime.now());
        this.updateById(record);
        return AjaxResult.success();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult batchComplete(String dateStr) {
        if (StringUtils.isBlank(dateStr)) {
            return AjaxResult.error("日期参数不能为空");
        }
        LocalDate date = LocalDate.parse(dateStr);
        LocalDateTime startOfDay = date.atStartOfDay();
        LocalDateTime endOfDay = date.atTime(LocalTime.MAX);
        List<DeviceMaintenance> records = this.lambdaQuery()
                .eq(DeviceMaintenance::getStatus, 0)
                .ge(DeviceMaintenance::getCreateTime, startOfDay)
                .le(DeviceMaintenance::getCreateTime, endOfDay)
                .list();
        if (records.isEmpty()) {
            return AjaxResult.error("该日期下没有待完成的保养记录");
        }
        String nickName = SecurityUtils.getLoginUser().getNickName();
        LocalDateTime now = LocalDateTime.now();
        for (DeviceMaintenance record : records) {
            record.setStatus(1);
            record.setMaintenanceActuallyName(nickName);
            record.setMaintenanceActuallyTime(now);
        }
        this.updateBatchById(records);
        return AjaxResult.success("已完成 " + records.size() + " æ¡ä¿å…»è®°å½•");
    }
}
src/main/java/com/ruoyi/home/controller/HomeController.java
@@ -73,8 +73,10 @@
    /********************************************************营销采购类**************************************************/
    @GetMapping("/supplierPurchaseRanking")
    @Operation(summary = "供应商采购排名")
    public AjaxResult supplierPurchaseRanking(@DefaultType Integer type) {
        List<SupplierPurchaseRankingDto> list = homeService.supplierPurchaseRanking(type);
    public AjaxResult supplierPurchaseRanking(@DefaultType Integer type,
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate) {
        List<SupplierPurchaseRankingDto> list = homeService.supplierPurchaseRanking(type, startDate, endDate);
        return AjaxResult.success(list);
    }
@@ -87,22 +89,28 @@
    @GetMapping("/customerContributionRanking")
    @Operation(summary = "客户金额贡献排名")
    public AjaxResult customerContributionRanking(@DefaultType Integer type) {
        List<CustomerContributionRankingDto> list = homeService.customerContributionRanking(type);
    public AjaxResult customerContributionRanking(@DefaultType Integer type,
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate) {
        List<CustomerContributionRankingDto> list = homeService.customerContributionRanking(type, startDate, endDate);
        return AjaxResult.success(list);
    }
    @GetMapping("/productSalesAnalysis")
    @Operation(summary = "各产品销售金额分析")
    public AjaxResult productSalesAnalysis() {
        List<MapDto> list = homeService.productSalesAnalysis();
    public AjaxResult productSalesAnalysis(
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate) {
        List<MapDto> list = homeService.productSalesAnalysis(startDate, endDate);
        return AjaxResult.success(list);
    }
    @GetMapping("/rawMaterialPurchaseAmountRatio")
    @Operation(summary = "原材料采购金额占比")
    public AjaxResult rawMaterialPurchaseAmountRatio(){
        List<MapDto> list = homeService.rawMaterialPurchaseAmountRatio();
    public AjaxResult rawMaterialPurchaseAmountRatio(
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate){
        List<MapDto> list = homeService.rawMaterialPurchaseAmountRatio(startDate, endDate);
        return AjaxResult.success(list);
    }
@@ -302,22 +310,28 @@
    @GetMapping("/salesPurchaseStorageProductCount")
    @Operation(summary = "销售-采购-储存产品数")
    public AjaxResult salesPurchaseStorageProductCount(){
        List<MapDto> list = homeService.salesPurchaseStorageProductCount();
    public AjaxResult salesPurchaseStorageProductCount(
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate){
        List<MapDto> list = homeService.salesPurchaseStorageProductCount(startDate, endDate);
        return AjaxResult.success(list);
    }
    @GetMapping("/productInOutAnalysis")
    @Operation(summary = "产品出入库分析")
    public AjaxResult productInOutAnalysis(@DefaultType Integer type){
        List<Map<String, Object>> result = homeService.productInOutAnalysis(type);
    public AjaxResult productInOutAnalysis(@DefaultType Integer type,
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate){
        List<Map<String, Object>> result = homeService.productInOutAnalysis(type, startDate, endDate);
        return AjaxResult.success(result);
    }
    @GetMapping("/productTurnoverDays")
    @Operation(summary = "产品周转天数")
    public AjaxResult productTurnoverDays(){
        List<MapDto> list = homeService.productTurnoverDays();
    public AjaxResult productTurnoverDays(
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate){
        List<MapDto> list = homeService.productTurnoverDays(startDate, endDate);
        return AjaxResult.success(list);
    }
src/main/java/com/ruoyi/home/mapper/HomeMapper.java
@@ -2,6 +2,7 @@
import com.ruoyi.dto.MapDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
@@ -13,8 +14,17 @@
public interface HomeMapper {
    /**
     * æŸ¥è¯¢äº§å“å‘¨è½¬å¤©æ•°
     *
     *
     * @return äº§å“å‘¨è½¬å¤©æ•°åˆ—表
     */
    List<MapDto> productTurnoverDays();
    List<MapDto> productTurnoverDaysByDate(@Param("startDate") String startDate, @Param("endDate") String endDate);
    /**
     * æŸ¥è¯¢é‡‡è´­å“å¤§ç±»åˆ†å¸ƒï¼ˆå„产品小类的采购数量/金额)
     *
     * @return é‡‡è´­åˆ†å¸ƒåˆ—表
     */
    List<MapDto> selectPurchaseCategoryDistribution();
}
src/main/java/com/ruoyi/home/service/HomeService.java
@@ -38,23 +38,23 @@
    HomeSummaryDto summaryStatistics();
    List<SupplierPurchaseRankingDto> supplierPurchaseRanking(Integer type);
    List<SupplierPurchaseRankingDto> supplierPurchaseRanking(Integer type, String startDate, String endDate);
    CustomerRevenueAnalysisDto customerRevenueAnalysis(Long customerId, Integer type);
    ProductCategoryDistributionDto productCategoryDistribution();
    List<CustomerContributionRankingDto> customerContributionRanking(Integer type);
    List<CustomerContributionRankingDto> customerContributionRanking(Integer type, String startDate, String endDate);
    List<MapDto> productSalesAnalysis();
    List<MapDto> productSalesAnalysis(String startDate, String endDate);
    List<MapDto> rawMaterialPurchaseAmountRatio();
    List<MapDto> rawMaterialPurchaseAmountRatio(String startDate, String endDate);
    List<MapDto> salesPurchaseStorageProductCount();
    List<MapDto> salesPurchaseStorageProductCount(String startDate, String endDate);
    List<Map<String, Object>> productInOutAnalysis(Integer type);
    List<Map<String, Object>> productInOutAnalysis(Integer type, String startDate, String endDate);
    List<MapDto> productTurnoverDays();
    List<MapDto> productTurnoverDays(String startDate, String endDate);
    List<Map<String, Object>> incomeExpenseAnalysis(Integer type);
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java
@@ -694,36 +694,39 @@
    }
    @Override
    public List<SupplierPurchaseRankingDto> supplierPurchaseRanking(Integer type) {
        LocalDate today = LocalDate.now();
    public List<SupplierPurchaseRankingDto> supplierPurchaseRanking(Integer type, String startDateStr, String endDateStr) {
        LocalDate startDate;
        LocalDate endDate;
        switch (type) {
            case 1: // å‘¨
                startDate = today.with(DayOfWeek.MONDAY);
                endDate = today.with(DayOfWeek.SUNDAY);
                break;
            case 2: // æœˆ
                startDate = today.with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.with(TemporalAdjusters.lastDayOfMonth());
                break;
            case 3: // å­£åº¦
                Month currentMonth = today.getMonth();
                Month firstMonthOfQuarter = currentMonth.firstMonthOfQuarter();
                Month lastMonthOfQuarter = Month.of(firstMonthOfQuarter.getValue() + 2);
                startDate = today.withMonth(firstMonthOfQuarter.getValue()).with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.withMonth(lastMonthOfQuarter.getValue()).with(TemporalAdjusters.lastDayOfMonth());
                break;
            default:
                return new ArrayList<>();
        if (com.ruoyi.common.utils.StringUtils.isNotBlank(startDateStr) && com.ruoyi.common.utils.StringUtils.isNotBlank(endDateStr)) {
            startDate = LocalDate.parse(startDateStr);
            endDate = LocalDate.parse(endDateStr);
        } else {
            LocalDate today = LocalDate.now();
            switch (type) {
                case 1: // å‘¨
                    startDate = today.with(DayOfWeek.MONDAY);
                    endDate = today.with(DayOfWeek.SUNDAY);
                    break;
                case 2: // æœˆ
                    startDate = today.with(TemporalAdjusters.firstDayOfMonth());
                    endDate = today.with(TemporalAdjusters.lastDayOfMonth());
                    break;
                case 3: // å­£åº¦
                    Month currentMonth = today.getMonth();
                    Month firstMonthOfQuarter = currentMonth.firstMonthOfQuarter();
                    Month lastMonthOfQuarter = Month.of(firstMonthOfQuarter.getValue() + 2);
                    startDate = today.withMonth(firstMonthOfQuarter.getValue()).with(TemporalAdjusters.firstDayOfMonth());
                    endDate = today.withMonth(lastMonthOfQuarter.getValue()).with(TemporalAdjusters.lastDayOfMonth());
                    break;
                default:
                    return new ArrayList<>();
            }
        }
        QueryWrapper<PurchaseLedger> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("supplier_name", "SUM(contract_amount) as total_amount")
                .ge("entry_date", startDate)
                .le("entry_date", endDate)
                // .ne("approval_status", 3)
                .groupBy("supplier_name")
                .orderByDesc("total_amount")
                .last("LIMIT 5");
@@ -859,17 +862,81 @@
                majorDto.setRate("0.00");
            }
            // æŸ¥è¯¢å„产品小类的采购数量
            Map<String, BigDecimal> purchaseDataMap = new java.util.HashMap<>();
            List<com.ruoyi.dto.MapDto> purchaseDistribution = homeMapper.selectPurchaseCategoryDistribution();
            if (purchaseDistribution != null) {
                for (com.ruoyi.dto.MapDto item : purchaseDistribution) {
                    if (item.getName() != null && item.getValue() != null) {
                        purchaseDataMap.put(item.getName(), new BigDecimal(item.getValue()));
                    }
                }
            }
            BigDecimal majorTotalChildrenValue = BigDecimal.ZERO;
            List<ProductCategoryDistributionDto.MinorCategoryDto> minorDtos = new ArrayList<>();
            for (Product minor : children) {
                ProductCategoryDistributionDto.MinorCategoryDto minorDto = new ProductCategoryDistributionDto.MinorCategoryDto();
                minorDto.setName(minor.getProductName());
                minorDto.setValue("0");
                BigDecimal minorValue = purchaseDataMap.getOrDefault(minor.getProductName(), BigDecimal.ZERO);
                minorDto.setValue(minorValue.toString());
                minorDto.setRate("0.00");
                minorDto.setChildren(new ArrayList<>());
                minorDtos.add(minorDto);
                majorTotalChildrenValue = majorTotalChildrenValue.add(minorValue);
            }
            majorDto.setChildren(minorDtos);
            // ä½¿ç”¨å®žé™…采购数据更新大类的 value
            if (majorTotalChildrenValue.compareTo(BigDecimal.ZERO) > 0) {
                majorDto.setValue(majorTotalChildrenValue.toString());
            }
            majorItems.add(majorDto);
            // è®¡ç®—大类占比
            BigDecimal totalAllValue = BigDecimal.ZERO;
            for (ProductCategoryDistributionDto.MajorCategoryDto m : majorItems) {
                try {
                    totalAllValue = totalAllValue.add(new BigDecimal(m.getValue()));
                } catch (NumberFormatException ignored) {
                }
            }
            if (totalAllValue.compareTo(BigDecimal.ZERO) > 0) {
                for (ProductCategoryDistributionDto.MajorCategoryDto m : majorItems) {
                    try {
                        BigDecimal val = new BigDecimal(m.getValue());
                        m.setRate(val.divide(totalAllValue, 4, RoundingMode.HALF_UP)
                                .multiply(new BigDecimal("100"))
                                .setScale(2, RoundingMode.HALF_UP)
                                .toString());
                    } catch (NumberFormatException ignored) {
                    }
                }
            }
            // è®¡ç®—小类占比(在同大类内)
            for (ProductCategoryDistributionDto.MajorCategoryDto m : majorItems) {
                if (m.getChildren() == null || m.getChildren().isEmpty()) {
                    continue;
                }
                BigDecimal parentVal = BigDecimal.ZERO;
                for (ProductCategoryDistributionDto.MinorCategoryDto child : m.getChildren()) {
                    try {
                        parentVal = parentVal.add(new BigDecimal(child.getValue()));
                    } catch (NumberFormatException ignored) {
                    }
                }
                if (parentVal.compareTo(BigDecimal.ZERO) > 0) {
                    for (ProductCategoryDistributionDto.MinorCategoryDto child : m.getChildren()) {
                        try {
                            BigDecimal childVal = new BigDecimal(child.getValue());
                            child.setRate(childVal.divide(parentVal, 4, RoundingMode.HALF_UP)
                                    .multiply(new BigDecimal("100"))
                                    .setScale(2, RoundingMode.HALF_UP)
                                    .toString());
                        } catch (NumberFormatException ignored) {
                        }
                    }
                }
            }
        }
        dto.setItems(majorItems);
@@ -877,38 +944,42 @@
    }
    @Override
    public List<CustomerContributionRankingDto> customerContributionRanking(Integer type) {
        LocalDate today = LocalDate.now();
        LocalDate startDate = null;
        LocalDate endDate = null;
        switch (type) {
            case 1:
                startDate = today.with(DayOfWeek.MONDAY);
                endDate = today.with(DayOfWeek.SUNDAY);
                break;
            case 2:
                startDate = today.with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.with(TemporalAdjusters.lastDayOfMonth());
                break;
            case 3:
                Month currentMonth = today.getMonth();
                Month firstMonthOfQuarter = currentMonth.firstMonthOfQuarter();
                Month lastMonthOfQuarter = Month.of(firstMonthOfQuarter.getValue() + 2);
                startDate = today.withMonth(firstMonthOfQuarter.getValue()).with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.withMonth(lastMonthOfQuarter.getValue()).with(TemporalAdjusters.lastDayOfMonth());
                break;
    public List<CustomerContributionRankingDto> customerContributionRanking(Integer type, String startDateStr, String endDateStr) {
        LocalDate startDate;
        LocalDate endDate;
        if (com.ruoyi.common.utils.StringUtils.isNotBlank(startDateStr) && com.ruoyi.common.utils.StringUtils.isNotBlank(endDateStr)) {
            startDate = LocalDate.parse(startDateStr);
            endDate = LocalDate.parse(endDateStr);
        } else {
            LocalDate today = LocalDate.now();
            switch (type) {
                case 1:
                    startDate = today.with(DayOfWeek.MONDAY);
                    endDate = today.with(DayOfWeek.SUNDAY);
                    break;
                case 2:
                    startDate = today.with(TemporalAdjusters.firstDayOfMonth());
                    endDate = today.with(TemporalAdjusters.lastDayOfMonth());
                    break;
                case 3:
                    Month currentMonth = today.getMonth();
                    Month firstMonthOfQuarter = currentMonth.firstMonthOfQuarter();
                    Month lastMonthOfQuarter = Month.of(firstMonthOfQuarter.getValue() + 2);
                    startDate = today.withMonth(firstMonthOfQuarter.getValue()).with(TemporalAdjusters.firstDayOfMonth());
                    endDate = today.withMonth(lastMonthOfQuarter.getValue()).with(TemporalAdjusters.lastDayOfMonth());
                    break;
                default:
                    return new ArrayList<>();
            }
        }
        QueryWrapper<SalesLedger> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("customer_name", "SUM(contract_amount) as total_amount")
                .isNotNull("customer_name")
                .between("entry_date", startDate, endDate)
                .groupBy("customer_name")
                .orderByDesc("total_amount")
                .last("LIMIT 5");
        if (startDate != null && endDate != null) {
            queryWrapper.between("entry_date", startDate, endDate);
        }
        List<Map<String, Object>> maps = salesLedgerMapper.selectMaps(queryWrapper);
        List<CustomerContributionRankingDto> result = new ArrayList<>();
@@ -924,9 +995,13 @@
    }
    @Override
    public List<MapDto> productSalesAnalysis() {
        // èŽ·å–æ‰€æœ‰äº§å“å¤§ç±»çš„é”€å”®é¢
        List<Map<String, Object>> analysisResults = salesLedgerProductMapper.selectProductSalesAnalysis();
    public List<MapDto> productSalesAnalysis(String startDateStr, String endDateStr) {
        List<Map<String, Object>> analysisResults;
        if (com.ruoyi.common.utils.StringUtils.isNotBlank(startDateStr) && com.ruoyi.common.utils.StringUtils.isNotBlank(endDateStr)) {
            analysisResults = salesLedgerProductMapper.selectProductSalesAnalysisByDate(startDateStr, endDateStr);
        } else {
            analysisResults = salesLedgerProductMapper.selectProductSalesAnalysis();
        }
        if (CollectionUtils.isEmpty(analysisResults)) {
            return new ArrayList<>();
        }
@@ -961,9 +1036,13 @@
    }
    @Override
    public List<MapDto> rawMaterialPurchaseAmountRatio() {
        // èŽ·å–æ‰€æœ‰åŽŸææ–™çš„é‡‡è´­é¢
        List<Map<String, Object>> analysisResults = salesLedgerProductMapper.selectRawMaterialPurchaseAnalysis();
    public List<MapDto> rawMaterialPurchaseAmountRatio(String startDateStr, String endDateStr) {
        List<Map<String, Object>> analysisResults;
        if (com.ruoyi.common.utils.StringUtils.isNotBlank(startDateStr) && com.ruoyi.common.utils.StringUtils.isNotBlank(endDateStr)) {
            analysisResults = salesLedgerProductMapper.selectRawMaterialPurchaseAnalysisByDate(startDateStr, endDateStr);
        } else {
            analysisResults = salesLedgerProductMapper.selectRawMaterialPurchaseAnalysis();
        }
        if (CollectionUtils.isEmpty(analysisResults)) {
            return new ArrayList<>();
        }
@@ -998,25 +1077,47 @@
    }
    @Override
    public List<MapDto> salesPurchaseStorageProductCount() {
    public List<MapDto> salesPurchaseStorageProductCount(String startDate, String endDate) {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime currentMonthStart = now.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
        LocalDateTime lastMonth = now.minusMonths(1);
        LocalDateTime lastMonthStart = lastMonth.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
        LocalDateTime lastMonthEnd = lastMonth.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
        LocalDateTime currentMonthStart;
        LocalDateTime currentMonthEnd;
        LocalDateTime lastMonthStart;
        LocalDateTime lastMonthEnd;
        //  é”€å”®
        int currentSales = salesLedgerProductMapper.selectProductCountByTypeAndDate(1, currentMonthStart, now);
        int lastSales = salesLedgerProductMapper.selectProductCountByTypeAndDate(1, lastMonthStart, lastMonthEnd);
        //  é‡‡è´­
        int currentPurchase = salesLedgerProductMapper.selectProductCountByTypeAndDate(2, currentMonthStart, now);
        if (StringUtils.isNotBlank(startDate) && StringUtils.isNotBlank(endDate)) {
            LocalDate start = LocalDate.parse(startDate);
            LocalDate end = LocalDate.parse(endDate);
            currentMonthStart = start.atStartOfDay();
            currentMonthEnd = end.atTime(LocalTime.MAX);
            long daysBetween = java.time.temporal.ChronoUnit.DAYS.between(start, end) + 1;
            LocalDate lastStart = start.minusDays(daysBetween);
            LocalDate lastEnd = start.minusDays(1);
            lastMonthStart = lastStart.atStartOfDay();
            lastMonthEnd = lastEnd.atTime(LocalTime.MAX);
        } else {
            currentMonthStart = now.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
            currentMonthEnd = now;
            LocalDateTime lastMonth = now.minusMonths(1);
            lastMonthStart = lastMonth.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
            lastMonthEnd = lastMonth.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
        }
        // é”€å”®è®¢å•数(统计sales_ledger表)
        int currentSales = Math.toIntExact(salesLedgerMapper.selectCount(new LambdaQueryWrapper<SalesLedger>()
                .ge(SalesLedger::getEntryDate, currentMonthStart)
                .le(SalesLedger::getEntryDate, currentMonthEnd)));
        int lastSales = Math.toIntExact(salesLedgerMapper.selectCount(new LambdaQueryWrapper<SalesLedger>()
                .ge(SalesLedger::getEntryDate, lastMonthStart)
                .le(SalesLedger::getEntryDate, lastMonthEnd)));
        // é‡‡è´­äº§å“æ•°
        int currentPurchase = salesLedgerProductMapper.selectProductCountByTypeAndDate(2, currentMonthStart, currentMonthEnd);
        int lastPurchase = salesLedgerProductMapper.selectProductCountByTypeAndDate(2, lastMonthStart, lastMonthEnd);
        //  å‚¨å­˜
        int currentStorage = stockInventoryMapper.selectStorageProductCountByDate(currentMonthStart, now);
        // å‚¨å­˜äº§å“æ•°
        int currentStorage = stockInventoryMapper.selectStorageProductCountByDate(currentMonthStart, currentMonthEnd);
        int lastStorage = stockInventoryMapper.selectStorageProductCountByDate(lastMonthStart, lastMonthEnd);
        List<MapDto> list = new ArrayList<>();
        list.add(createMapDto("销售产品数", currentSales, lastSales));
        list.add(createMapDto("销售订单数", currentSales, lastSales));
        list.add(createMapDto("采购产品数", currentPurchase, lastPurchase));
        list.add(createMapDto("储存产品数", currentStorage, lastStorage));
@@ -1045,7 +1146,7 @@
    }
    @Override
    public List<Map<String, Object>> productInOutAnalysis(Integer type) {
    public List<Map<String, Object>> productInOutAnalysis(Integer type, String startDateStr, String endDateStr) {
        String targetName;
        if (type == 1) {
            targetName = "原材料";
@@ -1070,8 +1171,17 @@
        LocalDate now = LocalDate.now();
        DateTimeFormatter displayDtf = DateTimeFormatter.ofPattern("M/d");
        String startDate = now.minusDays(6).toString();
        String endDate = now.toString();
        LocalDate start;
        LocalDate end;
        if (com.ruoyi.common.utils.StringUtils.isNotBlank(startDateStr) && com.ruoyi.common.utils.StringUtils.isNotBlank(endDateStr)) {
            start = LocalDate.parse(startDateStr);
            end = LocalDate.parse(endDateStr);
        } else {
            end = now;
            start = now.minusDays(6);
        }
        String startDate = start.toString();
        String endDate = end.toString();
        List<Map<String, Object>> inCounts = stockInventoryMapper.selectDailyStockInCounts(rootCategoryId, startDate,
                endDate + " 23:59:59");
@@ -1108,7 +1218,10 @@
    }
    @Override
    public List<MapDto> productTurnoverDays() {
    public List<MapDto> productTurnoverDays(String startDateStr, String endDateStr) {
        if (com.ruoyi.common.utils.StringUtils.isNotBlank(startDateStr) && com.ruoyi.common.utils.StringUtils.isNotBlank(endDateStr)) {
            return homeMapper.productTurnoverDaysByDate(startDateStr, endDateStr);
        }
        return homeMapper.productTurnoverDays();
    }
src/main/java/com/ruoyi/inspectiontask/controller/InspectionTaskController.java
@@ -73,4 +73,24 @@
        return R.ok(inspectionTaskService.delByIds(ids));
    }
    /**
     * å·¡æ£€å®Œæˆ
     */
    @PutMapping("/complete/{id}")
    @Operation(summary = "巡检完成")
    @Transactional(rollbackFor = Exception.class)
    public R complete(@PathVariable Long id) {
        return R.ok(inspectionTaskService.complete(id));
    }
    /**
     * ä¸€é”®å·¡æ£€å®Œæˆ
     */
    @PutMapping("/batchComplete")
    @Operation(summary = "一键巡检完成")
    @Transactional(rollbackFor = Exception.class)
    public R batchComplete(String dateStr) {
        return R.ok(inspectionTaskService.batchComplete(dateStr));
    }
}
src/main/java/com/ruoyi/inspectiontask/pojo/InspectionTask.java
@@ -82,6 +82,9 @@
    @Schema(description = "巡检地点详细描述")
    private String inspectionLocation;
    @Schema(description = "状态 0=待巡检, 1=已完成")
    private Integer status;
    @Schema(description = "软删除标志,0=未删除,1=已删除")
    private Integer deleted;
src/main/java/com/ruoyi/inspectiontask/service/InspectionTaskService.java
@@ -17,4 +17,8 @@
    int addOrEditInspectionTask(InspectionTaskDto inspectionTaskDto);
    int delByIds(Long[] ids);
    int complete(Long id);
    int batchComplete(String dateStr);
}
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java
@@ -378,6 +378,52 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int complete(Long id) {
        InspectionTask task = inspectionTaskMapper.selectById(id);
        if (task == null) {
            throw new IllegalArgumentException("巡检任务不存在");
        }
        if (task.getStatus() != null && task.getStatus() == 1) {
            throw new IllegalArgumentException("该巡检任务已完成");
        }
        task.setStatus(1);
        if (task.getInspectionResult() == null) {
            task.setInspectionResult("1");
        }
        return inspectionTaskMapper.updateById(task);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int batchComplete(String dateStr) {
        if (StringUtils.isBlank(dateStr)) {
            throw new IllegalArgumentException("日期参数不能为空");
        }
        java.time.LocalDate date = java.time.LocalDate.parse(dateStr);
        java.time.LocalDateTime startOfDay = date.atStartOfDay();
        java.time.LocalDateTime endOfDay = date.atTime(java.time.LocalTime.MAX);
        List<InspectionTask> tasks = inspectionTaskMapper.selectList(
                new LambdaQueryWrapper<InspectionTask>()
                        .eq(InspectionTask::getStatus, 0)
                        .ge(InspectionTask::getCreateTime, startOfDay)
                        .le(InspectionTask::getCreateTime, endOfDay));
        if (tasks.isEmpty()) {
            throw new IllegalArgumentException("该日期下没有待完成的巡检记录");
        }
        int count = 0;
        for (InspectionTask task : tasks) {
            task.setStatus(1);
            if (task.getInspectionResult() == null) {
                task.setInspectionResult("1");
            }
            inspectionTaskMapper.updateById(task);
            count++;
        }
        return count;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int delByIds(Long[] ids) {
        // æ£€æŸ¥å‚æ•°
        if (ids == null || ids.length == 0) {
src/main/java/com/ruoyi/measuringinstrumentledger/controller/SparePartsRequisitionRecordController.java
@@ -3,12 +3,15 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.measuringinstrumentledger.dto.SparePartsOutboundDto;
import com.ruoyi.measuringinstrumentledger.dto.SparePartsRequisitionRecordDto;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRequisitionRecordService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
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;
@@ -33,4 +36,11 @@
        IPage<SparePartsRequisitionRecordDto> listPage = sparePartsRequisitionRecordService.listPage(page, sparePartsRequisitionRecordDto);
        return AjaxResult.success(listPage);
    }
    @PostMapping("/outbound")
    @Operation(summary = "备件独立出库")
    public AjaxResult outbound(@RequestBody SparePartsOutboundDto dto){
        sparePartsRequisitionRecordService.outbound(dto);
        return AjaxResult.success();
    }
}
src/main/java/com/ruoyi/measuringinstrumentledger/dto/SparePartsOutboundDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.measuringinstrumentledger.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "备件出库请求")
public class SparePartsOutboundDto {
    @Schema(description = "备件ID", requiredMode = Schema.RequiredMode.REQUIRED)
    private Long sparePartsId;
    @Schema(description = "出库数量", requiredMode = Schema.RequiredMode.REQUIRED)
    private Integer quantity;
    @Schema(description = "设备ID(可选)")
    private Long deviceLedgerId;
    @Schema(description = "备注(可选)")
    private String remark;
}
src/main/java/com/ruoyi/measuringinstrumentledger/service/SparePartsRequisitionRecordService.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.measuringinstrumentledger.dto.SparePartsOutboundDto;
import com.ruoyi.measuringinstrumentledger.dto.SparePartsRequisitionRecordDto;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRequisitionRecord;
@@ -16,4 +17,6 @@
 */
public interface SparePartsRequisitionRecordService extends IService<SparePartsRequisitionRecord> {
    IPage<SparePartsRequisitionRecordDto> listPage(Page page, SparePartsRequisitionRecordDto sparePartsRequisitionRecordDto);
    void outbound(SparePartsOutboundDto dto);
}
src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/SparePartsRequisitionRecordServiceImpl.java
@@ -4,12 +4,18 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.SparePartsRequisitionRecordSourceTypeEnum;
import com.ruoyi.measuringinstrumentledger.dto.SparePartsOutboundDto;
import com.ruoyi.measuringinstrumentledger.dto.SparePartsRequisitionRecordDto;
import com.ruoyi.measuringinstrumentledger.mapper.SparePartsMapper;
import com.ruoyi.measuringinstrumentledger.mapper.SparePartsRequisitionRecordMapper;
import com.ruoyi.measuringinstrumentledger.pojo.SpareParts;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRequisitionRecord;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRequisitionRecordService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
/**
 * <p>
@@ -23,6 +29,7 @@
@RequiredArgsConstructor
public class SparePartsRequisitionRecordServiceImpl extends ServiceImpl<SparePartsRequisitionRecordMapper, SparePartsRequisitionRecord> implements SparePartsRequisitionRecordService {
    private final SparePartsRequisitionRecordMapper sparePartsRequisitionRecordMapper;
    private final SparePartsMapper sparePartsMapper;
    @Override
    public IPage<SparePartsRequisitionRecordDto> listPage(Page page, SparePartsRequisitionRecordDto sparePartsRequisitionRecordDto) {
@@ -35,4 +42,26 @@
        });
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void outbound(SparePartsOutboundDto dto) {
        SpareParts spareParts = sparePartsMapper.selectById(dto.getSparePartsId());
        if (spareParts == null) {
            throw new IllegalArgumentException("备件不存在");
        }
        if (spareParts.getQuantity().compareTo(new BigDecimal(dto.getQuantity())) < 0) {
            throw new IllegalArgumentException("备件 " + spareParts.getName() + " åº“存不足");
        }
        // æ‰£å‡åº“å­˜
        spareParts.setQuantity(spareParts.getQuantity().subtract(new BigDecimal(dto.getQuantity())));
        sparePartsMapper.updateById(spareParts);
        // åˆ›å»ºé¢†ç”¨è®°å½•
        SparePartsRequisitionRecord record = new SparePartsRequisitionRecord();
        record.setSourceType(SparePartsRequisitionRecordSourceTypeEnum.SparePartsRequisitionRecordSourceTypeDirectOutbound.getCode());
        record.setSparePartsId(dto.getSparePartsId());
        record.setQuantity(dto.getQuantity());
        record.setDeviceLedgerId(dto.getDeviceLedgerId());
        this.save(record);
    }
}
src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java
@@ -65,4 +65,11 @@
        ExcelUtil<VatDto> util = new ExcelUtil<VatDto>(VatDto.class);
        util.exportExcel(response, result.getRecords() , "增值税比对");
    }
    @GetMapping("/listVatDetail")
    @Operation(summary = "增值税明细-按发票/订单展示")
    public AjaxResult listVatDetail(Page page, String month) {
        IPage<VatDto> result = invoicePurchaseService.listVatDetail(page, month);
        return AjaxResult.success(result);
    }
}
src/main/java/com/ruoyi/purchase/dto/VatDto.java
@@ -1,15 +1,17 @@
package com.ruoyi.purchase.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class VatDto {
    //月份
    //月份(汇总视图)
    @Excel(name = "月份")
    private String month ;
@@ -24,4 +26,28 @@
    @Excel(name = "销-进")
    private BigDecimal taxAmount;
    // ========== æ˜Žç»†è§†å›¾å­—段 ==========
    @Excel(name = "发票号")
    private String invoiceNo;
    @Excel(name = "销售合同号")
    private String salesContractNo;
    @Excel(name = "客户名称")
    private String customerName;
    @Excel(name = "供应商名称")
    private String supplierName;
    @Excel(name = "类型")
    private String orderType;
    @Excel(name = "开票日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate invoiceDate;
    @Excel(name = "税率")
    private BigDecimal taxRate;
}
src/main/java/com/ruoyi/purchase/mapper/InvoicePurchaseMapper.java
@@ -25,4 +25,6 @@
    IPage<VatDto> listVat(Page page,@Param("month") String month);
    List<VatDto>  listVat1();
    IPage<VatDto> listVatDetail(Page page, @Param("month") String month);
}
src/main/java/com/ruoyi/purchase/service/IInvoicePurchaseService.java
@@ -32,4 +32,6 @@
    IPage<InvoicePurchaseReportDto> listPurchaseReport(Page page, InvoicePurchaseReportDto invoicePurchaseReportDto);
    IPage<VatDto> listVat(Page page,String month);
    IPage<VatDto> listVatDetail(Page page, String month);
}
src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java
@@ -175,4 +175,9 @@
        });
        return vatDtos;
    }
    @Override
    public IPage<VatDto> listVatDetail(Page page, String month) {
        return invoicePurchaseMapper.listVatDetail(page, month);
    }
}
src/main/java/com/ruoyi/sales/mapper/SalesLedgerProductMapper.java
@@ -37,8 +37,12 @@
    List<Map<String, Object>> selectProductSalesAnalysis();
    List<Map<String, Object>> selectProductSalesAnalysisByDate(@Param("startDate") String startDate, @Param("endDate") String endDate);
    List<Map<String, Object>> selectRawMaterialPurchaseAnalysis();
    List<Map<String, Object>> selectRawMaterialPurchaseAnalysisByDate(@Param("startDate") String startDate, @Param("endDate") String endDate);
    int selectProductCountByTypeAndDate(@Param("type") Integer type, @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
    BigDecimal selectRawMaterialExpense();
src/main/resources/application-dev.yml
@@ -28,7 +28,7 @@
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口,默认为8080
  port: 7006
  port: 7005
  servlet:
    # åº”用的访问路径
    context-path: /
src/main/resources/mapper/device/DeviceMaintenanceMapper.xml
@@ -50,6 +50,7 @@
                and dm.maintenance_actually_time &lt; date_add(str_to_date(#{deviceMaintenanceDto.maintenanceActuallyTime}, '%Y-%m-%d'), interval 1 day)
            </if>
        </where>
        ORDER BY dm.create_time DESC
    </select>
    <select id="detailById" resultType="com.ruoyi.device.vo.DeviceMaintenanceVo">
        select dm.id,
src/main/resources/mapper/home/HomeMapper.xml
@@ -13,4 +13,28 @@
        GROUP BY p.id, p.product_name
    </select>
    <select id="productTurnoverDaysByDate" resultType="com.ruoyi.dto.MapDto">
        SELECT p.product_name                                                      AS name,
               DATEDIFF(IFNULL(MAX(sor.create_time), NOW()), MAX(sir.create_time)) AS value
        FROM product p
                 JOIN product_model pm ON p.id = pm.product_id
                 JOIN stock_in_record sir ON pm.id = sir.product_model_id
                        AND sir.create_time &gt;= #{startDate}
                        AND sir.create_time &lt;= #{endDate}
                 LEFT JOIN stock_out_record sor ON pm.id = sor.product_model_id
        GROUP BY p.id, p.product_name
    </select>
    <select id="selectPurchaseCategoryDistribution" resultType="com.ruoyi.dto.MapDto">
        SELECT p2.product_name   AS name,
               COUNT(DISTINCT slp.id) AS value
        FROM sales_ledger_product slp
                 LEFT JOIN product p ON p.id = slp.product_category
                 LEFT JOIN product p2 ON p2.id = p.parent_id
        WHERE slp.type = 2
          AND p.parent_id IS NOT NULL
        GROUP BY p2.product_name
        ORDER BY value DESC
    </select>
</mapper>
src/main/resources/mapper/purchase/InvoicePurchaseMapper.xml
@@ -143,4 +143,55 @@
#         GROUP BY DATE_FORMAT(a.issue_date, '%Y-%m')
    </select>
    <select id="listVatDetail" resultType="com.ruoyi.purchase.dto.VatDto">
        SELECT *
        FROM (
            -- è¿›é¡¹ç¨Žæ˜Žç»†ï¼ˆé‡‡è´­è®¢å•)
            SELECT
                pl.purchase_contract_number AS invoiceNo,
                pl.sales_contract_no AS salesContractNo,
                pl.supplier_name AS supplierName,
                NULL AS customerName,
                '进项' AS orderType,
                pl.entry_date AS invoiceDate,
                slp.tax_rate AS taxRate,
                ROUND(SUM(slp.tax_inclusive_total_price - slp.tax_exclusive_total_price), 2) AS jTaxAmount,
                0 AS xTaxAmount,
                ROUND(SUM(slp.tax_inclusive_total_price - slp.tax_exclusive_total_price), 2) AS taxAmount
            FROM sales_ledger_product slp
            LEFT JOIN purchase_ledger pl ON pl.id = slp.sales_ledger_id
            WHERE slp.type = 2
              AND slp.tax_rate IS NOT NULL
              AND slp.tax_rate > 0
            GROUP BY pl.id
            UNION ALL
            -- é”€é¡¹ç¨Žæ˜Žç»†ï¼ˆé”€å”®è®¢å•)
            SELECT
                sl.sales_contract_no AS invoiceNo,
                sl.sales_contract_no AS salesContractNo,
                NULL AS supplierName,
                sl.customer_name AS customerName,
                '销项' AS orderType,
                sl.entry_date AS invoiceDate,
                slp.tax_rate AS taxRate,
                0 AS jTaxAmount,
                ROUND(SUM(slp.tax_inclusive_total_price - slp.tax_exclusive_total_price), 2) AS xTaxAmount,
                ROUND(SUM(slp.tax_inclusive_total_price - slp.tax_exclusive_total_price), 2) AS taxAmount
            FROM sales_ledger_product slp
            LEFT JOIN sales_ledger sl ON sl.id = slp.sales_ledger_id
            WHERE slp.type = 1
              AND slp.tax_rate IS NOT NULL
              AND slp.tax_rate > 0
            GROUP BY sl.id
        ) a
        <where>
            <if test="month != null and month != ''">
                AND DATE_FORMAT(a.invoiceDate, '%Y-%m') = #{month}
            </if>
        </where>
        ORDER BY a.invoiceDate DESC
    </select>
</mapper>
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml
@@ -240,6 +240,23 @@
        LIMIT 5
    </select>
    <select id="selectProductSalesAnalysisByDate" resultType="java.util.Map">
        SELECT
            product_category as name,
            SUM( tax_inclusive_total_price ) AS value
        FROM
            sales_ledger_product
        WHERE
            type = 1
            AND register_date &gt;= #{startDate}
            AND register_date &lt;= #{endDate}
        GROUP BY
            product_category
        ORDER BY
            value DESC
        LIMIT 5
    </select>
    <select id="selectRawMaterialPurchaseAnalysis" resultType="java.util.Map">
        SELECT
            pr.product_name AS name,
@@ -258,6 +275,26 @@
        LIMIT 5
    </select>
    <select id="selectRawMaterialPurchaseAnalysisByDate" resultType="java.util.Map">
        SELECT
            pr.product_name AS name,
            SUM( slp.tax_inclusive_total_price ) AS value
        FROM
            sales_ledger_product slp
            JOIN product pr ON slp.product_id = pr.id
        WHERE
            slp.type = 2
            AND slp.register_date &gt;= #{startDate}
            AND slp.register_date &lt;= #{endDate}
            AND pr.parent_id = ( SELECT id FROM product WHERE product_name = '原材料' )
        GROUP BY
            pr.id,
            pr.product_name
        ORDER BY
            value DESC
        LIMIT 5
    </select>
    <select id="selectProductCountByTypeAndDate" resultType="int">
        SELECT IFNULL(COUNT(*), 0)
        FROM sales_ledger_product