From 440df7d5387f17ec9d99359933e5c152e7e3d2bc Mon Sep 17 00:00:00 2001 From: Leo Date: Tue, 6 Jan 2026 18:50:24 +0800 Subject: [PATCH] 1 --- .../service/impl/TencentDocServiceImpl.java | 216 +++++++++--------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java index f9e7a88..d865dd9 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java @@ -17,32 +17,32 @@ import java.util.List; /** * 腾讯文档服务实现类 - * + * * @author system */ @Service public class TencentDocServiceImpl implements ITencentDocService { - + private static final Logger log = LoggerFactory.getLogger(TencentDocServiceImpl.class); - + @Autowired private TencentDocConfig tencentDocConfig; - + @Autowired private com.ruoyi.common.core.redis.RedisCache redisCache; - + @Override public String getAuthUrl() { if (tencentDocConfig == null) { throw new RuntimeException("腾讯文档配置未加载,请检查TencentDocConfig是否正确注入"); } - + String appId = tencentDocConfig.getAppId(); String redirectUri = tencentDocConfig.getRedirectUri(); String oauthUrl = tencentDocConfig.getOauthUrl(); - + log.debug("获取授权URL - appId: {}, redirectUri: {}, oauthUrl: {}", appId, redirectUri, oauthUrl); - + // 验证配置参数 if (appId == null || appId.trim().isEmpty()) { throw new RuntimeException("腾讯文档应用ID未配置,请检查application-dev.yml中的tencent.doc.app-id配置"); @@ -54,7 +54,7 @@ public class TencentDocServiceImpl implements ITencentDocService { oauthUrl = "https://docs.qq.com/oauth/v2/authorize"; // 使用默认值 log.warn("OAuth URL未配置,使用默认值: {}", oauthUrl); } - + // 构建授权URL(根据腾讯文档官方文档:https://docs.qq.com/open/document/app/oauth2/authorize.html) StringBuilder authUrl = new StringBuilder(); authUrl.append(oauthUrl); @@ -72,16 +72,16 @@ public class TencentDocServiceImpl implements ITencentDocService { } authUrl.append("&response_type=code"); authUrl.append("&scope=all"); // 根据官方文档,scope固定为all - + // 添加state参数(用于防CSRF攻击,可选但建议带上) String state = java.util.UUID.randomUUID().toString(); authUrl.append("&state=").append(state); - + String result = authUrl.toString(); log.info("生成授权URL: {}", result); return result; } - + @Override public JSONObject getAccessTokenByCode(String code) { try { @@ -97,7 +97,7 @@ public class TencentDocServiceImpl implements ITencentDocService { throw new RuntimeException("获取访问令牌失败: " + e.getMessage(), e); } } - + @Override public JSONObject refreshAccessToken(String refreshToken) { try { @@ -112,14 +112,14 @@ public class TencentDocServiceImpl implements ITencentDocService { throw new RuntimeException("刷新访问令牌失败: " + e.getMessage(), e); } } - + @Override public JSONObject uploadLogisticsToSheet(String accessToken, String fileId, String sheetId, List orders) { try { if (orders == null || orders.isEmpty()) { throw new IllegalArgumentException("订单列表不能为空"); } - + // 获取用户信息(包含Open-Id) // 官方响应格式:{ "ret": 0, "msg": "Succeed", "data": { "openID": "xxx", ... } } JSONObject userInfo = TencentDocApiUtil.getUserInfo(accessToken); @@ -131,11 +131,11 @@ public class TencentDocServiceImpl implements ITencentDocService { if (openId == null || openId.isEmpty()) { throw new RuntimeException("无法获取Open-Id,请检查Access Token是否有效"); } - + // 构建要写入的数据(二维数组格式) JSONArray values = new JSONArray(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - + for (JDOrder order : orders) { JSONArray row = new JSONArray(); // 根据表格列顺序添加数据 @@ -150,18 +150,18 @@ public class TencentDocServiceImpl implements ITencentDocService { row.add(order.getPaymentAmount() != null ? order.getPaymentAmount().toString() : ""); row.add(order.getRebateAmount() != null ? order.getRebateAmount().toString() : ""); row.add(order.getStatus() != null ? order.getStatus() : ""); - + values.add(row); } - + // 追加数据到表格 return TencentDocApiUtil.appendSheetData( - accessToken, - tencentDocConfig.getAppId(), - openId, - fileId, - sheetId, - values, + accessToken, + tencentDocConfig.getAppId(), + openId, + fileId, + sheetId, + values, tencentDocConfig.getApiBaseUrl() ); } catch (Exception e) { @@ -169,38 +169,38 @@ public class TencentDocServiceImpl implements ITencentDocService { throw new RuntimeException("上传物流信息失败: " + e.getMessage(), e); } } - + @Override public JSONObject appendLogisticsToSheet(String accessToken, String fileId, String sheetId, Integer startRow, JDOrder order) { try { if (order == null) { throw new IllegalArgumentException("订单信息不能为空"); } - - log.info("录单自动写入腾讯文档 - fileId: {}, sheetId: {}, startRow: {}, 订单单号: {}", + + log.info("录单自动写入腾讯文档 - fileId: {}, sheetId: {}, startRow: {}, 订单单号: {}", fileId, sheetId, startRow, order.getThirdPartyOrderNo()); - + // 1. 读取表头(从Redis或配置获取headerRow) final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:"; Integer headerRowNum = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "headerRow"); if (headerRowNum == null) { headerRowNum = tencentDocConfig.getHeaderRow(); } - + String headerRange = String.format("A%d:Z%d", headerRowNum, headerRowNum); JSONObject headerData = readSheetData(accessToken, fileId, sheetId, headerRange); - + if (headerData == null || !headerData.containsKey("values")) { throw new RuntimeException("无法读取表头数据"); } - + JSONArray headerValues = headerData.getJSONArray("values"); if (headerValues == null || headerValues.isEmpty()) { throw new RuntimeException("表头数据为空"); } - + JSONArray headerCells = headerValues.getJSONArray(0); - + // 2. 识别列位置(根据表头) Integer dateColumn = null; Integer companyColumn = null; @@ -214,7 +214,7 @@ public class TencentDocServiceImpl implements ITencentDocService { Integer remarkColumn = null; Integer arrangedColumn = null; // 是否安排列 Integer logisticsColumn = null; - + for (int i = 0; i < headerCells.size(); i++) { String cellText = headerCells.getString(i); if (cellText != null) { @@ -232,24 +232,24 @@ public class TencentDocServiceImpl implements ITencentDocService { else if (cellText.contains("物流")) logisticsColumn = i; } } - + if (orderNoColumn == null) { throw new RuntimeException("未找到'单号'列,请检查表头配置"); } - + log.info("表头识别完成 - 单号列: {}, 物流列: {}", orderNoColumn, logisticsColumn); - + // 3. 读取数据区域,查找第一个空行(单号列为空) String dataRange = String.format("A%d:Z%d", startRow, startRow + 999); JSONObject sheetData = readSheetData(accessToken, fileId, sheetId, dataRange); - + if (sheetData == null || !sheetData.containsKey("values")) { throw new RuntimeException("无法读取数据区域"); } - + JSONArray dataRows = sheetData.getJSONArray("values"); int targetRow = -1; - + // 查找第一个单号列为空的行 if (dataRows == null || dataRows.isEmpty()) { // 数据区域完全为空,使用startRow @@ -267,22 +267,22 @@ public class TencentDocServiceImpl implements ITencentDocService { break; } } - + // 如果没找到空行,追加到数据区域末尾 if (targetRow == -1) { targetRow = startRow + dataRows.size(); } } - + log.info("找到空行位置:第 {} 行", targetRow); - + // 4. 构建要写入的数据(按表头列顺序) SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMdd"); String today = dateFormat.format(new java.util.Date()); - + JSONArray requests = new JSONArray(); int rowIndex = targetRow - 1; // 转为0索引 - + // 写入各列数据 if (dateColumn != null) { requests.add(buildUpdateRequest(sheetId, rowIndex, dateColumn, today, false)); @@ -298,13 +298,13 @@ public class TencentDocServiceImpl implements ITencentDocService { } // 数量列 - JDOrder 没有 quantity 字段,暂时跳过 // if (quantityColumn != null) { ... } - + if (nameColumn != null && order.getBuyer() != null) { requests.add(buildUpdateRequest(sheetId, rowIndex, nameColumn, order.getBuyer(), false)); } // 电话列 - JDOrder 没有 phone 字段,暂时跳过 // if (phoneColumn != null) { ... } - + if (addressColumn != null && order.getAddress() != null) { requests.add(buildUpdateRequest(sheetId, rowIndex, addressColumn, order.getAddress(), false)); } @@ -322,49 +322,49 @@ public class TencentDocServiceImpl implements ITencentDocService { if (logisticsColumn != null && order.getLogisticsLink() != null && !order.getLogisticsLink().isEmpty()) { requests.add(buildUpdateRequest(sheetId, rowIndex, logisticsColumn, order.getLogisticsLink(), true)); // 超链接 } - + // 5. 使用 batchUpdate 写入 if (requests.isEmpty()) { throw new RuntimeException("没有数据可以写入"); } - + JSONObject batchUpdateBody = new JSONObject(); batchUpdateBody.put("requests", requests); - + JSONObject result = batchUpdate(accessToken, fileId, batchUpdateBody); - + log.info("✓ 订单成功写入腾讯文档 - 行: {}, 单号: {}", targetRow, order.getThirdPartyOrderNo()); - + JSONObject response = new JSONObject(); response.put("row", targetRow); response.put("orderNo", order.getThirdPartyOrderNo()); response.put("success", true); - + return response; } catch (Exception e) { log.error("追加物流信息到表格失败", e); throw new RuntimeException("追加物流信息失败: " + e.getMessage(), e); } } - + /** * 构建单元格更新请求 */ private JSONObject buildUpdateRequest(String sheetId, int rowIndex, int columnIndex, String value, boolean isLink) { JSONObject updateRangeRequest = new JSONObject(); updateRangeRequest.put("sheetId", sheetId); - + JSONObject gridData = new JSONObject(); gridData.put("startRow", rowIndex); gridData.put("startColumn", columnIndex); - + JSONArray rows = new JSONArray(); JSONObject rowData = new JSONObject(); JSONArray cellValues = new JSONArray(); - + JSONObject cellData = new JSONObject(); JSONObject cellValue = new JSONObject(); - + if (isLink) { JSONObject link = new JSONObject(); link.put("url", value); @@ -373,64 +373,64 @@ public class TencentDocServiceImpl implements ITencentDocService { } else { cellValue.put("text", value); } - + cellData.put("cellValue", cellValue); cellValues.add(cellData); - + rowData.put("values", cellValues); rows.add(rowData); gridData.put("rows", rows); - + updateRangeRequest.put("gridData", gridData); - + JSONObject request = new JSONObject(); request.put("updateRangeRequest", updateRangeRequest); - + return request; } - + @Override public JSONObject readSheetData(String accessToken, String fileId, String sheetId, String range) { try { log.info("Service层 - 开始读取表格数据: fileId={}, sheetId={}, range={}", fileId, sheetId, range); - + // 获取用户信息(包含Open-Id) // 官方响应格式:{ "ret": 0, "msg": "Succeed", "data": { "openID": "xxx", ... } } log.debug("正在获取用户信息..."); JSONObject userInfo = TencentDocApiUtil.getUserInfo(accessToken); log.debug("用户信息响应: {}", userInfo != null ? userInfo.toJSONString() : "null"); - + if (userInfo == null) { throw new RuntimeException("getUserInfo 返回 null,Access Token 可能无效"); } - + JSONObject data = userInfo.getJSONObject("data"); if (data == null) { log.error("用户信息响应中没有 data 字段,完整响应: {}", userInfo.toJSONString()); throw new RuntimeException("无法获取用户数据,请检查Access Token是否有效。响应: " + userInfo.toJSONString()); } - + String openId = data.getString("openID"); // 注意:官方返回的字段名是 openID(大写ID) if (openId == null || openId.isEmpty()) { log.error("data 对象中没有 openID 字段,data内容: {}", data.toJSONString()); throw new RuntimeException("无法获取Open-Id,请检查Access Token是否有效。data: " + data.toJSONString()); } - + log.info("成功获取 Open ID: {}", openId); log.info("准备调用API - appId: {}, apiBaseUrl: {}", tencentDocConfig.getAppId(), tencentDocConfig.getApiBaseUrl()); - + JSONObject result = TencentDocApiUtil.readSheetData( - accessToken, - tencentDocConfig.getAppId(), - openId, - fileId, - sheetId, - range, + accessToken, + tencentDocConfig.getAppId(), + openId, + fileId, + sheetId, + range, tencentDocConfig.getApiBaseUrl() ); - - log.info("API调用成功,原始返回结果: {}", result != null ? result.toJSONString() : "null"); - + + //log.info("API调用成功,原始返回结果: {}", result != null ? result.toJSONString() : "null"); + // 检查API响应中的错误码 // 根据官方文档,成功响应包含 ret=0,错误响应包含 code!=0 // 参考:https://docs.qq.com/open/document/app/openapi/v3/sheet/get/get_range.html @@ -454,27 +454,27 @@ public class TencentDocServiceImpl implements ITencentDocService { } } } - + // 解析数据为统一的简单格式 JSONArray parsedValues = TencentDocDataParser.parseToSimpleArray(result); log.info("解析后的数据行数: {}", parsedValues != null ? parsedValues.size() : 0); if (parsedValues != null && !parsedValues.isEmpty()) { TencentDocDataParser.printDataStructure(result, 3); } - + // 返回包含简化格式的响应 JSONObject response = new JSONObject(); response.put("values", parsedValues); response.put("_原始数据", result); // 保留原始数据供调试 - + return response; - + } catch (Exception e) { log.error("读取表格数据失败 - fileId: {}, sheetId: {}, range: {}", fileId, sheetId, range, e); throw new RuntimeException("读取表格数据失败: " + e.getMessage(), e); } } - + @Override public JSONObject writeSheetData(String accessToken, String fileId, String sheetId, String range, Object values) { try { @@ -489,15 +489,15 @@ public class TencentDocServiceImpl implements ITencentDocService { if (openId == null || openId.isEmpty()) { throw new RuntimeException("无法获取Open-Id,请检查Access Token是否有效"); } - + return TencentDocApiUtil.writeSheetData( - accessToken, - tencentDocConfig.getAppId(), - openId, - fileId, - sheetId, - range, - values, + accessToken, + tencentDocConfig.getAppId(), + openId, + fileId, + sheetId, + range, + values, tencentDocConfig.getApiBaseUrl() ); } catch (Exception e) { @@ -505,7 +505,7 @@ public class TencentDocServiceImpl implements ITencentDocService { throw new RuntimeException("写入表格数据失败: " + e.getMessage(), e); } } - + @Override public JSONObject getFileInfo(String accessToken, String fileId) { try { @@ -520,12 +520,12 @@ public class TencentDocServiceImpl implements ITencentDocService { if (openId == null || openId.isEmpty()) { throw new RuntimeException("无法获取Open-Id,请检查Access Token是否有效"); } - + return TencentDocApiUtil.getFileInfo( - accessToken, - tencentDocConfig.getAppId(), - openId, - fileId, + accessToken, + tencentDocConfig.getAppId(), + openId, + fileId, tencentDocConfig.getApiBaseUrl() ); } catch (Exception e) { @@ -533,7 +533,7 @@ public class TencentDocServiceImpl implements ITencentDocService { throw new RuntimeException("获取文件信息失败: " + e.getMessage(), e); } } - + @Override public JSONObject getSheetList(String accessToken, String fileId) { try { @@ -548,12 +548,12 @@ public class TencentDocServiceImpl implements ITencentDocService { if (openId == null || openId.isEmpty()) { throw new RuntimeException("无法获取Open-Id,请检查Access Token是否有效"); } - + return TencentDocApiUtil.getSheetList( - accessToken, - tencentDocConfig.getAppId(), - openId, - fileId, + accessToken, + tencentDocConfig.getAppId(), + openId, + fileId, tencentDocConfig.getApiBaseUrl() ); } catch (Exception e) { @@ -561,7 +561,7 @@ public class TencentDocServiceImpl implements ITencentDocService { throw new RuntimeException("获取工作表列表失败: " + e.getMessage(), e); } } - + @Override public JSONObject getUserInfo(String accessToken) { try { @@ -571,7 +571,7 @@ public class TencentDocServiceImpl implements ITencentDocService { throw new RuntimeException("获取用户信息失败: " + e.getMessage(), e); } } - + @Override public JSONObject batchUpdate(String accessToken, String fileId, JSONObject requestBody) { try { @@ -585,7 +585,7 @@ public class TencentDocServiceImpl implements ITencentDocService { if (openId == null || openId.isEmpty()) { throw new RuntimeException("无法获取Open-Id,请检查Access Token是否有效"); } - + return TencentDocApiUtil.batchUpdate( accessToken, tencentDocConfig.getAppId(),