8 小时以前 22b57a30be7f73e13585ae4bd4925f0c66846ba9
feat(production): 优化工单查询接口按订单维度分组并新增订单明细查询

- 修改/page接口为按生产订单维度分组展示,返回订单级别汇总数据
- 新增/byOrder/{orderId}接口用于查询指定生产订单下所有工单明细
- 在ProductionOperationTaskDto中添加良品数量、工单任务数量等新字段
- 更新数据库查询映射,实现订单维度的数据聚合统计功能
- 添加详细的接口文档说明前后端联调要点和字段映射关系
已添加2个文件
已修改6个文件
946 ■■■■■ 文件已修改
docs/生产工单接口变更文档.md 264 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/生产工单模块前端联调文档.md 505 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOperationTaskMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOperationTaskService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/Éú²ú¹¤µ¥½Ó¿Ú±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,264 @@
# ç”Ÿäº§å·¥å•接口变更文档
> æ–‡æ¡£ç”Ÿæˆæ—¥æœŸï¼š2026-06-11
> é€‚用分支:dev_天津_中兴实强
---
## å˜æ›´è®°å½•
| æ—¥æœŸ | å˜æ›´å†…容 | å˜æ›´äºº |
|------|----------|--------|
| 2026-06-11 | ä¼˜åŒ–/page接口为按订单分组;新增/byOrder/{orderId}接口 | - |
---
## ä¸€ã€æŽ¥å£æ¦‚览
| æŽ¥å£è·¯å¾„ | è¯·æ±‚方式 | è¯´æ˜Ž |
|----------|----------|------|
| `/productionOperationTask/page` | GET | ç”Ÿäº§å·¥å•分页查询(按生产订单维度分组) |
| `/productionOperationTask/byOrder/{orderId}` | GET | æ ¹æ®ç”Ÿäº§è®¢å•ID查询工单列表 |
---
## äºŒã€æŽ¥å£è¯¦æƒ…
### 2.1 ç”Ÿäº§å·¥å•分页查询 `/page`
#### è¯·æ±‚参数
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| current | Long | å¦ | å½“前页码 |
| size | Long | å¦ | æ¯é¡µæ¡æ•° |
| npsNo | String | å¦ | ç”Ÿäº§è®¢å•号(模糊查询) |
| productionOrderId | Long | å¦ | ç”Ÿäº§è®¢å•ID(精确匹配) |
#### å“åº”字段
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž |
|--------|------|------|
| productionOrderId | Long | ç”Ÿäº§è®¢å•ID |
| npsNo | String | ç”Ÿäº§è®¢å•号 |
| endOrder | Boolean | æ˜¯å¦ç»“束 |
| productName | String | äº§å“åç§° |
| model | String | è§„格型号 |
| unit | String | å•位 |
| operationName | String | å·¥åºåç§°ï¼ˆå¤šä¸ªé€—号分隔) |
| productionTaskCount | Long | ç”Ÿäº§ä»»åŠ¡æ•°é‡ |
| planQuantity | BigDecimal | è®¡åˆ’数量(订单级别) |
| completeQuantity | BigDecimal | å®Œæˆæ•°é‡ï¼ˆè®¢å•级别) |
| goodQuantity | BigDecimal | è‰¯å“æ•°é‡ |
| scrapQty | BigDecimal | æŠ¥åºŸæ•°é‡ |
| completionStatus | BigDecimal | å®Œæˆè¿›åº¦ï¼ˆç™¾åˆ†æ¯”) |
| type | Integer | å·¥åºç±»åž‹ï¼ˆ0计时/1计件) |
| workOrderType | String | å·¥å•类型(正常/返工返修) |
| workOrderNo | String | å·¥å•编号(多个逗号分隔) |
| planStartTime | LocalDate | è®¡åˆ’开始时间 |
| planEndTime | LocalDate | è®¡åˆ’结束时间 |
| actualStartTime | LocalDate | å®žé™…开始时间 |
| actualEndTime | LocalDate | å®žé™…结束时间 |
| status | Integer | çŠ¶æ€ |
#### æ ¸å¿ƒSQL
```sql
SELECT
    po.id                   AS productionOrderId,
    po.nps_no               AS npsNo,
    po.is_end_order         AS endOrder,
    p.product_name          AS productName,
    pm.model                AS model,
    pm.unit                 AS unit,
    GROUP_CONCAT(DISTINCT poro.operation_name ORDER BY poro.drag_sort, poro.operation_name SEPARATOR ',') AS operationName,
    COUNT(DISTINCT pot.id)  AS productionTaskCount,
    po.quantity             AS planQuantity,
    po.complete_quantity    AS completeQuantity,
    po.complete_quantity    AS goodQuantity,
    SUM(IFNULL(scrapStat.scrapQty, 0)) AS scrapQty,
    ROUND(IFNULL(po.complete_quantity, 0) / NULLIF(po.quantity, 0) * 100, 2) AS completionStatus,
    MAX(poro.type)          AS type,
    CASE WHEN SUM(CASE WHEN pot.work_order_no LIKE 'FG%' THEN 1 ELSE 0 END) > 0
        THEN '返工返修' ELSE '正常' END AS workOrderType,
    GROUP_CONCAT(DISTINCT pot.work_order_no ORDER BY pot.work_order_no SEPARATOR ',') AS workOrderNo,
    MIN(pot.plan_start_time)   AS planStartTime,
    MAX(pot.plan_end_time)     AS planEndTime,
    MIN(pot.actual_start_time) AS actualStartTime,
    MAX(pot.actual_end_time)   AS actualEndTime,
    MAX(pot.status)            AS status
FROM production_operation_task pot
    LEFT JOIN production_order po ON pot.production_order_id = po.id
    LEFT JOIN production_order_routing_operation poro ON pot.production_order_routing_operation_id = poro.id
    LEFT JOIN product_model pm ON pm.id = po.product_model_id
    LEFT JOIN product p ON pm.product_id = p.id
    LEFT JOIN (
        SELECT ppm.production_operation_task_id AS taskId,
               SUM(IFNULL(ppo.scrap_qty, 0)) AS scrapQty
        FROM production_product_main ppm
            LEFT JOIN production_product_output ppo ON ppo.production_product_main_id = ppm.id
        GROUP BY ppm.production_operation_task_id
    ) scrapStat ON scrapStat.taskId = pot.id
WHERE ...
GROUP BY po.id
ORDER BY po.id DESC
```
---
### 2.2 æ ¹æ®è®¢å•ID查询工单列表 `/byOrder/{orderId}`
#### è¯·æ±‚参数
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| orderId | Long | æ˜¯ | ç”Ÿäº§è®¢å•ID(路径参数) |
#### å“åº”字段
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž |
|--------|------|------|
| id | Long | å·¥å•ID |
| workOrderNo | String | å·¥å•编号 |
| planStartTime | LocalDate | è®¡åˆ’开始时间 |
| planEndTime | LocalDate | è®¡åˆ’结束时间 |
| actualStartTime | LocalDate | å®žé™…开始时间 |
| actualEndTime | LocalDate | å®žé™…结束时间 |
| status | Integer | çŠ¶æ€ |
| productionOrderId | Long | ç”Ÿäº§è®¢å•ID |
| planQuantity | BigDecimal | è®¡åˆ’数量 |
| completeQuantity | BigDecimal | å®Œæˆæ•°é‡ |
| npsNo | String | ç”Ÿäº§è®¢å•号 |
| endOrder | Boolean | æ˜¯å¦ç»“束 |
| productName | String | äº§å“åç§° |
| model | String | è§„格型号 |
| unit | String | å•位 |
| operationName | String | å·¥åºåç§° |
| type | Integer | å·¥åºç±»åž‹ï¼ˆ0计时/1计件) |
| workOrderType | String | å·¥å•类型(正常/返工返修) |
| scrapQty | BigDecimal | æŠ¥åºŸæ•°é‡ |
| completionStatus | BigDecimal | å®Œæˆè¿›åº¦ï¼ˆç™¾åˆ†æ¯”) |
| userNames | String | æŠ¥å·¥äººå‘˜åç§°ï¼ˆå¤šä¸ªé€—号分隔) |
#### æ ¸å¿ƒSQL
```sql
SELECT
    pot.id,
    pot.work_order_no,
    pot.plan_start_time AS planStartTime,
    pot.plan_end_time AS planEndTime,
    pot.actual_start_time AS actualStartTime,
    pot.actual_end_time AS actualEndTime,
    pot.status,
    pot.production_order_id AS productionOrderId,
    pot.plan_quantity AS planQuantity,
    pot.complete_quantity AS completeQuantity,
    po.nps_no AS npsNo,
    po.is_end_order AS endOrder,
    p.product_name AS productName,
    pm.model AS model,
    pm.unit AS unit,
    poro.operation_name AS operationName,
    poro.type AS type,
    CASE WHEN pot.work_order_no LIKE 'FG%' THEN '返工返修' ELSE '正常' END AS workOrderType,
    IFNULL(scrapStat.scrapQty, 0) AS scrapQty,
    ROUND(IFNULL(pot.complete_quantity, 0) / NULLIF(pot.plan_quantity, 0) * 100, 2) AS completionStatus
FROM production_operation_task pot
    LEFT JOIN production_order po ON pot.production_order_id = po.id
    LEFT JOIN production_order_routing_operation poro ON pot.production_order_routing_operation_id = poro.id
    LEFT JOIN product_model pm ON pm.id = IFNULL(poro.product_model_id, po.product_model_id)
    LEFT JOIN product p ON pm.product_id = p.id
    LEFT JOIN (
        SELECT ppm.production_operation_task_id AS taskId,
               SUM(IFNULL(ppo.scrap_qty, 0)) AS scrapQty
        FROM production_product_main ppm
            LEFT JOIN production_product_output ppo ON ppo.production_product_main_id = ppm.id
        GROUP BY ppm.production_operation_task_id
    ) scrapStat ON scrapStat.taskId = pot.id
WHERE pot.production_order_id = #{orderId}
ORDER BY pot.id DESC
```
---
## ä¸‰ã€ä¸¤ä¸ªæŽ¥å£å¯¹æ¯”
| å¯¹æ¯”项 | /page | /byOrder/{orderId} |
|--------|-------|---------------------|
| æŸ¥è¯¢ç»´åº¦ | æŒ‰ç”Ÿäº§è®¢å•分组 | æŒ‰å·¥å•明细 |
| åˆ†ç»„依据 | GROUP BY po.id | æ— åˆ†ç»„ |
| è®¡åˆ’数量来源 | è®¢å•级别(po.quantity) | å·¥å•级别(pot.plan_quantity) |
| å®Œæˆæ•°é‡æ¥æº | è®¢å•级别(po.complete_quantity) | å·¥å•级别(pot.complete_quantity) |
| å®Œæˆè¿›åº¦è®¡ç®— | è®¢å•完成数/订单计划数 | å·¥å•完成数/工单计划数 |
| å·¥åºåˆ—表展示 | GROUP_CONCAT聚合 | å•个工序 |
| ç”¨é€” | å·¥å•列表页(汇总视图) | è®¢å•详情页(工单明细) |
---
## å››ã€å…³è”表说明
| è¡¨å | åˆ«å | è¯´æ˜Ž |
|------|------|------|
| production_operation_task | pot | ç”Ÿäº§å·¥å•表 |
| production_order | po | ç”Ÿäº§è®¢å•表 |
| production_order_routing_operation | poro | ç”Ÿäº§è®¢å•工艺路线工序表 |
| product_model | pm | äº§å“è§„格表 |
| product | p | äº§å“è¡¨ |
| production_product_main | ppm | æŠ¥å·¥ä¸»è¡¨ |
| production_product_output | ppo | æŠ¥å·¥äº§å‡ºè¡¨ |
---
## äº”、枚举值说明
### 5.1 å·¥å•状态
| çŠ¶æ€å€¼ | è¯´æ˜Ž |
|--------|------|
| 1 | å¾…确认 |
| 2 | å¾…生产 |
| 3 | ç”Ÿäº§ä¸­ |
| 4 | å·²ç”Ÿäº§ |
### 5.2 å·¥åºç±»åž‹
| ç±»åž‹å€¼ | è¯´æ˜Ž |
|--------|------|
| 0 | è®¡æ—¶ |
| 1 | è®¡ä»¶ |
### 5.3 å·¥å•类型判断
- å·¥å•号以 `FG` å¼€å¤´ â†’ è¿”工返修
- å…¶ä»– â†’ æ­£å¸¸
---
## å…­ã€åŽå¤„理逻辑
### 6.1 å¡«å……工序类型 `fillOperationTypes`
- ä»Ž `production_order_routing_operation` è¡¨æ‰¹é‡æŸ¥è¯¢å·¥åºç±»åž‹
- æ ¹æ® `productionOrderRoutingOperationId` å…³è”回填
### 6.2 å¡«å……报工人员名称 `fillUserNames`
- è§£æžå·¥å•çš„ `userIds` å­—段(JSON数组格式)
- ä»Ž `sys_user` è¡¨æ‰¹é‡æŸ¥è¯¢ç”¨æˆ·æ˜µç§°
- æ‹¼æŽ¥ä¸ºé€—号分隔的字符串
---
## ä¸ƒã€æ¶‰åŠæ–‡ä»¶
| æ–‡ä»¶è·¯å¾„ | è¯´æ˜Ž |
|----------|------|
| `controller/ProductionOperationTaskController.java` | æŽ§åˆ¶å™¨ |
| `service/ProductionOperationTaskService.java` | æœåŠ¡æŽ¥å£ |
| `service/impl/ProductionOperationTaskServiceImpl.java` | æœåŠ¡å®žçŽ° |
| `mapper/ProductionOperationTaskMapper.java` | Mapper接口 |
| `mapper/production/ProductionOperationTaskMapper.xml` | Mapper XML |
| `pojo/ProductionOperationTask.java` | å·¥å•实体 |
| `bean/dto/ProductionOperationTaskDto.java` | æŸ¥è¯¢DTO |
| `bean/vo/ProductionOperationTaskVo.java` | å“åº”VO |
docs/Éú²ú¹¤µ¥Ä£¿éǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,505 @@
# ç”Ÿäº§å·¥å•模块前端联调文档
> æ–‡æ¡£ç”Ÿæˆæ—¥æœŸï¼š2026-06-11
> é€‚用分支:dev_天津_中兴实强
---
## å˜æ›´è®°å½•
| æ—¥æœŸ | å˜æ›´å†…容 | å½±å“èŒƒå›´ |
|------|----------|----------|
| 2026-06-11 | /page接口改为按订单分组聚合 | åˆ—表页展示逻辑调整 |
| 2026-06-11 | æ–°å¢ž/byOrder/{orderId}接口 | è®¢å•详情页工单列表 |
---
## ä¸€ã€æŽ¥å£åˆ—表
| æŽ¥å£åç§° | æŽ¥å£è·¯å¾„ | è¯·æ±‚方式 | è¯´æ˜Ž |
|----------|----------|----------|------|
| å·¥å•分页查询 | `/productionOperationTask/page` | GET | æŒ‰ç”Ÿäº§è®¢å•维度分组展示 |
| è®¢å•工单列表 | `/productionOperationTask/byOrder/{orderId}` | GET | æ ¹æ®è®¢å•ID查询工单明细 |
---
## äºŒã€æŽ¥å£è¯¦æƒ…
### 2.1 å·¥å•分页查询
#### åŸºæœ¬ä¿¡æ¯
```
GET /productionOperationTask/page
```
#### è¯·æ±‚参数(Query)
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| current | Number | å¦ | å½“前页码,默认1 |
| size | Number | å¦ | æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10 |
| npsNo | String | å¦ | ç”Ÿäº§è®¢å•号,模糊查询 |
| productionOrderId | Number | å¦ | ç”Ÿäº§è®¢å•ID,精确匹配 |
#### è¯·æ±‚示例
```javascript
// åŸºç¡€åˆ†é¡µæŸ¥è¯¢
axios.get('/productionOperationTask/page', {
  params: {
    current: 1,
    size: 10
  }
})
// æŒ‰è®¢å•号筛选
axios.get('/productionOperationTask/page', {
  params: {
    current: 1,
    size: 10,
    npsNo: 'SC202606'
  }
})
```
#### å“åº”结构
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "productionOrderId": 123,
        "npsNo": "SC20260611001",
        "endOrder": false,
        "productName": "电线电缆",
        "model": "RVV 3*2.5",
        "unit": "ç±³",
        "operationName": "绞线,绝缘,成缆",
        "productionTaskCount": 3,
        "planQuantity": 1000.00,
        "completeQuantity": 500.00,
        "goodQuantity": 500.00,
        "scrapQty": 10.00,
        "completionStatus": 50.00,
        "type": 1,
        "workOrderType": "正常",
        "workOrderNo": "GD20260611001,GD20260611002,GD20260611003",
        "planStartTime": "2026-06-10",
        "planEndTime": "2026-06-15",
        "actualStartTime": "2026-06-11",
        "actualEndTime": null,
        "status": 3
      }
    ],
    "total": 100,
    "current": 1,
    "size": 10
  }
}
```
#### å“åº”字段说明
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž | å‰ç«¯å±•示建议 |
|--------|------|------|--------------|
| productionOrderId | Number | ç”Ÿäº§è®¢å•ID | ç”¨äºŽè·³è½¬è®¢å•详情 |
| npsNo | String | ç”Ÿäº§è®¢å•号 | åˆ—表主标题 |
| endOrder | Boolean | æ˜¯å¦ç»“束 | å·²ç»“束时显示"已结束"标签 |
| productName | String | äº§å“åç§° | åˆ—表副标题 |
| model | String | è§„格型号 | äº§å“è§„格展示 |
| unit | String | å•位 | æ•°é‡å•位 |
| operationName | String | å·¥åºåç§°ï¼ˆé€—号分隔) | å·¥åºåˆ—表展示 |
| productionTaskCount | Number | å·¥å•任务数量 | æ˜¾ç¤º"共X个工单" |
| planQuantity | Number | è®¡åˆ’数量(订单级) | è¿›åº¦æ¡æ€»æ•° |
| completeQuantity | Number | å®Œæˆæ•°é‡ï¼ˆè®¢å•级) | è¿›åº¦æ¡å·²å®Œæˆ |
| goodQuantity | Number | è‰¯å“æ•°é‡ | è‰¯å“ç»Ÿè®¡ |
| scrapQty | Number | æŠ¥åºŸæ•°é‡ | æŠ¥åºŸç»Ÿè®¡ï¼ˆçº¢è‰²æ ‡è®°ï¼‰ |
| completionStatus | Number | å®Œæˆè¿›åº¦ç™¾åˆ†æ¯” | è¿›åº¦æ¡æ•°å€¼ |
| type | Number | å·¥åºç±»åž‹ | 0=计时,1=计件 |
| workOrderType | String | å·¥å•类型 | "正常"或"返工返修" |
| workOrderNo | String | å·¥å•编号(逗号分隔) | ç‚¹å‡»å¯å±•开查看 |
| planStartTime | String | è®¡åˆ’开始时间 | æ ¼å¼ï¼šYYYY-MM-DD |
| planEndTime | String | è®¡åˆ’结束时间 | æ ¼å¼ï¼šYYYY-MM-DD |
| actualStartTime | String | å®žé™…开始时间 | æ ¼å¼ï¼šYYYY-MM-DD |
| actualEndTime | String | å®žé™…结束时间 | æ ¼å¼ï¼šYYYY-MM-DD |
| status | Number | çŠ¶æ€ | è§çŠ¶æ€æžšä¸¾è¡¨ |
---
### 2.2 è®¢å•工单列表
#### åŸºæœ¬ä¿¡æ¯
```
GET /productionOperationTask/byOrder/{orderId}
```
#### è¯·æ±‚参数(Path)
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| orderId | Number | æ˜¯ | ç”Ÿäº§è®¢å•ID |
#### è¯·æ±‚示例
```javascript
// æŸ¥è¯¢è®¢å•ID为123的所有工单
axios.get('/productionOperationTask/byOrder/123')
```
#### å“åº”结构
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": [
    {
      "id": 1,
      "workOrderNo": "GD20260611001",
      "planStartTime": "2026-06-10",
      "planEndTime": "2026-06-12",
      "actualStartTime": "2026-06-11",
      "actualEndTime": null,
      "status": 3,
      "productionOrderId": 123,
      "planQuantity": 500.00,
      "completeQuantity": 300.00,
      "npsNo": "SC20260611001",
      "endOrder": false,
      "productName": "电线电缆",
      "model": "RVV 3*2.5",
      "unit": "ç±³",
      "operationName": "绞线",
      "type": 1,
      "workOrderType": "正常",
      "scrapQty": 5.00,
      "completionStatus": 60.00,
      "userNames": "张三,李四"
    },
    {
      "id": 2,
      "workOrderNo": "FG20260611001",
      "planStartTime": "2026-06-12",
      "planEndTime": "2026-06-15",
      "actualStartTime": "2026-06-13",
      "actualEndTime": null,
      "status": 3,
      "productionOrderId": 123,
      "planQuantity": 100.00,
      "completeQuantity": 50.00,
      "npsNo": "SC20260611001",
      "endOrder": false,
      "productName": "电线电缆",
      "model": "RVV 3*2.5",
      "unit": "ç±³",
      "operationName": "绝缘",
      "type": 0,
      "workOrderType": "返工返修",
      "scrapQty": 2.00,
      "completionStatus": 50.00,
      "userNames": "王五"
    }
  ]
}
```
#### å“åº”字段说明
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž | å‰ç«¯å±•示建议 |
|--------|------|------|--------------|
| id | Number | å·¥å•ID | ç”¨äºŽæŠ¥å·¥ã€è¯¦æƒ…跳转 |
| workOrderNo | String | å·¥å•编号 | åˆ—表主标题 |
| planStartTime | String | è®¡åˆ’开始时间 | æ—¶é—´èŒƒå›´å±•示 |
| planEndTime | String | è®¡åˆ’结束时间 | æ—¶é—´èŒƒå›´å±•示 |
| actualStartTime | String | å®žé™…开始时间 | å®žé™…时间展示 |
| actualEndTime | String | å®žé™…结束时间 | å®žé™…时间展示 |
| status | Number | å·¥å•状态 | çŠ¶æ€æ ‡ç­¾ |
| productionOrderId | Number | ç”Ÿäº§è®¢å•ID | å…³è”订单 |
| planQuantity | Number | è®¡åˆ’数量(工单级) | è¿›åº¦æ¡æ€»æ•° |
| completeQuantity | Number | å®Œæˆæ•°é‡ï¼ˆå·¥å•级) | è¿›åº¦æ¡å·²å®Œæˆ |
| npsNo | String | ç”Ÿäº§è®¢å•号 | è®¢å•号展示 |
| endOrder | Boolean | æ˜¯å¦ç»“束 | ç»“束标签 |
| productName | String | äº§å“åç§° | äº§å“ä¿¡æ¯ |
| model | String | è§„格型号 | è§„格展示 |
| unit | String | å•位 | æ•°é‡å•位 |
| operationName | String | å·¥åºåç§° | å•个工序 |
| type | Number | å·¥åºç±»åž‹ | 0=计时,1=计件 |
| workOrderType | String | å·¥å•类型 | è¿”工返修用红色标记 |
| scrapQty | Number | æŠ¥åºŸæ•°é‡ | æŠ¥åºŸç»Ÿè®¡ |
| completionStatus | Number | å®Œæˆè¿›åº¦ç™¾åˆ†æ¯” | è¿›åº¦æ¡æ•°å€¼ |
| userNames | String | æŠ¥å·¥äººå‘˜ï¼ˆé€—号分隔) | äººå‘˜åˆ—表 |
---
## ä¸‰ã€æžšä¸¾å€¼æ˜ å°„
### 3.1 å·¥å•状态
| çŠ¶æ€å€¼ | æ–‡æ¡ˆ | æ ‡ç­¾é¢œè‰²å»ºè®® |
|--------|------|--------------|
| 1 | å¾…确认 | ç°è‰²/默认 |
| 2 | å¾…生产 | è“è‰²/info |
| 3 | ç”Ÿäº§ä¸­ | ç»¿è‰²/success |
| 4 | å·²ç”Ÿäº§ | æ·±ç»¿è‰²/success-dark |
#### å‰ç«¯çŠ¶æ€æ˜ å°„ç¤ºä¾‹
```javascript
const statusMap = {
  1: { text: '待确认', color: 'default' },
  2: { text: '待生产', color: 'info' },
  3: { text: '生产中', color: 'success' },
  4: { text: '已生产', color: 'success-dark' }
}
function getStatusTag(status) {
  return statusMap[status] || { text: '未知', color: 'default' }
}
```
### 3.2 å·¥åºç±»åž‹
| ç±»åž‹å€¼ | æ–‡æ¡ˆ | è¯´æ˜Ž |
|--------|------|------|
| 0 | è®¡æ—¶ | æŒ‰å·¥æ—¶è®¡ç®—工资 |
| 1 | è®¡ä»¶ | æŒ‰äº§é‡è®¡ç®—工资 |
```javascript
const typeMap = {
  0: '计时',
  1: '计件'
}
```
### 3.3 å·¥å•类型判断
| ç±»åž‹ | åˆ¤æ–­è§„则 | æ ‡ç­¾é¢œè‰²å»ºè®® |
|------|----------|--------------|
| æ­£å¸¸ | workOrderNo ä¸ä»¥ 'FG' å¼€å¤´ | é»˜è®¤è‰² |
| è¿”工返修 | workOrderNo ä»¥ 'FG' å¼€å¤´ | çº¢è‰²/error |
```javascript
function getWorkOrderType(workOrderNo) {
  return workOrderNo?.startsWith('FG') ? '返工返修' : '正常'
}
```
---
## å››ã€é¡µé¢è®¾è®¡å»ºè®®
### 4.1 å·¥å•列表页(使用 /page æŽ¥å£ï¼‰
#### è¡¨æ ¼åˆ—设计
| åˆ—名 | å­—段 | å®½åº¦ | è¯´æ˜Ž |
|------|------|------|------|
| ç”Ÿäº§è®¢å•号 | npsNo | 150px | å¯ç‚¹å‡»è·³è½¬è¯¦æƒ… |
| äº§å“åç§° | productName | 120px | - |
| è§„格型号 | model | 150px | - |
| å·¥åº | operationName | 200px | å¤šä¸ªå·¥åºé€—号分隔 |
| å·¥å•类型 | workOrderType | 100px | æ­£å¸¸/返工返修 |
| è®¡åˆ’数量 | planQuantity | 100px | è®¢å•级别 |
| å®Œæˆæ•°é‡ | completeQuantity | 100px | è®¢å•级别 |
| å®Œæˆè¿›åº¦ | completionStatus | 120px | è¿›åº¦æ¡+百分比 |
| æŠ¥åºŸæ•°é‡ | scrapQty | 80px | çº¢è‰²æ ‡è®° |
| å·¥å•æ•° | productionTaskCount | 80px | å…±X个工单 |
| çŠ¶æ€ | status | 80px | çŠ¶æ€æ ‡ç­¾ |
| æ“ä½œ | - | 150px | æŸ¥çœ‹è¯¦æƒ…、报工等 |
#### è¿›åº¦æ¡å±•示
```javascript
// ä½¿ç”¨è¿›åº¦æ¡ç»„ä»¶
<Progress
  percent={item.completionStatus}
  strokeColor="#52c41a"
  format={percent => `${percent}%`}
/>
```
### 4.2 è®¢å•详情页工单列表(使用 /byOrder/{orderId} æŽ¥å£ï¼‰
#### è¡¨æ ¼åˆ—设计
| åˆ—名 | å­—段 | å®½åº¦ | è¯´æ˜Ž |
|------|------|------|------|
| å·¥å•编号 | workOrderNo | 150px | ä¸»æ ‡è¯† |
| å·¥åºåç§° | operationName | 120px | å•个工序 |
| å·¥å•类型 | workOrderType | 100px | æ­£å¸¸/返工返修 |
| å·¥åºç±»åž‹ | type | 80px | è®¡æ—¶/计件 |
| è®¡åˆ’数量 | planQuantity | 100px | å·¥å•级别 |
| å®Œæˆæ•°é‡ | completeQuantity | 100px | å·¥å•级别 |
| å®Œæˆè¿›åº¦ | completionStatus | 120px | è¿›åº¦æ¡ |
| æŠ¥åºŸæ•°é‡ | scrapQty | 80px | çº¢è‰²æ ‡è®° |
| æŠ¥å·¥äººå‘˜ | userNames | 150px | å¤šäººé€—号分隔 |
| è®¡åˆ’æ—¶é—´ | planStartTime ~ planEndTime | 180px | æ—¶é—´èŒƒå›´ |
| å®žé™…æ—¶é—´ | actualStartTime ~ actualEndTime | 180px | æ—¶é—´èŒƒå›´ |
| çŠ¶æ€ | status | 80px | çŠ¶æ€æ ‡ç­¾ |
| æ“ä½œ | - | 150px | æŠ¥å·¥ã€æµè½¬å¡ç­‰ |
---
## äº”、两个接口数据差异对比
| å¯¹æ¯”项 | /page æŽ¥å£ | /byOrder/{orderId} æŽ¥å£ |
|--------|------------|-------------------------|
| æ•°æ®ç»´åº¦ | è®¢å•汇总 | å·¥å•明细 |
| planQuantity | è®¢å•计划数 | å·¥å•计划数 |
| completeQuantity | è®¢å•完成数 | å·¥å•完成数 |
| completionStatus | è®¢å•进度 | å·¥å•进度 |
| operationName | å¤šå·¥åºèšåˆ | å•个工序 |
| workOrderNo | å¤šå·¥å•聚合 | å•个工单 |
| userNames | æ—  | æœ‰æŠ¥å·¥äººå‘˜ |
| ç”¨é€” | åˆ—表汇总页 | è¯¦æƒ…明细页 |
---
## å…­ã€å‰ç«¯è°ƒç”¨æ³¨æ„äº‹é¡¹
### 6.1 åˆ†é¡µå‚æ•°
- `/page` æŽ¥å£æ”¯æŒåˆ†é¡µï¼Œéœ€ä¼  `current` å’Œ `size`
- `/byOrder/{orderId}` æŽ¥å£ä¸åˆ†é¡µï¼Œè¿”回全部工单列表
### 6.2 æ•°é‡å­—段精度
- æ‰€æœ‰æ•°é‡å­—段为 `BigDecimal` ç±»åž‹ï¼Œå‰ç«¯å±•示时保留2位小数
- å®Œæˆè¿›åº¦ `completionStatus` å·²ä¸ºç™¾åˆ†æ¯”数值(如50.00表示50%)
### 6.3 æ—¶é—´å­—段格式
- æ‰€æœ‰æ—¶é—´å­—段为 `LocalDate` æ ¼å¼ï¼š`YYYY-MM-DD`
- æ— æ—¶é—´å€¼æ—¶è¿”回 `null`,前端需做空值处理
### 6.4 å·¥å•类型判断
- æŽ¥å£å·²è¿”回 `workOrderType` å­—段,无需前端判断
- è¿”工返修工单建议使用红色标签突出显示
### 6.5 æŠ¥å·¥äººå‘˜å­—段
- `userNames` ä¸ºé€—号分隔字符串,可能为空
- å¤šäººæ—¶å»ºè®®ä½¿ç”¨æ ‡ç­¾æˆ–头像列表展示
---
## ä¸ƒã€å¸¸è§é—®é¢˜å¤„理
### 7.1 è¿›åº¦ä¸ºç©ºæˆ–除零
- `completionStatus` å·²åœ¨SQL中处理除零情况
- è‹¥è®¡åˆ’数量为0,进度显示为0
### 7.2 å·¥å•号为空
- `workOrderNo` ä¸ºå¿…填字段,不会为空
- è‹¥èšåˆå±•示时多个工单号用逗号分隔
### 7.3 æŠ¥åºŸæ•°é‡å±•示
- `scrapQty` ä¸ºç´¯è®¡æŠ¥åºŸæ•°é‡
- å»ºè®®ä½¿ç”¨çº¢è‰²å­—体或警告图标标识
---
## å…«ã€Mock数据示例
### 8.1 /page æŽ¥å£Mock
```json
{
  "code": 200,
  "data": {
    "records": [
      {
        "productionOrderId": 1,
        "npsNo": "SC20260611001",
        "endOrder": false,
        "productName": "电线电缆",
        "model": "RVV 3*2.5",
        "unit": "ç±³",
        "operationName": "绞线,绝缘,成缆",
        "productionTaskCount": 3,
        "planQuantity": 1000.00,
        "completeQuantity": 500.00,
        "goodQuantity": 500.00,
        "scrapQty": 10.00,
        "completionStatus": 50.00,
        "type": 1,
        "workOrderType": "正常",
        "workOrderNo": "GD20260611001,GD20260611002,GD20260611003",
        "planStartTime": "2026-06-10",
        "planEndTime": "2026-06-15",
        "actualStartTime": "2026-06-11",
        "actualEndTime": null,
        "status": 3
      }
    ],
    "total": 1,
    "current": 1,
    "size": 10
  }
}
```
### 8.2 /byOrder/{orderId} æŽ¥å£Mock
```json
{
  "code": 200,
  "data": [
    {
      "id": 1,
      "workOrderNo": "GD20260611001",
      "planStartTime": "2026-06-10",
      "planEndTime": "2026-06-12",
      "actualStartTime": "2026-06-11",
      "actualEndTime": null,
      "status": 3,
      "productionOrderId": 1,
      "planQuantity": 500.00,
      "completeQuantity": 300.00,
      "npsNo": "SC20260611001",
      "endOrder": false,
      "productName": "电线电缆",
      "model": "RVV 3*2.5",
      "unit": "ç±³",
      "operationName": "绞线",
      "type": 1,
      "workOrderType": "正常",
      "scrapQty": 5.00,
      "completionStatus": 60.00,
      "userNames": "张三,李四"
    },
    {
      "id": 2,
      "workOrderNo": "FG20260611001",
      "planStartTime": "2026-06-12",
      "planEndTime": "2026-06-15",
      "actualStartTime": "2026-06-13",
      "actualEndTime": null,
      "status": 3,
      "productionOrderId": 1,
      "planQuantity": 100.00,
      "completeQuantity": 50.00,
      "npsNo": "SC20260611001",
      "endOrder": false,
      "productName": "电线电缆",
      "model": "RVV 3*2.5",
      "unit": "ç±³",
      "operationName": "绝缘",
      "type": 0,
      "workOrderType": "返工返修",
      "scrapQty": 2.00,
      "completionStatus": 50.00,
      "userNames": "王五"
    }
  ]
}
```
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java
@@ -47,4 +47,16 @@
    @Schema(description = "是否生产")
    private Integer isProduction;
    @Schema(description = "良品数量")
    private BigDecimal goodQuantity;
    @Schema(description = "工单任务数量")
    private Long productionTaskCount;
    @Schema(description = "工单类型(正常/返工返修)")
    private String workOrderType;
    @Schema(description = "报工人员名称")
    private String userNames;
}
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java
@@ -1,5 +1,6 @@
package com.ruoyi.production.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
@@ -23,11 +24,17 @@
    private final ProductionOperationTaskService productionOperationTaskService;
    @GetMapping("/page")
    @Operation(summary = "分页查询")
    public R page(Page<ProductionOperationTaskDto> page, ProductionOperationTaskDto dto) {
    @Operation(summary = "工单分页查询", description = "按生产订单维度分组展示,返回订单级别的汇总数据")
    public R<IPage<ProductionOperationTaskVo>> page(Page<ProductionOperationTaskDto> page, ProductionOperationTaskDto dto) {
        return R.ok(productionOperationTaskService.pageProductionOperationTask(page, dto));
    }
    @GetMapping("/byOrder/{orderId}")
    @Operation(summary = "根据订单ID查询工单列表", description = "查询指定生产订单下的所有工单明细")
    public R<List<ProductionOperationTaskVo>> byOrder(@PathVariable Long orderId) {
        return R.ok(productionOperationTaskService.listByOrderId(orderId));
    }
    @GetMapping("/list")
    @Operation(summary = "工单列表")
    public R<List<ProductionOperationTaskVo>> list(ProductionOperationTaskDto dto) {
src/main/java/com/ruoyi/production/mapper/ProductionOperationTaskMapper.java
@@ -26,9 +26,17 @@
@Mapper
public interface ProductionOperationTaskMapper extends BaseMapper<ProductionOperationTask> {
    /**
     * å·¥å•分页查询(按生产订单维度分组)
     */
    IPage<ProductionOperationTaskVo> pageProductionOperationTask(Page<ProductionOperationTaskVo> page,
                                                                 @Param("c") ProductionOperationTaskDto dto);
    /**
     * æ ¹æ®ç”Ÿäº§è®¢å•ID查询工单列表
     */
    List<ProductionOperationTaskVo> listByOrderId(@Param("orderId") Long orderId);
    List<ProductionTaskStatisticsDto> selectTaskStatisticsByDate(@Param("startDate") LocalDate startDate,
                                                                 @Param("endDate") LocalDate endDate);
src/main/java/com/ruoyi/production/service/ProductionOperationTaskService.java
@@ -12,9 +12,17 @@
public interface ProductionOperationTaskService extends IService<ProductionOperationTask> {
    /**
     * å·¥å•分页查询(按生产订单维度分组)
     */
    IPage<ProductionOperationTaskVo> pageProductionOperationTask(Page<ProductionOperationTaskDto> page,
                                                                 ProductionOperationTaskDto productionOperationTaskDto);
    /**
     * æ ¹æ®ç”Ÿäº§è®¢å•ID查询工单列表(工单明细)
     */
    List<ProductionOperationTaskVo> listByOrderId(Long orderId);
    List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto productionOperationTaskDto);
    ProductionOperationTaskVo getProductionOperationTaskInfo(Long id);
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
@@ -61,7 +61,7 @@
    @Override
    public IPage<ProductionOperationTaskVo> pageProductionOperationTask(Page<ProductionOperationTaskDto> page, ProductionOperationTaskDto dto) {
        // åˆ†é¡µæŸ¥è¯¢ç”Ÿäº§å·¥åºä»»åŠ¡
        // åˆ†é¡µæŸ¥è¯¢ï¼ˆæŒ‰ç”Ÿäº§è®¢å•维度分组)
        Page<ProductionOperationTaskVo> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        IPage<ProductionOperationTaskVo> result = baseMapper.pageProductionOperationTask(voPage, dto);
        fillOperationTypes(result.getRecords());
@@ -70,6 +70,17 @@
    }
    @Override
    public List<ProductionOperationTaskVo> listByOrderId(Long orderId) {
        // æ ¹æ®ç”Ÿäº§è®¢å•ID查询工单列表
        if (orderId == null) {
            return Collections.emptyList();
        }
        List<ProductionOperationTaskVo> result = baseMapper.listByOrderId(orderId);
        fillUserNames(result);
        return result;
    }
    @Override
    public List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto dto) {
        // æŸ¥è¯¢å·¥åºä»»åŠ¡åˆ—è¡¨
        List<ProductionOperationTaskVo> result = BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOperationTaskVo.class);
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -21,57 +21,92 @@
        <result column="dept_id" property="deptId" />
    </resultMap>
    <!-- æŒ‰ç”Ÿäº§è®¢å•分组的工单分页查询 -->
    <select id="pageProductionOperationTask" resultType="com.ruoyi.production.bean.vo.ProductionOperationTaskVo">
        select pot.*,
               po.nps_no as npsNo,
               po.is_end_order as endOrder,
               p.product_name as productName,
               pm.model as model,
               pm.unit as unit,
               poro.operation_name as operationName,
               poro.type as type,
               IFNULL(scrapStat.scrapQty, 0) AS scrapQty,
        ROUND(IFNULL(pot.complete_quantity, 0) / NULLIF(pot.plan_quantity, 0) * 100, 2) AS completionStatus,
        CASE
            WHEN pot.work_order_no LIKE 'FG%' THEN '返工返修'
            ELSE '正常'
            END AS work_order_type
        from production_operation_task pot
                 left join production_order po on pot.production_order_id = po.id
                 left join production_order_routing_operation poro on pot.production_order_routing_operation_id = poro.id
                 left join product_model pm on pm.id = ifnull(poro.product_model_id, po.product_model_id)
                 left join product p on pm.product_id = p.id
                 left join (
            select ppm.production_operation_task_id as taskId,
                   sum(ifnull(ppo.scrap_qty, 0)) as scrapQty
            from production_product_main ppm
                     left join production_product_output ppo on ppo.production_product_main_id = ppm.id
            group by ppm.production_operation_task_id
        ) scrapStat on scrapStat.taskId = pot.id
        SELECT
            po.id                   AS productionOrderId,
            po.nps_no               AS npsNo,
            po.is_end_order         AS endOrder,
            p.product_name          AS productName,
            pm.model                AS model,
            pm.unit                 AS unit,
            GROUP_CONCAT(DISTINCT poro.operation_name ORDER BY poro.drag_sort, poro.operation_name SEPARATOR ',') AS operationName,
            COUNT(DISTINCT pot.id)  AS productionTaskCount,
            po.quantity             AS planQuantity,
            po.complete_quantity    AS completeQuantity,
            po.complete_quantity    AS goodQuantity,
            SUM(IFNULL(scrapStat.scrapQty, 0)) AS scrapQty,
            ROUND(IFNULL(po.complete_quantity, 0) / NULLIF(po.quantity, 0) * 100, 2) AS completionStatus,
            MAX(poro.type)          AS type,
            CASE WHEN SUM(CASE WHEN pot.work_order_no LIKE 'FG%' THEN 1 ELSE 0 END) > 0
                THEN '返工返修' ELSE '正常' END AS workOrderType,
            GROUP_CONCAT(DISTINCT pot.work_order_no ORDER BY pot.work_order_no SEPARATOR ',') AS workOrderNo,
            MIN(pot.plan_start_time)   AS planStartTime,
            MAX(pot.plan_end_time)     AS planEndTime,
            MIN(pot.actual_start_time) AS actualStartTime,
            MAX(pot.actual_end_time)   AS actualEndTime,
            MAX(pot.status)            AS status
        FROM production_operation_task pot
            LEFT JOIN production_order po ON pot.production_order_id = po.id
            LEFT JOIN production_order_routing_operation poro ON pot.production_order_routing_operation_id = poro.id
            LEFT JOIN product_model pm ON pm.id = po.product_model_id
            LEFT JOIN product p ON pm.product_id = p.id
            LEFT JOIN (
                SELECT ppm.production_operation_task_id AS taskId,
                       SUM(IFNULL(ppo.scrap_qty, 0)) AS scrapQty
                FROM production_product_main ppm
                    LEFT JOIN production_product_output ppo ON ppo.production_product_main_id = ppm.id
                GROUP BY ppm.production_operation_task_id
            ) scrapStat ON scrapStat.taskId = pot.id
        <where>
            <if test="c != null and c.id != null">
                and pot.id = #{c.id}
            </if>
            <if test="c != null and c.npsNo != null">
                and po.nps_no like concat('%', #{c.npsNo}, '%')
            <if test="c != null and c.npsNo != null and c.npsNo != ''">
                AND po.nps_no LIKE CONCAT('%', #{c.npsNo}, '%')
            </if>
            <if test="c != null and c.productionOrderId != null">
                and pot.production_order_id = #{c.productionOrderId}
            </if>
            <if test="c != null and c.productionOrderRoutingOperationId != null">
                and pot.production_order_routing_operation_id = #{c.productionOrderRoutingOperationId}
            </if>
            <if test="c != null and c.status != null">
                and pot.status = #{c.status}
            </if>
            <if test="c != null and c.isProduction != null">
                and poro.is_production = #{c.isProduction}
            </if>
            <if test="c != null and c.workOrderNo != null and c.workOrderNo != ''">
                and pot.work_order_no like concat('%', #{c.workOrderNo}, '%')
                AND pot.production_order_id = #{c.productionOrderId}
            </if>
        </where>
        order by pot.id desc
        GROUP BY po.id
        ORDER BY po.id DESC
    </select>
    <!-- æ ¹æ®è®¢å•ID查询工单列表 -->
    <select id="listByOrderId" resultType="com.ruoyi.production.bean.vo.ProductionOperationTaskVo">
        SELECT
            pot.id,
            pot.work_order_no                   AS workOrderNo,
            pot.plan_start_time                 AS planStartTime,
            pot.plan_end_time                   AS planEndTime,
            pot.actual_start_time               AS actualStartTime,
            pot.actual_end_time                 AS actualEndTime,
            pot.status,
            pot.production_order_id             AS productionOrderId,
            pot.plan_quantity                   AS planQuantity,
            pot.complete_quantity               AS completeQuantity,
            po.nps_no                           AS npsNo,
            po.is_end_order                     AS endOrder,
            p.product_name                      AS productName,
            pm.model                            AS model,
            pm.unit                             AS unit,
            poro.operation_name                 AS operationName,
            poro.type                           AS type,
            CASE WHEN pot.work_order_no LIKE 'FG%' THEN '返工返修' ELSE '正常' END AS workOrderType,
            IFNULL(scrapStat.scrapQty, 0)       AS scrapQty,
            ROUND(IFNULL(pot.complete_quantity, 0) / NULLIF(pot.plan_quantity, 0) * 100, 2) AS completionStatus
        FROM production_operation_task pot
            LEFT JOIN production_order po ON pot.production_order_id = po.id
            LEFT JOIN production_order_routing_operation poro ON pot.production_order_routing_operation_id = poro.id
            LEFT JOIN product_model pm ON pm.id = IFNULL(poro.product_model_id, po.product_model_id)
            LEFT JOIN product p ON pm.product_id = p.id
            LEFT JOIN (
                SELECT ppm.production_operation_task_id AS taskId,
                       SUM(IFNULL(ppo.scrap_qty, 0)) AS scrapQty
                FROM production_product_main ppm
                    LEFT JOIN production_product_output ppo ON ppo.production_product_main_id = ppm.id
                GROUP BY ppm.production_operation_task_id
            ) scrapStat ON scrapStat.taskId = pot.id
        WHERE pot.production_order_id = #{orderId}
        ORDER BY pot.id DESC
    </select>
    <select id="selectTaskStatisticsByDate" resultType="com.ruoyi.home.dto.ProductionTaskStatisticsDto">