package com.ruoyi.productionPlan.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.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.bean.BeanUtils; 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.ProductOrder; import com.ruoyi.production.service.ProductOrderService; import com.ruoyi.productionPlan.dto.ProductionPlanDto; import com.ruoyi.productionPlan.dto.ProductionPlanImportDto; import com.ruoyi.productionPlan.dto.ProductionPlanSummaryDto; import com.ruoyi.productionPlan.mapper.ProductionPlanMapper; import com.ruoyi.productionPlan.pojo.ProductionPlan; import com.ruoyi.productionPlan.service.ProductionPlanService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.time.Instant; 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.*; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import static com.ruoyi.productionPlan.enums.DataSourceTypeEnum.PRODUCTION_FORECAST; /** *
* 销售生产需求接口实现类 *
* * @author deslrey * @version 1.0 * @since 2026/03/10 10:00 */ @Slf4j @Service public class ProductionPlanServiceImpl extends ServiceImpl implements ProductionPlanService { @Autowired private AliDingConfig aliDingConfig; @Autowired private ProductionPlanMapper productionPlanMapper; @Autowired private ProductOrderService productOrderService; /** * 同步锁,确保手动和定时任务不同时执行 */ private final ReentrantLock syncLock = new ReentrantLock(); @Override public IPage listPage(Page page, ProductionPlanDto productionPlanDto) { return productionPlanMapper.listPage(page, productionPlanDto); } /** * 页面手动同步 */ @Override public void loadProdData() { syncProdData(1); } /** * 定时任务同步 */ @Override public void syncProdDataJob() { syncProdData(2); } /** * 合并生产计划 */ @Override @Transactional(rollbackFor = Exception.class) public boolean combine(ProductionPlanDto productionPlanDto) { if (productionPlanDto.getIds() == null || productionPlanDto.getIds().isEmpty()) { return false; } // 查询主生产计划 List plans = productionPlanMapper.selectBatchIds(productionPlanDto.getIds()); // 校验是否存在不同的产品名称 String firstProductName = plans.get(0).getProductName(); if (plans.stream().anyMatch(p -> !p.getProductName().equals(firstProductName))) { log.warn("合并失败,存在不同的产品名称"); return false; } // 校验是否存在不同的产品规格 String firstProductSpec = plans.get(0).getProductSpec(); if (plans.stream().anyMatch(p -> !p.getProductSpec().equals(firstProductSpec))) { log.warn("合并失败,存在不同的产品规格"); return false; } // 叠加方数 BigDecimal totalVolume = plans.stream() .map(ProductionPlan::getVolume) .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)); break; } // 分配当前计划方数 plan.setAssignedQuantity(volume); productionPlanMapper.updateById(plan); assignedVolume = assignedVolume.add(volume); } // 创建生产订单 ProductOrder productOrder = new ProductOrder(); String combineIds = StringUtils.join(productionPlanDto.getIds(), ","); productOrder.setCombineProductionPlanIds(combineIds); productOrder.setQuantity(productionPlanDto.getTotalAssignedQuantity()); productOrder.setPlanCompleteTime(productionPlanDto.getPlanCompleteTime()); productOrderService.addProductOrder(productOrder); return true; } @Override @Transactional(rollbackFor = Exception.class) public boolean add(ProductionPlanDto productionPlanDto) { productionPlanDto.setDataSourceType(PRODUCTION_FORECAST.getCode()); productionPlanMapper.insert(productionPlanDto); return true; } /** * 同步数据 */ @Transactional(rollbackFor = Exception.class) public void syncProdData(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 list = parseProductionPlans(dataArr, dataSyncType, 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() { // 查询本地数据库中 formModifiedTime 最大的记录 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.orderByDesc(ProductionPlan::getFormModifiedTime).last("LIMIT 1"); ProductionPlan 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.getFormUuid()); searchParam.put("pageSize", pageSize); searchParam.put("pageNumber", pageNumber); // 默认按修改时间升序排序,确保分页拉取数据的连续性 // "+" 表示升序,"gmt_modified" 是官方内置字段 searchParam.put("orderConfigJson", "{\"gmt_modified\":\"+\"}"); // 设置修改时间筛选区间 (格式必须为yyyy-MM-dd HH:mm:ss) 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 parseProductionPlans(JSONArray dataArr, Integer dataSyncType, Integer totalCount) { List 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"); String serialNo = item.getString("serialNo"); JSONObject originator = item.getJSONObject("originator"); String originatorName = originator != null && originator.containsKey("userName") ? originator.getJSONObject("userName").getString("nameInChinese") : "未知"; JSONObject formData = item.getJSONObject("formData"); JSONArray tableArr = formData.getJSONArray("tableField_l7fytfcn"); if (tableArr == null || tableArr.isEmpty()) { continue; } for (int j = 0; j < tableArr.size(); j++) { JSONObject row = tableArr.getJSONObject(j); ProductionPlan plan = new ProductionPlan(); plan.setFormInstanceId(formInstanceId); plan.setSerialNo(serialNo); plan.setApplyNo(formData.getString("textField_l7fytfco")); plan.setCustomerName(formData.getString("textField_lbkozohg")); plan.setMaterialCode(row.getString("textField_l9xo62q5")); plan.setProductName(row.getString("textField_l9xo62q7")); plan.setProductSpec(row.getString("textField_l9xo62q8")); plan.setLength(row.getInteger("numberField_lb7lgatg_value")); plan.setWidth(row.getInteger("numberField_lb7lgath_value")); plan.setHeight(row.getInteger("numberField_lb7lgati_value")); plan.setQuantity(row.getInteger("numberField_lb7lgatj_value")); plan.setVolume(row.getBigDecimal("numberField_l7fytfd3_value")); plan.setStrength(row.getString("radioField_m9urarr2_id")); JSONArray dateArr = row.getJSONArray("cascadeDateField_lfxqqluw"); if (dateArr != null && dateArr.size() == 2) { try { long start = Long.parseLong(dateArr.getString(0)); long end = Long.parseLong(dateArr.getString(1)); plan.setStartDate(Instant.ofEpochMilli(start).atZone(ZoneId.systemDefault()).toLocalDateTime()); plan.setEndDate(Instant.ofEpochMilli(end).atZone(ZoneId.systemDefault()).toLocalDateTime()); } catch (Exception e) { log.warn("解析日期失败: {}", dateArr); } } plan.setSubmitter(originatorName); plan.setSubmitOrg("宁夏中创绿能实业集团有限公司"); plan.setRemarkOne(formData.getString("textareaField_l7fytfcy")); plan.setRemarkTwo(formData.getString("textField_l7fytfcx")); plan.setCreatorName(originatorName); JSONObject modifyUser = item.getJSONObject("modifyUser"); if (modifyUser != null && modifyUser.containsKey("userName")) { plan.setModifierName(modifyUser.getJSONObject("userName").getString("nameInChinese")); } plan.setFormCreatedTime(parseUtcTime(item.getString("createdTimeGMT"))); plan.setFormModifiedTime(parseUtcTime(item.getString("modifiedTimeGMT"))); plan.setDataSyncType(dataSyncType); plan.setDataSourceType(1); plan.setCreateTime(now); plan.setUpdateTime(now); plan.setTotalCount(totalCount); list.add(plan); } } return list; } private int processSaveOrUpdate(List list) { if (list == null || list.isEmpty()) { return 0; } int affected = 0; // 去重 formInstanceId Set formIds = list.stream() .map(ProductionPlan::getFormInstanceId) .collect(Collectors.toSet()); // 查询数据库已有数据 List existList = this.list(new LambdaQueryWrapper().in(ProductionPlan::getFormInstanceId, formIds)); // Map (formInstanceId + materialCode) Map existMap = new HashMap<>(); for (ProductionPlan p : existList) { String key = p.getFormInstanceId() + "_" + p.getMaterialCode(); existMap.put(key, p); } // 遍历同步数据 for (ProductionPlan plan : list) { String key = plan.getFormInstanceId() + "_" + plan.getMaterialCode(); ProductionPlan exist = existMap.get(key); if (exist == null) { // 新增 this.save(plan); affected++; log.info("新增数据 formInstanceId={}, materialCode={}", plan.getFormInstanceId(), plan.getMaterialCode()); } else { // 判断是否需要更新 if (exist.getFormModifiedTime() == null || !exist.getFormModifiedTime().equals(plan.getFormModifiedTime())) { plan.setId(exist.getId()); plan.setCreateTime(exist.getCreateTime()); this.updateById(plan); affected++; log.info("更新数据 formInstanceId={}, materialCode={}", plan.getFormInstanceId(), plan.getMaterialCode()); } } } 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; } } @Override public List summaryByProductType(ProductionPlanSummaryDto query) { return baseMapper.selectSummaryByProductType(query); } @Override @Transactional(rollbackFor = Exception.class) public void importProdData(MultipartFile file) { if (file == null || file.isEmpty()) { throw new ServiceException("导入数据不能为空"); } ExcelUtil excelUtil = new ExcelUtil<>(ProductionPlanImportDto.class); List list; try { list = excelUtil.importExcel(file.getInputStream()); } catch (Exception e) { log.error("生产需求Excel导入失败", e); throw new ServiceException("Excel解析失败"); } if (list == null || list.isEmpty()) { throw new ServiceException("Excel没有数据"); } List entityList = new ArrayList<>(list.size()); ProductionPlan entity; for (ProductionPlanImportDto dto : list) { entity = new ProductionPlan(); BeanUtils.copyProperties(dto, entity); entity.setCreateTime(LocalDateTime.now()); entity.setUpdateTime(LocalDateTime.now()); entity.setDataSourceType(2); entity.setDataSyncType(1); entityList.add(entity); } this.saveBatch(entityList); } @Override public void exportProdData(HttpServletResponse response, List ids) { List list; if (ids != null && !ids.isEmpty()) { list = baseMapper.selectBatchIds(ids); } else { list = baseMapper.selectList(null); } List exportList = new ArrayList<>(); for (ProductionPlan entity : list) { ProductionPlanImportDto dto = new ProductionPlanImportDto(); BeanUtils.copyProperties(entity, dto); exportList.add(dto); } ExcelUtil util = new ExcelUtil<>(ProductionPlanImportDto.class); util.exportExcel(response, exportList, "销售生产需求数据"); } }