This commit is contained in:
2025-11-04 22:59:55 +08:00
parent 0146e0776a
commit 41f338446d
7 changed files with 1386 additions and 0 deletions

View File

@@ -0,0 +1,520 @@
package com.ruoyi.web.controller.jarvis;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.jarvis.domain.JDOrder;
import com.ruoyi.jarvis.service.ITencentDocService;
import com.ruoyi.jarvis.service.IJDOrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 腾讯文档控制器
*
* @author system
*/
@RestController
@RequestMapping("/jarvis/tendoc")
public class TencentDocController extends BaseController {
private static final Logger log = LoggerFactory.getLogger(TencentDocController.class);
@Autowired
private ITencentDocService tencentDocService;
@Autowired
private IJDOrderService jdOrderService;
@Autowired
private RedisCache redisCache;
/** Redis key前缀用于存储上次处理的最大行数 */
private static final String LAST_PROCESSED_ROW_KEY_PREFIX = "tendoc:last_row:";
/**
* 获取授权URL
*/
@GetMapping("/authUrl")
public AjaxResult getAuthUrl() {
try {
String authUrl = tencentDocService.getAuthUrl();
return AjaxResult.success("获取授权URL成功", authUrl);
} catch (Exception e) {
log.error("获取授权URL失败", e);
return AjaxResult.error("获取授权URL失败: " + e.getMessage());
}
}
/**
* OAuth回调 - 通过授权码获取访问令牌
*/
@GetMapping("/oauth/callback")
public AjaxResult oauthCallback(@RequestParam("code") String code) {
try {
JSONObject tokenInfo = tencentDocService.getAccessTokenByCode(code);
return AjaxResult.success("授权成功", tokenInfo);
} catch (Exception e) {
log.error("OAuth回调处理失败", e);
return AjaxResult.error("授权失败: " + e.getMessage());
}
}
/**
* 刷新访问令牌
*/
@PostMapping("/refreshToken")
public AjaxResult refreshToken(@RequestBody Map<String, String> params) {
try {
String refreshToken = params.get("refreshToken");
if (refreshToken == null || refreshToken.isEmpty()) {
return AjaxResult.error("refreshToken不能为空");
}
JSONObject tokenInfo = tencentDocService.refreshAccessToken(refreshToken);
return AjaxResult.success("刷新令牌成功", tokenInfo);
} catch (Exception e) {
log.error("刷新访问令牌失败", e);
return AjaxResult.error("刷新令牌失败: " + e.getMessage());
}
}
/**
* 将订单物流信息上传到腾讯文档表格
*/
@PostMapping("/uploadLogistics")
public AjaxResult uploadLogistics(@RequestBody Map<String, Object> params) {
try {
String accessToken = (String) params.get("accessToken");
String fileId = (String) params.get("fileId");
String sheetId = (String) params.get("sheetId");
if (accessToken == null || fileId == null || sheetId == null) {
return AjaxResult.error("accessToken、fileId和sheetId不能为空");
}
// 获取订单ID列表
@SuppressWarnings("unchecked")
List<Long> orderIds = (List<Long>) params.get("orderIds");
if (orderIds == null || orderIds.isEmpty()) {
return AjaxResult.error("订单ID列表不能为空");
}
// 查询订单信息
List<JDOrder> orders = new java.util.ArrayList<>();
for (Long orderId : orderIds) {
JDOrder order = jdOrderService.selectJDOrderById(orderId);
if (order != null) {
orders.add(order);
}
}
if (orders.isEmpty()) {
return AjaxResult.error("未找到有效的订单信息");
}
JSONObject result = tencentDocService.uploadLogisticsToSheet(accessToken, fileId, sheetId, orders);
return AjaxResult.success("上传物流信息成功", result);
} catch (Exception e) {
log.error("上传物流信息失败", e);
return AjaxResult.error("上传物流信息失败: " + e.getMessage());
}
}
/**
* 将单个订单的物流信息追加到表格
*/
@PostMapping("/appendLogistics")
public AjaxResult appendLogistics(@RequestBody Map<String, Object> params) {
try {
String accessToken = (String) params.get("accessToken");
String fileId = (String) params.get("fileId");
String sheetId = (String) params.get("sheetId");
Long orderId = params.get("orderId") != null ? Long.valueOf(params.get("orderId").toString()) : null;
if (accessToken == null || fileId == null || sheetId == null || orderId == null) {
return AjaxResult.error("accessToken、fileId、sheetId和orderId不能为空");
}
// 查询订单信息
JDOrder order = jdOrderService.selectJDOrderById(orderId);
if (order == null) {
return AjaxResult.error("未找到订单信息");
}
JSONObject result = tencentDocService.appendLogisticsToSheet(accessToken, fileId, sheetId, order);
return AjaxResult.success("追加物流信息成功", result);
} catch (Exception e) {
log.error("追加物流信息失败", e);
return AjaxResult.error("追加物流信息失败: " + e.getMessage());
}
}
/**
* 读取表格数据
*/
@GetMapping("/readSheet")
public AjaxResult readSheet(@RequestParam("accessToken") String accessToken,
@RequestParam("fileId") String fileId,
@RequestParam("sheetId") String sheetId,
@RequestParam(value = "range", defaultValue = "A1:Z100") String range) {
try {
JSONObject result = tencentDocService.readSheetData(accessToken, fileId, sheetId, range);
return AjaxResult.success("读取表格数据成功", result);
} catch (Exception e) {
log.error("读取表格数据失败", e);
return AjaxResult.error("读取表格数据失败: " + e.getMessage());
}
}
/**
* 获取文件信息
*/
@GetMapping("/fileInfo")
public AjaxResult getFileInfo(@RequestParam("accessToken") String accessToken,
@RequestParam("fileId") String fileId) {
try {
JSONObject result = tencentDocService.getFileInfo(accessToken, fileId);
return AjaxResult.success("获取文件信息成功", result);
} catch (Exception e) {
log.error("获取文件信息失败", e);
return AjaxResult.error("获取文件信息失败: " + e.getMessage());
}
}
/**
* 获取工作表列表
*/
@GetMapping("/sheetList")
public AjaxResult getSheetList(@RequestParam("accessToken") String accessToken,
@RequestParam("fileId") String fileId) {
try {
JSONObject result = tencentDocService.getSheetList(accessToken, fileId);
return AjaxResult.success("获取工作表列表成功", result);
} catch (Exception e) {
log.error("获取工作表列表失败", e);
return AjaxResult.error("获取工作表列表失败: " + e.getMessage());
}
}
/**
* 自动发货 - 将指定订单的物流信息上传到腾讯文档并标记为已发货
*/
@PostMapping("/autoShip")
public AjaxResult autoShip(@RequestBody Map<String, Object> params) {
try {
String accessToken = (String) params.get("accessToken");
String fileId = (String) params.get("fileId");
String sheetId = (String) params.get("sheetId");
Long orderId = params.get("orderId") != null ? Long.valueOf(params.get("orderId").toString()) : null;
if (accessToken == null || fileId == null || sheetId == null || orderId == null) {
return AjaxResult.error("accessToken、fileId、sheetId和orderId不能为空");
}
// 查询订单信息
JDOrder order = jdOrderService.selectJDOrderById(orderId);
if (order == null) {
return AjaxResult.error("未找到订单信息");
}
// 检查是否有物流链接
if (order.getLogisticsLink() == null || order.getLogisticsLink().trim().isEmpty()) {
return AjaxResult.error("订单缺少物流链接,无法自动发货");
}
// 上传物流信息到腾讯文档
JSONObject uploadResult = tencentDocService.appendLogisticsToSheet(accessToken, fileId, sheetId, order);
// 更新订单状态为已发货(可选)
// order.setStatus("已发货");
// jdOrderService.updateJDOrder(order);
JSONObject result = new JSONObject();
result.put("uploadResult", uploadResult);
result.put("orderId", orderId);
result.put("message", "物流信息已上传到腾讯文档,自动发货成功");
return AjaxResult.success("自动发货成功", result);
} catch (Exception e) {
log.error("自动发货失败", e);
return AjaxResult.error("自动发货失败: " + e.getMessage());
}
}
/**
* 根据单号填充物流链接 - 读取表格数据,根据单号查询订单系统中的物流链接,并填充到表格
* 优化:记录上次处理的最大行数,每次从最大行数-100开始读取避免重复处理历史数据
*/
@PostMapping("/fillLogisticsByOrderNo")
public AjaxResult fillLogisticsByOrderNo(@RequestBody Map<String, Object> params) {
try {
String accessToken = (String) params.get("accessToken");
String fileId = (String) params.get("fileId");
String sheetId = (String) params.get("sheetId");
// 可选参数:指定列位置
Integer orderNoColumn = params.get("orderNoColumn") != null ?
Integer.valueOf(params.get("orderNoColumn").toString()) : null; // 单号列索引从0开始
Integer logisticsLinkColumn = params.get("logisticsLinkColumn") != null ?
Integer.valueOf(params.get("logisticsLinkColumn").toString()) : null; // 物流链接列索引从0开始
Integer headerRow = params.get("headerRow") != null ?
Integer.valueOf(params.get("headerRow").toString()) : 1; // 表头所在行默认第1行从1开始
// 可选参数是否强制从指定行开始如果为true则忽略Redis记录的最大行数
Boolean forceStart = params.get("forceStart") != null ?
Boolean.valueOf(params.get("forceStart").toString()) : false;
Integer forceStartRow = params.get("forceStartRow") != null ?
Integer.valueOf(params.get("forceStartRow").toString()) : null;
if (accessToken == null || fileId == null || sheetId == null) {
return AjaxResult.error("accessToken、fileId和sheetId不能为空");
}
// 生成Redis key用于存储该文件的工作表的上次处理最大行数
String redisKey = LAST_PROCESSED_ROW_KEY_PREFIX + fileId + ":" + sheetId;
// 获取上次处理的最大行数
Integer lastMaxRow = null;
if (!forceStart && redisCache.hasKey(redisKey)) {
Object cacheObj = redisCache.getCacheObject(redisKey);
if (cacheObj != null) {
lastMaxRow = Integer.valueOf(cacheObj.toString());
}
}
// 计算起始行:从上次最大行数-100开始或者从强制指定的行开始或者从表头下一行开始
int startRow;
if (forceStartRow != null) {
startRow = forceStartRow;
} else if (lastMaxRow != null && lastMaxRow > headerRow) {
startRow = Math.max(headerRow + 1, lastMaxRow - 100); // 从最大行数-100开始但至少是表头下一行
} else {
startRow = headerRow + 1; // 默认从表头下一行开始
}
// 计算读取范围从起始行开始读取足够多的行假设每次最多处理200行
int endRow = startRow + 200; // 每次最多读取200行
String startColumn = "A";
String endColumn = "Z";
String range = String.format("%s%d:%s%d", startColumn, startRow, endColumn, endRow);
log.info("开始填充物流链接 - 文件ID: {}, 工作表ID: {}, 起始行: {}, 结束行: {}, 上次最大行: {}",
fileId, sheetId, startRow, endRow, lastMaxRow);
// 读取表格数据(先读取表头行用于识别列位置)
String headerRange = String.format("%s%d:%s%d", startColumn, headerRow, endColumn, headerRow);
JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange);
JSONArray headerValues = headerData.getJSONArray("values");
if (headerValues == null || headerValues.isEmpty()) {
return AjaxResult.error("无法读取表头请检查headerRow参数");
}
// 自动识别列位置(如果未指定)
if (orderNoColumn == null || logisticsLinkColumn == null) {
JSONArray headerRowData = headerValues.getJSONArray(0);
if (headerRowData == null || headerRowData.isEmpty()) {
return AjaxResult.error("无法识别表头,请手动指定列位置");
}
// 查找单号列和物流链接列
for (int i = 0; i < headerRowData.size(); i++) {
String cellValue = headerRowData.getString(i);
if (cellValue != null) {
String cellValueLower = cellValue.toLowerCase().trim();
if (orderNoColumn == null && (cellValueLower.contains("单号") || cellValueLower.contains("order"))) {
orderNoColumn = i;
}
if (logisticsLinkColumn == null && (cellValueLower.contains("物流链接") ||
(cellValueLower.contains("物流") && cellValueLower.contains("链接")))) {
logisticsLinkColumn = i;
}
}
}
if (orderNoColumn == null) {
return AjaxResult.error("无法找到单号列请手动指定orderNoColumn参数");
}
if (logisticsLinkColumn == null) {
return AjaxResult.error("无法找到物流链接列请手动指定logisticsLinkColumn参数");
}
}
// 读取数据行
JSONObject sheetData = tencentDocService.readSheetData(accessToken, fileId, sheetId, range);
JSONArray values = sheetData.getJSONArray("values");
if (values == null || values.isEmpty()) {
log.info("指定范围内没有数据,可能已处理完毕");
JSONObject result = new JSONObject();
result.put("startRow", startRow);
result.put("endRow", endRow);
result.put("lastMaxRow", lastMaxRow);
result.put("filledCount", 0);
result.put("skippedCount", 0);
result.put("errorCount", 0);
result.put("message", "指定范围内没有数据");
return AjaxResult.success("没有需要处理的数据", result);
}
// 处理数据行
int filledCount = 0;
int skippedCount = 0;
int errorCount = 0;
int currentMaxRow = startRow - 1; // 记录当前处理的最大行号Excel行号从1开始
JSONArray updates = new JSONArray(); // 存储需要更新的行和值
for (int i = 0; i < values.size(); i++) {
JSONArray row = values.getJSONArray(i);
if (row == null || row.size() <= Math.max(orderNoColumn, logisticsLinkColumn)) {
continue; // 跳过空行或列数不足的行
}
// 计算实际的行号Excel行号从1开始
int excelRow = startRow + i;
currentMaxRow = Math.max(currentMaxRow, excelRow);
// 获取单号
String orderNo = row.getString(orderNoColumn);
if (orderNo == null || orderNo.trim().isEmpty()) {
skippedCount++;
continue; // 跳过空单号的行
}
orderNo = orderNo.trim();
// 检查物流链接列是否已有值
String existingLogisticsLink = row.getString(logisticsLinkColumn);
if (existingLogisticsLink != null && !existingLogisticsLink.trim().isEmpty()) {
skippedCount++; // 已有物流链接,跳过
continue;
}
try {
// 根据单号查询订单
JDOrder order = jdOrderService.selectJDOrderByRemark(orderNo);
if (order != null && order.getLogisticsLink() != null && !order.getLogisticsLink().trim().isEmpty()) {
String logisticsLink = order.getLogisticsLink().trim();
// 构建更新请求
JSONObject update = new JSONObject();
update.put("row", excelRow);
update.put("column", logisticsLinkColumn);
update.put("orderNo", orderNo);
update.put("logisticsLink", logisticsLink);
updates.add(update);
filledCount++;
log.info("找到订单物流链接 - 单号: {}, 物流链接: {}, 行号: {}", orderNo, logisticsLink, excelRow);
} else {
errorCount++;
log.warn("未找到订单或物流链接为空 - 单号: {}, 行号: {}", orderNo, excelRow);
}
} catch (Exception e) {
errorCount++;
log.error("处理订单失败 - 单号: {}, 行号: {}", orderNo, excelRow, e);
}
}
// 批量更新表格
if (!updates.isEmpty()) {
// 将更新按行分组,批量写入
Map<Integer, JSONObject> rowUpdates = new java.util.HashMap<>();
for (int i = 0; i < updates.size(); i++) {
JSONObject update = updates.getJSONObject(i);
int row = update.getIntValue("row");
rowUpdates.put(row, update);
}
// 批量写入每行单独写入因为腾讯文档API可能不支持批量更新不同行
int successUpdates = 0;
for (Map.Entry<Integer, JSONObject> entry : rowUpdates.entrySet()) {
try {
int row = entry.getKey();
JSONObject update = entry.getValue();
String logisticsLink = update.getString("logisticsLink");
// 计算列字母A, B, C...
String columnLetter = getColumnLetter(logisticsLinkColumn);
String cellRange = columnLetter + row;
// 构建写入数据(二维数组格式)
JSONArray writeValues = new JSONArray();
JSONArray writeRow = new JSONArray();
writeRow.add(logisticsLink);
writeValues.add(writeRow);
// 写入单个单元格
tencentDocService.writeSheetData(accessToken, fileId, sheetId, cellRange, writeValues);
successUpdates++;
log.info("成功写入物流链接 - 单元格: {}, 单号: {}, 物流链接: {}", cellRange, update.getString("orderNo"), logisticsLink);
} catch (Exception e) {
log.error("写入物流链接失败 - 行: {}", entry.getKey(), e);
errorCount++;
}
// 添加延迟避免API调用频率过高
try {
Thread.sleep(100); // 100ms延迟
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
log.info("批量填充物流链接完成 - 成功: {}, 跳过: {}, 错误: {}", successUpdates, skippedCount, errorCount);
}
// 更新Redis中记录的最大行数如果本次处理了数据
if (currentMaxRow >= startRow) {
redisCache.setCacheObject(redisKey, currentMaxRow, 30, TimeUnit.DAYS);
log.info("更新上次处理的最大行数 - 文件ID: {}, 工作表ID: {}, 最大行: {}", fileId, sheetId, currentMaxRow);
}
JSONObject result = new JSONObject();
result.put("startRow", startRow);
result.put("endRow", endRow);
result.put("currentMaxRow", currentMaxRow);
result.put("lastMaxRow", lastMaxRow);
result.put("filledCount", filledCount);
result.put("skippedCount", skippedCount);
result.put("errorCount", errorCount);
result.put("orderNoColumn", orderNoColumn);
result.put("logisticsLinkColumn", logisticsLinkColumn);
result.put("message", String.format("处理完成:成功填充 %d 条,跳过 %d 条,错误 %d 条,最大行号: %d",
filledCount, skippedCount, errorCount, currentMaxRow));
return AjaxResult.success("填充物流链接完成", result);
} catch (Exception e) {
log.error("填充物流链接失败", e);
return AjaxResult.error("填充物流链接失败: " + e.getMessage());
}
}
/**
* 将列索引转换为Excel列字母0->A, 1->B, ..., 25->Z, 26->AA, ...
*/
private String getColumnLetter(int columnIndex) {
StringBuilder sb = new StringBuilder();
columnIndex++; // 转换为1-basedA=1, B=2, ...
while (columnIndex > 0) {
columnIndex--;
sb.insert(0, (char)('A' + (columnIndex % 26)));
columnIndex /= 26;
}
return sb.toString();
}
}

View File

@@ -186,3 +186,21 @@ xss:
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# 腾讯文档开放平台配置
tencent:
doc:
# 应用ID需要在腾讯文档开放平台申请
app-id: your_app_id
# 应用密钥(需要在腾讯文档开放平台申请)
app-secret: your_app_secret
# 授权回调地址(需要在腾讯文档开放平台配置)
redirect-uri: http://localhost:30313/jarvis/tendoc/oauth/callback
# API基础地址
api-base-url: https://docs.qq.com/open/v1
# OAuth授权地址
oauth-url: https://docs.qq.com/oauth/v2/authorize
# 获取Token地址
token-url: https://docs.qq.com/oauth/v2/token
# 刷新Token地址
refresh-token-url: https://docs.qq.com/oauth/v2/token