feat(production): 新增生产核算和生产工单功能模块
- 添加生产核算控制器、服务接口及实现类
- 实现生产核算分页查询和工人生产工资信息查询功能
- 添加生产工单控制器、服务接口及实现类
- 实现生产工单的增删改查和状态统计功能
- 集成工单流转卡下载和二维码生成功能
- 添加工单相关的数据传输对象和值对象
- 实现工单与用户关联的分配功能
- 完善工单附件图片处理和展示功能
已添加2个文件
已修改20个文件
975 ■■■■■ 文件已修改
src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionAccountDto.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionProductMainDto.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionAccountVo.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionAccountController.java 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionAccountMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOperationTaskMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionProductMainMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrderRoutingOperationParam.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionAccountService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOperationTaskService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionAccountServiceImpl.java 69 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationParamServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionAccountMapper.xml 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionProductMainMapper.xml 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java
@@ -1,16 +1,10 @@
package com.ruoyi.basic.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.dto.StorageAttachmentDTO;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.basic.dto.SupplierManageDto;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.pojo.StorageAttachment;
import com.ruoyi.basic.service.StorageAttachmentService;
import com.ruoyi.common.constant.StorageAttachmentConstants;
import com.ruoyi.common.enums.StorageAttachmentRecordType;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
@@ -18,26 +12,31 @@
@RestController
@AllArgsConstructor
    @RequestMapping("/basic/storage_attachment")
