This commit is contained in:
2025-11-06 19:36:24 +08:00
parent 714fce242f
commit 8f68b7a4d5
7 changed files with 426 additions and 13 deletions

View File

@@ -45,9 +45,15 @@ public class TencentDocController extends BaseController {
@Autowired
private com.ruoyi.jarvis.config.TencentDocConfig tencentDocConfig;
@Autowired
private com.ruoyi.jarvis.mapper.TencentDocOperationLogMapper operationLogMapper;
/** Redis key前缀用于存储上次处理的最大行数 */
private static final String LAST_PROCESSED_ROW_KEY_PREFIX = "tendoc:last_row:";
/** Redis key前缀用于分布式锁 */
private static final String TENCENT_DOC_LOCK_KEY = "tendoc:write:lock:";
/**
* 测试回调接口是否可访问(用于调试)
*/
@@ -452,9 +458,14 @@ public class TencentDocController extends BaseController {
*/
@PostMapping("/fillSingleLogistics")
public AjaxResult fillSingleLogistics(@RequestBody Map<String, Object> params) {
String thirdPartyOrderNo = null;
String fileId = null;
String sheetId = null;
String lockKey = null;
try {
// 1. 获取参数
String thirdPartyOrderNo = (String) params.get("thirdPartyOrderNo");
thirdPartyOrderNo = (String) params.get("thirdPartyOrderNo");
String logisticsLink = (String) params.get("logisticsLink");
if (thirdPartyOrderNo == null || thirdPartyOrderNo.isEmpty()) {
@@ -469,13 +480,15 @@ public class TencentDocController extends BaseController {
try {
accessToken = tencentDocTokenService.getValidAccessToken();
} catch (Exception e) {
logOperation(null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "访问令牌无效: " + e.getMessage());
return AjaxResult.error("访问令牌无效,请先完成授权");
}
// 3. 从配置中读取文档信息
final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:";
String fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId");
String sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId");
fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId");
sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId");
Integer headerRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "headerRow");
Integer configStartRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "startRow");
@@ -493,21 +506,40 @@ public class TencentDocController extends BaseController {
}
if (fileId == null || fileId.isEmpty() || sheetId == null || sheetId.isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "文档配置不完整");
return AjaxResult.error("文档配置不完整,请先完成配置");
}
// 4. 读取表头(从配置的 headerRow 读取,与 startRow 独立
// 4. 获取分布式锁(针对该订单单号+文档+工作表
lockKey = TENCENT_DOC_LOCK_KEY + fileId + ":" + sheetId + ":" + thirdPartyOrderNo;
boolean lockAcquired = redisCache.setCacheObject(lockKey, "locked", 30, TimeUnit.SECONDS);
if (!lockAcquired) {
log.warn("获取锁失败,该订单正在被其他请求处理:{}", thirdPartyOrderNo);
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "获取分布式锁失败,该订单正在处理中");
return AjaxResult.error("该订单正在处理中,请稍后再试");
}
log.info("✓ 获取分布式锁成功 - 单号: {}, lockKey: {}", thirdPartyOrderNo, lockKey);
// 5. 读取表头(从配置的 headerRow 读取,与 startRow 独立)
String headerRange = String.format("A%d:Z%d", headerRow, headerRow);
JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange);
if (headerData == null || !headerData.containsKey("gridData")) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "无法读取表头数据");
return AjaxResult.error("无法读取表头数据");
}
// 5. 解析表头,找到"单号"和"物流单号"列
// 6. 解析表头,找到"单号"和"物流单号"列
JSONObject gridData = headerData.getJSONObject("gridData");
JSONArray rows = gridData.getJSONArray("rows");
if (rows == null || rows.isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "表头数据为空");
return AjaxResult.error("表头数据为空");
}
@@ -532,16 +564,20 @@ public class TencentDocController extends BaseController {
}
if (orderNoColumn == -1 || logisticsColumn == -1) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "未找到'单号'或'物流'列");
return AjaxResult.error("未找到'单号'或'物流'列,请检查表头配置");
}
log.info("表头解析完成 - 单号列: {}, 物流列: {}", orderNoColumn, logisticsColumn);
// 6. 读取数据区域,查找指定单号
// 7. 读取数据区域,查找指定单号
String dataRange = String.format("A%d:Z%d", configStartRow, configStartRow + 999);
JSONObject data = tencentDocService.readSheetData(accessToken, fileId, sheetId, dataRange);
if (data == null || !data.containsKey("gridData")) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "无法读取数据区域");
return AjaxResult.error("无法读取数据区域");
}
@@ -549,10 +585,12 @@ public class TencentDocController extends BaseController {
JSONArray dataRows = dataGridData.getJSONArray("rows");
if (dataRows == null || dataRows.isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "数据区域为空");
return AjaxResult.error("数据区域为空");
}
// 7. 查找匹配的单号
// 8. 查找匹配的单号
int targetRow = -1;
for (int i = 0; i < dataRows.size(); i++) {
JSONObject row = dataRows.getJSONObject(i);
@@ -571,23 +609,85 @@ public class TencentDocController extends BaseController {
}
if (targetRow == -1) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "未找到单号");
return AjaxResult.error("未找到单号: " + thirdPartyOrderNo);
}
log.info("找到单号 {} 在第 {} 行", thirdPartyOrderNo, targetRow);
// 8. 获取用户信息(获取 openId
// 9. 再次读取该行数据,验证单号和物流链接列是否为空(防止并发覆盖
String verifyRange = String.format("A%d:Z%d", targetRow, targetRow);
JSONObject verifyData = tencentDocService.readSheetData(accessToken, fileId, sheetId, verifyRange);
if (verifyData == null || !verifyData.containsKey("gridData")) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"FAILED", "验证读取失败");
return AjaxResult.error("验证读取失败");
}
JSONObject verifyGridData = verifyData.getJSONObject("gridData");
JSONArray verifyRows = verifyGridData.getJSONArray("rows");
if (verifyRows == null || verifyRows.isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"FAILED", "验证行数据为空");
return AjaxResult.error("验证行数据为空");
}
JSONObject verifyRowData = verifyRows.getJSONObject(0);
JSONArray verifyCells = verifyRowData.getJSONArray("values");
// 验证单号是否仍然匹配
if (verifyCells == null || verifyCells.size() <= orderNoColumn) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"SKIPPED", "验证时单号列为空,行已变化");
return AjaxResult.error("验证失败:行数据已变化,单号列为空");
}
JSONObject verifyOrderNoCell = verifyCells.getJSONObject(orderNoColumn);
String verifyOrderNo = null;
if (verifyOrderNoCell.containsKey("cellValue")) {
verifyOrderNo = verifyOrderNoCell.getJSONObject("cellValue").getString("text");
}
if (!thirdPartyOrderNo.equals(verifyOrderNo)) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"SKIPPED", String.format("验证失败:单号不匹配,期望 %s实际 %s", thirdPartyOrderNo, verifyOrderNo));
return AjaxResult.error("验证失败:单号不匹配,行数据已变化");
}
// 验证物流链接列是否为空
if (verifyCells.size() > logisticsColumn) {
JSONObject logisticsCell = verifyCells.getJSONObject(logisticsColumn);
if (logisticsCell.containsKey("cellValue")) {
String existingLogistics = logisticsCell.getJSONObject("cellValue").getString("text");
if (existingLogistics != null && !existingLogistics.trim().isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"SKIPPED", String.format("物流链接列已有值:%s", existingLogistics));
return AjaxResult.error(String.format("该订单物流链接已存在:%s", existingLogistics));
}
}
}
log.info("✓ 验证通过 - 单号匹配且物流列为空,可以写入");
// 10. 获取用户信息(获取 openId
JSONObject userInfo = com.ruoyi.jarvis.util.TencentDocApiUtil.getUserInfo(accessToken);
JSONObject userData = userInfo.getJSONObject("data");
if (userData == null) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"FAILED", "无法获取用户数据");
return AjaxResult.error("无法获取用户数据");
}
String openId = userData.getString("openID");
if (openId == null || openId.isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"FAILED", "无法获取Open-Id");
return AjaxResult.error("无法获取Open-Id");
}
// 9. 写入物流链接
// 11. 写入物流链接
com.ruoyi.jarvis.util.TencentDocApiUtil.writeSheetData(
accessToken,
tencentDocConfig.getAppId(),
@@ -599,6 +699,12 @@ public class TencentDocController extends BaseController {
tencentDocConfig.getApiBaseUrl()
);
// 12. 记录成功日志
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"SUCCESS", null);
log.info("✓ 物流链接写入成功 - 单号: {}, 行: {}, 链接: {}", thirdPartyOrderNo, targetRow, logisticsLink);
JSONObject result = new JSONObject();
result.put("thirdPartyOrderNo", thirdPartyOrderNo);
result.put("logisticsLink", logisticsLink);
@@ -609,7 +715,42 @@ public class TencentDocController extends BaseController {
} catch (Exception e) {
log.error("填充物流链接失败", e);
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, null,
"FAILED", "异常: " + e.getMessage());
return AjaxResult.error("填充物流链接失败: " + e.getMessage());
} finally {
// 释放分布式锁
if (lockKey != null) {
try {
redisCache.deleteObject(lockKey);
log.info("✓ 释放分布式锁 - lockKey: {}", lockKey);
} catch (Exception e) {
log.error("释放分布式锁失败", e);
}
}
}
}
/**
* 记录操作日志到数据库
*/
private void logOperation(String fileId, String sheetId, String operationType,
String orderNo, Integer targetRow, String logisticsLink,
String status, String errorMessage) {
try {
com.ruoyi.jarvis.domain.TencentDocOperationLog log = new com.ruoyi.jarvis.domain.TencentDocOperationLog();
log.setFileId(fileId);
log.setSheetId(sheetId);
log.setOperationType(operationType);
log.setOrderNo(orderNo);
log.setTargetRow(targetRow);
log.setLogisticsLink(logisticsLink);
log.setOperationStatus(status);
log.setErrorMessage(errorMessage);
log.setOperator(getUsername()); // 从BaseController继承的方法
operationLogMapper.insertLog(log);
} catch (Exception e) {
TencentDocController.log.error("记录操作日志失败", e);
}
}