1
This commit is contained in:
213
doc/腾讯文档在线编辑功能说明.md
Normal file
213
doc/腾讯文档在线编辑功能说明.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# 腾讯文档在线编辑功能说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
本功能实现了将物流信息直接上传到腾讯文档表格,实现自动发货的功能。系统通过腾讯文档开放平台的API,可以将订单的物流信息自动写入到指定的腾讯文档表格中。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
1. **OAuth2.0授权**:支持腾讯文档的OAuth2.0授权流程
|
||||||
|
2. **物流信息上传**:支持批量或单个订单的物流信息上传
|
||||||
|
3. **自动发货**:将物流信息上传到腾讯文档后,自动完成发货流程
|
||||||
|
4. **表格操作**:支持读取、写入、追加表格数据
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### 1. 申请腾讯文档开放平台应用
|
||||||
|
|
||||||
|
1. 访问 [腾讯文档开放平台](https://docs.qq.com/open/document/app/)
|
||||||
|
2. 注册开发者账号并创建应用
|
||||||
|
3. 获取 `AppID` 和 `AppSecret`
|
||||||
|
4. 配置授权回调地址:`http://your-domain/jarvis/tendoc/oauth/callback`
|
||||||
|
|
||||||
|
### 2. 配置应用参数
|
||||||
|
|
||||||
|
在 `application-dev.yml` 中配置腾讯文档相关参数:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tencent:
|
||||||
|
doc:
|
||||||
|
app-id: your_app_id # 替换为你的AppID
|
||||||
|
app-secret: your_app_secret # 替换为你的AppSecret
|
||||||
|
redirect-uri: http://localhost:30313/jarvis/tendoc/oauth/callback # 替换为你的回调地址
|
||||||
|
```
|
||||||
|
|
||||||
|
## API接口说明
|
||||||
|
|
||||||
|
### 1. 获取授权URL
|
||||||
|
|
||||||
|
**接口地址:** `GET /jarvis/tendoc/authUrl`
|
||||||
|
|
||||||
|
**返回示例:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "获取授权URL成功",
|
||||||
|
"data": "https://docs.qq.com/oauth/v2/authorize?client_id=xxx&redirect_uri=xxx&response_type=code&scope=file.read_write"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. OAuth回调
|
||||||
|
|
||||||
|
**接口地址:** `GET /jarvis/tendoc/oauth/callback?code=xxx`
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `code`: 授权码(由腾讯文档返回)
|
||||||
|
|
||||||
|
**返回示例:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "授权成功",
|
||||||
|
"data": {
|
||||||
|
"access_token": "xxx",
|
||||||
|
"refresh_token": "xxx",
|
||||||
|
"expires_in": 7200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 刷新访问令牌
|
||||||
|
|
||||||
|
**接口地址:** `POST /jarvis/tendoc/refreshToken`
|
||||||
|
|
||||||
|
**请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"refreshToken": "xxx"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 上传物流信息(批量)
|
||||||
|
|
||||||
|
**接口地址:** `POST /jarvis/tendoc/uploadLogistics`
|
||||||
|
|
||||||
|
**请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accessToken": "xxx",
|
||||||
|
"fileId": "xxx",
|
||||||
|
"sheetId": "xxx",
|
||||||
|
"orderIds": [1, 2, 3]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `accessToken`: 访问令牌
|
||||||
|
- `fileId`: 腾讯文档文件ID(从文档URL中获取)
|
||||||
|
- `sheetId`: 工作表ID(从文档URL中获取)
|
||||||
|
- `orderIds`: 订单ID列表
|
||||||
|
|
||||||
|
### 5. 追加物流信息(单个)
|
||||||
|
|
||||||
|
**接口地址:** `POST /jarvis/tendoc/appendLogistics`
|
||||||
|
|
||||||
|
**请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accessToken": "xxx",
|
||||||
|
"fileId": "xxx",
|
||||||
|
"sheetId": "xxx",
|
||||||
|
"orderId": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 自动发货
|
||||||
|
|
||||||
|
**接口地址:** `POST /jarvis/tendoc/autoShip`
|
||||||
|
|
||||||
|
**请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accessToken": "xxx",
|
||||||
|
"fileId": "xxx",
|
||||||
|
"sheetId": "xxx",
|
||||||
|
"orderId": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能说明:**
|
||||||
|
- 检查订单是否有物流链接
|
||||||
|
- 将物流信息上传到腾讯文档表格
|
||||||
|
- 完成自动发货流程
|
||||||
|
|
||||||
|
### 7. 读取表格数据
|
||||||
|
|
||||||
|
**接口地址:** `GET /jarvis/tendoc/readSheet`
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `accessToken`: 访问令牌
|
||||||
|
- `fileId`: 文件ID
|
||||||
|
- `sheetId`: 工作表ID
|
||||||
|
- `range`: 范围(可选,默认:A1:Z100)
|
||||||
|
|
||||||
|
### 8. 获取文件信息
|
||||||
|
|
||||||
|
**接口地址:** `GET /jarvis/tendoc/fileInfo`
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `accessToken`: 访问令牌
|
||||||
|
- `fileId`: 文件ID
|
||||||
|
|
||||||
|
### 9. 获取工作表列表
|
||||||
|
|
||||||
|
**接口地址:** `GET /jarvis/tendoc/sheetList`
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `accessToken`: 访问令牌
|
||||||
|
- `fileId`: 文件ID
|
||||||
|
|
||||||
|
## 使用流程
|
||||||
|
|
||||||
|
### 1. 授权流程
|
||||||
|
|
||||||
|
1. 调用 `GET /jarvis/tendoc/authUrl` 获取授权URL
|
||||||
|
2. 用户在浏览器中访问授权URL,完成授权
|
||||||
|
3. 授权成功后,腾讯文档会重定向到回调地址,并携带 `code` 参数
|
||||||
|
4. 调用 `GET /jarvis/tendoc/oauth/callback?code=xxx` 获取访问令牌
|
||||||
|
|
||||||
|
### 2. 上传物流信息
|
||||||
|
|
||||||
|
1. 获取腾讯文档的文件ID和工作表ID
|
||||||
|
- 打开腾讯文档,从URL中获取:`https://docs.qq.com/sheet/Dxxxxxxxxxxxxx?tab=BB08J2`
|
||||||
|
- `Dxxxxxxxxxxxxx` 为文件ID
|
||||||
|
- `BB08J2` 为工作表ID
|
||||||
|
|
||||||
|
2. 调用上传接口
|
||||||
|
- 批量上传:`POST /jarvis/tendoc/uploadLogistics`
|
||||||
|
- 单个追加:`POST /jarvis/tendoc/appendLogistics`
|
||||||
|
- 自动发货:`POST /jarvis/tendoc/autoShip`
|
||||||
|
|
||||||
|
### 3. 表格格式
|
||||||
|
|
||||||
|
上传的数据格式(按列顺序):
|
||||||
|
1. 内部单号(remark)
|
||||||
|
2. 订单号(orderId)
|
||||||
|
3. 下单时间(orderTime)
|
||||||
|
4. 型号(modelNumber)
|
||||||
|
5. 地址(address)
|
||||||
|
6. 物流链接(logisticsLink)
|
||||||
|
7. 下单人(buyer)
|
||||||
|
8. 付款金额(paymentAmount)
|
||||||
|
9. 后返金额(rebateAmount)
|
||||||
|
10. 备注/状态(status)
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **访问令牌有效期**:访问令牌通常有效期为2小时,过期后需要使用 `refresh_token` 刷新
|
||||||
|
2. **API调用频率**:腾讯文档API有调用频率限制,请参考[腾讯文档开放平台文档](https://docs.qq.com/open/document/app/)
|
||||||
|
3. **文件权限**:确保应用有权限访问目标文档
|
||||||
|
4. **表格格式**:建议在腾讯文档中先创建表头,确保列顺序与系统一致
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
- 如果访问令牌过期,系统会返回错误信息,需要重新授权或刷新令牌
|
||||||
|
- 如果文件ID或工作表ID错误,会返回相应的错误提示
|
||||||
|
- 如果订单信息不完整(如缺少物流链接),自动发货会失败并提示
|
||||||
|
|
||||||
|
## 技术支持
|
||||||
|
|
||||||
|
如有问题,请参考:
|
||||||
|
- [腾讯文档开放平台文档](https://docs.qq.com/open/document/app/)
|
||||||
|
- 系统日志文件
|
||||||
|
|
||||||
@@ -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-based(A=1, B=2, ...)
|
||||||
|
while (columnIndex > 0) {
|
||||||
|
columnIndex--;
|
||||||
|
sb.insert(0, (char)('A' + (columnIndex % 26)));
|
||||||
|
columnIndex /= 26;
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -186,3 +186,21 @@ xss:
|
|||||||
# 匹配链接
|
# 匹配链接
|
||||||
urlPatterns: /system/*,/monitor/*,/tool/*
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user