2026-08-07 bb1d7eeda426e66393cf4403cb3ef082db696bd9
feat(report): 新增会计报表功能并完善设备维护和首页统计

- 新增增值税比对报表功能,支持进项销项税额统计
- 添加增值税图表数据接口,实现进销项对比可视化
- 完善设备维护模块,增加批量保养完成功能
- 优化首页统计功能,支持自定义时间范围查询
- 更新数据库配置,调整开发环境数据源连接
- 重构采购报表模块,增强数据统计准确性
- 添加增值税明细查询功能,支持按发票和订单分类显示
- 完善供应商往来账务管理功能
- 优化产品销售分析和原材料采购占比统计
- 新增产品周转天数统计功能,支持指定时间段查询
- 添加采购品大类分布分析功能
- 优化设备保养完成流程,支持手动和自动标记完成状态
- 扩展首页统计接口,支持按时间区间过滤数据
- 优化数据导出功能,提升Excel导出性能
- 添加安全权限控制,集成登录用户信息验证
- 重构报表查询逻辑,统一分页和排序处理方式
- 完善异常处理机制,增强系统稳定性
- 优化SQL查询语句,提升数据检索效率
- 添加数据校验功能,确保业务数据准确性
- 重构设备维护服务层,优化批量操作性能
已修改27个文件
已添加5个文件
1324 ■■■■ 文件已修改
doc/旭宝峰系统功能优化-前端联调文档.md 349 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/旭宝峰系统功能优化需求整理.md 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/AccountStatementMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/SparePartsRequisitionRecordSourceTypeEnum.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/controller/DeviceMaintenanceController.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/dto/DeviceMaintenanceBatchCompleteDto.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/dto/DeviceMaintenanceDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/IDeviceMaintenanceService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceServiceImpl.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/controller/HomeController.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/mapper/HomeMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/HomeService.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java 183 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/controller/InspectionTaskController.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/pojo/InspectionTask.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/InspectionTaskService.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/controller/SparePartsRequisitionRecordController.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/dto/SparePartsOutboundDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/service/SparePartsRequisitionRecordService.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/SparePartsRequisitionRecordServiceImpl.java 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/VatDto.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/VatSummaryDto.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/PurchaseReportService.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReportServiceImpl.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/SalesLedgerProductMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountStatementMapper.xml 129 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/home/HomeMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/Ðñ±¦·åϵͳ¹¦ÄÜÓÅ»¯-ǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,349 @@
# æ—­å®å³°ç³»ç»ŸåŠŸèƒ½ä¼˜åŒ– - å‰ç«¯è”调文档
---
## ä¸€ã€å¤‡ä»¶ç®¡ç† - å¤‡ä»¶ç‹¬ç«‹å‡ºåº“
### 1.1 æ–°å¢žæŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/sparePartsRequisitionRecord/outbound` | å¤‡ä»¶ç‹¬ç«‹å‡ºåº“(不依赖维修/保养) |
### 1.2 è¯·æ±‚参数
```json
{
  "sparePartsId": 10,       // å¤‡ä»¶ID (Long, å¿…å¡«)
  "quantity": 5,            // å‡ºåº“数量 (Integer, å¿…å¡«)
  "deviceLedgerId": 1,      // è®¾å¤‡å°è´¦ID (Long, å¯é€‰)
  "remark": "手动出库备注"    // å¤‡æ³¨ (String, å¯é€‰)
}
```
### 1.3 è¿”回值
成功返回 `AjaxResult.success()`,失败抛出 `IllegalArgumentException`("备件不存在" / "备件 xxx åº“存不足"),前端用 `msg` å±•示。
### 1.4 æžšä¸¾å˜æ›´
`sourceType` æ–°å¢ž `2` = "直接出库"(`SparePartsRequisitionRecordSourceTypeEnum`)。列表接口 `/listPage` è¿”回的 `sourceText` ä¼šè‡ªåŠ¨æ˜ å°„ã€‚
### 1.5 å‰ç«¯æ”¹é€ æŒ‡å¼•
**文件**: `src/views/equipmentManagement/spareParts/index.vue`
- Tab「备件领用记录」区域新增 **「直接出库」** æŒ‰é’®
- ç‚¹å‡»å¼¹å‡ºè¡¨å•弹窗,字段:
  - å¤‡ä»¶é€‰æ‹©ï¼ˆ`el-select`,必填)
  - å‡ºåº“数量(`el-input-number`,min=1,必填)
  - è®¾å¤‡é€‰æ‹©ï¼ˆå¯é€‰ï¼Œå¯ä¸å…³è”设备)
  - å¤‡æ³¨ï¼ˆå¯é€‰ï¼‰