@Tag(name = "通用上传")
@RequestMapping("/storageAttachment")
public class StorageAttachmentController {
    private StorageAttachmentService storageAttachmentService;
    /**
     * åˆ†é¡µæŸ¥è¯¢é€šç”¨æ–‡ä»¶ä¸Šä¼ çš„附件信息
     *
     * @param storageAttachmentDTO å…³è”记录信息
     * @return åˆ†é¡µç»“æžœ
     */
    @GetMapping("/list")
    @Operation(summary = "分页查询通用文件上传的附件信息")
    public R list(StorageAttachmentDTO storageAttachmentDTO) {
        return R.ok(storageAttachmentService.list(storageAttachmentDTO));
    }
    /**
     * åˆ é™¤é€šç”¨æ–‡ä»¶ä¸Šä¼ çš„附件信息
     *
     * @param ids æ–‡ä»¶id列表
     * @return åˆ é™¤ç»“æžœ
     */
    @DeleteMapping("/delete")
    @Operation(summary = "删除通用文件上传的附件信息")
    public R batchDelete(@RequestBody List<Long> ids) {
        return R.ok(storageAttachmentService.batchDeleteStorageAttachment(ids));
    }
@@ -46,6 +45,7 @@
     * ä¿å­˜é€šç”¨æ–‡ä»¶ä¸Šä¼ çš„附件信息
     */
    @PostMapping("/add")
    @Operation(summary = "保存通用文件上传的附件信息")
    public R add(@RequestBody StorageAttachmentDTO storageAttachmentDTO) {
        storageAttachmentService.saveStorageAttachment(storageAttachmentDTO);
        return R.ok();
src/main/java/com/ruoyi/production/bean/dto/ProductionAccountDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package com.ruoyi.production.bean.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.production.pojo.ProductionAccount;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
@Data
@Schema(name = "ProductionAccountDto", description = "production account query dto")
public class ProductionAccountDto extends ProductionAccount {
    @Schema(description = "sales contract no")
    private String salesContractNo;
    @Schema(description = "customer contract no")
    private String customerContractNo;
    @Schema(description = "project name")
    private String projectName;
    @Schema(description = "customer name")
    private String customerName;
    @Schema(description = "product category")
    private String productCategory;
    @Schema(description = "specification model")
    private String specificationModel;
    @Schema(description = "scheduling user id")
    private Long schedulingUserId;
    @Schema(description = "scheduling user name")
    private String schedulingUserName;
    @Schema(description = "process")
    private String process;
    @Schema(description = "date type(day/month)")
    private String dateType;
    @Schema(description = "day query date")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate entryDate;
    @Schema(description = "date range")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate[] dateRange;
    @Schema(description = "start date")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate entryDateStart;
    @Schema(description = "end date")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate entryDateEnd;
}
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java
@@ -1,10 +1,34 @@
package com.ruoyi.production.bean.dto;
import com.ruoyi.production.pojo.ProductionOperationTask;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@EqualsAndHashCode(callSuper = true)
@Data
public class ProductionOperationTaskDto extends ProductionOperationTask {
    @Schema(description = "工序名称")
    private String processName;
    @Schema(description = "生产订单号")
    private String productOrderNpsNo;
    @Schema(description = "产品名称")
    private String productName;
    @Schema(description = "规格型号")
    private String model;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "报废数量")
    private BigDecimal scrapQty;
    @Schema(description = "完成进度")
    private BigDecimal completionStatus;
}
src/main/java/com/ruoyi/production/bean/dto/ProductionProductMainDto.java
@@ -12,57 +12,63 @@
@EqualsAndHashCode(callSuper = true)
@Data
@Schema(name = "ProductionProductMainDto", description = "生产报工查询对象")
@Schema(name = "ProductionProductMainDto", description = "production report query dto")
public class ProductionProductMainDto extends ProductionProductMain {
    @Schema(description = "产品工艺路线明细ID")
    @Schema(description = "product process route item id")
    private Long productProcessRouteItemId;
    @Schema(description = "生产报工表id")
    @Schema(description = "production report id")
    private Long productMainId;
    @Schema(description = "租户ID")
    @Schema(description = "tenant id")
    private Long tenantId;
    @Schema(description = "工单编号")
    @Schema(description = "work order no")
    private String workOrderNo;
    @Schema(description = "工单状态")
    @Schema(description = "work order status")
    private String workOrderStatus;
    @Schema(description = "昵称")
    @Schema(description = "nick name")
    private String nickName;
    @Schema(description = "数量")
    @Schema(description = "quantity")
    private BigDecimal quantity;
    @Schema(description = "报废数量")
    @Schema(description = "scrap quantity")
    private BigDecimal scrapQty;
    @Schema(description = "产品名称")
    @Schema(description = "product name")
    private String productName;
    @Schema(description = "产品型号名称")
    @Schema(description = "product model name")
    private String productModelName;
    @Schema(description = "单位")
    @Schema(description = "unit")
    private String unit;
    @Schema(description = "销售合同编号")
    @Schema(description = "sales contract no")
    private String salesContractNo;
    @Schema(description = "排产日期")
    @Schema(description = "scheduling date")
    private LocalDate schedulingDate;
    @Schema(description = "排产人员名称")
    @Schema(description = "scheduling user name")
    private String schedulingUserName;
    @Schema(description = "客户名称")
    @Schema(description = "customer name")
    private String customerName;
    @Schema(description = "工序")
    @Schema(description = "process")
    private String process;
    @Schema(description = "工序参数列表")
    @Schema(description = "salary quota")
    private BigDecimal workHours;
    @Schema(description = "wages")
    private BigDecimal wages;
    @Schema(description = "operation param list")
    private List<ProductionOrderRoutingOperationParam> productionOperationParamList;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionAccountVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package com.ruoyi.production.bean.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@Schema(name = "ProductionAccountVo", description = "生产核算分页返回对象")
public class ProductionAccountVo {
    @Schema(description = "客户合同号")
    private String customerContractNo;
    @Schema(description = "项目名称")
    private String projectName;
    @Schema(description = "客户名称")
    private String customerName;
    @Schema(description = "产品大类")
    private String productCategory;
    @Schema(description = "规格型号")
    private String specificationModel;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "生产人ID")
    private Long schedulingUserId;
    @Schema(description = "生产人名称")
    private String schedulingUserName;
    @Schema(description = "工资")
    private BigDecimal wages;
    @Schema(description = "生产数量")
    private BigDecimal finishedNum;
    @Schema(description = "工时定额")
    private BigDecimal workHours;
    @Schema(description = "工序")
    private String process;
    @Schema(description = "生产日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate schedulingDate;
    @Schema(description = "生产月份(yyyy-MM)")
    private String schedulingMonth;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java
@@ -26,9 +26,12 @@
    @Schema(description = "工序名称")
    private String operationName;
    @Schema(description = "工单类型 æ­£å¸¸ /返工返修")
    @Schema(description = "工单类型 æ­£å¸¸/返工返修")
    private String workOrderType;
    @Schema(description = "完成进度")
    private BigDecimal completionStatus;
    @Schema(description = "报工人员名称,多个使用逗号分隔")
    private String userNames;
}
src/main/java/com/ruoyi/production/controller/ProductionAccountController.java
@@ -1,5 +1,16 @@
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.ProductionAccountDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.bean.vo.ProductionAccountVo;
import com.ruoyi.production.service.ProductionAccountService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -7,12 +18,24 @@
 * <p>
 * ç”Ÿäº§æ ¸ç®—表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-04-21 03:55:52
 */
@RestController
@RequestMapping("/productionAccount")
@RequiredArgsConstructor
@Tag(name = "生产核算")
public class ProductionAccountController {
    private final ProductionAccountService productionAccountService;
    @GetMapping("/listPage")
    @Operation(summary = "生产核算分页查询")
    public R<IPage<ProductionAccountVo>> listPage(Page<ProductionAccountDto> page, ProductionAccountDto dto) {
        return R.ok(productionAccountService.listPage(page, dto));
    }
    @GetMapping("/listProductionDetails")
    @Operation(summary ="查询工人生产工资信息")
    public R<IPage<ProductionProductMainDto>> listProductionDetails(ProductionAccountDto productionAccountDto, Page page) {
        return R.ok(productionAccountService.listProductionDetails(productionAccountDto,page));
    }
}
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java
@@ -6,18 +6,11 @@
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.service.ProductionOperationTaskService;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -71,4 +64,20 @@
        return R.ok(productionOperationTaskService.updateProductWorkOrder(dto));
    }
    @Operation(summary = "指派报工人")
    @PostMapping("/assign")
    public R<Boolean> assign(@RequestBody ProductionOperationTaskDto dto) {
        return R.ok(productionOperationTaskService.assign(dto));
    }
    /**
     * å·¥å•流转卡下载
     * @param response
     * @param dto
     */
    @PostMapping("/down")
    public void down(HttpServletResponse response, @RequestBody ProductionOperationTaskDto dto) {
        productionOperationTaskService.down(response, dto);
    }
}
src/main/java/com/ruoyi/production/mapper/ProductionAccountMapper.java
@@ -1,7 +1,11 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.production.bean.dto.ProductionAccountDto;
import com.ruoyi.production.bean.dto.UserAccountDto;
import com.ruoyi.production.bean.vo.ProductionAccountVo;
import com.ruoyi.production.pojo.ProductionAccount;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -19,6 +23,8 @@
 */
@Mapper
public interface ProductionAccountMapper extends BaseMapper<ProductionAccount> {
    IPage<ProductionAccountVo> listPage(Page<ProductionAccountDto> page, @Param("c") ProductionAccountDto dto);
    UserAccountDto selectUserAccount(@Param("userId") Long userId, @Param("date") String date);
    List<Map<String, Object>> selectDailyWagesStats(@Param("startDate") String startDate, @Param("endDate") String endDate);
src/main/java/com/ruoyi/production/mapper/ProductionOperationTaskMapper.java
@@ -40,4 +40,5 @@
                                                                           @Param("userId") Long userId,
                                                                           @Param("processIds") List<Long> processIds);
    ProductionOperationTaskDto getProductWorkOrderFlowCard(@Param("id") Long id);
}
src/main/java/com/ruoyi/production/mapper/ProductionProductMainMapper.java
@@ -3,8 +3,8 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.production.bean.dto.ProductionAccountDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.bean.dto.SalesLedgerProductionAccountingDto;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionProductMain;
import org.apache.ibatis.annotations.Mapper;
@@ -30,7 +30,7 @@
     */
    ProductionOrder getOrderByMainId(@Param("productMainId") Long productMainId);
    IPage<ProductionProductMainDto> listProductionDetails(@Param("ew") SalesLedgerProductionAccountingDto salesLedgerProductionAccountingDto, Page page);
    IPage<ProductionProductMainDto> listProductionDetails(@Param("c") ProductionAccountDto productionAccountDto, Page page);
    ArrayList<Long> listMain(List<Long> idList);
}
src/main/java/com/ruoyi/production/pojo/ProductionOrderRoutingOperationParam.java
@@ -90,4 +90,7 @@
    @Schema(description = "生产订单工艺路线工序ID")
    private Long productionOrderRoutingOperationId;
    @Schema(description = "生产报工表ID")
    private Long productionProductMainId;
}
src/main/java/com/ruoyi/production/service/ProductionAccountService.java
@@ -1,5 +1,10 @@
package com.ruoyi.production.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.production.bean.dto.ProductionAccountDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.bean.vo.ProductionAccountVo;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.production.pojo.ProductionAccount;
@@ -12,5 +17,7 @@
 * @since 2026-04-21 03:55:52
 */
