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,92 @@
package com.ruoyi.jarvis.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 腾讯文档开放平台配置
*
* @author system
*/
@Configuration
@ConfigurationProperties(prefix = "tencent.doc")
public class TencentDocConfig {
/** 应用ID */
private String appId;
/** 应用密钥 */
private String appSecret;
/** 授权回调地址 */
private String redirectUri;
/** API基础地址 */
private String apiBaseUrl = "https://docs.qq.com/open/v1";
/** OAuth授权地址 */
private String oauthUrl = "https://docs.qq.com/oauth/v2/authorize";
/** 获取Token地址 */
private String tokenUrl = "https://docs.qq.com/oauth/v2/token";
/** 刷新Token地址 */
private String refreshTokenUrl = "https://docs.qq.com/oauth/v2/token";
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
public String getRedirectUri() {
return redirectUri;
}
public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
public String getApiBaseUrl() {
return apiBaseUrl;
}
public void setApiBaseUrl(String apiBaseUrl) {
this.apiBaseUrl = apiBaseUrl;
}
public String getOauthUrl() {
return oauthUrl;
}
public void setOauthUrl(String oauthUrl) {
this.oauthUrl = oauthUrl;
}
public String getTokenUrl() {
return tokenUrl;
}
public void setTokenUrl(String tokenUrl) {
this.tokenUrl = tokenUrl;
}
public String getRefreshTokenUrl() {
return refreshTokenUrl;
}
public void setRefreshTokenUrl(String refreshTokenUrl) {
this.refreshTokenUrl = refreshTokenUrl;
}
}

View File

@@ -0,0 +1,101 @@
package com.ruoyi.jarvis.service;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.jarvis.domain.JDOrder;
import java.util.List;
/**
* 腾讯文档服务接口
*
* @author system
*/
public interface ITencentDocService {
/**
* 获取授权URL
*
* @return 授权URL
*/
String getAuthUrl();
/**
* 通过授权码获取访问令牌
*
* @param code 授权码
* @return 访问令牌信息
*/
JSONObject getAccessTokenByCode(String code);
/**
* 刷新访问令牌
*
* @param refreshToken 刷新令牌
* @return 新的访问令牌信息
*/
JSONObject refreshAccessToken(String refreshToken);
/**
* 将物流信息上传到腾讯文档表格
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param sheetId 工作表ID
* @param orders 订单列表
* @return 上传结果
*/
JSONObject uploadLogisticsToSheet(String accessToken, String fileId, String sheetId, List<JDOrder> orders);
/**
* 将单个订单的物流信息追加到表格
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param sheetId 工作表ID
* @param order 订单信息
* @return 上传结果
*/
JSONObject appendLogisticsToSheet(String accessToken, String fileId, String sheetId, JDOrder order);
/**
* 读取表格数据
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param sheetId 工作表ID
* @param range 范围
* @return 表格数据
*/
JSONObject readSheetData(String accessToken, String fileId, String sheetId, String range);
/**
* 写入表格数据
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param sheetId 工作表ID
* @param range 范围,例如 "A1"
* @param values 要写入的数据,二维数组格式
* @return 写入结果
*/
JSONObject writeSheetData(String accessToken, String fileId, String sheetId, String range, Object values);
/**
* 获取文件信息
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @return 文件信息
*/
JSONObject getFileInfo(String accessToken, String fileId);
/**
* 获取工作表列表
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @return 工作表列表
*/
JSONObject getSheetList(String accessToken, String fileId);
}

View File

