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.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.framework.config.AliDingConfig;
import com.ruoyi.production.dto.ProductMaterialGroupDto;
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.pojo.ProductMaterialSku;
import com.ruoyi.production.service.ProductMaterialConfigService;
import com.ruoyi.production.service.ProductMaterialService;
import com.ruoyi.production.service.ProductMaterialSkuService;
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.*;
import java.util.concurrent.locks.ReentrantLock;
/**
*
* 产品物料信息接口实现类
*
*
* @author deslrey
* @version 1.0
* @since 2026/03/11 16:36
*/
@Slf4j
@Service
public class ProductMaterialServiceImpl extends ServiceImpl implements ProductMaterialService {
@Autowired
private AliDingConfig aliDingConfig;
@Autowired
private ProductMaterialConfigService productMaterialConfigService;
@Autowired
private ProductMaterialSkuService productMaterialSkuService;
/**
* 同步锁,防止手动和定时任务同时执行
*/
private final ReentrantLock syncLock = new ReentrantLock();
@Override
@Transactional(rollbackFor = Exception.class)
public void loadProductMaterialData() {
syncProductMaterialData(1);
}
@Override
@Transactional(rollbackFor = Exception.class)
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 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 queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(ProductMaterialSku::getFormModifiedTime).last("LIMIT 1");
ProductMaterialSku lastRecord = productMaterialSkuService.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 parseProductMaterials(JSONArray dataArr, 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");
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.setMaterialName(formData.getString("textField_l92f36f5"));
material.setBaseUnit(formData.getString("textField_la147lnw"));
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()));
Long materialId = getOrCreateMaterial(material);
// 处理物料规格数据
ProductMaterialSku sku = new ProductMaterialSku();
sku.setMaterialId(materialId);
sku.setFormInstanceId(formInstanceId);
sku.setIdentifierCode(formData.getString("textField_l92h77ju"));
sku.setMaterialCode(formData.getString("textField_l92f36f2"));
sku.setSpecification(formData.getString("textField_l92f36f6"));
sku.setSupplyType(formData.getString("selectField_la14k51j"));
sku.setOriginatorName(originatorName);
sku.setOriginatorOrg("宁夏中创绿能实业集团有限公司");
sku.setFormModifiedTime(parseUtcTime(item.getString("modifiedTimeGMT")));
sku.setCreateTime(now);
sku.setUpdateTime(now);
list.add(sku);
}
return list;
}
private Long getOrCreateMaterial(ProductMaterial material) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ProductMaterial::getMaterialName, material.getMaterialName());
ProductMaterial exist = this.getOne(queryWrapper);
if (exist == null) {
material.setCreateTime(LocalDateTime.now());
material.setUpdateTime(LocalDateTime.now());
this.save(material);
return material.getId();
} else {
// 如果已存在,但关键属性发生变化,则进行更新(以宜搭数据为准)
boolean needUpdate = false;
if (material.getMaterialTypeId() != null && !material.getMaterialTypeId().equals(exist.getMaterialTypeId())) {
exist.setMaterialTypeId(material.getMaterialTypeId());
needUpdate = true;
}
if (material.getInventoryCategoryId() != null && !material.getInventoryCategoryId().equals(exist.getInventoryCategoryId())) {
exist.setInventoryCategoryId(material.getInventoryCategoryId());
needUpdate = true;
}
if (StringUtils.isNotEmpty(material.getBaseUnit()) && !material.getBaseUnit().equals(exist.getBaseUnit())) {
exist.setBaseUnit(material.getBaseUnit());
needUpdate = true;
}
if (needUpdate) {
exist.setUpdateTime(LocalDateTime.now());
this.updateById(exist);
}
return exist.getId();
}
}
private Integer getOrCreateConfigId(String name, String type) {
if (StringUtils.isEmpty(name)) {
return null;
}
ProductMaterialConfig config = productMaterialConfigService.getOne(new LambdaQueryWrapper().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 list) {
if (list == null || list.isEmpty()) {
return 0;
}
int affected = 0;
for (ProductMaterialSku sku : list) {
ProductMaterialSku exist =
productMaterialSkuService.getOne(new LambdaQueryWrapper()
.eq(ProductMaterialSku::getMaterialId, sku.getMaterialId())
.eq(ProductMaterialSku::getSpecification, sku.getSpecification()));
if (exist == null) {
productMaterialSkuService.save(sku);
affected++;
log.info("新增物料规格 {}", sku.getSpecification());
} else {
if (exist.getFormModifiedTime() == null || !exist.getFormModifiedTime().equals(sku.getFormModifiedTime())) {
sku.setId(exist.getId());
sku.setCreateTime(exist.getCreateTime());
productMaterialSkuService.updateById(sku);
affected++;
log.info("更新物料规格 {}", sku.getSpecification());
}
}
}
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 ProductMaterialList(String materialName) {
List materialConfigList = productMaterialConfigService.list(new LambdaQueryWrapper()
.eq(ProductMaterialConfig::getConfigType, MaterialConfigTypeEnum.MATERIAL_TYPE.name()));
List productMaterialMap = new ArrayList<>();
if (materialConfigList == null || materialConfigList.isEmpty()) {
return productMaterialMap;
}
for (ProductMaterialConfig materialConfig : materialConfigList) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ProductMaterial::getMaterialTypeId, materialConfig.getId())
.like(materialName != null && !materialName.isEmpty(), ProductMaterial::getMaterialName, materialName);
List productMaterialList = list(wrapper);
ProductMaterialGroupDto dto = new ProductMaterialGroupDto();
dto.setConfigId(materialConfig.getId());
dto.setConfigName(materialConfig.getConfigName());
dto.setMaterialList(productMaterialList);
productMaterialMap.add(dto);
}
return productMaterialMap;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void addProductMaterial(ProductMaterial productMaterial) {
validateProductMaterial(productMaterial, false);
if (existsMaterialName(productMaterial.getMaterialName(), null)) {
throw new ServiceException("物料名称已存在");
}
LocalDateTime now = LocalDateTime.now();
if (productMaterial.getCreateTime() == null) {
productMaterial.setCreateTime(now);
}
productMaterial.setUpdateTime(now);
if (!this.save(productMaterial)) {
throw new ServiceException("新增物料失败");
}
log.info("新增物料成功 materialName={}", productMaterial.getMaterialName());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateProductMaterial(ProductMaterial productMaterial) {
validateProductMaterial(productMaterial, true);
ProductMaterial exist = this.getById(productMaterial.getId());
if (exist == null) {
throw new ServiceException("物料不存在");
}
if (existsMaterialName(productMaterial.getMaterialName(), productMaterial.getId())) {
throw new ServiceException("物料名称已存在");
}
productMaterial.setUpdateTime(LocalDateTime.now());
if (!this.updateById(productMaterial)) {
throw new ServiceException("修改物料失败");
}
log.info("修改物料成功 id={}", productMaterial.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteProductMaterial(List ids) {
if (ids == null || ids.isEmpty()) {
throw new ServiceException("请选择至少一条数据");
}
if (!this.removeByIds(ids)) {
throw new ServiceException("删除物料失败");
}
log.info("删除物料成功 ids={}", ids);
}
private void validateProductMaterial(ProductMaterial productMaterial, boolean requireId) {
if (productMaterial == null) {
throw new ServiceException("参数不能为空");
}
if (requireId && productMaterial.getId() == null) {
throw new ServiceException("主键ID不能为空");
}
if (StringUtils.isEmpty(productMaterial.getMaterialName())) {
throw new ServiceException("物料名称不能为空");
}
}
private boolean existsMaterialName(String materialName, Long excludeId) {
if (StringUtils.isEmpty(materialName)) {
return false;
}
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ProductMaterial::getMaterialName, materialName);
if (excludeId != null) {
queryWrapper.ne(ProductMaterial::getId, excludeId);
}
return this.count(queryWrapper) > 0;
}
}