已添加12个文件
已修改10个文件
873 ■■■■■ 文件已修改
doc/宁夏-中盛建材.sql 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/framework/config/AliDingConfig.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductMaterialController.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/enums/MaterialConfigTypeEnum.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductMaterialConfigMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductMaterialMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductMaterial.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductMaterialConfig.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductOrder.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductMaterialConfigService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductMaterialService.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductMaterialConfigServiceImpl.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductMaterialServiceImpl.java 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductOrderServiceImpl.java 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/task/ProductMaterialTask.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/productionPlan/dto/ProductionPlanDto.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/productionPlan/pojo/ProductOrderPlan.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/productionPlan/pojo/ProductionPlan.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/productionPlan/service/impl/ProductionPlanServiceImpl.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-zsjc.yml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductMaterialMapper.xml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/productionPlan/ProductionPlanMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/ÄþÏÄ-ÖÐÊ¢½¨²Ä.sql
@@ -92,7 +92,7 @@
    add assigned_quantity DECIMAL(10, 4) default 0 not null COMMENT '下发数量';
alter table product_order
    add plan_complete_time datetime(0) NULL DEFAULT NULL COMMENT '计划完成时间';
    add plan_complete_time date NULL DEFAULT NULL COMMENT '计划完成时间';
# ç”Ÿäº§è®¢å•与生产计划关联表
drop table if exists product_order_plan;
@@ -103,7 +103,50 @@
    production_plan_id  bigint not null default 0 comment '生产计划id',
    create_time              datetime null comment '录入时间',
    update_time              datetime null comment '更新时间',
    assigned_quantity DECIMAL(10, 4) default 0 not null comment '下发数量',
    index idx_product_order_id (product_order_id),
    index idx_production_plan_id (production_plan_id),
    unique idx_product_order_production_plan (product_order_id, production_plan_id)
);
);
CREATE TABLE `product_material`
(
    `id`                    INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    `tenant_id`             BIGINT       DEFAULT NULL COMMENT '租户ID',
    `material_type_id`      INT          DEFAULT NULL COMMENT '关联物料类型ID',
    `inventory_category_id` INT          DEFAULT NULL COMMENT '关联存货类别ID',
    `identifier_code`       VARCHAR(100) DEFAULT NULL COMMENT '标识编码',
    `material_code`         VARCHAR(100) DEFAULT NULL COMMENT '物料代码',
    `product_name`          VARCHAR(255) DEFAULT NULL COMMENT '产品名称',
    `material_name`         VARCHAR(255) DEFAULT NULL COMMENT '物料品名',
    `specification`         VARCHAR(255) DEFAULT NULL COMMENT '规格型号',
    `base_unit`             VARCHAR(50)  DEFAULT NULL COMMENT '基本单位',
    `material_attribute`    VARCHAR(100) DEFAULT NULL COMMENT '物料属性',
    `finished_product_name` VARCHAR(100) DEFAULT NULL COMMENT '成品品名',
    `originator_name`       VARCHAR(100) DEFAULT NULL COMMENT '提交人姓名',
    `originator_org`        VARCHAR(255) DEFAULT '宁夏中创绿能实业集团有限公司',
    `remark`                TEXT COMMENT '备注',
    `create_time`           DATETIME     DEFAULT CURRENT_TIMESTAMP,
    `update_time`           DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX `idx_type_id` (`material_type_id`),
    INDEX `idx_cat_id` (`inventory_category_id`)
) ENGINE = INNODB
  DEFAULT CHARSET = utf8mb4 COMMENT = '物料信息表';
ALTER TABLE product_material
    ADD COLUMN form_instance_id   VARCHAR(100) DEFAULT NULL COMMENT '宜搭表单实例ID',
    ADD COLUMN form_modified_time DATETIME     DEFAULT NULL COMMENT '宜搭修改时间';
CREATE TABLE `product_material_config`
(
    `id`          int          NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `config_type` varchar(50)  NOT NULL COMMENT '区分类型: MATERIAL_TYPE æˆ– INVENTORY_CAT',
    `config_name` varchar(100) NOT NULL COMMENT '显示的名称'
) ENGINE = InnoDB COMMENT ='物料信息表配置表';
ALTER TABLE `production_plan`
    ADD COLUMN `product_material_id` int DEFAULT NULL COMMENT '关联物料信息表ID' AFTER `material_code`;
-- å»ºè®®é¡ºä¾¿åŠ ä¸Šç´¢å¼•ï¼Œæå‡å…³è”æŸ¥è¯¢é€Ÿåº¦
ALTER TABLE `production_plan`
    ADD INDEX `idx_product_material_id` (`product_material_id`);