public interface ProductionAccountService extends IService<ProductionAccount> {
    IPage<ProductionAccountVo> listPage(Page<ProductionAccountDto> page, ProductionAccountDto dto);
    IPage<ProductionProductMainDto> listProductionDetails(ProductionAccountDto productionAccountDto, Page page);
}
src/main/java/com/ruoyi/production/service/ProductionOperationTaskService.java
@@ -6,6 +6,7 @@
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
import com.ruoyi.production.pojo.ProductionOperationTask;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
@@ -23,4 +24,8 @@
    boolean removeProductionOperationTask(List<Long> ids);
    int updateProductWorkOrder(ProductionOperationTaskDto dto);
    boolean assign(ProductionOperationTaskDto dto);
    void down(HttpServletResponse response, ProductionOperationTaskDto dto);
}
src/main/java/com/ruoyi/production/service/impl/ProductionAccountServiceImpl.java
@@ -1,20 +1,73 @@
package com.ruoyi.production.service.impl;
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.production.bean.dto.ProductionAccountDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.bean.vo.ProductionAccountVo;
import com.ruoyi.production.mapper.ProductionAccountMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.pojo.ProductionAccount;
import com.ruoyi.production.service.ProductionAccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
 * <p>
 * ç”Ÿäº§æ ¸ç®—表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-04-21 03:55:52
 */
import java.time.LocalDate;
@Service
@RequiredArgsConstructor
public class ProductionAccountServiceImpl extends ServiceImpl<ProductionAccountMapper, ProductionAccount> implements ProductionAccountService {
    private final ProductionProductMainMapper productionProductMainMapper;
    @Override
    public IPage<ProductionAccountVo> listPage(Page<ProductionAccountDto> page, ProductionAccountDto dto) {
        ProductionAccountDto queryDto = normalizeDateQuery(dto);
        return baseMapper.listPage(page, queryDto);
    }
    @Override
    public IPage<ProductionProductMainDto> listProductionDetails(ProductionAccountDto dto, Page page) {
        return productionProductMainMapper.listProductionDetails(normalizeDateQuery(dto), page);
    }
    private ProductionAccountDto normalizeDateQuery(ProductionAccountDto dto) {
        if (dto == null) {
            return new ProductionAccountDto();
        }
        LocalDate[] dateRange = dto.getDateRange();
        if ((dto.getEntryDateStart() == null || dto.getEntryDateEnd() == null)
                && dateRange != null
                && dateRange.length > 0) {
            if (dto.getEntryDateStart() == null) {
                dto.setEntryDateStart(dateRange[0]);
            }
            if (dto.getEntryDateEnd() == null) {
                dto.setEntryDateEnd(dateRange.length > 1 ? dateRange[1] : dateRange[0]);
            }
        }
        String dateType = dto.getDateType();
        if ("day".equalsIgnoreCase(dateType)) {
            if (dto.getEntryDate() == null && dateRange != null && dateRange.length > 0) {
                dto.setEntryDate(dateRange[0]);
            }
            if (dto.getEntryDate() == null) {
                dto.setEntryDate(dto.getEntryDateStart());
            }
            dto.setEntryDateStart(null);
            dto.setEntryDateEnd(null);
        } else if ("month".equalsIgnoreCase(dateType)) {
            if ((dto.getEntryDateStart() == null || dto.getEntryDateEnd() == null) && dto.getEntryDate() != null) {
                LocalDate monthDate = dto.getEntryDate();
                dto.setEntryDateStart(monthDate.withDayOfMonth(1));
                dto.setEntryDateEnd(monthDate.withDayOfMonth(monthDate.lengthOfMonth()));
            }
            dto.setEntryDate(null);
        }
        return dto;
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
@@ -1,42 +1,82 @@
package com.ruoyi.production.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType;
import com.deepoove.poi.data.Pictures;
import com.ruoyi.basic.dto.StorageAttachmentDTO;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.config.FileProperties;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MatrixToImageWriter;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.service.ProductionOperationTaskService;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ProductionOperationTaskServiceImpl extends ServiceImpl<ProductionOperationTaskMapper, ProductionOperationTask> implements ProductionOperationTaskService {
    private final SysUserMapper sysUserMapper;
    private final FileUtil fileUtil;
    private final FileProperties fileProperties;
    @Value("${file.temp-dir}")
    private String tempDir;
    @Override
    public IPage<ProductionOperationTaskVo> pageProductionOperationTask(Page<ProductionOperationTaskDto> page, ProductionOperationTaskDto dto) {
        Page<ProductionOperationTaskVo> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        return baseMapper.pageProductionOperationTask(voPage, dto);
        IPage<ProductionOperationTaskVo> result = baseMapper.pageProductionOperationTask(voPage, dto);
        fillUserNames(result.getRecords());
        return result;
    }
    @Override
    public List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto dto) {
        return BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOperationTaskVo.class);
        List<ProductionOperationTaskVo> result = BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOperationTaskVo.class);
        fillUserNames(result);
        return result;
    }
    @Override
    public ProductionOperationTaskVo getProductionOperationTaskInfo(Long id) {
        ProductionOperationTask item = this.getById(id);
        return item == null ? null : BeanUtil.copyProperties(item, ProductionOperationTaskVo.class);
        if (item == null) {
            return null;
        }
        ProductionOperationTaskVo vo = BeanUtil.copyProperties(item, ProductionOperationTaskVo.class);
        fillUserNames(Collections.singletonList(vo));
        return vo;
    }
    @Override
