@@ -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 < JDOrder > 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 ( ) ,