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.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.framework.config.AliDingConfig;
import com.ruoyi.productionPlan.dto.ProductionPlanDto;
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 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;
/**
*
* 销售生产需求接口实现类
*
*
* @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;
/**
* 同步锁,确保手动和定时任务不同时执行
*/
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);
}
/**
* 同步数据
*/
@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;
}
}
}