src/main/java/com/ruoyi/framework/config/AliDingConfig.java
@@ -42,9 +42,15 @@
    private String appType;
    /**
     * å®œæ­è¡¨å•ID
     * é”€å”®ç”Ÿäº§éœ€æ±‚-宜搭表单ID
     */
    private String formUuid;
    private String producePlanFormUuid;
    /**
     * ç‰©æ–™ç¼–码-宜搭表单ID
     */
    private String materialCodeFormUuid;
    /**
     * å®œæ­åº”用密钥
src/main/java/com/ruoyi/production/controller/ProductMaterialController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package com.ruoyi.production.controller;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.production.service.ProductMaterialService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <br>
 * äº§å“ç‰©æ–™ä¿¡æ¯æŽ§åˆ¶å±‚
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/11 16:38
 */
@RestController
@RequestMapping("/productMaterial")
public class ProductMaterialController {
    @Autowired
    private ProductMaterialService productMaterialService;
    @GetMapping("/loadData")
    @ApiOperation("拉取物料编码数据")
    @Log(title = "拉取物料编码数据", businessType = BusinessType.INSERT)
    public AjaxResult loadProductMaterialData() {
        productMaterialService.loadProductMaterialData();
        return AjaxResult.success();
    }
}
src/main/java/com/ruoyi/production/enums/MaterialConfigTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.ruoyi.production.enums;
/**
 * <br>
 * ç‰©æ–™é…ç½®ç±»åž‹æžšä¸¾
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/11 17:24
 */
public enum MaterialConfigTypeEnum {
    /**
     * ç‰©æ–™ç±»åž‹
     * å¯¹åº”数据库 config_type = MATERIAL_TYPE
     */
    MATERIAL_TYPE,
    /**
     * å­˜è´§ç±»åˆ«
     * å¯¹åº”数据库 config_type = INVENTORY_CAT
     */
    INVENTORY_CAT
}
src/main/java/com/ruoyi/production/mapper/ProductMaterialConfigMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.production.pojo.ProductMaterialConfig;
/**
 * <br>
 * ç‰©æ–™ä¿¡æ¯è¡¨é…ç½®Mapper
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/11 16:58
 */
public interface ProductMaterialConfigMapper extends BaseMapper<ProductMaterialConfig> {
}
src/main/java/com/ruoyi/production/mapper/ProductMaterialMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.production.pojo.ProductMaterial;
/**
 * <br>
 * äº§å“ç‰©æ–™ä¿¡æ¯Mapper
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/11 16:35
 */
public interface ProductMaterialMapper extends BaseMapper<ProductMaterial> {
}
src/main/java/com/ruoyi/production/pojo/ProductMaterial.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
package com.ruoyi.production.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * <br>
 * äº§å“ç‰©æ–™ä¿¡æ¯è¡¨
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/11 16:31
 */