- æäº¤å‰æ ¡éªŒæ•°é‡ > 0 ä¸”不超过库存
- æˆåŠŸåŽåˆ·æ–°åˆ—è¡¨
**API å°è£…** (`src/api/equipmentManagement/sparePartsUsage.js`):
```js
export function outboundSpareParts(data) {
  return request({
    url: '/sparePartsRequisitionRecord/outbound',
    method: 'post',
    data
  })
}
```
---
## äºŒã€è®¾å¤‡ä¿å…» - ä¿å…»å®Œæˆ / æ‰¹é‡å®Œæˆ
### 2.1 æ–°å¢žæŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| PUT | `/device/maintenance/complete/{id}` | å•条保养完成 |
| PUT | `/device/maintenance/batchComplete` | ä¸€é”®ä¿å…»å®Œæˆï¼ˆæŒ‰åˆ›å»ºæ—¥æœŸæ‰¹é‡ï¼‰ |
### 2.2 å•条完成 `PUT /complete/{id}`
- Path å‚æ•°: `id` â€” ä¿å…»è®°å½•ID
- æ—  RequestBody
- åŽç«¯è‡ªåŠ¨è®¾ç½® `status=1`、`maintenanceActuallyName`=当前登录用户、`maintenanceActuallyTime`=当前时间
- è¿”回: æˆåŠŸ `AjaxResult.success()`,失败 `AjaxResult.error("保养记录不存在" / "该保养记录已完成")`
### 2.3 æ‰¹é‡å®Œæˆ `PUT /batchComplete`
- Query å‚æ•°: `dateStr` â€” æ—¥æœŸå­—符串,格式 `yyyy-MM-dd`
- åŽç«¯æŒ‰ `createTime` æ—¥æœŸèŒƒå›´æŸ¥è¯¢è¯¥æ—¥æ‰€æœ‰ `status=0` è®°å½•,批量设置为完结
- è¿”回: æˆåŠŸ `AjaxResult.success("已完成 N æ¡ä¿å…»è®°å½•")`,失败 `AjaxResult.error("日期参数不能为空" / "该日期下没有待完成的保养记录")`
### 2.4 å®žä½“变更
`DeviceMaintenanceDto` æ–°å¢ž `dateStr` å­—段(格式 `yyyy-MM-dd`),列表查询时已自动填充,前端可用于日期分组。
### 2.5 å‰ç«¯æ”¹é€ æŒ‡å¼•
**文件**: `src/views/equipmentManagement/upkeep/index.vue`
1. **按 `dateStr` æ—¥æœŸåˆ†ç»„折叠展示**(参照销售台账 `type="expand"` æ¨¡å¼ï¼‰
   - ä¸»è¡Œæ˜¾ç¤ºï¼šæ—¥æœŸã€è®°å½•数、**「一键保养完成」** æŒ‰é’®
   - å±•开后子表显示每条明细及 **「保养完成」** æŒ‰é’®
2. **单条保养完成按钮**
   - æ¯æ¡æ˜Žç»†æ“ä½œåˆ—新增「保养完成」
   - ä»… `status === 0`(待保养)时显示
   - ç‚¹å‡»ç¡®è®¤åŽè°ƒç”¨ `PUT /device/maintenance/complete/{id}`
3. **一键保养完成按钮**
   - åˆ†ç»„行上显示,传入该分组的 `dateStr`
   - ç‚¹å‡»ç¡®è®¤åŽè°ƒç”¨ `PUT /device/maintenance/batchComplete?dateStr=2026-08-07`
**API å°è£…** (`src/api/equipmentManagement/upkeep.js`):
```js
export function completeMaintenance(id) {
  return request({
    url: '/device/maintenance/complete/' + id,
    method: 'put'
  })
}
export function batchCompleteMaintenance(dateStr) {
  return request({
    url: '/device/maintenance/batchComplete',
    method: 'put',
    params: { dateStr }
  })
}
```
---
## ä¸‰ã€è®¾å¤‡å·¡æ£€ - å·¡æ£€å®Œæˆ / æ‰¹é‡å®Œæˆ
### 3.1 æ–°å¢žæŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| PUT | `/inspectionTask/complete/{id}` | å•条巡检完成 |
| PUT | `/inspectionTask/batchComplete` | ä¸€é”®å·¡æ£€å®Œæˆï¼ˆæŒ‰åˆ›å»ºæ—¥æœŸæ‰¹é‡ï¼‰ |
### 3.2 å•条完成 `PUT /complete/{id}`
- Path å‚æ•°: `id` â€” å·¡æ£€ä»»åŠ¡ID
- æ—  RequestBody
- åŽç«¯è®¾ç½® `status=1`,若 `inspectionResult` ä¸ºç©ºåˆ™é»˜è®¤è®¾ä¸º `"1"`(正常)
- è¿”回: `R.ok(受影响行数)`,已完成的记录会报错
### 3.3 æ‰¹é‡å®Œæˆ `PUT /batchComplete`
- Query å‚æ•°: `dateStr` â€” æ—¥æœŸå­—符串,格式 `yyyy-MM-dd`
- åŽç«¯æŒ‰ `createTime` æ—¥æœŸèŒƒå›´æŸ¥è¯¢è¯¥æ—¥æ‰€æœ‰ `status=0` è®°å½•,逐条设置为 `status=1`
- è¿”回: `R.ok(完成的记录数)`,无记录时报错
### 3.4 å®žä½“变更
`InspectionTask` æ–°å¢žå­—段:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| status | Integer | çŠ¶æ€ï¼š0=待巡检,1=已完成 |
> `GET /inspectionTask/list` è¿”回中已包含 `status` å­—段和 `dateStr` åˆ†ç»„字段。
### 3.5 æ•°æ®åº“变更(需 DBA æ‰§è¡Œï¼‰
```sql
ALTER TABLE `inspection_task`
ADD COLUMN `status` tinyint NULL DEFAULT 0
COMMENT '状态 0=待巡检, 1=已完成';
```
### 3.6 å‰ç«¯æ”¹é€ æŒ‡å¼•
**文件**: `src/views/equipmentManagement/inspectionManagement/index.vue`
1. **按 `dateStr` æ—¥æœŸåˆ†ç»„折叠展示**
2. **单条巡检完成按钮** â€” ä»… `status === 0` æ—¶æ˜¾ç¤ºï¼Œè°ƒç”¨ `PUT /inspectionTask/complete/{id}`
3. **一键巡检完成按钮** â€” åˆ†ç»„行上显示,调用 `PUT /inspectionTask/batchComplete?dateStr=xxx`
4. **状态列** â€” ä½¿ç”¨ `status` å­—段:0=待巡检(warning)、1=已完成(success)
**API å°è£…** (`src/api/inspectionManagement/index.js`):
```js
export function completeInspection(id) {
  return request({
    url: '/inspectionTask/complete/' + id,
    method: 'put'
  })
}
export function batchCompleteInspection(dateStr) {
  return request({
    url: '/inspectionTask/batchComplete',
    method: 'put',
    params: { dateStr }
  })
}
```
---
## å››ã€å¢žå€¼ç¨Žå¯¹æ¯”页面
### 4.1 æ–°å¢žæŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/purchase/report/listVatDetail` | å¢žå€¼ç¨Žæ˜Žç»†ï¼ˆæŒ‰å‘票/订单展示) |
### 4.2 `GET /listVatDetail` è¯·æ±‚参数
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| current | int | å¦ | é¡µç  |
| size | int | å¦ | æ¯é¡µæ¡æ•° |
| month | String | å¦ | æœˆä»½ç­›é€‰ï¼Œæ ¼å¼ `yyyy-MM` |
### 4.3 `GET /listVatDetail` è¿”回结构
```json
{
  "records": [
    {
      "invoiceNo": "CG202608001",       // å‘票号(采购合同号/销售合同号)
      "salesContractNo": "XS202608001", // é”€å”®åˆåŒå·
      "supplierName": "XX供应商",        // ä¾›åº”商名称(进项时有值)
      "customerName": "XX客户",          // å®¢æˆ·åç§°ï¼ˆé”€é¡¹æ—¶æœ‰å€¼ï¼‰
      "orderType": "进项",               // è¿›é¡¹ / é”€é¡¹
      "invoiceDate": "2026-08-03",      // å¼€ç¥¨æ—¥æœŸ
      "taxRate": 13.00,                 // ç¨Žçއ
      "jTaxAmount": 13000.00,           // è¿›é¡¹ç¨Žé¢
      "xTaxAmount": 0.00,               // é”€é¡¹ç¨Žé¢
      "taxAmount": 13000.00             // ç¨Žé¢
    }
  ]
}
```
### 4.4 å·²æœ‰æŽ¥å£ï¼ˆä¿æŒä¸å˜ï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/purchase/report/listVat` | å¢žå€¼ç¨Žå‘票汇总 |
| GET | `/purchase/report/vatChart` | å¢žå€¼ç¨Žè¿›é”€é¡¹å›¾è¡¨æ•°æ®ï¼ˆæŒ‰æœˆèšåˆï¼‰ |
### 4.5 å‰ç«¯æ”¹é€ æŒ‡å¼•
**文件**: `src/views/reportAnalysis/taxComparison/index.vue`
1. æ–°å¢ž **Tab åˆ‡æ¢**:「汇总视图」/「明细视图」
   - æ±‡æ€»è§†å›¾ â†’ è°ƒç”¨åŽŸæœ‰ `listVat` æŽ¥å£
   - æ˜Žç»†è§†å›¾ â†’ è°ƒç”¨æ–°å¢ž `listVatDetail` æŽ¥å£
2. æ˜Žç»†è§†å›¾è¡¨æ ¼åˆ—: å‘票号、销售合同号、客户/供应商、类型(tag)、开票日期、税率、税额
3. ä¿ç•™æœˆä»½ç­›é€‰å’Œå›¾è¡¨åŒºåŸŸ
**API å°è£…** (`src/api/procurementManagement/taxComparison.js`):
```js
export function getVatDetail(query) {
  return request({
    url: '/purchase/report/listVatDetail',
    method: 'get',
    params: query  // { current, size, month }
  })
}
```
---
## äº”、管理驾驶舱(首页 PSI æ•°æ®åˆ†æžå¤§å±ï¼‰
### 5.1 åŽç«¯å˜æ›´
以下接口新增可选日期范围参数 `startDate` / `endDate`(格式 `yyyy-MM-dd`),不传则按原有 type é€»è¾‘计算(周/月/季度):
| æ–¹æ³• | è·¯å¾„ | å˜æ›´è¯´æ˜Ž |
|------|------|----------|
| GET | `/home/supplierPurchaseRanking` | æ–°å¢ž `startDate`/`endDate` å¯é€‰å‚æ•° |
| GET | `/home/customerContributionRanking` | æ–°å¢ž `startDate`/`endDate` å¯é€‰å‚æ•° |
| GET | `/home/productSalesAnalysis` | æ–°å¢ž `startDate`/`endDate` å¯é€‰å‚æ•° |
| GET | `/home/rawMaterialPurchaseAmountRatio` | æ–°å¢ž `startDate`/`endDate` å¯é€‰å‚æ•° |
| GET | `/home/salesPurchaseStorageProductCount` | æ–°å¢ž `startDate`/`endDate` å¯é€‰å‚æ•°ï¼›label æ”¹ä¸º"销售订单数" |
| GET | `/home/productInOutAnalysis` | æ–°å¢ž `startDate`/`endDate` å¯é€‰å‚æ•° |
| GET | `/home/productTurnoverDays` | æ–°å¢ž `startDate`/`endDate` å¯é€‰å‚æ•° |
### 5.2 å…³é”®å˜æ›´è¯¦æƒ…
**`/home/salesPurchaseStorageProductCount`**:
- ç¬¬ä¸€ä¸ªæŒ‡æ ‡ä»Žã€Œé”€å”®äº§å“æ•°ã€æ”¹ä¸º **「销售订单数」**(统计 `sales_ledger` è¡¨è®¢å•数,非产品数)
- Query å‚数(均为可选):
  - `startDate`: å¼€å§‹æ—¥æœŸ `yyyy-MM-dd`
  - `endDate`: ç»“束日期 `yyyy-MM-dd`
  - `type`: ä¸ä¼ æ—¥æœŸå‚数时使用(1=周, 2=月, 3=季度)
- è¿”回结构不变 (`List<MapDto>`): `[{ name, value, rate }]`
**`/home/productCategoryDistribution`**:
- é‡‡è´­å“å¤§ç±»åˆ†å¸ƒçŽ¯å½¢å›¾æ•°æ®å·²ä¿®å¤ï¼Œå°ç±»çš„ `value`/`rate` çŽ°åœ¨åŒ…å«çœŸå®žé‡‡è´­æ•°æ®
- æ— éœ€å‰ç«¯æ”¹åЍ
### 5.3 å‰ç«¯æ”¹é€ æŒ‡å¼•
**文件**: `src/views/reportAnalysis/PSIDataAnalysis/index.vue`
1. **日期筛选控件**
   - é¡µé¢é¡¶éƒ¨æ–°å¢ž `el-date-picker`(类型 `daterange`,格式 `yyyy-MM-dd`)
   - å¿«æ·é€‰é¡¹: æœ¬æœˆã€è¿‘7天、近30天、近90天
   - é€šè¿‡ props / provide å°†é€‰ä¸­æ—¥æœŸä¼ ç»™å„子组件
2. **标签文字修改** (`components/center-top.vue`)
   - ç¬¬ä¸€å¼ å¡ç‰‡æ ‡é¢˜: ã€Œé”€å”®äº§å“æ•°ã€â†’ **「销售订单数」**
3. **API è°ƒç”¨æ›´æ–°**
   - å„子组件调用接口时传入 `startDate` / `endDate` å‚æ•°
**API å°è£…示例** (`src/api/viewIndex.js`):
```js
export function getSalesPurchaseStorageProductCount(params) {
  return request({
    url: '/home/salesPurchaseStorageProductCount',
    method: 'get',
    params  // { startDate, endDate } æˆ– { type } äºŒé€‰ä¸€
  })
}
```
> æ‰€æœ‰ `/home` æŽ¥å£å‡éµå¾ªç›¸åŒæ¨¡å¼ï¼šä¼  `startDate`/`endDate` æ—¶ä¸ä¼  `type`,反之亦然。
---
## å…­ã€æ”¹åŠ¨æ–‡ä»¶æ¸…å•
| é¡µé¢ | æ–‡ä»¶è·¯å¾„ | æ”¹åŠ¨ç±»åž‹ |
|------|----------|----------|
| å¤‡ä»¶ç®¡ç† | `src/views/equipmentManagement/spareParts/index.vue` | æ–°å¢žã€Œç›´æŽ¥å‡ºåº“」按钮+弹窗 |
| å¤‡ä»¶API | `src/api/equipmentManagement/sparePartsUsage.js` | æ–°å¢ž `outboundSpareParts` |
| è®¾å¤‡ä¿å…» | `src/views/equipmentManagement/upkeep/index.vue` | æ—¥æœŸåˆ†ç»„+保养完成+一键保养完成 |
| ä¿å…»API | `src/api/equipmentManagement/upkeep.js` | æ–°å¢ž `completeMaintenance` / `batchCompleteMaintenance` |
| è®¾å¤‡å·¡æ£€ | `src/views/equipmentManagement/inspectionManagement/index.vue` | æ—¥æœŸåˆ†ç»„+巡检完成+一键巡检完成 |
| å·¡æ£€API | `src/api/inspectionManagement/index.js` | æ–°å¢ž `completeInspection` / `batchCompleteInspection` |
| å¢žå€¼ç¨Žå¯¹æ¯” | `src/views/reportAnalysis/taxComparison/index.vue` | æ–°å¢žæ˜Žç»†è§†å›¾ Tab |
| å¢žå€¼ç¨ŽAPI | `src/api/procurementManagement/taxComparison.js` | æ–°å¢ž `getVatDetail` |
| PSI大屏 | `src/views/reportAnalysis/PSIDataAnalysis/index.vue` | æ–°å¢žæ—¥æœŸç­›é€‰æŽ§ä»¶ |
| PSI统计卡 | `src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue` | æ ‡ç­¾æ”¹"销售订单数"+日期参数 |
| å¤§å±API | `src/api/viewIndex.js` | æ‰€æœ‰æŽ¥å£å¢žåŠ  `startDate`/`endDate` å‚æ•° |
---
## ä¸ƒã€æ³¨æ„äº‹é¡¹
1. **巡检 DDL**: éœ€é€šçŸ¥ DBA æ‰§è¡Œ `ALTER TABLE inspection_task ADD COLUMN status tinyint ...`
2. **HTTP æ–¹æ³•**: å®Œæˆç±»æŽ¥å£ä½¿ç”¨ **PUT**,新增出库使用 **POST**,查询使用 **GET**
3. **日期格式**: æ‰€æœ‰æ—¥æœŸå‚数使用 `yyyy-MM-dd` å­—符串格式
4. **空参数兼容**: æ—¥æœŸå‚数均为可选,不传则保持原有 type é€»è¾‘
5. **批量完成**: ä¿å…»/巡检的批量完成均按 `createTime` æ—¥æœŸèŒƒå›´åŒ¹é…ï¼Œéž `planTime`
6. **错误处理**: å¤‡ä»¶å‡ºåº“失败会抛异常(非 AjaxResult),前端需在 catch ä¸­å±•示错误信息
doc/Ðñ±¦·åϵͳ¹¦ÄÜÓÅ»¯ÐèÇóÕûÀí.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
# ä¸€ã€è®¾å¤‡ç®¡ç†æ¨¡å—
## 1\. å¤‡ä»¶ç®¡ç† - å¤‡ä»¶é¢†ç”¨è®°å½•
现有备件出库仅能依托设备检修场景,出库渠道存在局限,需新增独立备件出库功能入口,支持直接完成备件领用出库操作。
## å¤‡ä»¶é¢†ç”¨è®°å½•页面2. è®¾å¤‡ä¿å…» - ä¿å…»ä»»åŠ¡
保养任务下发后,页面参照销售 / é‡‡è´­è®¢å•样式,支持按日期分组折叠展示,分组条目可展开查看明细;单条明细行末尾增加【保养完成】操作按钮,可单独办结单条保养任务;折叠汇总状态下,新增【一键保养完成】批量操作按钮,快速办结当前分组全部保养项。
## ä¿å…»ä»»åŠ¡é¡µé¢3. è®¾å¤‡å·¡æ£€ - å·¡æ£€ä»»åŠ¡
巡检任务下发完成后,页面参照销售 / é‡‡è´­è®¢å•布局,按日期分组折叠展示,分组支持展开查看明细;每条巡检明细后增设【巡检完成】按钮,支持单独办结单条巡检记录;分组折叠未展开状态下,提供【一键巡检完成】批量操作,统一办结该分组下所有巡检任务。
# äºŒã€å†³ç­–分析模块
## 1\. å¢žå€¼ç¨Žå¯¹æ¯”页面
当前页面查询无数据,按监理要求优化数据展示逻辑,列表展示各订单增值税明细;页面右侧新增柱状可视化图表,直观对比进销项增值税数据。
## å¢žå€¼ç¨Žå¯¹æ¯”页面2. ç®¡ç†é©¾é©¶èˆ±ï¼ˆPSI æ•°æ®åˆ†æžå¤§å±ï¼‰
指标修改:将顶部「销售产品数」调整为「销售订单数」;时间筛选:页面左上角新增日期范围下拉筛选控件,所有统计指标标注当前筛选时段;数据修复:解决「采购品分布」环形图无数据展示问题。
src/main/java/com/ruoyi/account/mapper/AccountStatementMapper.java
@@ -7,22 +7,20 @@
import com.ruoyi.account.bean.vo.StatementAccountVo;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.purchase.dto.VatDto;
import com.ruoyi.purchase.dto.VatSummaryDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 *  Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 09:42:47
 */
import java.util.List;
@Mapper
public interface AccountStatementMapper extends BaseMapper<AccountStatement> {
    IPage<StatementAccountVo> listPageAccountStatement(Page page, @Param("req") StatementAccountDto statementAccountDto);
    IPage<VatDto> selectVatDtoPage(Page page, @Param("month") String month);
    IPage<VatDto> selectVatDtoPage(Page page, @Param("month") String month, @Param("type") String type);
    List<VatSummaryDto> selectVatSummary(@Param("year") String year);
    IPage<VatDto> listVatDetail(Page page, @Param("month") String 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
@@ -2,6 +2,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.device.dto.DeviceMaintenanceBatchCompleteDto;
import com.ruoyi.device.dto.DeviceMaintenanceDto;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.pojo.DeviceMaintenance;
@@ -12,6 +13,7 @@
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
@@ -79,5 +81,23 @@
        deviceMaintenanceService.export(response, ids);
    }
    @PostMapping("/batchComplete")
    @Operation(summary = "一键保养完成")
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult batchComplete(@RequestBody DeviceMaintenanceBatchCompleteDto dto) {
        return deviceMaintenanceService.batchComplete(dto);
    }
    @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/DeviceMaintenanceBatchCompleteDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@Schema(name = "设备保养批量完成请求")
public class DeviceMaintenanceBatchCompleteDto {
    @Schema(description = "计划保养日期", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDate maintenancePlanDate;
    @Schema(description = "实际保养人")
    private String maintenanceActuallyName;
    @Schema(description = "实际保养时间")
    private LocalDateTime maintenanceActuallyTime;
    @Schema(description = "保养结果")
    private String maintenanceResult;
}
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
@@ -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.device.dto.DeviceMaintenanceBatchCompleteDto;
import com.ruoyi.device.dto.DeviceMaintenanceDto;
import com.ruoyi.device.pojo.DeviceMaintenance;
import com.ruoyi.device.vo.DeviceMaintenanceVo;
@@ -21,4 +22,10 @@
    void export(HttpServletResponse response, Long[] ids);
    DeviceMaintenanceVo detailById(Long id);
    AjaxResult batchComplete(DeviceMaintenanceBatchCompleteDto dto);
    AjaxResult complete(Long id);
    AjaxResult batchComplete(String dateStr);
}
src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceServiceImpl.java
@@ -1,13 +1,16 @@
package com.ruoyi.device.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.DeviceMaintenanceBatchCompleteDto;
import com.ruoyi.device.dto.DeviceMaintenanceDto;
import com.ruoyi.device.execl.DeviceMaintenanceExeclDto;
import com.ruoyi.device.mapper.DeviceMaintenanceMapper;
@@ -27,6 +30,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;
@@ -95,6 +101,11 @@
            }
        }
        // è‹¥è®¾ç½®äº†å®žé™…保养日期,则自动标记为完结
        if (deviceMaintenance.getMaintenanceActuallyTime() != null && deviceMaintenance.getStatus() == null) {
            deviceMaintenance.setStatus(1);
        }
        if (this.updateById(deviceMaintenance)) {
            // å¤„理图片上传
            fileUtil.saveStorageAttachmentByRecordTypeAndRecordId("file", RecordTypeEnum.DEVICE_MAINTENANCE, deviceMaintenance.getId(), deviceMaintenance.getStorageBlobDTOs());
@@ -104,6 +115,80 @@
    }
    @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() + " æ¡ä¿å…»è®°å½•");
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult batchComplete(DeviceMaintenanceBatchCompleteDto dto) {
        if (dto.getMaintenancePlanDate() == null) {
            return AjaxResult.error("计划保养日期不能为空");
        }
        LambdaQueryWrapper<DeviceMaintenance> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(DeviceMaintenance::getStatus, 0)
               .apply("DATE(maintenance_plan_time) = {0}", dto.getMaintenancePlanDate().toString());
        List<DeviceMaintenance> list = this.list(wrapper);
        if (list.isEmpty()) {
            return AjaxResult.success("该日期下没有待保养的记录");
        }
        list.forEach(task -> {
            task.setStatus(1);
            if (StringUtils.isNotBlank(dto.getMaintenanceActuallyName())) {
                task.setMaintenanceActuallyName(dto.getMaintenanceActuallyName());
            }
            if (dto.getMaintenanceActuallyTime() != null) {
                task.setMaintenanceActuallyTime(dto.getMaintenanceActuallyTime());
            }
            if (StringUtils.isNotBlank(dto.getMaintenanceResult())) {
                task.setMaintenanceResult(dto.getMaintenanceResult());
            }
        });
        this.updateBatchById(list);
        return AjaxResult.success("共完成 " + list.size() + " æ¡ä¿å…»è®°å½•");
    }
    @Override
    public void export(HttpServletResponse response, Long[] ids) {
        List<DeviceMaintenance> supplierManageList = deviceMaintenanceMapper.selectList(null);
        ArrayList<DeviceMaintenanceExeclDto> deviceLedgerExeclDtos = new ArrayList<>();
src/main/java/com/ruoyi/home/controller/HomeController.java
@@ -103,8 +103,10 @@
    /********************************************************营销采购类**************************************************/
    @GetMapping("/supplierPurchaseRanking")
    @Operation(summary = "供应商采购排名")
    public R supplierPurchaseRanking(@DefaultType Integer type) {
        List<SupplierPurchaseRankingDto> list = homeService.supplierPurchaseRanking(type);
    public R supplierPurchaseRanking(@DefaultType Integer type,
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate) {
        List<SupplierPurchaseRankingDto> list = homeService.supplierPurchaseRanking(type, startDate, endDate);
        return R.ok(list);
    }
@@ -117,22 +119,28 @@
    @GetMapping("/customerContributionRanking")
    @Operation(summary = "客户金额贡献排名")
    public R customerContributionRanking(@DefaultType Integer type) {
        List<CustomerContributionRankingDto> list = homeService.customerContributionRanking(type);
    public R customerContributionRanking(@DefaultType Integer type,
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate) {
        List<CustomerContributionRankingDto> list = homeService.customerContributionRanking(type, startDate, endDate);
        return R.ok(list);
    }
    @GetMapping("/productSalesAnalysis")
    @Operation(summary = "各产品销售金额分析")
    public R productSalesAnalysis() {
        List<MapDto> list = homeService.productSalesAnalysis();
    public R productSalesAnalysis(
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate) {
        List<MapDto> list = homeService.productSalesAnalysis(startDate, endDate);
        return R.ok(list);
    }
    @GetMapping("/rawMaterialPurchaseAmountRatio")
    @Operation(summary = "原材料采购金额占比")
    public R rawMaterialPurchaseAmountRatio(){
        List<MapDto> list = homeService.rawMaterialPurchaseAmountRatio();
    public R rawMaterialPurchaseAmountRatio(
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate){
        List<MapDto> list = homeService.rawMaterialPurchaseAmountRatio(startDate, endDate);
        return R.ok(list);
    }
@@ -473,22 +481,28 @@
    @GetMapping("/salesPurchaseStorageProductCount")
    @Operation(summary = "销售-采购-储存产品数")
    public R salesPurchaseStorageProductCount(){
        List<MapDto> list = homeService.salesPurchaseStorageProductCount();
    public R salesPurchaseStorageProductCount(
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate){
        List<MapDto> list = homeService.salesPurchaseStorageProductCount(startDate, endDate);
        return R.ok(list);
    }
    @GetMapping("/productInOutAnalysis")
    @Operation(summary = "产品出入库分析")
    public R productInOutAnalysis(@DefaultType Integer type){
        List<Map<String, Object>> result = homeService.productInOutAnalysis(type);
    public R 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 R.ok(result);
    }
    @GetMapping("/productTurnoverDays")
    @Operation(summary = "产品周转天数")
    public R productTurnoverDays(){
        List<MapDto> list = homeService.productTurnoverDays();
    public R productTurnoverDays(
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate){
        List<MapDto> list = homeService.productTurnoverDays(startDate, endDate);
        return R.ok(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;
/**
@@ -17,4 +18,13 @@
     * @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
@@ -697,10 +697,14 @@
    }
    @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;
        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);
@@ -714,19 +718,18 @@
                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");
@@ -862,17 +865,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);
@@ -880,10 +947,14 @@
    }
    @Override
    public List<CustomerContributionRankingDto> customerContributionRanking(Integer type) {
    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();
        LocalDate startDate = null;
        LocalDate endDate = null;
        switch (type) {
            case 1:
                startDate = today.with(DayOfWeek.MONDAY);
@@ -900,18 +971,18 @@
                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<>();
@@ -927,9 +998,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<>();
        }
@@ -964,9 +1039,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<>();
        }
@@ -1001,25 +1080,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));
@@ -1048,7 +1149,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 = "原材料";
@@ -1073,8 +1174,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");
@@ -1111,7 +1221,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
@@ -15,12 +15,6 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
/**
 * @author :yys
 * @date : 2025/9/19 10:52
 */
@RestController
@Tag(name = "巡检任务记录")
@RequestMapping("/inspectionTask")
@@ -29,20 +23,13 @@
    private InspectionTaskService inspectionTaskService;
    /**
     * å·¡æ£€ä»»åŠ¡è¡¨è¡¨æŸ¥è¯¢
     */
    @GetMapping("/list")
    @Operation(summary = "巡检任务表表查询")
    @Operation(summary = "巡检任务表查询")
    public R<IPage<InspectionTaskDto>> list(Page<InspectionTask> page, InspectionTaskDto inspectionTaskDto) {
        IPage<InspectionTaskDto> list = inspectionTaskService.selectInspectionTaskList(page, inspectionTaskDto);
        return R.ok(list);
    }
    /**
     * å¯¼å‡º
     */
    @PostMapping("/export")
    @Operation(summary = "导出定时任务记录")
    public void export(HttpServletResponse response) {
@@ -53,9 +40,6 @@
        util.exportExcel(response, list.getRecords(), "导出定时任务记录");
    }
    /**
     * å·¡æ£€ä»»åŠ¡è¡¨æ–°å¢žä¿®æ”¹
     */
    @PostMapping("/addOrEditInspectionTask")
    @Operation(summary = "巡检任务表新增修改")
    @Transactional(rollbackFor = Exception.class)
@@ -63,9 +47,6 @@
        return R.ok(inspectionTaskService.addOrEditInspectionTask(inspectionTaskDto));
    }
    /**
     * å·¡æ£€ä»»åŠ¡è¡¨åˆ é™¤
     */
    @DeleteMapping("/delInspectionTask")
    @Operation(summary = "巡检任务表删除")
    @Transactional(rollbackFor = Exception.class)
@@ -73,4 +54,18 @@
        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
@@ -6,10 +6,6 @@
import com.ruoyi.inspectiontask.dto.InspectionTaskDto;
import com.ruoyi.inspectiontask.pojo.InspectionTask;
/**
 * @author :yys
 * @date : 2025/9/19 10:49
 */
public interface InspectionTaskService extends IService<InspectionTask> {
    IPage<InspectionTaskDto> selectInspectionTaskList(Page<InspectionTask> page, InspectionTaskDto inspectionTaskDto);
@@ -17,4 +13,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
@@ -187,9 +187,63 @@
        // ä¿å­˜æ–‡ä»¶ï¼ˆå­—段不传则保留历史)
        saveInspectionAttachments(inspectionTask.getId(), inspectionTaskDto);
        // å·¡æ£€ç»“果设置后,自动标记为已完成
        if (StringUtils.isNotBlank(inspectionTaskDto.getInspectionResult())) {
            InspectionTask statusUpdate = new InspectionTask();
            statusUpdate.setId(inspectionTask.getId());
            statusUpdate.setStatus(1);
            inspectionTaskMapper.updateById(statusUpdate);
        }
        return i;
    }
    @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;
    }
    private void validateInspectionInput(InspectionTaskDto inspectionTaskDto, InspectionTask oldInspectionTask) {
        String inspectionResult = inspectionTaskDto.getInspectionResult();
        if (StringUtils.isBlank(inspectionResult) && oldInspectionTask != null) {
src/main/java/com/ruoyi/measuringinstrumentledger/controller/SparePartsRequisitionRecordController.java
@@ -3,23 +3,18 @@
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;
/**
 * <p>
 *  å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-04-02 03:59:56
 */
@RestController
@RequestMapping("/sparePartsRequisitionRecord")
@Tag(name = "备件领用记录接口")
@@ -28,9 +23,16 @@
    private SparePartsRequisitionRecordService sparePartsRequisitionRecordService;
    @GetMapping("/listPage")
    @Operation(summary = "备件分类-分页查询")
    @Operation(summary = "备件领用记录-分页查询")
    public AjaxResult listPage(Page page, SparePartsRequisitionRecordDto sparePartsRequisitionRecordDto){
        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,17 +3,12 @@
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;
/**
 * <p>
 *  æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-04-02 03:59:56
 */
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,35 +4,58 @@
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;
/**
 * <p>
 * æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-04-02 03:59:56
 */
import java.math.BigDecimal;
@Service
@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) {
        IPage<SparePartsRequisitionRecordDto> result = sparePartsRequisitionRecordMapper.listPage(page, sparePartsRequisitionRecordDto);
        // å¤„理来源类型文本
        result.getRecords().forEach(record -> {
            if (record.getSourceType() != null) {
                record.setSourceText(SparePartsRequisitionRecordSourceTypeEnum.getByCode(record.getSourceType()).getValue());
                SparePartsRequisitionRecordSourceTypeEnum typeEnum = SparePartsRequisitionRecordSourceTypeEnum.getByCode(record.getSourceType());
                if (typeEnum != null) {
                    record.setSourceText(typeEnum.getValue());
                }
            }
        });
        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
@@ -1,12 +1,15 @@
package com.ruoyi.purchase.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.service.ISupplierService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.purchase.dto.VatDto;
import com.ruoyi.purchase.dto.VatSummaryDto;
import com.ruoyi.purchase.service.PurchaseReportService;
import com.ruoyi.purchase.vo.PurchaseReportVo;
import io.swagger.v3.oas.annotations.Operation;
@@ -29,7 +32,6 @@
    private final ISupplierService supplierService;
    private final PurchaseReportService purchaseReportService;
    @GetMapping("/list")
    @Log(title = "采购报表-项目利润", businessType = BusinessType.OTHER)
    public R list(Page page, String customerName) {
@@ -43,24 +45,38 @@
        List<PurchaseReportVo> list = purchaseReportService.list(new Page(1,-1),customerName).getRecords();
        ExcelUtil<PurchaseReportVo> util = new ExcelUtil<>(PurchaseReportVo.class);
        util.exportExcel(response, list , "项目利润");
    }
    @Log(title = "采购报表-增值税比对", businessType = BusinessType.OTHER)
    @GetMapping("/listVat")
    public R listVat(Page page,String month) {
        return R.ok(purchaseReportService.listVat(page,month));
    public R listVat(Page page, String month, String type) {
        return R.ok(purchaseReportService.listVat(page, month, type));
    }
    @Log(title = "采购报表-增值税比对", businessType = BusinessType.EXPORT)
    @PostMapping("/exportTwo")
    @Operation(summary = "采购报表-增值税比对")
    @Operation(summary = "采购报表-增值税比对导出")
    public void exportTwo(HttpServletResponse response,String month) {
        List<VatDto> list = purchaseReportService.listVat(new Page(1,-1),month).getRecords();
        List<VatDto> list = purchaseReportService.listVat(new Page(1, -1), month, null).getRecords();
        ExcelUtil<VatDto> util = new ExcelUtil<>(VatDto.class);
        util.exportExcel(response, list , "增值税比对");
    }
    @Log(title = "采购报表-增值税图表数据", businessType = BusinessType.OTHER)
    @GetMapping("/vatChart")
    @Operation(summary = "增值税进销项对比图表数据")
    public R vatChart(String year) {
        List<VatSummaryDto> list = purchaseReportService.vatSummary(year);
        return R.ok(list);
    }
    @GetMapping("/listVatDetail")
    @Operation(summary = "增值税明细-按发票/订单展示")
    public AjaxResult listVatDetail(Page page, String month) {
        IPage<VatDto> result = purchaseReportService.listVatDetail(page, month);
        return AjaxResult.success(result);
    }
    @GetMapping("/supplierTransactions")
    @Log(title = "供应商往来", businessType = BusinessType.OTHER)
    @Operation(summary = "供应商往来")
src/main/java/com/ruoyi/purchase/dto/VatDto.java
@@ -1,15 +1,55 @@
package com.ruoyi.purchase.dto;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@Schema(name = "VatDto", description = "管理驾驶舱--增值税比对参数")
@Schema(name = "VatDto", description = "增值税比对明细")
@ExcelIgnoreUnannotated
public class VatDto {
    @Excel(name = "ID")
    @Schema(description = "发票ID")
    private Long id;
    @Excel(name = "发票号码")
    @Schema(description = "发票号码")
    private String invoiceNumber;
    @Excel(name = "开票日期")
    @Schema(description = "开票日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate issueDate;
    @Excel(name = "类型")
    @Schema(description = "类型 sales-销项 purchase-进项")
    private String type;
    @Excel(name = "发票类型")
    @Schema(description = "发票类型")
    private String invoiceType;
    @Excel(name = "税额")
    @Schema(description = "税额")
    private BigDecimal taxAmount;
    @Excel(name = "价税合计")
    @Schema(description = "价税合计")
    private BigDecimal totalAmount;
    @Excel(name = "客户名称")
    @Schema(description = "客户名称")
    private String customerName;
    @Excel(name = "供应商名称")
    @Schema(description = "供应商名称")
    private String supplierName;
    @Excel(name = "月份")
    @Schema(description = "月份")
@@ -23,8 +63,27 @@
    @Schema(description = "进项税额")
    private BigDecimal xTaxAmount;
    @Excel(name = "销-进")
    @Schema(description = "销-进")
    private BigDecimal taxAmount;
    // ========== æ˜Žç»†è§†å›¾å­—段 ==========
    @Excel(name = "发票号")
    @Schema(description = "发票号")
    private String invoiceNo;
    @Excel(name = "销售合同号")
    @Schema(description = "销售合同号")
    private String salesContractNo;
    @Excel(name = "订单类型")
    @Schema(description = "类型 è¿›é¡¹/销项")
    private String orderType;
    @Excel(name = "开票日期")
    @Schema(description = "开票日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate invoiceDate;
    @Excel(name = "税率")
    @Schema(description = "税率")
    private BigDecimal taxRate;
}
src/main/java/com/ruoyi/purchase/dto/VatSummaryDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package com.ruoyi.purchase.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(description = "增值税进销项按月汇总(图表用)")
public class VatSummaryDto {
    @Schema(description = "月份")
    private String month;
    @Schema(description = "销项税额合计")
    private BigDecimal jTaxAmount;
    @Schema(description = "进项税额合计")
    private BigDecimal xTaxAmount;
    @Schema(description = "差额(销-进)")
    private BigDecimal taxAmount;
}
src/main/java/com/ruoyi/purchase/service/PurchaseReportService.java
@@ -1,14 +1,20 @@
package com.ruoyi.purchase.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.purchase.dto.VatDto;
import com.ruoyi.purchase.dto.VatSummaryDto;
import com.ruoyi.purchase.vo.PurchaseReportVo;
import java.util.List;
public interface PurchaseReportService {
    IPage<PurchaseReportVo> list(Page page, String customerName);
    IPage<VatDto> listVat(Page page, String month);
    IPage<VatDto> listVat(Page page, String month, String type);
    List<VatSummaryDto> vatSummary(String year);
    IPage<VatDto> listVatDetail(Page page, String month);
}
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReportServiceImpl.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.mapper.AccountStatementMapper;
import com.ruoyi.purchase.dto.VatDto;
import com.ruoyi.purchase.dto.VatSummaryDto;
import com.ruoyi.purchase.service.PurchaseReportService;
import com.ruoyi.purchase.vo.PurchaseReportVo;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
@@ -11,6 +12,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
@@ -26,7 +28,17 @@
    }
    @Override
    public IPage<VatDto> listVat(Page page, String month) {
        return accountStatementMapper.selectVatDtoPage(page, month);
    public IPage<VatDto> listVat(Page page, String month, String type) {
        return accountStatementMapper.selectVatDtoPage(page, month, type);
    }
    @Override
    public List<VatSummaryDto> vatSummary(String year) {
        return accountStatementMapper.selectVatSummary(year);
    }
    @Override
    public IPage<VatDto> listVatDetail(Page page, String month) {
        return accountStatementMapper.listVatDetail(page, month);
    }
}
src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java
@@ -89,5 +89,4 @@
    IPage<SalesLedgerDto> listSalesLedgerAndShipped(Page page, @Param("ew") SalesLedgerDto salesLedgerDto);
    IPage<PurchaseReportVo> selectPurchaseReportVoPage(Page page, @Param("customerName") String customerName);
}
src/main/java/com/ruoyi/sales/mapper/SalesLedgerProductMapper.java
@@ -32,8 +32,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
@@ -74,7 +74,7 @@
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://localhost:3306/product-inventory-management-bdpro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        url: jdbc:mysql://localhost:3306/product-inventory-management-new-pro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      # ä»Žåº“数据源
src/main/resources/mapper/account/AccountStatementMapper.xml
@@ -41,38 +41,129 @@
    ORDER BY lj.statement_month DESC
    </select>
    <select id="selectVatDtoPage" resultType="com.ruoyi.purchase.dto.VatDto">
        SELECT * FROM (
            SELECT
                asi.id,
                asi.invoice_number AS invoiceNumber,
                asi.issue_date AS issueDate,
                'sales' AS type,
                asi.invoice_type AS invoiceType,
                asi.tax_price AS taxAmount,
                asi.tax_inclusive_price AS totalAmount,
                c.customer_name AS customerName,
                NULL AS supplierName,
                DATE_FORMAT(asi.issue_date, '%Y-%m') AS month
            FROM account_sales_invoice asi
            LEFT JOIN customer c ON c.id = asi.customer_id
            WHERE (asi.status IS NULL OR asi.status = 0)
            UNION ALL
            SELECT
                api.id,
                api.invoice_number AS invoiceNumber,
                api.issue_date AS issueDate,
                'purchase' AS type,
                api.invoice_type AS invoiceType,
                api.tax_price AS taxAmount,
                api.tax_inclusive_price AS totalAmount,
                NULL AS customerName,
                s.supplier_name AS supplierName,
                DATE_FORMAT(api.issue_date, '%Y-%m') AS month
            FROM account_purchase_invoice api
            LEFT JOIN supplier_manage s ON s.id = api.supplier_id
            WHERE (api.status IS NULL OR api.status = 0)
        ) AS invoice_detail
        <where>
            <if test="month != null and month != ''">
                AND month = #{month}
            </if>
            <if test="type != null and type != ''">
                AND type = #{type}
            </if>
        </where>
        ORDER BY issueDate DESC, type
    </select>
    <select id="selectVatSummary" resultType="com.ruoyi.purchase.dto.VatSummaryDto">
    SELECT
        month,
        jTaxAmount,
        xTaxAmount,
        (jTaxAmount - xTaxAmount) AS taxAmount
    FROM (
        SELECT
            month,
            SUM(IF(type = 'purchase', tax_price, 0)) AS xTaxAmount,
            SUM(IF(type = 'sales', tax_price, 0)) AS jTaxAmount
            SUM(jTaxAmount) AS jTaxAmount,
            SUM(xTaxAmount) AS xTaxAmount,
            SUM(jTaxAmount) - SUM(xTaxAmount) AS taxAmount
        FROM (
            SELECT
                DATE_FORMAT(issue_date, '%Y-%m') AS month,
                tax_price,
                'sales' AS type
                SUM(tax_price) AS jTaxAmount,
                0 AS xTaxAmount
            FROM account_sales_invoice
            WHERE status != 1
            WHERE (status IS NULL OR status = 0)
            <if test="year != null and year != ''">
                AND YEAR(issue_date) = #{year}
            </if>
            GROUP BY DATE_FORMAT(issue_date, '%Y-%m')
            UNION ALL
            SELECT
                DATE_FORMAT(issue_date, '%Y-%m') AS month,
                tax_price,
                'purchase' AS type
                0 AS jTaxAmount,
                SUM(tax_price) AS xTaxAmount
            FROM account_purchase_invoice
            WHERE status != 1
        ) AS all_data
            WHERE (status IS NULL OR status = 0)
            <if test="year != null and year != ''">
                AND YEAR(issue_date) = #{year}
            </if>
            GROUP BY DATE_FORMAT(issue_date, '%Y-%m')
        ) t
    GROUP BY month
    ) AS TT
        ORDER BY month
    </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 TT.month = #{month}
            <if test="month != null and month != ''">
                AND DATE_FORMAT(a.invoiceDate, '%Y-%m') = #{month}
            </if>
     </where>
    ORDER BY TT.month
        ORDER BY a.invoiceDate DESC
    </select>
</mapper>
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/sales/SalesLedgerProductMapper.xml
@@ -219,7 +219,29 @@
        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">
        WITH RECURSIVE raw_material_tree AS (
            SELECT id FROM product WHERE product_name = '原材料' AND parent_id IS NULL
            UNION ALL
            SELECT p.id FROM product p INNER JOIN raw_material_tree rmt ON p.parent_id = rmt.id
        )
        SELECT
            pr.product_name AS name,
            SUM( slp.tax_inclusive_total_price ) AS value
@@ -228,6 +250,26 @@
            JOIN product pr ON slp.product_id = pr.id
        WHERE
            slp.type = 2
            AND pr.id IN (SELECT id FROM raw_material_tree)
        GROUP BY
            pr.id,
            pr.product_name
        ORDER BY
            value DESC
        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,
@@ -252,7 +294,7 @@
    <select id="selectRawMaterialExpense" resultType="java.math.BigDecimal">
        WITH RECURSIVE product_tree AS (SELECT id
                                        FROM product
                                        WHERE product_name = '原材料'
                                        WHERE product_name = '原材料' AND parent_id IS NULL
                                        UNION ALL