已添加2个文件
已修改22个文件
480 ■■■■■ 文件已修改
doc/20260209_create_personal_attendance_records.sql 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/ProductController.java 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/ProductModelExportDto.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/IProductModelService.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/controller/InspectionTaskController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/controller/TimingTaskController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductOrderController.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/dto/ProductionProductMainDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductOrderServiceImpl.java 104 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/domain/SysNotice.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/SysNoticeServiceImpl.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/UnipushService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/mapper/QualityUnqualifiedMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityUnqualifiedServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/mapper/PersonalAttendanceRecordsMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/pojo/PersonalAttendanceRecords.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/PersonalAttendanceRecordsServiceImpl.java 79 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/task/PersonalAttendanceRecordsTask.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionProductMainMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/PersonalAttendanceRecordsMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysNoticeMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260209_create_personal_attendance_records.sql
@@ -13,5 +13,6 @@
    tenant_id       bigint not null comment '租户id',
    create_time     datetime null comment '录入时间',
    update_time     datetime null comment '更新时间',
    index idx_staff_on_job_id (staff_on_job_id)
    index idx_staff_on_job_id (staff_on_job_id),
    unique idx_staff_on_job_id_date (staff_on_job_id, date)
);
src/main/java/com/ruoyi/basic/controller/ProductController.java
@@ -5,10 +5,12 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.dto.ProductDto;
import com.ruoyi.basic.dto.ProductModelDto;
import com.ruoyi.basic.dto.ProductModelExportDto;
import com.ruoyi.basic.dto.ProductTreeDto;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.service.IProductModelService;
import com.ruoyi.basic.service.IProductService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
@@ -23,6 +25,7 @@
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@RestController
@@ -124,9 +127,20 @@
    /**
     * å¯¼å…¥äº§å“
     */
    @Log(title = "导入产品",businessType = BusinessType.IMPORT)
    @PostMapping("import")
    public AjaxResult importProduct(MultipartFile file) {
        return AjaxResult.success(productModelService.importProduct(file));
    @PostMapping("/import")
    @Log(title = "导入产品", businessType = BusinessType.IMPORT)
    public AjaxResult importProductModel(@RequestParam("file") MultipartFile file, Integer productId) {
        return productModelService.importProductModel(file, productId);
    }
    /**
     * äº§å“å¯¼å…¥æ¨¡æ¿
     */
    @GetMapping("/export")
    @ApiOperation("产品导入模板")
    @Log(title = "产品导入模板", businessType = BusinessType.EXPORT)
    public void importProduct(HttpServletResponse response) {
        ExcelUtil<ProductModelExportDto> excelUtil = new ExcelUtil<>(ProductModelExportDto.class);
        excelUtil.importTemplateExcel(response, "产品规格导入模板");
    }
}
src/main/java/com/ruoyi/basic/dto/ProductModelExportDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.basic.dto;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import lombok.Data;
/**
 * <br>
 * äº§å“å¯¼å‡ºæ¨¡æ¿
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/02/10 14:39
 */
@Data
public class ProductModelExportDto {
    @Excel(name = "规格型号")
    private String model;
    @Excel(name = "单位")
    private String unit;
}
src/main/java/com/ruoyi/basic/service/IProductModelService.java
@@ -6,6 +6,7 @@
import com.ruoyi.basic.dto.ProductDto;
import com.ruoyi.basic.dto.ProductModelDto;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.framework.web.domain.AjaxResult;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@@ -33,5 +34,5 @@
     */
    IPage<ProductModel> modelListPage(Page page , ProductDto productDto);
    Boolean importProduct(MultipartFile file);
    AjaxResult importProductModel(MultipartFile file, Integer productId);
}
src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java
@@ -12,17 +12,17 @@
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.service.IProductModelService;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -47,13 +47,12 @@
        if (productModelDto.getId() == null) {
            ProductModel productModel = new ProductModel();
            BeanUtils.copyProperties(productModelDto,productModel);
            BeanUtils.copyProperties(productModelDto, productModel);
            return productModelMapper.insert(productModel);
        } else {
            return productModelMapper.updateById(productModelDto);
        }
    }
    @Override