@@ -47,6 +87,27 @@
    @Override
    public boolean removeProductionOperationTask(List<Long> ids) {
        return ids != null && !ids.isEmpty() && this.removeByIds(ids);
    }
    @Override
    public int updateProductWorkOrder(ProductionOperationTaskDto dto) {
        return baseMapper.updateById(dto);
    }
    @Override
    public boolean assign(ProductionOperationTaskDto dto) {
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("工单ID不能为空");
        }
        ProductionOperationTask update = new ProductionOperationTask();
        update.setId(dto.getId());
        update.setUserIds(dto.getUserIds());
        int rows = baseMapper.updateById(update);
        if (rows <= 0) {
            throw new ServiceException("工单不存在或已删除");
        }
        return true;
    }
    private LambdaQueryWrapper<ProductionOperationTask> buildQueryWrapper(ProductionOperationTaskDto dto) {
@@ -62,8 +123,219 @@
                .orderByDesc(ProductionOperationTask::getId);
    }
    private void fillUserNames(List<ProductionOperationTaskVo> voList) {
        if (voList == null || voList.isEmpty()) {
            return;
        }
        Set<Long> userIdSet = new LinkedHashSet<>();
        for (ProductionOperationTaskVo vo : voList) {
            if (vo == null) {
                continue;
            }
            userIdSet.addAll(parseUserIdList(vo.getUserIds(), false));
        }
        if (userIdSet.isEmpty()) {
            return;
        }
        List<SysUser> userList = sysUserMapper.selectUsersByIds(new ArrayList<>(userIdSet));
        if (userList == null || userList.isEmpty()) {
            return;
        }
        Map<Long, String> userNameById = userList.stream()
                .filter(item -> item.getUserId() != null)
                .collect(Collectors.toMap(SysUser::getUserId, SysUser::getNickName, (left, right) -> left));
        for (ProductionOperationTaskVo vo : voList) {
            if (vo == null) {
                continue;
            }
            List<Long> userIds = parseUserIdList(vo.getUserIds(), false);
            if (userIds.isEmpty()) {
                vo.setUserNames(null);
                continue;
            }
            String userNames = userIds.stream()
                    .map(userNameById::get)
                    .filter(StringUtils::isNotBlank)
                    .collect(Collectors.joining(","));
            vo.setUserNames(userNames);
        }
    }
    private List<Long> parseUserIdList(String userIds, boolean strict) {
        if (StringUtils.isBlank(userIds)) {
            if (strict) {
                throw new ServiceException("userIds格式不正确,必须为JSON数字数组");
            }
            return new ArrayList<>();
        }
        String text = userIds.trim();
        try {
            List<Long> parsed = JSON.parseArray(text, Long.class);
            LinkedHashSet<Long> idSet = parsed == null ? new LinkedHashSet<>() : parsed.stream()
                    .filter(Objects::nonNull)
                    .collect(Collectors.toCollection(LinkedHashSet::new));
            if (strict && idSet.isEmpty()) {
                throw new ServiceException("userIds格式不正确,必须为JSON数字数组");
            }
            return new ArrayList<>(idSet);
        } catch (Exception e) {
            if (strict) {
                throw new ServiceException("userIds格式不正确,必须为JSON数字数组");
            }
            return new ArrayList<>();
        }
    }
    @Override
    public int updateProductWorkOrder(ProductionOperationTaskDto dto) {
        return baseMapper.updateById(dto);
    public void down(HttpServletResponse response, ProductionOperationTaskDto dto) {
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("工单ID不能为空");
        }
        ProductionOperationTaskDto taskDto = baseMapper.getProductWorkOrderFlowCard(dto.getId());
        if (taskDto == null) {
            throw new ServiceException("工单不存在,ID=" + dto.getId());
        }
        String codePath;
        try {
            codePath = new MatrixToImageWriter().code(taskDto.getId().toString(), tempDir);
        } catch (Exception e) {
            throw new ServiceException("生成二维码失败");
        }
        List<Map<String, Object>> images = buildTaskAttachmentImages(taskDto.getId());
        Map<String, Object> renderData = new HashMap<>();
        renderData.put("process", taskDto.getProcessName());
        renderData.put("workOrderNo", taskDto.getWorkOrderNo());
        renderData.put("productOrderNpsNo", taskDto.getProductOrderNpsNo());
        renderData.put("productName", taskDto.getProductName());
        renderData.put("planQuantity", taskDto.getPlanQuantity());
        renderData.put("model", taskDto.getModel());
        renderData.put("completeQuantity", taskDto.getCompleteQuantity());
        renderData.put("scrapQty", taskDto.getScrapQty());
        renderData.put("planStartTime", taskDto.getPlanStartTime());
        renderData.put("planEndTime", taskDto.getPlanEndTime());
        renderData.put("actualStartTime", taskDto.getActualStartTime());
        renderData.put("actualEndTime", taskDto.getActualEndTime());
        renderData.put("twoCode", Pictures.ofLocal(codePath).create());
        renderData.put("images", images.isEmpty() ? null : images);
        try (InputStream inputStream = this.getClass().getResourceAsStream("/static/work-order-template.docx")) {
            if (inputStream == null) {
                throw new ServiceException("流转卡模板不存在");
            }
            XWPFTemplate template = XWPFTemplate.compile(inputStream).render(renderData);
            response.setContentType("application/msword");
            String fileName = URLEncoder.encode("流转卡", "UTF-8");
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
            response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".docx");
            try (OutputStream os = response.getOutputStream()) {
                template.write(os);
                os.flush();
            }
        } catch (Exception e) {
            throw new RuntimeException("导出失败");
        }
    }
    private List<Map<String, Object>> buildTaskAttachmentImages(Long taskId) {
        List<Map<String, Object>> images = new ArrayList<>();
        StorageAttachmentDTO storageAttachmentDTO = new StorageAttachmentDTO();
        storageAttachmentDTO.setRecordType(RecordTypeEnum.PRODUCTION_OPERATION_TASK.getType());
        storageAttachmentDTO.setRecordId(taskId);
        List<StorageBlobVO> taskWorkOrderFiles =
                fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(storageAttachmentDTO);
        if (CollectionUtils.isEmpty(taskWorkOrderFiles)) {
            return images;
        }
        for (StorageBlobVO blobVO : taskWorkOrderFiles) {
            if (blobVO == null) {
                continue;
            }
            PictureType pictureType = resolvePictureType(blobVO);
            if (pictureType == null) {
                continue;
            }
            File imageFile = resolveImageFile(blobVO);
            if (imageFile == null || !imageFile.exists() || !imageFile.isFile()) {
                continue;
            }
            try (InputStream imageInputStream = new FileInputStream(imageFile)) {
                Map<String, Object> image = new HashMap<>();
                PictureRenderData pictureRenderData = Pictures.ofStream(imageInputStream, pictureType)
                        .sizeInCm(17, 20)
                        .create();
                image.put("url", pictureRenderData);
                images.add(image);
            } catch (Exception ignored) {
                // å•个附件解析失败时跳过,避免影响整个流转卡导出
            }
        }
        return images;
    }
    private File resolveImageFile(StorageBlobVO blobVO) {
        if (blobVO == null || StringUtils.isBlank(blobVO.getUidFilename())) {
            return null;
        }
        if (StringUtils.isBlank(blobVO.getPath())) {
            return new File(fileProperties.getPath(), blobVO.getUidFilename());
        }
        return new File(new File(fileProperties.getPath(), blobVO.getPath()), blobVO.getUidFilename());
    }
    private PictureType resolvePictureType(StorageBlobVO blobVO) {
        if (blobVO == null) {
            return null;
        }
        PictureType type = parsePictureTypeByFileName(blobVO.getOriginalFilename());
        if (type != null) {
            return type;
        }
        type = parsePictureTypeByFileName(blobVO.getUidFilename());
        if (type != null) {
            return type;
        }
        return parsePictureTypeByContentType(blobVO.getContentType());
    }
    private PictureType parsePictureTypeByFileName(String fileName) {
        if (StringUtils.isBlank(fileName) || !fileName.contains(".")) {
            return null;
        }
        try {
            return PictureType.suggestFileType(fileName);
        } catch (Exception ex) {
            return null;
        }
    }
    private PictureType parsePictureTypeByContentType(String contentType) {
        if (StringUtils.isBlank(contentType)) {
            return null;
        }
        String normalized = contentType.trim().toLowerCase(Locale.ROOT);
        switch (normalized) {
            case "image/jpeg":
            case "image/jpg":
            case "image/pjpeg":
                return PictureType.JPEG;
            case "image/png":
                return PictureType.PNG;
            case "image/gif":
                return PictureType.GIF;
            case "image/bmp":
            case "image/x-ms-bmp":
                return PictureType.BMP;
            case "image/tiff":
            case "image/tif":
                return PictureType.TIFF;
            case "image/svg+xml":
                return PictureType.SVG;
            default:
                return null;
        }
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationParamServiceImpl.java
@@ -75,6 +75,10 @@
        ProductionOrderRoutingOperationParam query = dto == null ? new ProductionOrderRoutingOperationParam() : dto;
        return Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                .eq(query.getId() != null, ProductionOrderRoutingOperationParam::getId, query.getId())
                .eq(query.getProductionProductMainId() != null,
                        ProductionOrderRoutingOperationParam::getProductionProductMainId, query.getProductionProductMainId())
                .isNull(query.getProductionProductMainId() == null,
                        ProductionOrderRoutingOperationParam::getProductionProductMainId)
                .eq(query.getProductionOrderId() != null, ProductionOrderRoutingOperationParam::getProductionOrderId, query.getProductionOrderId())
                .eq(query.getProductionOrderRoutingOperationId() != null, ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, query.getProductionOrderRoutingOperationId())
                .eq(query.getTechnologyOperationId() != null,
@@ -148,6 +152,7 @@
    private void checkDuplicate(ProductionOrderRoutingOperationParam item) {
        boolean duplicate = productionOrderRoutingOperationParamMapper.selectCount(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .isNull(ProductionOrderRoutingOperationParam::getProductionProductMainId)
                        .eq(ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, item.getProductionOrderRoutingOperationId())
                        .eq(item.getTechnologyRoutingOperationParamId() != null,
                                ProductionOrderRoutingOperationParam::getTechnologyRoutingOperationParamId, item.getTechnologyRoutingOperationParamId())
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -40,10 +40,14 @@
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@@ -75,7 +79,9 @@
    @Override
    public IPage<ProductionProductMainDto> listPageProductionProductMainDto(Page page, ProductionProductMainDto productionProductMainDto) {
        return productionProductMainMapper.listPageProductionProductMainDto(page, productionProductMainDto);
        IPage<ProductionProductMainDto> result = productionProductMainMapper.listPageProductionProductMainDto(page, productionProductMainDto);
        fillOperationParamList(result.getRecords());
        return result;
    }
    @Override
@@ -85,9 +91,119 @@
    @Override
    public ProductionProductMainDto getProductionProductMainInfo(Long id) {
        return productionProductMainMapper.listPageProductionProductMainDto(new Page<>(1, 1), new ProductionProductMainDto() {{
        return listPageProductionProductMainDto(new Page<>(1, 1), new ProductionProductMainDto() {{
            setId(id);
        }}).getRecords().stream().findFirst().orElse(null);
    }
    private void fillOperationParamList(List<ProductionProductMainDto> recordList) {
        if (recordList == null || recordList.isEmpty()) {
            return;
        }
        Set<Long> mainIdSet = recordList.stream()
                .map(ProductionProductMainDto::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (mainIdSet.isEmpty()) {
            recordList.forEach(item -> item.setProductionOperationParamList(Collections.emptyList()));
            return;
        }
        List<ProductionOrderRoutingOperationParam> paramList = productionOrderRoutingOperationParamMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .in(ProductionOrderRoutingOperationParam::getProductionProductMainId, mainIdSet)
                        .orderByAsc(ProductionOrderRoutingOperationParam::getId));
        Map<Long, List<ProductionOrderRoutingOperationParam>> paramGroupMap = new HashMap<>();
        for (ProductionOrderRoutingOperationParam param : paramList) {
            if (param == null || param.getProductionProductMainId() == null) {
                continue;
            }
            paramGroupMap.computeIfAbsent(param.getProductionProductMainId(), key -> new ArrayList<>()).add(param);
        }
        Set<Long> missingMainIdSet = new LinkedHashSet<>();
        for (ProductionProductMainDto item : recordList) {
            Long mainId = item.getId();
            if (mainId == null) {
                item.setProductionOperationParamList(Collections.emptyList());
                continue;
            }
            List<ProductionOrderRoutingOperationParam> params = paramGroupMap.get(mainId);
            if (params != null && !params.isEmpty()) {
                item.setProductionOperationParamList(params);
                continue;
            }
            missingMainIdSet.add(mainId);
        }
        if (missingMainIdSet.isEmpty()) {
            return;
        }
        // å…¼å®¹åŽ†å²æ•°æ®ï¼šæ—§æŠ¥å·¥è®°å½•æ²¡æœ‰æŒ‰æŠ¥å·¥ID落参数快照时,回退展示工序模板参数。
        List<ProductionProductMain> mainList = productionProductMainMapper.selectBatchIds(missingMainIdSet);
        Map<Long, Long> mainIdToTaskIdMap = mainList.stream()
                .filter(Objects::nonNull)
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(ProductionProductMain::getId,
                        ProductionProductMain::getProductionOperationTaskId, (left, right) -> left));
        Set<Long> taskIdSet = mainIdToTaskIdMap.values().stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (taskIdSet.isEmpty()) {
            for (ProductionProductMainDto item : recordList) {
                if (item.getId() != null && missingMainIdSet.contains(item.getId())) {
                    item.setProductionOperationParamList(Collections.emptyList());
                }
            }
            return;
        }
        List<ProductionOperationTask> taskList = productionOperationTaskMapper.selectList(
                Wrappers.<ProductionOperationTask>lambdaQuery()
                        .in(ProductionOperationTask::getId, taskIdSet));
        Map<Long, Long> taskIdToRoutingOperationIdMap = taskList.stream()
                .filter(Objects::nonNull)
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(ProductionOperationTask::getId,
                        ProductionOperationTask::getProductionOrderRoutingOperationId, (left, right) -> left));
        Set<Long> routingOperationIdSet = taskIdToRoutingOperationIdMap.values().stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (routingOperationIdSet.isEmpty()) {
            for (ProductionProductMainDto item : recordList) {
                if (item.getId() != null && missingMainIdSet.contains(item.getId())) {
                    item.setProductionOperationParamList(Collections.emptyList());
                }
            }
            return;
        }
        List<ProductionOrderRoutingOperationParam> fallbackParamList = productionOrderRoutingOperationParamMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .in(ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, routingOperationIdSet)
                        .isNull(ProductionOrderRoutingOperationParam::getProductionProductMainId)
                        .orderByAsc(ProductionOrderRoutingOperationParam::getId));
        Map<Long, List<ProductionOrderRoutingOperationParam>> fallbackGroupMap = new HashMap<>();
        for (ProductionOrderRoutingOperationParam param : fallbackParamList) {
            if (param == null || param.getProductionOrderRoutingOperationId() == null) {
                continue;
            }
            fallbackGroupMap.computeIfAbsent(param.getProductionOrderRoutingOperationId(), key -> new ArrayList<>()).add(param);
        }
        for (ProductionProductMainDto item : recordList) {
            Long mainId = item.getId();
            if (mainId == null || !missingMainIdSet.contains(mainId)) {
                continue;
            }
            Long taskId = mainIdToTaskIdMap.get(mainId);
            Long routingOperationId = taskId == null ? null : taskIdToRoutingOperationIdMap.get(taskId);
            if (routingOperationId == null) {
                item.setProductionOperationParamList(Collections.emptyList());
                continue;
            }
            item.setProductionOperationParamList(fallbackGroupMap.getOrDefault(routingOperationId, Collections.emptyList()));
        }
    }
    @Override
@@ -132,7 +248,6 @@
        if (productionOrder == null) {
            throw new ServiceException("生产订单不存在");
        }
        syncOperationParamInputValue(dto, routingOperation.getId());
        TechnologyRoutingOperation technologyRoutingOperation = technologyRoutingOperationMapper.selectById(routingOperation.getTechnologyRoutingOperationId());
        TechnologyOperation technologyOperation = technologyRoutingOperation == null ? null
                : technologyOperationMapper.selectById(technologyRoutingOperation.getTechnologyOperationId());
@@ -149,11 +264,16 @@
        productionProductMain.setProductionOperationTaskId(taskId);
        productionProductMain.setStatus(0);
        productionProductMainMapper.insert(productionProductMain);
        syncOperationParamInputValue(dto, routingOperation.getId(), productionProductMain.getId());
        List<ProductStructureDto> productStructureDtos = resolveInputStructures(
                productionOrder.getId(), routingOperation, productModel.getId());
       // å¦‚果没有bom子节点了,那么投入就是他本身
        if (productStructureDtos.isEmpty()) {
            throw new ServiceException("未找到当前工序对应的BOM投入节点");
            ProductStructureDto fallbackInput = new ProductStructureDto();
            fallbackInput.setProductModelId(productModel.getId());
            fallbackInput.setUnitQuantity(BigDecimal.ONE);
            productStructureDtos.add(fallbackInput);
        }
        for (ProductStructureDto item : productStructureDtos) {
            // å½“前实现按工序成品直接作为投入,后续若接入领料记录可在这里替换来源。
@@ -199,7 +319,7 @@
                qualityInspect.setProductModelId(productModel.getId());
                qualityInspectMapper.insert(qualityInspect);
                List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(product.getId(), inspectType, process);
                if (qualityTestStandard.size() > 0) {
                if (!qualityTestStandard.isEmpty()) {
                    qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
                    qualityInspectMapper.updateById(qualityInspect);
                    qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
@@ -270,20 +390,33 @@
        return true;
    }
    private void syncOperationParamInputValue(ProductionProductMainDto dto, Long productionOrderRoutingOperationId) {
        if (dto == null || productionOrderRoutingOperationId == null) {
    private void syncOperationParamInputValue(ProductionProductMainDto dto,
                                              Long productionOrderRoutingOperationId,
                                              Long productionProductMainId) {
        if (dto == null || productionOrderRoutingOperationId == null || productionProductMainId == null) {
            return;
        }
        List<ProductionOrderRoutingOperationParam> paramList = dto.getProductionOperationParamList();
        if (paramList == null || paramList.isEmpty()) {
            return;
        }
        Set<Long> sourceParamIdSet = paramList.stream()
                .filter(Objects::nonNull)
                .map(ProductionOrderRoutingOperationParam::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (sourceParamIdSet.isEmpty()) {
            return;
        }
        List<ProductionOrderRoutingOperationParam> dbParamList = productionOrderRoutingOperationParamMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .in(ProductionOrderRoutingOperationParam::getId, sourceParamIdSet)
                        .eq(ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, productionOrderRoutingOperationId));
        if (dbParamList == null || dbParamList.isEmpty()) {
            return;
        }
        Map<Long, ProductionOrderRoutingOperationParam> dbParamMap = dbParamList.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionOrderRoutingOperationParam::getId, item -> item, (left, right) -> left));
@@ -295,14 +428,31 @@
            if (dbParam == null) {
                throw new ServiceException("工序参数不存在或不属于当前工单工序,ID=" + param.getId());
            }
            if (Objects.equals(dbParam.getInputValue(), param.getInputValue())) {
                continue;
            }
            ProductionOrderRoutingOperationParam updateParam = new ProductionOrderRoutingOperationParam();
            updateParam.setId(dbParam.getId());
            updateParam.setInputValue(param.getInputValue());
            productionOrderRoutingOperationParamMapper.updateById(updateParam);
            productionOrderRoutingOperationParamMapper.insert(buildReportParamSnapshot(dbParam, param.getInputValue(), productionProductMainId));
        }
    }
    private ProductionOrderRoutingOperationParam buildReportParamSnapshot(ProductionOrderRoutingOperationParam source,
                                                                          String inputValue,
                                                                          Long productionProductMainId) {
        ProductionOrderRoutingOperationParam target = new ProductionOrderRoutingOperationParam();
        target.setProductionOrderId(source.getProductionOrderId());
        target.setTechnologyRoutingOperationParamId(source.getTechnologyRoutingOperationParamId());
        target.setParamCode(source.getParamCode());
        target.setParamName(source.getParamName());
        target.setParamType(source.getParamType());
        target.setParamFormat(source.getParamFormat());
        target.setUnit(source.getUnit());
        target.setIsRequired(source.getIsRequired());
        target.setRemark(source.getRemark());
        target.setParamId(source.getParamId());
        target.setTechnologyOperationId(source.getTechnologyOperationId());
        target.setTechnologyOperationParamId(source.getTechnologyOperationParamId());
        target.setStandardValue(source.getStandardValue());
        target.setInputValue(inputValue);
        target.setProductionOrderRoutingOperationId(source.getProductionOrderRoutingOperationId());
        target.setProductionProductMainId(productionProductMainId);
        return target;
    }
    private List<ProductStructureDto> resolveInputStructures(Long productionOrderId,
@@ -431,6 +581,9 @@
                .eq(ProductionProductOutput::getProductionProductMainId, productionProductMain.getId()));
        productionProductInputMapper.delete(new LambdaQueryWrapper<ProductionProductInput>()
                .eq(ProductionProductInput::getProductionProductMainId, productionProductMain.getId()));
        productionOrderRoutingOperationParamMapper.delete(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .eq(ProductionOrderRoutingOperationParam::getProductionProductMainId, productionProductMain.getId()));
        stockUtils.deleteStockInRecord(productionProductMain.getId(), StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode());
        stockUtils.deleteStockInRecord(productionProductMain.getId(), StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode());
        stockUtils.deleteStockOutRecord(productionProductMain.getId(), StockOutQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_OUT.getCode());
src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java
@@ -122,7 +122,7 @@
            throw new ServiceException("bom产品结构为空!");
        }
        // åŒä¸€ä¸ª BOM ä¸­å¯èƒ½é‡å¤å¼•用相同工序,这里按首次出现顺序去重。
        // åŒä¸€ä¸ª BOM ä¸­å¯èƒ½é‡å¤å¼•用相同工序,按照上一层的父节点的产品是否相同和工序是否相同
        Map<Long, TechnologyBomStructure> structureById = new HashMap<>();
        for (TechnologyBomStructure bomStructure : bomStructures) {
            if (bomStructure != null && bomStructure.getId() != null) {
src/main/resources/mapper/production/ProductionAccountMapper.xml
@@ -21,6 +21,74 @@
        <result column="dept_id" property="deptId" />
    </resultMap>
    <select id="listPage" resultType="com.ruoyi.production.bean.vo.ProductionAccountVo">
        select
        group_concat(distinct p_parent.product_name order by p_parent.product_name separator ',') as productCategory,
        group_concat(distinct pm.model order by pm.model separator ',') as specificationModel,
        group_concat(distinct pm.unit order by pm.unit separator ',') as unit,
        pa.scheduling_user_id as schedulingUserId,
        pa.scheduling_user_name as schedulingUserName,
        cast(sum(
            ifnull(pa.work_hours, 0) * ifnull(pa.finished_num, 0) *
            case
                when substring_index(pm.model, '*', -1) regexp '^[0-9]+(\\.[0-9]+)?$'
                then cast(substring_index(pm.model, '*', -1) as decimal(18,4))
                else 1
            end
        ) as decimal(18,4)) as wages,
        cast(sum(ifnull(pa.finished_num, 0)) as decimal(18,4)) as finishedNum,
        cast(sum(ifnull(pa.work_hours, 0)) as decimal(18,4)) as workHours,
        group_concat(distinct pa.technology_operation_name order by pa.technology_operation_name separator ',') as process,
        case
            when count(distinct date(pa.scheduling_date)) = 1 then min(date(pa.scheduling_date))
            else null
        end as schedulingDate,
        case
            when count(distinct date_format(pa.scheduling_date, '%Y-%m')) = 1 then min(date_format(pa.scheduling_date, '%Y-%m'))
            else null
        end as schedulingMonth
        from production_account pa
        left join production_product_main ppm on ppm.id = pa.production_product_main_id
        left join production_operation_task pot on ppm.production_operation_task_id = pot.id
        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 product p_parent on p_parent.id = p.parent_id
        <where>
            <if test="c != null">
                <if test="c.productCategory != null and c.productCategory != ''">
                    and p_parent.product_name like concat('%', #{c.productCategory}, '%')
                </if>
                <if test="c.specificationModel != null and c.specificationModel != ''">
                    and pm.model like concat('%', #{c.specificationModel}, '%')
                </if>
                <if test="c.schedulingUserId != null">
                    and pa.scheduling_user_id = #{c.schedulingUserId}
                </if>
                <if test="c.schedulingUserName != null and c.schedulingUserName != ''">
                    and pa.scheduling_user_name like concat('%', #{c.schedulingUserName}, '%')
                </if>
                <if test="c.process != null and c.process != ''">
                    and pa.technology_operation_name like concat('%', #{c.process}, '%')
                </if>
                <if test="c.entryDate != null">
                    and date(pa.scheduling_date) = #{c.entryDate}
                </if>
                <if test="c.entryDateStart != null">
                    and date(pa.scheduling_date) &gt;= #{c.entryDateStart}
                </if>
                <if test="c.entryDateEnd != null">
                    and date(pa.scheduling_date) &lt;= #{c.entryDateEnd}
                </if>
            </if>
        </where>
        group by pa.scheduling_user_id,
        pa.scheduling_user_name
        order by wages desc,
        pa.scheduling_user_id asc
    </select>
    <select id="selectUserAccount" resultType="com.ruoyi.production.bean.dto.UserAccountDto">
        select ifnull(sum(finished_num), 0) as accountBalance,
               ifnull(sum(work_hours), 0) as account
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -120,4 +120,28 @@
        group by poro.technology_operation_id, poro.operation_name
    </select>
    <select id="getProductWorkOrderFlowCard" resultType="com.ruoyi.production.bean.dto.ProductionOperationTaskDto">
        SELECT pot.*,
               poro.operation_name AS processName,
               pm.model AS model,
               pm.unit AS unit,
               p.product_name AS productName,
               po.nps_no AS productOrderNpsNo,
               ROUND(IFNULL(pot.complete_quantity, 0) / NULLIF(pot.plan_quantity, 0) * 100, 2) AS completionStatus,
               IFNULL(scrapStat.scrapQty, 0) AS scrapQty
        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 p.id = pm.product_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.id = #{id}
    </select>
</mapper>
src/main/resources/mapper/production/ProductionProductMainMapper.xml
@@ -95,17 +95,59 @@
               p.product_name as productName,
               pm.model as productModelName,
               pm.unit,
               poro.operation_name as process,
               pa.technology_operation_name as process,
               ifnull(ppo.quantity, 0) as quantity,
               ifnull(ppo.scrap_qty, 0) as scrapQty
        from production_product_main ppm
               ifnull(ppo.scrap_qty, 0) as scrapQty,
               date(pa.scheduling_date) as schedulingDate,
               pa.scheduling_user_name as schedulingUserName,
               cast(ifnull(pa.work_hours, 0) as decimal(18,4)) as workHours,
               cast(
                   ifnull(pa.work_hours, 0) * ifnull(pa.finished_num, 0) *
                   case
                       when substring_index(pm.model, '*', -1) regexp '^[0-9]+(\\.[0-9]+)?$'
                       then cast(substring_index(pm.model, '*', -1) as decimal(18,4))
                       else 1
                   end
                   as decimal(18,4)
               ) as wages
        from production_account pa
                 left join production_product_main ppm on ppm.id = pa.production_product_main_id
                 left join production_operation_task pot on ppm.production_operation_task_id = pot.id
                 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 product p_parent on p_parent.id = p.parent_id
                 left join production_product_output ppo on ppo.production_product_main_id = ppm.id
        order by ppm.create_time desc
        <where>
            <if test="c != null">
                <if test="c.productCategory != null and c.productCategory != ''">
                    and p_parent.product_name like concat('%', #{c.productCategory}, '%')
                </if>
                <if test="c.specificationModel != null and c.specificationModel != ''">
                    and pm.model like concat('%', #{c.specificationModel}, '%')
                </if>
                <if test="c.schedulingUserId != null">
                    and pa.scheduling_user_id = #{c.schedulingUserId}
                </if>
                <if test="c.schedulingUserName != null and c.schedulingUserName != ''">
                    and pa.scheduling_user_name like concat('%', #{c.schedulingUserName}, '%')
                </if>
                <if test="c.process != null and c.process != ''">
                    and pa.technology_operation_name like concat('%', #{c.process}, '%')
                </if>
                <if test="c.entryDate != null">
                    and date(pa.scheduling_date) = #{c.entryDate}
                </if>
                <if test="c.entryDateStart != null">
                    and date(pa.scheduling_date) &gt;= #{c.entryDateStart}
                </if>
                <if test="c.entryDateEnd != null">
                    and date(pa.scheduling_date) &lt;= #{c.entryDateEnd}
                </if>
            </if>
        </where>
        order by pa.scheduling_date desc, pa.id desc
    </select>
    <select id="listMain" resultType="java.lang.Long">