@@ -0,0 +1,194 @@
package com.ruoyi.jarvis.service.impl;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.jarvis.config.TencentDocConfig;
import com.ruoyi.jarvis.domain.JDOrder;
import com.ruoyi.jarvis.service.ITencentDocService;
import com.ruoyi.jarvis.util.TencentDocApiUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
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;
@Override
public String getAuthUrl() {
String appId = tencentDocConfig.getAppId();
String redirectUri = tencentDocConfig.getRedirectUri();
String oauthUrl = tencentDocConfig.getOauthUrl();
// 构建授权URL
StringBuilder authUrl = new StringBuilder();
authUrl.append(oauthUrl);
authUrl.append("?client_id=").append(appId);
try {
authUrl.append("&redirect_uri=").append(java.net.URLEncoder.encode(redirectUri, "UTF-8"));
} catch (java.io.UnsupportedEncodingException e) {
log.error("URL编码失败", e);
authUrl.append("&redirect_uri=").append(redirectUri);
}
authUrl.append("&response_type=code");
authUrl.append("&scope=file.read_write");
return authUrl.toString();
}
@Override
public JSONObject getAccessTokenByCode(String code) {
try {
return TencentDocApiUtil.getAccessToken(
tencentDocConfig.getAppId(),
tencentDocConfig.getAppSecret(),
code,
tencentDocConfig.getRedirectUri(),
tencentDocConfig.getTokenUrl()
);
} catch (Exception e) {
log.error("通过授权码获取访问令牌失败", e);
throw new RuntimeException("获取访问令牌失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject refreshAccessToken(String refreshToken) {
try {
return TencentDocApiUtil.refreshAccessToken(
tencentDocConfig.getAppId(),
tencentDocConfig.getAppSecret(),
refreshToken,
tencentDocConfig.getRefreshTokenUrl()
);
} catch (Exception e) {
log.error("刷新访问令牌失败", e);
throw new RuntimeException("刷新访问令牌失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject uploadLogisticsToSheet(String accessToken, String fileId, String sheetId, List<JDOrder> orders) {
try {
if (orders == null || orders.isEmpty()) {
throw new IllegalArgumentException("订单列表不能为空");
}
// 构建要写入的数据(二维数组格式)
JSONArray values = new JSONArray();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (JDOrder order : orders) {
JSONArray row = new JSONArray();
// 根据表格列顺序添加数据
// 假设列顺序为:内部单号、订单号、下单时间、型号、地址、物流链接、下单人、付款金额、后返金额、备注
row.add(order.getRemark() != null ? order.getRemark() : "");
row.add(order.getOrderId() != null ? order.getOrderId() : "");
row.add(order.getOrderTime() != null ? sdf.format(order.getOrderTime()) : "");
row.add(order.getModelNumber() != null ? order.getModelNumber() : "");
row.add(order.getAddress() != null ? order.getAddress() : "");
row.add(order.getLogisticsLink() != null ? order.getLogisticsLink() : "");
row.add(order.getBuyer() != null ? order.getBuyer() : "");
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, fileId, sheetId, values, tencentDocConfig.getApiBaseUrl());
} catch (Exception e) {
log.error("上传物流信息到表格失败", e);
throw new RuntimeException("上传物流信息失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject appendLogisticsToSheet(String accessToken, String fileId, String sheetId, JDOrder order) {
try {
if (order == null) {
throw new IllegalArgumentException("订单信息不能为空");
}
// 构建单行数据
JSONArray values = new JSONArray();
JSONArray row = new JSONArray();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 根据表格列顺序添加数据
row.add(order.getRemark() != null ? order.getRemark() : "");
row.add(order.getOrderId() != null ? order.getOrderId() : "");
row.add(order.getOrderTime() != null ? sdf.format(order.getOrderTime()) : "");
row.add(order.getModelNumber() != null ? order.getModelNumber() : "");
row.add(order.getAddress() != null ? order.getAddress() : "");
row.add(order.getLogisticsLink() != null ? order.getLogisticsLink() : "");
row.add(order.getBuyer() != null ? order.getBuyer() : "");
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, fileId, sheetId, values, tencentDocConfig.getApiBaseUrl());
} catch (Exception e) {
log.error("追加物流信息到表格失败", e);
throw new RuntimeException("追加物流信息失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject readSheetData(String accessToken, String fileId, String sheetId, String range) {
try {
return TencentDocApiUtil.readSheetData(accessToken, fileId, sheetId, range, tencentDocConfig.getApiBaseUrl());
} catch (Exception e) {
log.error("读取表格数据失败", e);
throw new RuntimeException("读取表格数据失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject writeSheetData(String accessToken, String fileId, String sheetId, String range, Object values) {
try {
return TencentDocApiUtil.writeSheetData(accessToken, fileId, sheetId, range, values, tencentDocConfig.getApiBaseUrl());
} catch (Exception e) {
log.error("写入表格数据失败", e);
throw new RuntimeException("写入表格数据失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject getFileInfo(String accessToken, String fileId) {
try {
return TencentDocApiUtil.getFileInfo(accessToken, fileId, tencentDocConfig.getApiBaseUrl());
} catch (Exception e) {
log.error("获取文件信息失败", e);
throw new RuntimeException("获取文件信息失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject getSheetList(String accessToken, String fileId) {
try {
return TencentDocApiUtil.getSheetList(accessToken, fileId, tencentDocConfig.getApiBaseUrl());
} catch (Exception e) {
log.error("获取工作表列表失败", e);
throw new RuntimeException("获取工作表列表失败: " + e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,248 @@
package com.ruoyi.jarvis.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.http.HttpUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* 腾讯文档API工具类
*
* @author system
*/
public class TencentDocApiUtil {
private static final Logger log = LoggerFactory.getLogger(TencentDocApiUtil.class);
/**
* 获取访问令牌
*
* @param appId 应用ID
* @param appSecret 应用密钥
* @param code 授权码
* @param redirectUri 回调地址
* @return 包含access_token和refresh_token的JSON对象
*/
public static JSONObject getAccessToken(String appId, String appSecret, String code, String redirectUri, String tokenUrl) {
try {
// 构建请求参数
StringBuilder params = new StringBuilder();
params.append("grant_type=authorization_code");
params.append("&client_id=").append(appId);
params.append("&client_secret=").append(appSecret);
params.append("&code=").append(code);
params.append("&redirect_uri=").append(java.net.URLEncoder.encode(redirectUri, "UTF-8"));
String response = HttpUtils.sendPost(tokenUrl, params.toString());
log.info("获取访问令牌响应: {}", response);
return JSON.parseObject(response);
} catch (Exception e) {
log.error("获取访问令牌失败", e);
throw new RuntimeException("获取访问令牌失败: " + e.getMessage(), e);
}
}
/**
* 刷新访问令牌
*
* @param appId 应用ID
* @param appSecret 应用密钥
* @param refreshToken 刷新令牌
* @param refreshTokenUrl 刷新令牌地址
* @return 包含新的access_token和refresh_token的JSON对象
*/
public static JSONObject refreshAccessToken(String appId, String appSecret, String refreshToken, String refreshTokenUrl) {
try {
// 构建请求参数
StringBuilder params = new StringBuilder();
params.append("grant_type=refresh_token");
params.append("&client_id=").append(appId);
params.append("&client_secret=").append(appSecret);
params.append("&refresh_token=").append(refreshToken);
String response = HttpUtils.sendPost(refreshTokenUrl, params.toString());
log.info("刷新访问令牌响应: {}", response);
return JSON.parseObject(response);
} catch (Exception e) {
log.error("刷新访问令牌失败", e);
throw new RuntimeException("刷新访问令牌失败: " + e.getMessage(), e);
}
}
/**
* 调用腾讯文档API
*
* @param accessToken 访问令牌
* @param apiUrl API地址
* @param method 请求方法 GET/POST/PUT/DELETE
* @param body 请求体JSON格式
* @return API响应
*/
public static JSONObject callApi(String accessToken, String apiUrl, String method, String body) {
try {
URL url = new URL(apiUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(method);
conn.setRequestProperty("Authorization", "Bearer " + accessToken);
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Accept", "application/json");
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setConnectTimeout(10000);
conn.setReadTimeout(30000);
// 写入请求体
if (body != null && !body.isEmpty()) {
try (OutputStream os = conn.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8)) {
osw.write(body);
osw.flush();
}
}
// 读取响应
int statusCode = conn.getResponseCode();
BufferedReader reader;
if (statusCode >= 200 && statusCode < 300) {
reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
} else {
reader = new BufferedReader(new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8));
}
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
String responseStr = response.toString();
log.debug("腾讯文档API响应: statusCode={}, response={}", statusCode, responseStr);
JSONObject result = JSON.parseObject(responseStr);
// 检查错误码
if (result.containsKey("error_code") && result.getIntValue("error_code") != 0) {
String errorMsg = result.getString("error_msg");
log.error("腾讯文档API调用失败: error_code={}, error_msg={}",
result.getIntValue("error_code"), errorMsg);
throw new RuntimeException("腾讯文档API调用失败: " + errorMsg);
}
if (statusCode < 200 || statusCode >= 300) {
throw new RuntimeException("HTTP请求失败: statusCode=" + statusCode + ", response=" + responseStr);
}
return result;
} catch (Exception e) {
log.error("调用腾讯文档API失败: url={}, method={}", apiUrl, method, e);
throw new RuntimeException("调用腾讯文档API失败: " + e.getMessage(), e);
}
}
/**
* 读取表格数据
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param sheetId 工作表ID
* @param range 范围,例如 "A1:Z100"
* @param apiBaseUrl API基础地址
* @return 表格数据
*/
public static JSONObject readSheetData(String accessToken, String fileId, String sheetId, String range, String apiBaseUrl) {
String apiUrl = String.format("%s/files/%s/sheets/%s/ranges/%s", apiBaseUrl, fileId, sheetId, range);
return callApi(accessToken, apiUrl, "GET", null);
}
/**
* 写入表格数据
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param sheetId 工作表ID
* @param range 范围,例如 "A1"
* @param values 要写入的数据,二维数组格式 [[["值1"], ["值2"]], [["值3"], ["值4"]]]
* @param apiBaseUrl API基础地址
* @return 写入结果
*/
public static JSONObject writeSheetData(String accessToken, String fileId, String sheetId, String range, Object values, String apiBaseUrl) {
String apiUrl = String.format("%s/files/%s/sheets/%s/ranges/%s", apiBaseUrl, fileId, sheetId, range);
JSONObject requestBody = new JSONObject();
requestBody.put("values", values);
return callApi(accessToken, apiUrl, "PUT", requestBody.toJSONString());
}
/**
* 追加表格数据(在最后一行追加)
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param sheetId 工作表ID
* @param values 要追加的数据,二维数组格式
* @param apiBaseUrl API基础地址
* @return 追加结果
*/
public static JSONObject appendSheetData(String accessToken, String fileId, String sheetId, Object values, String apiBaseUrl) {
// 先获取表格信息,找到最后一行
String infoUrl = String.format("%s/files/%s/sheets/%s", apiBaseUrl, fileId, sheetId);
JSONObject sheetInfo = callApi(accessToken, infoUrl, "GET", null);
// 获取行数根据实际API响应调整
int rowCount = 0;
if (sheetInfo.containsKey("row_count")) {
rowCount = sheetInfo.getIntValue("row_count");
} else if (sheetInfo.containsKey("data") && sheetInfo.getJSONObject("data").containsKey("row_count")) {
rowCount = sheetInfo.getJSONObject("data").getIntValue("row_count");
}
if (rowCount == 0) {
rowCount = 1; // 至少有一行(表头)
}
// 计算要写入的起始位置(假设追加一行数据)
String range = "A" + (rowCount + 1);
return writeSheetData(accessToken, fileId, sheetId, range, values, apiBaseUrl);
}
/**
* 获取文件信息
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param apiBaseUrl API基础地址
* @return 文件信息
*/
public static JSONObject getFileInfo(String accessToken, String fileId, String apiBaseUrl) {
String apiUrl = String.format("%s/files/%s", apiBaseUrl, fileId);
return callApi(accessToken, apiUrl, "GET", null);
}
/**
* 获取工作表列表
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param apiBaseUrl API基础地址
* @return 工作表列表
*/
public static JSONObject getSheetList(String accessToken, String fileId, String apiBaseUrl) {
String apiUrl = String.format("%s/files/%s/sheets", apiBaseUrl, fileId);
return callApi(accessToken, apiUrl, "GET", null);
}
}