@Data
@TableName("product_material")
@ApiModel(value = "ProductMaterial", description = "产品物料信息表")
public class ProductMaterial {
    @TableId(type = IdType.AUTO)
    @ApiModelProperty("主键ID")
    private Integer id;
    @ApiModelProperty("租户ID")
    private Long tenantId;
    @ApiModelProperty("物料类型ID")
    private Integer materialTypeId;
    @ApiModelProperty("存货类别ID")
    private Integer inventoryCategoryId;
    @ApiModelProperty("标识编码")
    private String identifierCode;
    @ApiModelProperty("物料代码")
    private String materialCode;
    @ApiModelProperty("物料品名")
    private String materialName;
    @ApiModelProperty("规格型号")
    private String specification;
    @ApiModelProperty("基本单位")
    private String baseUnit;
    @ApiModelProperty("物料属性")
    private String materialAttribute;
    @ApiModelProperty("成品品名")
    private String finishedProductName;
    @ApiModelProperty("提交人姓名")
    private String originatorName;
    @ApiModelProperty("提交人组织")
    private String originatorOrg;
    @ApiModelProperty("备注")
    private String remark;
    @ApiModelProperty("创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @ApiModelProperty("修改时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @ApiModelProperty("宜搭表单实例ID")
    private String formInstanceId;
    @ApiModelProperty("宜搭修改时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime formModifiedTime;
}
src/main/java/com/ruoyi/production/pojo/ProductMaterialConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.production.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * <br>
 * ç‰©æ–™ä¿¡æ¯è¡¨é…ç½®è¡¨
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/11 16:57
 */
@Data
@TableName("product_material_config")
@ApiModel(value = "ProductMaterialConfig", description = "物料信息配置表")
public class ProductMaterialConfig {
    @TableId(type = IdType.AUTO)
    @ApiModelProperty("主键ID")
    private Integer id;
    @ApiModelProperty("区分类型: MATERIAL_TYPE æˆ– INVENTORY_CAT")
    private String configType;
    @ApiModelProperty("配置名称")
    private String configName;
}
src/main/java/com/ruoyi/production/pojo/ProductOrder.java
@@ -10,6 +10,7 @@
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@@ -36,18 +37,12 @@
    private String npsNo;
    /**
     * åˆå¹¶ç”Ÿäº§è®¡åˆ’ids
     */
    @ApiModelProperty(value = "合并生产计划ids")
    private String combineProductionPlanIds;
    /**
     * è®¡åˆ’完成时间
     */
    @ApiModelProperty(value = "计划完成时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime planCompleteTime;
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate planCompleteTime;
    /**
     * ç§Ÿæˆ·id
src/main/java/com/ruoyi/production/service/ProductMaterialConfigService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.production.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.production.pojo.ProductMaterialConfig;
/**
 * <br>
 * ç‰©æ–™ä¿¡æ¯è¡¨é…ç½®æŽ¥å£
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/11 16:58
 */
public interface ProductMaterialConfigService extends IService<ProductMaterialConfig> {
}
src/main/java/com/ruoyi/production/service/ProductMaterialService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.production.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.production.pojo.ProductMaterial;
/**
 * <br>
 * äº§å“ç‰©æ–™ä¿¡æ¯æŽ¥å£
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/11 16:35
 */
public interface ProductMaterialService extends IService<ProductMaterial> {
    void loadProductMaterialData();
    void syncProductMaterialJob();
}
src/main/java/com/ruoyi/production/service/impl/ProductMaterialConfigServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.production.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.production.mapper.ProductMaterialConfigMapper;
import com.ruoyi.production.pojo.ProductMaterialConfig;
import com.ruoyi.production.service.ProductMaterialConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
 * <br>
 * ç‰©æ–™ä¿¡æ¯è¡¨é…ç½®æŽ¥å£å®žçŽ°ç±»
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/11 16:59
 */
@Slf4j
@Service
public class ProductMaterialConfigServiceImpl extends ServiceImpl<ProductMaterialConfigMapper, ProductMaterialConfig> implements ProductMaterialConfigService {
}
src/main/java/com/ruoyi/production/service/impl/ProductMaterialServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,306 @@
package com.ruoyi.production.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.framework.config.AliDingConfig;
import com.ruoyi.production.enums.MaterialConfigTypeEnum;
import com.ruoyi.production.mapper.ProductMaterialMapper;
import com.ruoyi.production.pojo.ProductMaterial;
import com.ruoyi.production.pojo.ProductMaterialConfig;
import com.ruoyi.production.service.ProductMaterialConfigService;
import com.ruoyi.production.service.ProductMaterialService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
/**
 * <br>
 * äº§å“ç‰©æ–™ä¿¡æ¯æŽ¥å£å®žçŽ°ç±»
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/11 16:36
 */
@Slf4j
@Service
public class ProductMaterialServiceImpl extends ServiceImpl<ProductMaterialMapper, ProductMaterial> implements ProductMaterialService {
    @Autowired
    private AliDingConfig aliDingConfig;
    @Autowired
    private ProductMaterialConfigService productMaterialConfigService;
    /**
     * åŒæ­¥é”ï¼Œé˜²æ­¢æ‰‹åŠ¨å’Œå®šæ—¶ä»»åŠ¡åŒæ—¶æ‰§è¡Œ
     */
    private final ReentrantLock syncLock = new ReentrantLock();
    @Override
    public void loadProductMaterialData() {
        syncProductMaterialData(1);
    }
    @Override
    public void syncProductMaterialJob() {
        syncProductMaterialData(2);
    }
    /**
     * åŒæ­¥æ•°æ®
     */
    @Transactional(rollbackFor = Exception.class)
    public void syncProductMaterialData(Integer dataSyncType) {
        if (!syncLock.tryLock()) {
            log.warn("同步正在进行中,本次 {} åŒæ­¥è¯·æ±‚被跳过", dataSyncType == 1 ? "手动" : "定时任务");
            return;
        }
        try {
            // èŽ·å– AccessToken
            String accessToken = getAccessToken();
            if (StringUtils.isEmpty(accessToken)) {
                return;
            }
            // èŽ·å–æœ¬åœ°æœ€åŽåŒæ­¥æ—¶é—´
            LocalDateTime lastSyncTime = getLastSyncTime();
            log.info("开始物料编码增量同步,本地最后修改时间: {}", lastSyncTime);
            int pageNumber = 1;
            int pageSize = 50;
            boolean hasMore = true;
            int totalSynced = 0;
            while (hasMore) {
                // æŸ¥è¯¢å‚æ•°
                JSONObject searchParam = buildSearchParam(lastSyncTime, pageNumber, pageSize);
                // è°ƒç”¨å®œæ­æŽ¥å£æ‹‰å–数据
                String dataRes = HttpUtils.sendPostJson(
                        aliDingConfig.getSearchFormDataUrl(),
                        searchParam.toJSONString(),
                        StandardCharsets.UTF_8.name(),
                        null,
                        accessToken
                );
                if (StringUtils.isEmpty(dataRes)) {
                    log.warn("第 {} é¡µæ‹‰å–数据为空", pageNumber);
                    break;
                }
                JSONObject resultObj = JSON.parseObject(dataRes);
                JSONArray dataArr = resultObj.getJSONArray("data");
                Integer totalCount = resultObj.getInteger("totalCount");
                if (dataArr == null || dataArr.isEmpty()) {
                    log.info("没有更多新数据需要同步");
                    break;
                }
                // è§£æžå¹¶ä¿å­˜æ•°æ®
                List<ProductMaterial> list = parseProductMaterials(dataArr, totalCount);
                if (!list.isEmpty()) {
                    // å¤„理更新或新增
                    int affected = processSaveOrUpdate(list);
                    totalSynced += affected;
                }
                // åˆ¤æ–­æ˜¯å¦è¿˜æœ‰ä¸‹ä¸€é¡µ
                hasMore = (pageNumber * pageSize) < totalCount;
                pageNumber++;
                log.info("正在同步第 {} é¡µï¼Œå½“前已同步 {}/{}", pageNumber - 1, totalSynced, totalCount);
            }
            log.info("物料数据同步完成,共同步 {} æ¡æ•°æ®", totalSynced);
        } catch (Exception e) {
            log.error("同步物料编码异常", e);
        } finally {
            // é‡Šæ”¾é”
            syncLock.unlock();
        }
    }
    private String getAccessToken() {
        String params = "appkey=" + aliDingConfig.getAppKey()
                + "&appsecret=" + aliDingConfig.getAppSecret();
        String tokenRes = HttpUtils.sendGet(aliDingConfig.getAccessTokenUrl(), params);
        JSONObject tokenObj = JSON.parseObject(tokenRes);
        String accessToken = tokenObj.getString("access_token");
        if (StringUtils.isEmpty(accessToken)) {
            log.error("获取钉钉AccessToken失败: {}", tokenRes);
        }
        return accessToken;
    }
    private LocalDateTime getLastSyncTime() {
        LambdaQueryWrapper<ProductMaterial> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByDesc(ProductMaterial::getFormModifiedTime).last("LIMIT 1");
        ProductMaterial lastRecord = this.getOne(queryWrapper);
        return lastRecord != null ? lastRecord.getFormModifiedTime() : null;
    }
    private JSONObject buildSearchParam(LocalDateTime lastSyncTime, int pageNumber, int pageSize) {
        JSONObject searchParam = new JSONObject();
        searchParam.put("appType", aliDingConfig.getAppType());
        searchParam.put("systemToken", aliDingConfig.getSystemToken());
        searchParam.put("userId", aliDingConfig.getUserId());
        searchParam.put("formUuid", aliDingConfig.getMaterialCodeFormUuid());
        searchParam.put("currentPage", pageNumber);
        searchParam.put("pageSize", pageSize);
        JSONArray searchConditions = new JSONArray();
        JSONObject statusCondition = new JSONObject();
        statusCondition.put("key", "processInstanceStatus");
        JSONArray statusValueArray = new JSONArray();
        statusValueArray.add("COMPLETED");
        statusCondition.put("value", statusValueArray);
        statusCondition.put("type", "ARRAY");
        statusCondition.put("operator", "in");
        statusCondition.put("componentName", "SelectField");
        searchConditions.add(statusCondition);
        JSONObject resultCondition = new JSONObject();
        resultCondition.put("key", "processApprovedResult");
        JSONArray resultValueArray = new JSONArray();
        resultValueArray.add("agree");
        resultCondition.put("value", resultValueArray);
        resultCondition.put("type", "ARRAY");
        resultCondition.put("operator", "in");
        resultCondition.put("componentName", "SelectField");
        searchConditions.add(resultCondition);
        searchParam.put("searchFieldJson", searchConditions.toJSONString());
        searchParam.put("orderConfigJson", "{\"gmt_modified\":\"+\"}");
        if (lastSyncTime != null) {
            String startTime = lastSyncTime.plusSeconds(1).atZone(ZoneId.systemDefault())
                    .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            searchParam.put("modifiedFromTimeGMT", startTime);
        }
        String endTime = LocalDateTime.now().atZone(ZoneId.systemDefault())
                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        searchParam.put("modifiedToTimeGMT", endTime);
        return searchParam;
    }
    private List<ProductMaterial> parseProductMaterials(JSONArray dataArr, Integer totalCount) {
        List<ProductMaterial> list = new ArrayList<>();
        LocalDateTime now = LocalDateTime.now();
        for (int i = 0; i < dataArr.size(); i++) {
            JSONObject item = dataArr.getJSONObject(i);
            String formInstanceId = item.getString("formInstanceId");
            JSONObject originator = item.getJSONObject("originator");
            String originatorName = originator != null && originator.containsKey("userName")
                    ? originator.getJSONObject("userName").getString("nameInChinese") : "未知";
            JSONObject formData = item.getJSONObject("formData");
            ProductMaterial material = new ProductMaterial();
            material.setFormInstanceId(formInstanceId);
            material.setIdentifierCode(formData.getString("textField_l92h77ju"));
            material.setMaterialCode(formData.getString("textField_l92f36f2"));
            material.setMaterialName(formData.getString("textField_l92f36f5"));
            material.setSpecification(formData.getString("textField_l92f36f6"));
            material.setBaseUnit(formData.getString("textField_la147lnw"));
            material.setMaterialAttribute(formData.getString("selectField_la14k51j"));
            material.setFinishedProductName(formData.getString("radioField_lbkk2nn2"));
            material.setRemark(formData.getString("textareaField_l92f36f9"));
            // å¤„理物料类型和存货类别
            String materialType = formData.getString("selectField_l92f36fb");
            String inventoryCat = formData.getString("selectField_la154noy");
            material.setMaterialTypeId(getOrCreateConfigId(materialType, MaterialConfigTypeEnum.MATERIAL_TYPE.name()));
            material.setInventoryCategoryId(getOrCreateConfigId(inventoryCat, MaterialConfigTypeEnum.INVENTORY_CAT.name()));
            material.setOriginatorName(originatorName);
            material.setOriginatorOrg("宁夏中创绿能实业集团有限公司");
            material.setFormModifiedTime(parseUtcTime(item.getString("modifiedTimeGMT")));
            material.setCreateTime(now);
            material.setUpdateTime(now);
            list.add(material);
        }
        return list;
    }
    private Integer getOrCreateConfigId(String name, String type) {
        if (StringUtils.isEmpty(name)) {
            return null;
        }
        ProductMaterialConfig config = productMaterialConfigService.getOne(new LambdaQueryWrapper<ProductMaterialConfig>()
                .eq(ProductMaterialConfig::getConfigName, name)
                .eq(ProductMaterialConfig::getConfigType, type));
        if (config == null) {
            config = new ProductMaterialConfig();
            config.setConfigName(name);
            config.setConfigType(type);
            productMaterialConfigService.save(config);
        }
        return config.getId();
    }
    private int processSaveOrUpdate(List<ProductMaterial> list) {
        if (list == null || list.isEmpty()) {
            return 0;
        }
        int affected = 0;
        for (ProductMaterial material : list) {
            ProductMaterial exist = this.getOne(new LambdaQueryWrapper<ProductMaterial>()
                    .eq(ProductMaterial::getFormInstanceId, material.getFormInstanceId()));
            if (exist == null) {
                this.save(material);
                affected++;
                log.info("新增物料数据 formInstanceId={}", material.getFormInstanceId());
            } else {
                if (exist.getFormModifiedTime() == null || !exist.getFormModifiedTime().equals(material.getFormModifiedTime())) {
                    material.setId(exist.getId());
                    material.setCreateTime(exist.getCreateTime());
                    this.updateById(material);
                    affected++;
                    log.info("更新物料数据 formInstanceId={}", material.getFormInstanceId());
                }
            }
        }
        return affected;
    }
    private LocalDateTime parseUtcTime(String utcString) {
        if (StringUtils.isEmpty(utcString)) {
            return null;
        }
        try {
            OffsetDateTime odt = OffsetDateTime.parse(utcString);
            return odt.toLocalDateTime();
        } catch (DateTimeParseException ex) {
            log.warn("解析时间 {} å¤±è´¥: {}", utcString, ex.getMessage());
            return null;
        }
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductOrderServiceImpl.java
@@ -15,10 +15,15 @@
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.pojo.*;
import com.ruoyi.production.service.ProductOrderService;
import com.ruoyi.productionPlan.mapper.ProductOrderPlanMapper;
import com.ruoyi.productionPlan.mapper.ProductionPlanMapper;
import com.ruoyi.productionPlan.pojo.ProductOrderPlan;
import com.ruoyi.productionPlan.pojo.ProductionPlan;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
@@ -49,6 +54,12 @@
    @Autowired
    private ProductionProductMainMapper productionProductMainMapper;
    @Autowired
    private ProductOrderPlanMapper productOrderPlanMapper;
    @Autowired
    private ProductionPlanMapper productionPlanMapper;
    @Autowired
    private ProductionProductOutputMapper productionProductOutputMapper;
@@ -123,14 +134,25 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean revoke(ProductOrder productOrder) {
        //判断是否产生工单
        List<ProductionProductMain> productionProductMains = productionProductMainMapper.selectList(Wrappers.<ProductionProductMain>lambdaQuery().eq(ProductionProductMain::getProductOrderId, productOrder.getId()));
        if (!productionProductMains.isEmpty()) {
            throw new RuntimeException("生产订单已经报工,不能撤销");
        }
        // todo åˆ¤æ–­æ˜¯å¦äº§ç”ŸæŠ¥å·¥ä¿¡æ¯
        // æŸ¥è¯¢åˆå¹¶çš„生产计划
        List<ProductOrderPlan> productOrderPlans = productOrderPlanMapper.selectList(Wrappers.<ProductOrderPlan>lambdaQuery().in(ProductOrderPlan::getProductOrderId, productOrder.getId()));
        if (productOrderPlans.isEmpty()) {
            throw new RuntimeException("合并的生产计划不存在");
        }
        for (ProductOrderPlan productOrderPlan : productOrderPlans) {
            ProductionPlan productionPlan = productionPlanMapper.selectById(productOrderPlan.getProductionPlanId());
            productionPlan.setAssignedQuantity(productionPlan.getAssignedQuantity().subtract(productOrderPlan.getAssignedQuantity()));
            productionPlanMapper.updateById(productionPlan);
        }
        // åˆ é™¤å…³è”关系
        productOrderPlanMapper.delete(Wrappers.<ProductOrderPlan>lambdaQuery().in(ProductOrderPlan::getProductOrderId, productOrder.getId()));
        // åˆ é™¤è®¢å•
        productOrderMapper.deleteById(productOrder.getId());
        // todo åˆ é™¤è®¢å•下的工艺路线子表
        return null;
    }
src/main/java/com/ruoyi/production/task/ProductMaterialTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package com.ruoyi.production.task;
import com.ruoyi.production.service.ProductMaterialService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
 * <br>
 *
 * </br>
 *
 * @author deslrey
 * @version 1.0
 * @since 2026/03/11 16:39
 */
@Component
public class ProductMaterialTask {
    @Autowired
    private ProductMaterialService productMaterialService;
    @Scheduled(cron = "0 0 * * * ?")
    public void syncProdDataJob() {
        productMaterialService.syncProductMaterialJob();
    }
}
src/main/java/com/ruoyi/productionPlan/dto/ProductionPlanDto.java
@@ -8,7 +8,7 @@
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.util.List;
@Data
@@ -20,7 +20,7 @@
    private BigDecimal totalAssignedQuantity;
    @ApiModelProperty(value = "计划完成时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime planCompleteTime;
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate planCompleteTime;
}
src/main/java/com/ruoyi/productionPlan/pojo/ProductOrderPlan.java
@@ -6,7 +6,10 @@
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
@@ -37,6 +40,10 @@
    @ApiModelProperty("生产计划id")
    private Long productionPlanId;
    @ApiModelProperty(value = "下发数量")
    @Excel(name = "下发数量")
    private BigDecimal assignedQuantity;
    @ApiModelProperty("录入时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
src/main/java/com/ruoyi/productionPlan/pojo/ProductionPlan.java
@@ -7,6 +7,7 @@
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -63,6 +64,13 @@
    @ApiModelProperty("物料编码")
    @Excel(name = "物料编码")
    private String materialCode;
    /**
     * å…³è”物料信息表ID
     */
    @ApiModelProperty("关联物料信息表ID")
    private Integer productMaterialId;
    /**
     * äº§å“åç§°
@@ -126,6 +134,7 @@
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @ApiModelProperty("开始日期")
    @Excel(name = "开始日期", width = 20, dateFormat = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date startDate;
    /**
@@ -134,6 +143,7 @@
    @ApiModelProperty("结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @Excel(name = "结束日期", width = 20, dateFormat = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
    /**
@@ -227,4 +237,20 @@
    @ApiModelProperty(value = "下发数量")
    @Excel(name = "下发数量")
    private BigDecimal assignedQuantity;
    /**
     * è®¡ç®—剩余方数
     *
     * @return å‰©ä½™æ–¹æ•°
     */
    @ApiModelProperty(value = "剩余方数")
    public BigDecimal getRemainingVolume() {
        if (volume == null) {
            return BigDecimal.ZERO;
        }
        if (assignedQuantity == null) {
            return volume;
        }
        return volume.subtract(assignedQuantity);
    }
}
src/main/java/com/ruoyi/productionPlan/service/impl/ProductionPlanServiceImpl.java
@@ -15,7 +15,9 @@
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.config.AliDingConfig;
import com.ruoyi.production.pojo.ProductMaterial;
import com.ruoyi.production.pojo.ProductOrder;
import com.ruoyi.production.service.ProductMaterialService;
import com.ruoyi.production.service.ProductOrderService;
import com.ruoyi.productionPlan.dto.ProductionPlanDto;
import com.ruoyi.productionPlan.dto.ProductionPlanImportDto;
@@ -69,6 +71,9 @@
    @Autowired
    private ProductOrderPlanMapper productOrderPlanMapper;
    @Autowired
    private ProductMaterialService productMaterialService;
    /**
     * åŒæ­¥é”ï¼Œç¡®ä¿æ‰‹åŠ¨å’Œå®šæ—¶ä»»åŠ¡ä¸åŒæ—¶æ‰§è¡Œ
     */
@@ -107,6 +112,7 @@
        //  æŸ¥è¯¢ä¸»ç”Ÿäº§è®¡åˆ’
        List<ProductionPlan> plans = productionPlanMapper.selectBatchIds(productionPlanDto.getIds());
        plans.sort(Comparator.comparingLong(ProductionPlan::getId));
        //  æ ¡éªŒæ˜¯å¦å­˜åœ¨ä¸åŒçš„产品名称
        String firstProductName = plans.get(0).getProductName();
@@ -121,37 +127,14 @@
        }
        // å åŠ æ–¹æ•°
        BigDecimal totalVolume = plans.stream()
                .map(ProductionPlan::getVolume)
        // å åŠ å‰©ä½™æ–¹æ•°
        BigDecimal totalRemainingVolume = plans.stream()
                .map(ProductionPlan::getRemainingVolume)
                .filter(v -> v != null)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // åˆ¤æ–­ä¸‹å‘数量是否大于等于方数
        if (productionPlanDto.getTotalAssignedQuantity().compareTo(totalVolume) > 0) {
            log.warn("操作失败,下发数量不能大于方数");
            return false;
        }
        // æ ¹æ®ä¸‹å‘数量,从第一个生产计划开始分配方数
        BigDecimal assignedVolume = BigDecimal.ZERO;
        for (ProductionPlan plan : plans) {
            BigDecimal volume = plan.getVolume();
            if (volume == null) {
                continue;
            }
            if (assignedVolume.add(volume).compareTo(productionPlanDto.getTotalAssignedQuantity()) >= 0) {
                // æœ€åŽä¸€ä¸ªè®¡åˆ’,分配剩余方数
                plan.setAssignedQuantity(productionPlanDto.getTotalAssignedQuantity().subtract(assignedVolume));
                productionPlanMapper.updateById(plan);
                break;
            }
            // åˆ†é…å½“前计划方数
            plan.setAssignedQuantity(volume);
            productionPlanMapper.updateById(plan);
            assignedVolume = assignedVolume.add(volume);
        // åˆ¤æ–­ä¸‹å‘数量是否大于等于剩余方数
        if (productionPlanDto.getTotalAssignedQuantity().compareTo(totalRemainingVolume) > 0) {
            throw new BaseException("操作失败,下发数量不能大于剩余方数");
        }
        // åˆ›å»ºç”Ÿäº§è®¢å•
@@ -160,11 +143,41 @@
        productOrder.setPlanCompleteTime(productionPlanDto.getPlanCompleteTime());
        productOrderService.addProductOrder(productOrder);
        for (Long planId : productionPlanDto.getIds()) {
        // æ ¹æ®ä¸‹å‘数量,从第一个生产计划开始分配方数
        BigDecimal assignedVolume = BigDecimal.ZERO;
        for (ProductionPlan plan : plans) {
            BigDecimal volume = plan.getVolume();
            if (volume == null) {
                continue;
            }
            // è®¡ç®—剩余方数
            BigDecimal remainingVolume = plan.getRemainingVolume();
            if (remainingVolume.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            ProductOrderPlan productOrderPlan = new ProductOrderPlan();
            productOrderPlan.setProductOrderId(productOrder.getId());
            productOrderPlan.setProductionPlanId(planId);
            productOrderPlan.setProductionPlanId(plan.getId());
            if (assignedVolume.add(remainingVolume).compareTo(productionPlanDto.getTotalAssignedQuantity()) >= 0) {
                // æœ€åŽä¸€ä¸ªè®¡åˆ’,分配剩余方数
                BigDecimal lastRemainingVolume = productionPlanDto.getTotalAssignedQuantity().subtract(assignedVolume);
                plan.setAssignedQuantity(plan.getAssignedQuantity().add(lastRemainingVolume));
                productOrderPlan.setAssignedQuantity(lastRemainingVolume);
                productionPlanMapper.updateById(plan);
                productOrderPlanMapper.insert(productOrderPlan);
                break;
            }
            // åˆ†é…å½“前计划方数
            plan.setAssignedQuantity(plan.getAssignedQuantity().add(remainingVolume));
            productOrderPlan.setAssignedQuantity(remainingVolume);
            // æ›´æ–°ç”Ÿäº§è®¡åˆ’
            productionPlanMapper.updateById(plan);
            // åˆ›å»ºå…³è”关系
            productOrderPlanMapper.insert(productOrderPlan);
            assignedVolume = assignedVolume.add(remainingVolume);
        }
        return true;
    }
@@ -306,7 +319,7 @@
        searchParam.put("appType", aliDingConfig.getAppType());
        searchParam.put("systemToken", aliDingConfig.getSystemToken());
        searchParam.put("userId", aliDingConfig.getUserId());
        searchParam.put("formUuid", aliDingConfig.getFormUuid());
        searchParam.put("formUuid", aliDingConfig.getProducePlanFormUuid());
        searchParam.put("currentPage", pageNumber);
        searchParam.put("pageSize", pageSize);
@@ -372,7 +385,19 @@
                plan.setApplyNo(formData.getString("textField_l7fytfco"));
                plan.setCustomerName(formData.getString("textField_lbkozohg"));
                plan.setMaterialCode(row.getString("textField_l9xo62q5"));
                String materialCode = row.getString("textField_l9xo62q5");
                plan.setMaterialCode(materialCode);
                // æ ¹æ®ç‰©æ–™ç¼–码查询物料信息表,关联物料ID
                if (StringUtils.isNotEmpty(materialCode)) {
                    LambdaQueryWrapper<ProductMaterial> queryWrapper = new LambdaQueryWrapper<>();
                    queryWrapper.eq(ProductMaterial::getMaterialCode, materialCode);
                    ProductMaterial productMaterial = productMaterialService.getOne(queryWrapper);
                    if (productMaterial != null) {
                        plan.setProductMaterialId(productMaterial.getId());
                    }
                }
                plan.setProductName(row.getString("textField_l9xo62q7"));
                plan.setProductSpec(row.getString("textField_l9xo62q8"));
                plan.setLength(row.getInteger("numberField_lb7lgatg_value"));
src/main/resources/application-zsjc.yml
@@ -30,7 +30,8 @@
    app-secret: "fO07qSZC5SMLw9It3Ydd3BuoFyVbRlsWXUnVr2kwPJXz0OpUntCAO5dqnr8G7zq5"
    user-id: "290166234126410562"
    app-type: "APP_UY8XMO7GNA8OF08EFNVQ"
    form-uuid: "FORM-4IA66891C5H3QWMDBSGO05C0OX9628GRPYF7L8"
    produce-plan-form-uuid: "FORM-4IA66891C5H3QWMDBSGO05C0OX9628GRPYF7L8"
    material-code-form-uuid: "FORM-8I666N718RW4ALO4ETYDDB0U6U8T3EIWTQ0ALL1"
    system-token: "4J766L91OFH3V612E7LG5B4DI8M13MQF9VF7LG4"
    # æŽ¥å£åœ°å€
    access-token-url: "https://oapi.dingtalk.com/gettoken"
src/main/resources/mapper/production/ProductMaterialMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.production.mapper.ProductMaterialMapper">
    <resultMap id="ProductMaterialResultMap" type="com.ruoyi.production.pojo.ProductMaterial">
        <id property="id" column="id"/>
        <result property="tenantId" column="tenant_id"/>
        <result property="materialTypeId" column="material_type_id"/>
        <result property="inventoryCategoryId" column="inventory_category_id"/>
        <result property="identifierCode" column="identifier_code"/>
        <result property="materialCode" column="material_code"/>
        <result property="materialName" column="material_name"/>
        <result property="specification" column="specification"/>
        <result property="baseUnit" column="base_unit"/>
        <result property="materialAttribute" column="material_attribute"/>
        <result property="finishedProductName" column="finished_product_name"/>
        <result property="originatorName" column="originator_name"/>
        <result property="originatorOrg" column="originator_org"/>
        <result property="remark" column="remark"/>
        <result property="createTime" column="create_time"/>
        <result property="updateTime" column="update_time"/>
    </resultMap>
</mapper>
src/main/resources/mapper/productionPlan/ProductionPlanMapper.xml
@@ -12,6 +12,7 @@
        <result property="applyNo" column="apply_no"/>
        <result property="customerName" column="customer_name"/>
        <result property="materialCode" column="material_code"/>
        <result property="productMaterialId" column="product_material_id"/>
        <result property="productName" column="product_name"/>
        <result property="productSpec" column="product_spec"/>
        <result property="length" column="length"/>
@@ -54,6 +55,12 @@
        <if test="c.materialCode != null and c.materialCode != '' ">
            AND pp.material_code LIKE CONCAT('%',#{c.materialCode},'%')
        </if>
        <if test="c.startDate != null">
            AND pp.start_date &gt;= DATE_FORMAT(#{c.startDate},'%Y-%m-%d')
        </if>
        <if test="c.endDate != null">
            AND pp.end_date &lt;= DATE_FORMAT(#{c.endDate},'%Y-%m-%d')
        </if>
    </select>
    <select id="selectSummaryByProductType" resultType="com.ruoyi.productionPlan.dto.ProductionPlanSummaryDto">