@@ -76,6 +75,7 @@
    /**
     * æ ¹æ®id查询产品规格分页查询
     *
     * @param page
     * @param productDto
     * @return
@@ -88,24 +88,43 @@
    }
    @Override
    public Boolean importProduct(MultipartFile file) {
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult importProductModel(MultipartFile file, Integer productId) {
        if (productId == null) {
            return AjaxResult.error("请先选择产品再导入规格型号");
        }
        Product product = productMapper.selectById(productId);
        if (product == null) {
            return AjaxResult.error("选择的产品不存在");
        }
        try {
            ExcelUtil<ProductModel> productModelExcelUtil = new ExcelUtil<>(ProductModel.class);
            List<ProductModel> productModelList = productModelExcelUtil.importExcel(file.getInputStream());
            Map<String, List<ProductModel>> collect = productModelList.stream().collect(Collectors.groupingBy(ProductModel::getProductName));
            collect.forEach((k,v)->{
                Product product = productMapper.selectOne(new LambdaQueryWrapper<Product>().eq(Product::getProductName, k).last("LIMIT 1"));
                if (product != null) {
                    v.forEach(productModel -> {
                        productModel.setProductId(product.getId());
                    });
                    this.saveOrUpdateBatch(v);
            if (productModelList == null || productModelList.isEmpty()) {
                return AjaxResult.error("导入数据不能为空");
            }
            for (int i = 0; i < productModelList.size(); i++) {
                ProductModel item = productModelList.get(i);
                int rowNum = i + 2;
                if (StringUtils.isEmpty(item.getModel())) {
                    return AjaxResult.error("第 " + rowNum + " è¡Œå¯¼å…¥å¤±è´¥: [规格型号] ä¸èƒ½ä¸ºç©º");
                }
            });
            return true;
        }catch (Exception e) {
            e.printStackTrace();
                if (StringUtils.isEmpty(item.getUnit())) {
                    return AjaxResult.error("第 " + rowNum + " è¡Œå¯¼å…¥å¤±è´¥: [单位] ä¸èƒ½ä¸ºç©º");
                }
                item.setProductId(product.getId());
            }
            saveOrUpdateBatch(productModelList);
            return AjaxResult.success("成功导入 " + productModelList.size() + " æ¡æ•°æ®");
        } catch (Exception e) {
            log.error("导入产品规格异常", e);
            return AjaxResult.error("导入失败");
        }
        return false;
    }
}
src/main/java/com/ruoyi/inspectiontask/controller/InspectionTaskController.java
@@ -22,7 +22,7 @@
 * @date : 2025/9/19 10:52
 */
