Merge remote-tracking branch 'origin/dev_天津_建诚恒商贸' into dev_天津_玮苓乐器

# Conflicts:
# src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java
已添加6个文件
已修改32个文件
1719 ■■■■ 文件已修改
.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 253 ●●●● 补丁 | 查看 | 原始文档 | 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 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/VatDto.java 69 ●●●●● 补丁 | 查看 | 原始文档 | 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/PurchaseLedgerServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | 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/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-jchsm.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountStatementMapper.xml 135 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/ProductModelMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/home/HomeMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockOutRecordMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -6,6 +6,7 @@
!gradle/wrapper/gradle-wrapper.jar
claude.md
target/
test/
!.mvn/wrapper/maven-wrapper.jar
######################################################################
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
@@ -104,8 +104,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);
    }
@@ -118,22 +120,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);
    }
@@ -474,22 +482,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,36 +697,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");
@@ -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,38 +947,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<>();
@@ -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,14 +1,17 @@
package com.ruoyi.purchase.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.service.purchase.AccountPurchasePaymentService;
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.SavePaymentDto;
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;
@@ -33,50 +36,63 @@
    private final PurchaseReportService purchaseReportService;
    private final AccountPurchasePaymentService accountPurchasePaymentService;
    @GetMapping("/list")
    @Log(title = "采购报表-项目利润", businessType = BusinessType.OTHER)
    public R list(Page page, String customerName) {
        return R.ok(purchaseReportService.list(page,customerName));
        return R.ok(purchaseReportService.list(page, customerName));
    }
    @Log(title = "采购报表-项目利润导出", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    @Operation(summary = "采购报表-项目利润导出")
    public void export(HttpServletResponse response, String customerName) {
        List<PurchaseReportVo> list = purchaseReportService.list(new Page(1,-1),customerName).getRecords();
        List<PurchaseReportVo> list = purchaseReportService.list(new Page(1, -1), customerName).getRecords();
        ExcelUtil<PurchaseReportVo> util = new ExcelUtil<>(PurchaseReportVo.class);
        util.exportExcel(response, list , "项目利润");
        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 = "采购报表-增值税比对")
    public void exportTwo(HttpServletResponse response,String month) {
        List<VatDto> list = purchaseReportService.listVat(new Page(1,-1),month).getRecords();
    @Operation(summary = "采购报表-增值税比对导出")
    public void exportTwo(HttpServletResponse response, String month) {
        List<VatDto> list = purchaseReportService.listVat(new Page(1, -1), month, null).getRecords();
        ExcelUtil<VatDto> util = new ExcelUtil<>(VatDto.class);
        util.exportExcel(response, list , "增值税比对");
        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 = "供应商往来")
    public R supplierTransactions(Page page, String supplierName) {
        return R.ok(supplierService.supplierTransactions(page,supplierName));
        return R.ok(supplierService.supplierTransactions(page, supplierName));
    }
    @GetMapping("/supplierTransactionsDetails")
    @Log(title = "供应商往来明细", businessType = BusinessType.OTHER)
    @Operation(summary = "供应商往来明细")
    public R supplierTransactionsDetails(Page page, Long supplierId) {
        return R.ok(supplierService.supplierTransactionsDetails(page,supplierId));
        return R.ok(supplierService.supplierTransactionsDetails(page, supplierId));
    }
    @PostMapping("/savePayment")
src/main/java/com/ruoyi/purchase/dto/VatDto.java
@@ -1,19 +1,59 @@
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 = "月份")
    private String month ;
    private String month;
    @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/PurchaseLedgerServiceImpl.java
@@ -997,8 +997,8 @@
            return AjaxResult.success("导入成功");
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.error("导入失败:" + e.getMessage());
        }
        return AjaxResult.success("导入失败");
    }
    @Override
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/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java
@@ -127,15 +127,25 @@
            throw new BaseException("编号为"+staffOnJobParams.getStaffNo()+"的员工不存在,无法更新!!!");
        }
        String[] ignoreProperties = {"id"};//排除更新属性
        // èŽ·å–æœ€æ–°åˆåŒæ•°æ®ï¼Œå¹¶ä¸”æ›´æ–°
        StaffContract contract = staffContractMapper.selectOne(Wrappers.<StaffContract>lambdaQuery()
                .eq(StaffContract::getStaffOnJobId, id)
                .last("limit 1")
                .orderByDesc(StaffContract::getId));
        if (contract != null){
            BeanUtils.copyProperties(staffOnJobParams,contract,ignoreProperties);
        boolean hasContractData = staffOnJobParams.getContractTerm() != null
                || staffOnJobParams.getContractStartTime() != null
                || staffOnJobParams.getContractEndTime() != null;
        if (contract == null && hasContractData) {
            contract = new StaffContract();
            contract.setStaffOnJobId(id);
            contract.setContractTerm(staffOnJobParams.getContractTerm());
            contract.setContractStartTime(staffOnJobParams.getContractStartTime());
            contract.setContractEndTime(staffOnJobParams.getContractEndTime());
            staffContractMapper.insert(contract);
        } else if (contract != null) {
            contract.setContractTerm(staffOnJobParams.getContractTerm());
            contract.setContractStartTime(staffOnJobParams.getContractStartTime());
            contract.setContractEndTime(staffOnJobParams.getContractEndTime());
            staffContractMapper.updateById(contract);
        }
@@ -145,6 +155,7 @@
        bingingStaffOnJobExtra(id,staffOnJobParams);
        // æ›´æ–°å‘˜å·¥æ•°æ®
        syncStudyInfoFromEducation(staffOnJobParams);
        staffOnJobParams.setId(id);
        staffOnJobParams.setContractExpireTime(staffOnJobParams.getContractEndTime());
        return staffOnJobMapper.updateById(staffOnJobParams);
    }