@RestController
@Api(tags = "巡检任务管理")
@Api(tags = "巡检任务记录")
@RequestMapping("/inspectionTask")
public class InspectionTaskController extends BaseController {
src/main/java/com/ruoyi/inspectiontask/controller/TimingTaskController.java
@@ -23,7 +23,7 @@
 * @date : 2025/9/19 10:53
 */
@RestController
@Api(tags = "定时任务管理")
@Api(tags = "巡检管理")
@RequestMapping("/timingTask")
public class TimingTaskController extends BaseController {
src/main/java/com/ruoyi/production/controller/ProductOrderController.java
@@ -53,29 +53,6 @@
    @PostMapping("/export")
    public void export(HttpServletResponse response, ProductOrderDto productOrderDto) {
        List<ProductOrderDto> list = productOrderService.pageProductOrder(new Page<>(1, -1), productOrderDto).getRecords();
        if (list != null && !list.isEmpty()) {
            list.forEach(item -> {
                // åˆ¤ç©º
                if (item.getQuantity() == null || item.getCompleteQuantity() == null) {
                    item.setCompletionStatus(BigDecimal.ZERO);
                    return;
                }
                // åˆ¤é›¶
                if (item.getQuantity().compareTo(BigDecimal.ZERO) == 0) {
                    item.setCompletionStatus(BigDecimal.ZERO);
                    return;
                }
                BigDecimal progress = item.getCompleteQuantity()
                        .divide(item.getQuantity(), 4, BigDecimal.ROUND_HALF_UP)
                        .multiply(new BigDecimal(100))
                        .setScale(2, BigDecimal.ROUND_HALF_UP);
                item.setCompletionStatus(progress);
            });
        }
        ExcelUtil<ProductOrderDto> util = new ExcelUtil<>(ProductOrderDto.class);
        util.exportExcel(response, list, "生产订单数据");
    }
src/main/java/com/ruoyi/production/dto/ProductionProductMainDto.java
@@ -55,6 +55,8 @@
    private LocalDate schedulingDate;
    private String schedulingUserName;
    private String customerName;
    //工序
    @Excel(name = "工序")
    private String process;
    private BigDecimal workHours;
    private BigDecimal wages;
src/main/java/com/ruoyi/production/service/impl/ProductOrderServiceImpl.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
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.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
@@ -145,92 +146,27 @@
    @Override
    public Boolean delete(Long[] ids) {
        //批量查询productOrder
        List<ProductOrder> productOrders = productOrderMapper.selectList(
                new LambdaQueryWrapper<ProductOrder>()
                        .in(ProductOrder::getId, ids)
        );
        if (!org.springframework.util.CollectionUtils.isEmpty(productOrders)) {
            // æ‰¹é‡æŸ¥è¯¢processRouteItems
            List<ProductProcessRouteItem> allRouteItems = productProcessRouteItemMapper.selectList(
                    new LambdaQueryWrapper<ProductProcessRouteItem>()
                            .in(ProductProcessRouteItem::getProductOrderId, ids)
            );
            if (!com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(allRouteItems)) {
                // èŽ·å–è¦åˆ é™¤çš„å·¥åºé¡¹ID
                List<Long> routeItemIds = allRouteItems.stream()
                        .map(ProductProcessRouteItem::getId)
                        .collect(Collectors.toList());
                // æŸ¥è¯¢å…³è”的工单ID
                List<ProductWorkOrder> workOrders = productWorkOrderMapper.selectList(
                        new LambdaQueryWrapper<ProductWorkOrder>()
                                .in(ProductWorkOrder::getProductProcessRouteItemId, routeItemIds)
                );
                if (!com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(workOrders)) {
                    List<Long> workOrderIds = workOrders.stream()
                            .map(ProductWorkOrder::getId)
                            .collect(Collectors.toList());
                    // æŸ¥è¯¢å…³è”的生产主表ID
                    List<ProductionProductMain> productMains = productionProductMainMapper.selectList(
                            new LambdaQueryWrapper<ProductionProductMain>()
                                    .in(ProductionProductMain::getWorkOrderId, workOrderIds)
                    );
                    List<Long> productMainIds = productMains.stream()
                            .map(ProductionProductMain::getId)
                            .collect(Collectors.toList());
                    // åˆ é™¤äº§å‡ºè¡¨ã€æŠ•入表数据
                    if (!com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(productMainIds)) {
                        productionProductOutputMapper.deleteByProductMainIds(productMainIds);
                        productionProductInputMapper.deleteByProductMainIds(productMainIds);
                        List<QualityInspect> qualityInspects = qualityInspectMapper.selectList(
                                new LambdaQueryWrapper<QualityInspect>()
                                        .in(QualityInspect::getProductMainId, productMainIds)
                        );
                        //删除出库记录
                        for (Long productMainId : productMainIds) {
                            //删除生产出库记录
                            stockUtils.deleteStockOutRecord(productMainId, StockOutQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_OUT.getCode());
                            //删除报废的入库记录
                            stockUtils.deleteStockInRecord(productMainId, StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode());
                        }
                        qualityInspects.forEach(qualityInspect -> {
                            //inspectState=1 å·²æäº¤ ä¸èƒ½åˆ é™¤
                            if (qualityInspect.getInspectState() == 1) {
                                throw new RuntimeException("已提交的检验单不能删除");
                            }
                        });
                        qualityInspectMapper.deleteByProductMainIds(productMainIds);
                        salesLedgerProductionAccountingMapper.delete(new LambdaQueryWrapper<SalesLedgerProductionAccounting>()
                                .in(SalesLedgerProductionAccounting::getProductMainId, productMainIds));
                    }
                    // åˆ é™¤ç”Ÿäº§ä¸»è¡¨æ•°æ®
                    productionProductMainMapper.deleteByWorkOrderIds(workOrderIds);
                    // åˆ é™¤å·¥å•数据
                    productWorkOrderMapper.delete(new LambdaQueryWrapper<ProductWorkOrder>()
                            .in(ProductWorkOrder::getProductProcessRouteItemId, routeItemIds));
                }
        //如果已经开始生产,不能删除
        //查询生产订单下的工单
        List<ProductWorkOrder> productWorkOrders = productWorkOrderMapper.selectList(Wrappers.<ProductWorkOrder>lambdaQuery().in(ProductWorkOrder::getProductOrderId, ids));
        if (productWorkOrders.size()>0){
            //判断是否有报工数据
            List<ProductionProductMain> productionProductMains = productionProductMainMapper.selectList(Wrappers.<ProductionProductMain>lambdaQuery()
                    .in(ProductionProductMain::getWorkOrderId, productWorkOrders.stream().map(ProductWorkOrder::getId).collect(Collectors.toList())));
            if (productionProductMains.size()>0){
                throw new RuntimeException("生产订单已经开始生产,不能删除");
            }
            // æ‰¹é‡åˆ é™¤processRouteItem
            productProcessRouteItemMapper.delete(new LambdaQueryWrapper<ProductProcessRouteItem>()
                    .in(ProductProcessRouteItem::getProductOrderId, ids));
            // æ‰¹é‡åˆ é™¤productProcessRoute
            productProcessRouteMapper.delete(new LambdaQueryWrapper<ProductProcessRoute>()
                    .in(ProductProcessRoute::getProductOrderId, ids));
            // æ‰¹é‡åˆ é™¤productOrder
            productOrderMapper.delete(new LambdaQueryWrapper<ProductOrder>()
                    .in(ProductOrder::getId, ids));
            //删除工单
            productWorkOrderMapper.delete(Wrappers.<ProductWorkOrder>lambdaQuery().in(ProductWorkOrder::getProductOrderId, ids));
        }
        //删除工艺路线
        productProcessRouteItemMapper.delete(new LambdaQueryWrapper<ProductProcessRouteItem>()
                .in(ProductProcessRouteItem::getProductOrderId, ids));
        productProcessRouteMapper.delete(new LambdaQueryWrapper<ProductProcessRoute>()
                .in(ProductProcessRoute::getProductOrderId, ids));
        //删除生产订单
        productOrderMapper.delete(new LambdaQueryWrapper<ProductOrder>()
                .in(ProductOrder::getId, ids));
        return true;
    }
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -260,10 +260,12 @@
    public Boolean removeProductMain(Long id) {
        //判断该条报工是否不合格处理,如果不合格处理了,则不允许删除
        List<QualityInspect> qualityInspects = qualityInspectMapper.selectList(Wrappers.<QualityInspect>lambdaQuery().eq(QualityInspect::getProductMainId, id));
        List<QualityUnqualified> qualityUnqualifieds = qualityUnqualifiedMapper.selectList(Wrappers.<QualityUnqualified>lambdaQuery()
                .in(QualityUnqualified::getInspectId, qualityInspects.stream().map(QualityInspect::getId).collect(Collectors.toList())));
        if (qualityUnqualifieds.size() > 0 && qualityUnqualifieds.get(0).getInspectState()==1) {
            throw new ServiceException("该条报工已经不合格处理了,不允许删除");
        if (qualityInspects.size() > 0){
            List<QualityUnqualified> qualityUnqualifieds = qualityUnqualifiedMapper.selectList(Wrappers.<QualityUnqualified>lambdaQuery()
                    .in(QualityUnqualified::getInspectId, qualityInspects.stream().map(QualityInspect::getId).collect(Collectors.toList())));
            if (qualityUnqualifieds.size() > 0 && qualityUnqualifieds.get(0).getInspectState()==1) {
                throw new ServiceException("该条报工已经不合格处理了,不允许删除");
            }
        }
        ProductionProductMain productionProductMain = productionProductMainMapper.selectById(id);
        //该报工对应的工艺路线详情
src/main/java/com/ruoyi/project/system/domain/SysNotice.java
@@ -48,6 +48,9 @@
    /** è·³è½¬è·¯å¾„ */
    private String jumpPath;
    /** APP跳转路径 */
    private String appJumpPath;
    /** åˆ›å»ºè€… */
    @TableField(fill = FieldFill.INSERT)
    private String createBy;
src/main/java/com/ruoyi/project/system/service/impl/SysNoticeServiceImpl.java
@@ -144,10 +144,12 @@
    }
    @Override
    public void simpleNoticeByUser(String title, String message,  List<Long> consigneeId, String jumpPath) {
    public void simpleNoticeByUser(String title, String message, List<Long> consigneeId, String jumpPath) {
        Long userId = SecurityUtils.getLoginUser().getUserId();
        Long tenantId = SecurityUtils.getLoginUser().getTenantId();
        List<SysNotice> sysNotices = consigneeId.stream().map(it -> convertSysNotice(title, message, it,tenantId, jumpPath, userId)).collect(Collectors.toList());
        List<SysNotice> sysNotices = consigneeId.stream()
                .map(it -> convertSysNotice(title, message, it, tenantId, jumpPath, unipushService.convertWebPathToAppPath(jumpPath), userId))
                .collect(Collectors.toList());
        sysNoticeService.saveBatch(sysNotices);
        try {
            unipushService.sendClientMessage(sysNotices);
@@ -200,6 +202,7 @@
                        it.getUserId(),
                        it.getTenantId(),
                        jumpPath,
                        unipushService.convertWebPathToAppPath(jumpPath),
                        userId
                ))
                .collect(Collectors.toList());
@@ -213,7 +216,7 @@
    }
    private SysNotice convertSysNotice(String title,String message,Long consigneeId, Long tenantId,String jumpPath,Long currentUserId) {
    private SysNotice convertSysNotice(String title,String message,Long consigneeId, Long tenantId,String jumpPath,String appJumpPath,Long currentUserId) {
        SysNotice sysNotice = new SysNotice();
        sysNotice.setNoticeType("1");
        sysNotice.setNoticeTitle(title);//标题
@@ -222,6 +225,7 @@
        sysNotice.setConsigneeId(consigneeId);
        sysNotice.setSenderId(currentUserId);
        sysNotice.setJumpPath(jumpPath);
        sysNotice.setAppJumpPath(appJumpPath);
        sysNotice.setTenantId(tenantId);
        return sysNotice;
    }
src/main/java/com/ruoyi/project/system/service/impl/UnipushService.java
@@ -95,7 +95,7 @@
    /**
     * å°† Web ç«¯åˆ†å±‚全路径转换为 App ç«¯ç»„件路由
     */
    private String convertWebPathToAppPath(String webPath) {
    public String convertWebPathToAppPath(String webPath) {
        if (StringUtils.isEmpty(webPath)) {
            return DEFAULT_APP_PAGE;
        }
src/main/java/com/ruoyi/quality/mapper/QualityUnqualifiedMapper.java
@@ -19,4 +19,7 @@
    List<QualityUnqualified> qualityUnqualifiedExport(@Param("qualityUnqualified") QualityUnqualified qualityUnqualified);
    QualityUnqualified getUnqualified(@Param("id") Integer id);
    //手动新增不合格的时候,根据产品名称和规格型号查出对应的规格型号id
    Long getModelId(@Param("productName") String productName, @Param("model") String model);
}
src/main/java/com/ruoyi/quality/service/impl/QualityUnqualifiedServiceImpl.java
@@ -137,14 +137,16 @@
                    break;
            }
        } else {
            //查询对应的规格型号id
            Long modelId = qualityUnqualifiedMapper.getModelId(qualityUnqualified.getProductName(), qualityUnqualified.getModel());
            switch (qualityUnqualified.getDealResult()) {
                case "报废":
                    //调用不合格库存接口 å…¥ä¸åˆæ ¼åº“
                    stockUtils.addUnStock(Long.valueOf(unqualified.getModel()), unqualified.getQuantity(), StockInUnQualifiedRecordTypeEnum.DEFECTIVE_SCRAP.getCode(), unqualified.getId());
                    stockUtils.addUnStock(modelId, unqualified.getQuantity(), StockInUnQualifiedRecordTypeEnum.DEFECTIVE_SCRAP.getCode(), unqualified.getId());
                    break;
                case "让步放行":
                    //调用提交合格的接口
                    stockUtils.addStock(Long.valueOf(unqualified.getModel()), unqualified.getQuantity(), StockInQualifiedRecordTypeEnum.DEFECTIVE_PASS.getCode(), unqualified.getId());
                    stockUtils.addStock(modelId, unqualified.getQuantity(), StockInQualifiedRecordTypeEnum.DEFECTIVE_PASS.getCode(), unqualified.getId());
                    break;
                default:
                    break;
src/main/java/com/ruoyi/staff/mapper/PersonalAttendanceRecordsMapper.java
@@ -10,6 +10,10 @@
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
 * <p>
 *  Mapper æŽ¥å£
@@ -21,4 +25,8 @@
@Mapper
public interface PersonalAttendanceRecordsMapper extends BaseMapper<PersonalAttendanceRecords> {
    IPage<PersonalAttendanceRecordsDto> listPage(Page page, @Param("params") PersonalAttendanceRecordsDto personalAttendanceRecordsDto);
    List<StaffOnJob> selectStaffWithoutAttendanceRecordBeforeTime(@Param("date") LocalDate date, @Param("entryDeadline") LocalDateTime entryDeadline);
    boolean existsAttendanceRecord(@Param("staffOnJobId") Long staffOnJobId, @Param("date") LocalDate date);
}
src/main/java/com/ruoyi/staff/pojo/PersonalAttendanceRecords.java
@@ -62,9 +62,9 @@
    @Excel(name = "工时(小时)", sort = 7)
    private BigDecimal workHours;
    @ApiModelProperty("状态 0正常 1迟到 2早退")
    @Excel(name = "状态", sort = 8,readConverterExp = "0=正常,1=迟到,2=早退")
    private Byte status;
    @ApiModelProperty("状态 0正常 1迟到 2早退 3迟到早退 4缺勤")
    @Excel(name = "状态", sort = 8,readConverterExp = "0=正常,1=迟到,2=早退,3=迟到、早退,4=缺勤")
    private Integer status;
    @ApiModelProperty("备注")
    @Excel(name = "备注", sort = 9)
src/main/java/com/ruoyi/staff/service/impl/PersonalAttendanceRecordsServiceImpl.java
@@ -18,6 +18,7 @@
import com.ruoyi.staff.pojo.StaffOnJob;
import com.ruoyi.staff.service.PersonalAttendanceRecordsService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.staff.task.PersonalAttendanceRecordsTask;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -48,6 +49,9 @@
    private StaffOnJobMapper staffOnJobMapper;
    @Autowired
    private PersonalAttendanceRecordsTask personalAttendanceRecordsTask;
    @Autowired
    private ISysDictDataService dictDataService;
    @Autowired
@@ -67,26 +71,43 @@
            throw new BaseException("当前用户没有对应的员工信息");
        }
        // å½“前时间
        LocalDateTime currentDateTime = LocalDateTime.now();
        // å¦‚果打卡时间超过考勤下班时间不能打卡
        // èŽ·å–è€ƒå‹¤ä¸‹ç­æ—¶é—´ç‚¹
        String[] timeConfigs = getAttendanceTimeConfig();
        String timeConfig = timeConfigs[1];
        String[] timeParts = timeConfig.split(":");
        int standardHour = Integer.parseInt(timeParts[0]);
        int standardMinute = Integer.parseInt(timeParts[1]);
        // å½“前时间
        int actualHour = currentDateTime.getHour();
        int actualMinute = currentDateTime.getMinute();
        // åˆ¤æ–­æ‰“卡时间是否晚于当前时间
        if (actualHour > standardHour || (actualHour == standardHour && actualMinute > standardMinute)) {
            throw new BaseException("打卡时间不能晚于下班时间");
        }
        // æ ¹æ®å‘˜å·¥ID和当前日期查询打卡记录
        QueryWrapper<PersonalAttendanceRecords> attendanceQueryWrapper = new QueryWrapper<>();
        attendanceQueryWrapper.eq("staff_on_job_id", staffOnJob.getId())
                .eq("date", currentDate);
        PersonalAttendanceRecords attendanceRecord = personalAttendanceRecordsMapper.selectOne(attendanceQueryWrapper);
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
        // æ ¹æ®å­—典设置的考勤时间判断迟到早退
        if (attendanceRecord == null) {
            // ä¸å­˜åœ¨æ‰“卡记录,创建新记录
            personalAttendanceRecords.setStaffOnJobId(staffOnJob.getId());
            personalAttendanceRecords.setDate(currentDate);
            personalAttendanceRecords.setWorkStartAt(LocalDateTime.now());
            personalAttendanceRecords.setStatus(determineAttendanceStatus(personalAttendanceRecords.getWorkStartAt(), true));
            personalAttendanceRecords.setWorkStartAt(currentDateTime);
            personalAttendanceRecords.setStatus(determineAttendanceStatus(personalAttendanceRecords, true));
            personalAttendanceRecords.setRemark(personalAttendanceRecords.getRemark());
            personalAttendanceRecords.setTenantId(staffOnJob.getTenantId());
            return personalAttendanceRecordsMapper.insert(personalAttendanceRecords);
        } else {
            if (attendanceRecord.getWorkEndAt() == null) {
                // æ›´æ–°å·¥ä½œç»“束时间和工作时长
                attendanceRecord.setWorkEndAt(LocalDateTime.now());
                attendanceRecord.setWorkEndAt(currentDateTime);
                // è®¡ç®—工作时长(精确到分钟,保留2位小数)
                LocalDateTime startTime = attendanceRecord.getWorkStartAt();
                LocalDateTime endTime = attendanceRecord.getWorkEndAt();
@@ -96,7 +117,7 @@
                        .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
                attendanceRecord.setWorkHours(workHours);
                // æ›´æ–°è€ƒå‹¤çŠ¶æ€
                attendanceRecord.setStatus(determineAttendanceStatus(attendanceRecord.getWorkEndAt(), false));
                attendanceRecord.setStatus(determineAttendanceStatus(attendanceRecord, false));
                return personalAttendanceRecordsMapper.updateById(attendanceRecord);
            } else {
                throw new BaseException("您已经打过卡了,无需重复打卡!!!");
@@ -104,27 +125,36 @@
        }
    }
    // èŽ·å–è€ƒå‹¤æ—¶é—´é…ç½®
    private String[] getAttendanceTimeConfig() {
        String[] timeConfigs = new String[2];
        try {
            String dictType = "sys_work_time";
            // èŽ·å–ä¸Šç­æ—¶é—´é…ç½®ï¼Œé»˜è®¤ä¸º09:00
            String startTimeConfig = dictDataService.selectDictLabel(dictType, "start_at");
            timeConfigs[0] = (startTimeConfig == null || startTimeConfig.trim().isEmpty()) ? "09:00" : startTimeConfig;
            // èŽ·å–ä¸‹ç­æ—¶é—´é…ç½®ï¼Œé»˜è®¤ä¸º18:00
            String endTimeConfig = dictDataService.selectDictLabel(dictType, "end_at");
            timeConfigs[1] = (endTimeConfig == null || endTimeConfig.trim().isEmpty()) ? "18:00" : endTimeConfig;
            return timeConfigs;
        } catch (Exception e) {
            timeConfigs[0] = "09:00"; // é»˜è®¤ä¸Šç­æ—¶é—´
            timeConfigs[1] = "18:00"; // é»˜è®¤ä¸‹ç­æ—¶é—´
            return timeConfigs;
        }
    }
    // æ ¹æ®å®žé™…时间和是否上班时间判断考勤状态
    // 0 æ­£å¸¸ 1 è¿Ÿåˆ° 2 æ—©é€€
    private byte determineAttendanceStatus(LocalDateTime actualTime, boolean isStart) {
    // 0 æ­£å¸¸ 1 è¿Ÿåˆ° 2 æ—©é€€ 3 è¿Ÿåˆ°æ—©é€€ 4 ç¼ºå‹¤
    private Integer determineAttendanceStatus(PersonalAttendanceRecords attendanceRecord, boolean isStart) {
        LocalDateTime actualTime = isStart ? attendanceRecord.getWorkStartAt() : attendanceRecord.getWorkEndAt();
        try {
            // èŽ·å–è€ƒå‹¤æ—¶é—´é…ç½®
            String dictType = "sys_work_time"; // è€ƒå‹¤æ—¶é—´å­—典类型
            String timeConfig;
            if (isStart) {
                // ä¸Šç­æ—¶é—´é…ç½®ï¼Œé»˜è®¤ä¸º09:00
                timeConfig = dictDataService.selectDictLabel(dictType, "work_start_time");
                if (timeConfig == null || timeConfig.trim().isEmpty()) {
                    timeConfig = "09:00";
                }
            } else {
                // ä¸‹ç­æ—¶é—´é…ç½®ï¼Œé»˜è®¤ä¸º18:00
                timeConfig = dictDataService.selectDictLabel(dictType, "work_end_time");
                if (timeConfig == null || timeConfig.trim().isEmpty()) {
                    timeConfig = "18:00";
                }
            }
            String[] timeConfigs = getAttendanceTimeConfig();
            String timeConfig = isStart ? timeConfigs[0] : timeConfigs[1];
            // è§£æžæ ‡å‡†æ—¶é—´
            String[] timeParts = timeConfig.split(":");
@@ -144,6 +174,9 @@
            } else {
                // ä¸‹ç­æ‰“卡:早于标准时间算早退
                if (actualHour < standardHour || (actualHour == standardHour && actualMinute < standardMinute)) {
                    if (attendanceRecord.getStatus() == 1) {
                        return 3; // è¿Ÿåˆ°æ—©é€€
                    }
                    return 2; // æ—©é€€
                }
            }
src/main/java/com/ruoyi/staff/task/PersonalAttendanceRecordsTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
package com.ruoyi.staff.task;
import com.ruoyi.staff.pojo.PersonalAttendanceRecords;
import com.ruoyi.staff.pojo.StaffOnJob;
import com.ruoyi.staff.service.PersonalAttendanceRecordsService;
import com.ruoyi.staff.mapper.PersonalAttendanceRecordsMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
 * ä¸ªäººè€ƒå‹¤è®°å½•定时任务
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-02-09
 */
@Slf4j
@Component
public class PersonalAttendanceRecordsTask {
    @Autowired
    private PersonalAttendanceRecordsMapper personalAttendanceRecordsMapper;
    @Autowired
    private PersonalAttendanceRecordsService personalAttendanceRecordsService;
    /**
     * æ¯å¤©å‡Œæ™¨ç”Ÿæˆæ˜¨æ—¥çš„缺勤记录
     * å®šæ—¶ä»»åŠ¡ï¼šæ¯å¤©å‡Œæ™¨1点执行
     * æŽ’除今天刚入职的员工
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void generateAbsenceRecords() {
        try {
            // èŽ·å–æ˜¨æ—¥æ—¥æœŸ
            LocalDate yesterday = LocalDate.now().minusDays(1);
            // ç›´æŽ¥æŸ¥è¯¢æ˜¨å¤©æ²¡æœ‰è€ƒå‹¤è®°å½•的在职员工(排除今天刚入职的)
            LocalDateTime todayStart = LocalDate.now().atStartOfDay();
            List<StaffOnJob> staffWithoutAttendance = personalAttendanceRecordsMapper.selectStaffWithoutAttendanceRecordBeforeTime(yesterday, todayStart);
            // éåŽ†æ²¡æœ‰è€ƒå‹¤è®°å½•çš„å‘˜å·¥ï¼Œç”Ÿæˆç¼ºå‹¤è®°å½•
            for (StaffOnJob staff : staffWithoutAttendance) {
                try {
                    boolean exists = personalAttendanceRecordsMapper.existsAttendanceRecord(staff.getId(), yesterday);
                    if (exists) {
                        continue;
                    }
                    PersonalAttendanceRecords absenceRecord = new PersonalAttendanceRecords();
                    absenceRecord.setStaffOnJobId(staff.getId());
                    absenceRecord.setDate(yesterday);
                    absenceRecord.setStatus(4); // è®¾ç½®çŠ¶æ€ä¸ºç¼ºå‹¤
                    absenceRecord.setRemark("系统自动生成-缺勤");
                    personalAttendanceRecordsService.save(absenceRecord);
                } catch (Exception e) {
                    log.error("为员工{}生成缺勤记录失败:{}", staff.getStaffName(), e.getMessage(), e);
                }
            }
            log.info("昨日缺勤记录生成完成");
        } catch (Exception e) {
            log.error("生成昨日缺勤记录任务执行失败:{}", e.getMessage(), e);
        }
    }
}
src/main/resources/mapper/production/ProductionProductMainMapper.xml
@@ -18,6 +18,7 @@
        pwo.status as workOrderStatus,
        u.nick_name as nickName,
        p.product_name as productName,
        pp.name as process,
        pm.model as productModelName,
        ppo.quantity,
        ppo.scrap_qty,
@@ -26,6 +27,8 @@
        from
        production_product_main ppm
        left join product_work_order pwo on pwo.id = ppm.work_order_id
        left join product_process_route_item ppri on ppri.id = pwo.product_process_route_item_id
        left join product_process pp on pp.id = ppri.process_id
        left join product_order po on po.id = pwo.product_order_id
        left join production_product_output ppo on ppm.id = ppo.product_main_id
        left join product_model pm on pm.id = ppo.product_model_id
src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml
@@ -89,4 +89,11 @@
            1=1
        and qu.id = #{id}
    </select>
    <select id="getModelId" resultType="java.lang.Long">
        select pm.id
        from product_model pm
        left join product p on pm.product_id=p.id
        where pm.model=#{model}
          and  p.product_name=#{productName}
    </select>
</mapper>
src/main/resources/mapper/staff/PersonalAttendanceRecordsMapper.xml
@@ -40,4 +40,28 @@
            and personal_attendance_records.date &lt; DATE_ADD(DATE(#{params.date}), INTERVAL 1 DAY)
        </if>
    </select>
    <!-- æŸ¥è¯¢æŒ‡å®šæ—¥æœŸæ²¡æœ‰è€ƒå‹¤è®°å½•的在职员工(在指定时间之前入职的) -->
    <select id="selectStaffWithoutAttendanceRecordBeforeTime" resultType="com.ruoyi.staff.pojo.StaffOnJob">
        SELECT soj.*
        FROM staff_on_job soj
        WHERE soj.staff_state = 1
        AND soj.create_time &lt; #{entryDeadline}
        AND NOT EXISTS (
        SELECT 1
        FROM personal_attendance_records par
        WHERE par.staff_on_job_id = soj.id
        AND par.date = #{date}
        )
    </select>
    <!-- æ£€æŸ¥æŒ‡å®šå‘˜å·¥åœ¨æŒ‡å®šæ—¥æœŸæ˜¯å¦å·²å­˜åœ¨è€ƒå‹¤è®°å½• -->
    <select id="existsAttendanceRecord" resultType="boolean">
        SELECT EXISTS (
        SELECT 1
        FROM personal_attendance_records
        WHERE staff_on_job_id = #{staffOnJobId}
        AND date = #{date}
        )
    </select>
</mapper>
src/main/resources/mapper/system/SysNoticeMapper.xml
@@ -31,6 +31,7 @@
               sender_id,
               consignee_id,
               jump_path,
               app_jump_path,
               tenant_id
        from sys_notice
    </sql>
@@ -75,6 +76,7 @@
        <if test="senderId != null and senderId != ''">sender_id,</if>
        <if test="consigneeId != null and consigneeId != ''">consignee_id,</if>
        <if test="jumpPath != null and jumpPath != ''">jump_path,</if>
        <if test="appJumpPath != null and appJumpPath != ''">app_jump_path,</if>
        <if test="createBy != null and createBy != ''">create_by,</if>
        <if test="tenantId != null and tenantId != ''">tenant_id,</if>
             create_time
@@ -87,6 +89,7 @@
        <if test="senderId != null and senderId != ''">#{senderId},</if>
        <if test="consigneeId != null and consigneeId != ''">#{consigneeId},</if>
        <if test="jumpPath != null and jumpPath != ''">#{jumpPath},</if>
        <if test="appJumpPath != null and appJumpPath != ''">#{appJumpPath},</if>
        <if test="pathParms != null and pathParms != ''">#{queryParms},</if>
        <if test="createBy != null and createBy != ''">#{createBy},</if>
        <if test="tenantId != null and tenantId != ''">#{tenantId},</if>