@@ -333,8 +344,16 @@
            // èŽ·å–æ‰€æœ‰è§’è‰²æ•°æ®
            List<SysRole> sysRoles = sysRoleMapper.selectRoleAll();
            staffOnJobs.forEach(staffOnJob -> {
                if (staffOnJob.getStaffNo() == null || staffOnJob.getStaffNo().trim().isEmpty()) {
                    throw new BaseException("员工编号不能为空,员工姓名:" + staffOnJob.getStaffName());
                }
                StaffOnJobDto staffOnJobDto = new StaffOnJobDto();
                BeanUtils.copyProperties(staffOnJob, staffOnJobDto);
                staffOnJobDto.setContractStartTime(staffOnJob.getContractStartTime());
                staffOnJobDto.setContractEndTime(staffOnJob.getContractEndTime());
                staffOnJobDto.setContractTerm(staffOnJob.getContractTerm());
                staffOnJobDto.setStaffNo(staffOnJob.getStaffNo());
                staffOnJobDto.setStaffName(staffOnJob.getStaffName());
                // é€šè¿‡åç§°èŽ·å–éƒ¨é—¨id
                staffOnJobDto.setSysDeptId(// ... existing code ...
                        sysDepts.stream()
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/application-jchsm.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,268 @@
# é¡¹ç›®ç›¸å…³é…ç½®
ruoyi:
  # åç§°
  name: RuoYi
  # ç‰ˆæœ¬
  version: 3.8.9
  # ç‰ˆæƒå¹´ä»½
  copyrightYear: 2025
  # æ–‡ä»¶è·¯å¾„ ç¤ºä¾‹ï¼ˆ Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
  profile: /javaWork/product-inventory-management/file
  # èŽ·å–ip地址开关
  addressEnabled: false
  # éªŒè¯ç ç±»åž‹ math æ•°å­—计算 char å­—符验证
  captchaType: math
  # ååŒå®¡æ‰¹ç¼–号前缀(配置文件后缀命名)
  approvalNumberPrefix: NEW
  # ä¸ªæŽ¨ Unipush é…ç½®
  getui:
    appId: PfjyAAE0FK64FaO1w2CMb1
    appKey: zTMb831OEL6J4GK1uE3Ob4
    masterSecret: K1GFtsv42v61tXGnF7SGE5
    domain: https://restapi.getui.cn/v2/
    # ç¦»çº¿æŽ¨é€ä½¿ç”¨çš„包名/组件名
    intentComponent: uni.app.UNI099A590/io.dcloud.PandoraEntry
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口,默认为8080
  port: 9003
  servlet:
    # åº”用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # è¿žæŽ¥æ•°æ»¡åŽçš„æŽ’队数,默认为100
    accept-count: 1000
    threads:
      # tomcat最大线程数,默认为200
      max: 800
      # Tomcat启动初始化的线程数,默认值10
      min-spare: 100
# æ—¥å¿—配置
logging:
  level:
    com.ruoyi: warn
    org.springframework: warn
minio:
  endpoint: http://114.132.189.42/
  port: 7019
  secure: false
  accessKey: admin
  secretKey: 12345678
  preview-expiry: 24 # é¢„览地址默认24小时
  default-bucket: jxc
# ç”¨æˆ·é…ç½®
user:
  password:
    # å¯†ç æœ€å¤§é”™è¯¯æ¬¡æ•°
    maxRetryCount: 5
    # å¯†ç é”å®šæ—¶é—´ï¼ˆé»˜è®¤10分钟)
    lockTime: 10
# Spring配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://172.17.0.1:3306/product-inventory-management-jchsm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: xd@123456..
      # ä»Žåº“数据源
      slave:
        # ä»Žæ•°æ®æºå¼€å…³/默认关闭
        enabled: false
        url:
        username:
        password:
      # åˆå§‹è¿žæŽ¥æ•°
      initialSize: 5
      # æœ€å°è¿žæŽ¥æ± æ•°é‡
      minIdle: 10
      # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
      maxActive: 20
      # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´
      maxWait: 60000
      # é…ç½®è¿žæŽ¥è¶…æ—¶æ—¶é—´
      connectTimeout: 30000
      # é…ç½®ç½‘络超时时间
      socketTimeout: 60000
      # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      minEvictableIdleTimeMillis: 300000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # è®¾ç½®ç™½åå•,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # æŽ§åˆ¶å°ç®¡ç†ç”¨æˆ·åå’Œå¯†ç 
        login-username: ruoyi
        login-password: 123456
      filter:
        stat:
          enabled: true
          # æ…¢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
  # èµ„源信息
  messages:
    # å›½é™…化资源文件路径
    basename: i18n/messages
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
      # å•个文件大小
      max-file-size: 1GB
      # è®¾ç½®æ€»ä¸Šä¼ çš„æ–‡ä»¶å¤§å°
      max-request-size: 2GB
  # æœåŠ¡æ¨¡å—
  devtools:
    restart:
      # çƒ­éƒ¨ç½²å¼€å…³
      enabled: false
  # redis é…ç½®
  data:
    mongodb:
      uri: mongodb://114.132.189.42:9028/chat_memory_db_jchsm
    # redis é…ç½®
    redis:
      # åœ°å€
#      host: 127.0.0.1
      host: 172.17.0.1
      # ç«¯å£ï¼Œé»˜è®¤ä¸º6379
      port: 6379
      # æ•°æ®åº“索引
      database: 0
      # å¯†ç 
      #    password: root2022!
      password:
      # è¿žæŽ¥è¶…æ—¶æ—¶é—´
      timeout: 10s
      lettuce:
        pool:
          # è¿žæŽ¥æ± ä¸­çš„æœ€å°ç©ºé—²è¿žæŽ¥
          min-idle: 0
          # è¿žæŽ¥æ± ä¸­çš„æœ€å¤§ç©ºé—²è¿žæŽ¥
          max-idle: 8
          # è¿žæŽ¥æ± çš„æœ€å¤§æ•°æ®åº“连接数
          max-active: 8
          # #连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
  # Quartz定时任务配置(新增部分)
  quartz:
    job-store-type: jdbc  # ä½¿ç”¨æ•°æ®åº“存储
    jdbc:
      initialize-schema: never  # é¦–次运行时自动创建表结构,成功后改为never
      schema: classpath:org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql  # MySQL表结构脚本
    properties:
      org:
        quartz:
          scheduler:
            instanceName: RuoYiScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate  # MySQL适配
            tablePrefix: qrtz_  # è¡¨åå‰ç¼€ï¼Œä¸Žè„šæœ¬ä¸€è‡´
            isClustered: false  # å•节点模式(集群需改为true)
            clusterCheckinInterval: 10000
            txIsolationLevelSerializable: true
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10  # çº¿ç¨‹æ± å¤§å°
            threadPriority: 5
            makeThreadsDaemons: true
          updateCheck: false  # å…³é—­ç‰ˆæœ¬æ£€æŸ¥
# token配置
token:
  # ä»¤ç‰Œè‡ªå®šä¹‰æ ‡è¯†
  header: Authorization
  # ä»¤ç‰Œå¯†é’¥
  secret: xpAVjhCjQDaDB7mjPAzMDSbQWXNu2zYkTdDNUsPMS5Xx8QMmQVYN7n74eZrYJxDJ
  # ä»¤ç‰Œæœ‰æ•ˆæœŸï¼ˆé»˜è®¤30分钟)
  expireTime: 450
# MyBatis Plus配置
mybatis-plus:
  # æœç´¢æŒ‡å®šåŒ…别名   æ ¹æ®è‡ªå·±çš„项目来
  typeAliasesPackage: com.ruoyi.**.pojo
  # é…ç½®mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # åŠ è½½å…¨å±€çš„é…ç½®æ–‡ä»¶
  configLocation: classpath:mybatis/mybatis-config.xml
  global-config:
    enable-sql-runner: true
    db-config:
      id-type: auto
# PageHelper分页插件
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true
  params: count=countSql
# Swagger配置
swagger:
  # æ˜¯å¦å¼€å¯swagger
  enabled: true
  # è¯·æ±‚前缀
  pathMapping: /dev-api
# é˜²æ­¢XSS攻击
xss:
  # è¿‡æ»¤å¼€å…³
  enabled: true
  # æŽ’除链接(多个用逗号分隔)
  excludes: /system/notice
  # åŒ¹é…é“¾æŽ¥
  urlPatterns: /system/*,/monitor/*,/tool/*
# ä»£ç ç”Ÿæˆ
gen:
  # ä½œè€…
  author: ruoyi
  # é»˜è®¤ç”ŸæˆåŒ…路径 system éœ€æ”¹æˆè‡ªå·±çš„æ¨¡å—名称 å¦‚ system monitor tool
  packageName: com.ruoyi.project.system
  # è‡ªåŠ¨åŽ»é™¤è¡¨å‰ç¼€ï¼Œé»˜è®¤æ˜¯true
  autoRemovePre: false
  # è¡¨å‰ç¼€ï¼ˆç”Ÿæˆç±»åä¸ä¼šåŒ…含表前缀,多个用逗号分隔)
  tablePrefix: sys_
  # æ˜¯å¦å…è®¸ç”Ÿæˆæ–‡ä»¶è¦†ç›–到本地(自定义路径),默认不允许
  allowOverwrite: false
# æ–‡ä»¶ä¸Šä¼ é…ç½®
file:
  temp-dir: /javaWork/product-inventory-management/file/temp/uploads   # ä¸´æ—¶ç›®å½•
  upload-dir: /javaWork/product-inventory-management/file/prod/uploads # æ­£å¼ç›®å½•
  path: /javaWork/product-inventory-management/file # ä¸Šä¼ ç›®å½•
  urlPrefix: /prod-api/common # é“¾æŽ¥å‰ç¼€
  domain: http://1.15.17.182:9107 # åŸŸåå‰ç¼€
  expired: 120 # è¿‡æœŸæ—¶é—´(单位:分钟)
  useLimit: 10 # ä½¿ç”¨æ¬¡æ•°
  compress: true # æ˜¯å¦åŽ‹ç¼©
  needCompressSize: 10MB # åŽ‹ç¼©é˜ˆå€¼
  compressQuality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
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
        month,
        jTaxAmount,
        xTaxAmount,
        (jTaxAmount - xTaxAmount) AS taxAmount
    FROM (
        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,
            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
    GROUP BY month
    ) AS TT
     <where>
            <if test="month != null">
                and TT.month = #{month}
            WHERE (status IS NULL OR status = 0)
            <if test="year != null and year != ''">
                AND YEAR(issue_date) = #{year}
            </if>
     </where>
    ORDER BY TT.month
            GROUP BY DATE_FORMAT(issue_date, '%Y-%m')
        ) t
        GROUP BY month
        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 month != ''">
                AND DATE_FORMAT(a.invoiceDate, '%Y-%m') = #{month}
            </if>
        </where>
        ORDER BY a.invoiceDate DESC
    </select>
</mapper>
src/main/resources/mapper/basic/ProductModelMapper.xml
@@ -137,6 +137,7 @@
               pm.model as  model
        from product_model pm
        left join product p on p.id = pm.product_id
        where p.id is not null
        order by p.id,pm.id desc
    </select>
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
@@ -228,7 +228,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
@@ -237,6 +259,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,
@@ -261,7 +303,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
src/main/resources/mapper/stock/StockOutRecordMapper.xml
@@ -62,7 +62,7 @@
                and p.id in (select id from product_tree)
            </if>
        </where>
        order by sor.id desc
        order by sor.create_time desc
    </select>
    <select id="listStockOutRecordExportData" resultType="com.ruoyi.stock.execl.StockOutRecordExportData">
        SELECT