From b43daf2965b882bdf3af0df62a2467ecbdd8c204 Mon Sep 17 00:00:00 2001 From: Leo Date: Wed, 14 Jan 2026 22:55:36 +0800 Subject: [PATCH] 1 --- doc/WPS365集成使用说明.md | 226 ++++++++ .../controller/jarvis/WPS365Controller.java | 486 ++++++++++++++++++ .../src/main/resources/application-dev.yml | 18 + .../com/ruoyi/jarvis/config/WPS365Config.java | 121 +++++ .../jarvis/domain/dto/WPS365TokenInfo.java | 108 ++++ .../jarvis/service/IWPS365ApiService.java | 94 ++++ .../jarvis/service/IWPS365OAuthService.java | 66 +++ .../service/impl/WPS365ApiServiceImpl.java | 200 +++++++ .../service/impl/WPS365OAuthServiceImpl.java | 207 ++++++++ .../com/ruoyi/jarvis/util/WPS365ApiUtil.java | 240 +++++++++ 10 files changed, 1766 insertions(+) create mode 100644 doc/WPS365集成使用说明.md create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/WPS365Controller.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/jarvis/config/WPS365Config.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/dto/WPS365TokenInfo.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWPS365ApiService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWPS365OAuthService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WPS365ApiServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WPS365OAuthServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/jarvis/util/WPS365ApiUtil.java diff --git a/doc/WPS365集成使用说明.md b/doc/WPS365集成使用说明.md new file mode 100644 index 0000000..9a8a11c --- /dev/null +++ b/doc/WPS365集成使用说明.md @@ -0,0 +1,226 @@ +# WPS365 在线表格API集成使用说明 + +## 概述 + +本功能实现了WPS365在线表格(KSheet)的完整API集成,支持用户授权、文档查看和编辑等功能。 + +## 功能特性 + +1. **OAuth用户授权**:支持WPS365标准的OAuth 2.0授权流程 +2. **Token管理**:自动保存和管理访问令牌,支持Token刷新 +3. **文件管理**:获取文件列表、文件信息 +4. **工作表管理**:获取工作表列表、创建数据表 +5. **单元格编辑**:支持读取和更新单元格数据,支持批量更新 + +## 配置说明 + +### 1. 在WPS365开放平台注册应用 + +1. 访问 [WPS365开放平台](https://open.wps.cn/) +2. 注册成为服务商并创建应用 +3. 获取 `app_id` 和 `app_key` +4. 配置授权回调地址(需要与配置文件中的 `redirect-uri` 一致) + +### 2. 更新配置文件 + +编辑 `application-dev.yml`(或对应的环境配置文件),添加WPS365配置: + +```yaml +wps365: + # 应用ID(从WPS365开放平台获取) + app-id: YOUR_APP_ID + # 应用密钥(从WPS365开放平台获取) + app-key: YOUR_APP_KEY + # 授权回调地址(需要在WPS365开放平台配置) + redirect-uri: https://your-domain.com/jarvis/wps365/oauth/callback + # API基础地址(一般不需要修改) + api-base-url: https://open.wps.cn/api/v1 + # OAuth授权地址(一般不需要修改) + oauth-url: https://open.wps.cn/oauth2/v1/authorize + # 获取Token地址(一般不需要修改) + token-url: https://open.wps.cn/oauth2/v1/token + # 刷新Token地址(一般不需要修改) + refresh-token-url: https://open.wps.cn/oauth2/v1/token +``` + +## API接口说明 + +### 1. 获取授权URL + +**接口**: `GET /jarvis/wps365/authUrl` + +**参数**: +- `state` (可选): 状态参数,用于防止CSRF攻击 + +**返回**: 授权URL,用户需要访问此URL完成授权 + +**示例**: +```javascript +// 前端调用 +import { getWPS365AuthUrl } from '@/api/jarvis/wps365' + +const response = await getWPS365AuthUrl() +const authUrl = response.data +// 打开授权页面 +window.open(authUrl, '_blank') +``` + +### 2. OAuth回调处理 + +**接口**: `GET /jarvis/wps365/oauth/callback` + +**参数**: +- `code`: 授权码(由WPS365回调时自动传入) +- `state`: 状态参数(可选) + +**说明**: 此接口处理WPS365的授权回调,自动获取并保存Token。 + +### 3. 获取Token状态 + +**接口**: `GET /jarvis/wps365/tokenStatus` + +**参数**: +- `userId`: 用户ID + +**返回**: Token状态信息(是否授权、是否有效等) + +### 4. 刷新Token + +**接口**: `POST /jarvis/wps365/refreshToken` + +**请求体**: +```json +{ + "refreshToken": "refresh_token_value" +} +``` + +### 5. 获取用户信息 + +**接口**: `GET /jarvis/wps365/userInfo` + +**参数**: +- `userId`: 用户ID + +### 6. 获取文件列表 + +**接口**: `GET /jarvis/wps365/files` + +**参数**: +- `userId`: 用户ID +- `page`: 页码(默认1) +- `pageSize`: 每页数量(默认20) + +### 7. 获取工作表列表 + +**接口**: `GET /jarvis/wps365/sheets` + +**参数**: +- `userId`: 用户ID +- `fileToken`: 文件token + +### 8. 读取单元格数据 + +**接口**: `GET /jarvis/wps365/readCells` + +**参数**: +- `userId`: 用户ID +- `fileToken`: 文件token +- `sheetIdx`: 工作表索引(从0开始) +- `range`: 单元格范围(如:A1:B10,可选) + +### 9. 更新单元格数据 + +**接口**: `POST /jarvis/wps365/updateCells` + +**请求体**: +```json +{ + "userId": "user_id", + "fileToken": "file_token", + "sheetIdx": 0, + "range": "A1:B2", + "values": [ + ["值1", "值2"], + ["值3", "值4"] + ] +} +``` + +### 10. 批量更新单元格数据 + +**接口**: `POST /jarvis/wps365/batchUpdateCells` + +**请求体**: +```json +{ + "userId": "user_id", + "fileToken": "file_token", + "sheetIdx": 0, + "updates": [ + { + "range": "A1:B2", + "values": [["值1", "值2"], ["值3", "值4"]] + }, + { + "range": "C1:D2", + "values": [["值5", "值6"], ["值7", "值8"]] + } + ] +} +``` + +## 使用流程 + +### 1. 用户授权流程 + +1. 前端调用 `/jarvis/wps365/authUrl` 获取授权URL +2. 在新窗口打开授权URL,用户完成授权 +3. WPS365会回调到配置的 `redirect-uri`,自动处理授权码 +4. 系统自动获取并保存Token到Redis + +### 2. 编辑文档流程 + +1. 调用 `/jarvis/wps365/files` 获取文件列表 +2. 选择要编辑的文件,调用 `/jarvis/wps365/sheets` 获取工作表列表 +3. 调用 `/jarvis/wps365/readCells` 读取现有数据 +4. 修改数据后,调用 `/jarvis/wps365/updateCells` 更新数据 + +### 3. Token刷新 + +- Token过期前,系统会自动尝试刷新 +- 也可以手动调用 `/jarvis/wps365/refreshToken` 刷新Token + +## 注意事项 + +1. **用户权限**:只有文档的所有者或被授予编辑权限的用户才能编辑文档 +2. **Token管理**:Token存储在Redis中,有效期30天。过期后需要重新授权 +3. **API限制**:注意WPS365的API调用频率限制 +4. **文件Token**:WPS365使用 `file_token` 而不是文件ID,需要从文件列表中获取 + +## 前端页面 + +访问 `/jarvis/wps365` 可以打开WPS365管理页面,包含: +- 授权状态显示 +- 用户信息查看 +- 文件列表浏览 +- 在线编辑表格功能 + +## 错误处理 + +- **未授权**:返回错误提示,引导用户完成授权 +- **Token过期**:自动尝试刷新Token,刷新失败则提示重新授权 +- **权限不足**:返回错误码10003,提示用户没有编辑权限 + +## 技术实现 + +- **后端**:Spring Boot + Redis(Token存储) +- **前端**:Vue.js + Element UI +- **HTTP客户端**:HttpURLConnection(不使用代理,直接连接) + +## 参考文档 + +- [WPS365开放平台文档](https://open.wps.cn/) +- [用户授权流程](https://open.wps.cn/documents/app-integration-dev/wps365/server/certification-authorization/user-authorization/flow) +- [KSheet API文档](https://developer.kdocs.cn/server/ksheet/) + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/WPS365Controller.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/WPS365Controller.java new file mode 100644 index 0000000..83b7e53 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/WPS365Controller.java @@ -0,0 +1,486 @@ +package com.ruoyi.web.controller.jarvis; + +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.annotation.Anonymous; +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.dto.WPS365TokenInfo; +import com.ruoyi.jarvis.service.IWPS365ApiService; +import com.ruoyi.jarvis.service.IWPS365OAuthService; +import com.ruoyi.jarvis.service.impl.WPS365OAuthServiceImpl; +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; + +/** + * WPS365控制器 + * + * @author system + */ +@RestController +@RequestMapping("/jarvis/wps365") +public class WPS365Controller extends BaseController { + + private static final Logger log = LoggerFactory.getLogger(WPS365Controller.class); + + @Autowired + private IWPS365OAuthService wps365OAuthService; + + @Autowired + private IWPS365ApiService wps365ApiService; + + @Autowired + private WPS365OAuthServiceImpl wps365OAuthServiceImpl; + + @Autowired + private RedisCache redisCache; + + /** + * 获取授权URL + */ + @GetMapping("/authUrl") + public AjaxResult getAuthUrl(@RequestParam(required = false) String state) { + try { + String authUrl = wps365OAuthService.getAuthUrl(state); + return AjaxResult.success("获取授权URL成功", authUrl); + } catch (Exception e) { + log.error("获取授权URL失败", e); + return AjaxResult.error("获取授权URL失败: " + e.getMessage()); + } + } + + /** + * OAuth回调处理 + */ + @Anonymous + @GetMapping("/oauth/callback") + public AjaxResult oauthCallback(@RequestParam String code, + @RequestParam(required = false) String state) { + try { + log.info("收到OAuth回调 - code: {}, state: {}", code, state); + + // 通过授权码获取访问令牌 + WPS365TokenInfo tokenInfo = wps365OAuthService.getAccessTokenByCode(code); + + // 保存Token到Redis(使用userId作为key) + if (tokenInfo.getUserId() != null) { + wps365OAuthService.saveToken(tokenInfo.getUserId(), tokenInfo); + } + + return AjaxResult.success("授权成功", tokenInfo); + } catch (Exception e) { + log.error("OAuth回调处理失败", e); + return AjaxResult.error("授权失败: " + e.getMessage()); + } + } + + /** + * 刷新访问令牌 + */ + @PostMapping("/refreshToken") + public AjaxResult refreshToken(@RequestBody Map params) { + try { + String refreshToken = (String) params.get("refreshToken"); + if (refreshToken == null || refreshToken.trim().isEmpty()) { + return AjaxResult.error("refreshToken不能为空"); + } + + WPS365TokenInfo tokenInfo = wps365OAuthService.refreshAccessToken(refreshToken); + + // 更新Token到Redis + if (tokenInfo.getUserId() != null) { + wps365OAuthService.saveToken(tokenInfo.getUserId(), tokenInfo); + } + + return AjaxResult.success("刷新令牌成功", tokenInfo); + } catch (Exception e) { + log.error("刷新访问令牌失败", e); + return AjaxResult.error("刷新令牌失败: " + e.getMessage()); + } + } + + /** + * 获取当前用户的Token状态 + */ + @GetMapping("/tokenStatus") + public AjaxResult getTokenStatus(@RequestParam(required = false) String userId) { + try { + // 如果没有提供userId,可以尝试从当前登录用户获取 + // 这里暂时需要前端传入userId,后续可以集成到认证系统中 + if (userId == null || userId.trim().isEmpty()) { + return AjaxResult.error("userId不能为空"); + } + + WPS365TokenInfo tokenInfo = wps365OAuthServiceImpl.getTokenByUserId(userId); + if (tokenInfo == null) { + return AjaxResult.success("未授权", false); + } + + boolean isValid = wps365OAuthService.isTokenValid(tokenInfo); + JSONObject result = new JSONObject(); + result.put("hasToken", true); + result.put("isValid", isValid); + result.put("userId", tokenInfo.getUserId()); + result.put("expired", tokenInfo.isExpired()); + + return AjaxResult.success("获取Token状态成功", result); + } catch (Exception e) { + log.error("获取Token状态失败", e); + return AjaxResult.error("获取Token状态失败: " + e.getMessage()); + } + } + + /** + * 手动设置Token(用于测试或手动授权) + */ + @PostMapping("/setToken") + public AjaxResult setToken(@RequestBody Map params) { + try { + String accessToken = (String) params.get("accessToken"); + String refreshToken = (String) params.get("refreshToken"); + String userId = (String) params.get("userId"); + Integer expiresIn = params.get("expiresIn") != null ? + Integer.valueOf(params.get("expiresIn").toString()) : 7200; + + if (accessToken == null || accessToken.trim().isEmpty()) { + return AjaxResult.error("accessToken不能为空"); + } + if (userId == null || userId.trim().isEmpty()) { + return AjaxResult.error("userId不能为空"); + } + + WPS365TokenInfo tokenInfo = new WPS365TokenInfo(); + tokenInfo.setAccessToken(accessToken); + tokenInfo.setRefreshToken(refreshToken); + tokenInfo.setExpiresIn(expiresIn); + tokenInfo.setUserId(userId); + + wps365OAuthService.saveToken(userId, tokenInfo); + + return AjaxResult.success("设置Token成功"); + } catch (Exception e) { + log.error("设置Token失败", e); + return AjaxResult.error("设置Token失败: " + e.getMessage()); + } + } + + /** + * 获取用户信息 + */ + @GetMapping("/userInfo") + public AjaxResult getUserInfo(@RequestParam String userId) { + try { + WPS365TokenInfo tokenInfo = wps365OAuthServiceImpl.getTokenByUserId(userId); + if (tokenInfo == null) { + return AjaxResult.error("用户未授权,请先完成授权"); + } + + // 检查Token是否有效,如果过期则尝试刷新 + if (tokenInfo.isExpired() && tokenInfo.getRefreshToken() != null) { + try { + tokenInfo = wps365OAuthService.refreshAccessToken(tokenInfo.getRefreshToken()); + wps365OAuthService.saveToken(userId, tokenInfo); + } catch (Exception e) { + log.error("刷新Token失败", e); + return AjaxResult.error("Token已过期且刷新失败,请重新授权"); + } + } + + JSONObject userInfo = wps365ApiService.getUserInfo(tokenInfo.getAccessToken()); + return AjaxResult.success("获取用户信息成功", userInfo); + } catch (Exception e) { + log.error("获取用户信息失败", e); + return AjaxResult.error("获取用户信息失败: " + e.getMessage()); + } + } + + /** + * 获取文件列表 + */ + @GetMapping("/files") + public AjaxResult getFileList(@RequestParam String userId, + @RequestParam(required = false, defaultValue = "1") Integer page, + @RequestParam(required = false, defaultValue = "20") Integer pageSize) { + try { + WPS365TokenInfo tokenInfo = wps365OAuthServiceImpl.getTokenByUserId(userId); + if (tokenInfo == null) { + return AjaxResult.error("用户未授权,请先完成授权"); + } + + // 检查Token是否有效,如果过期则尝试刷新 + if (tokenInfo.isExpired() && tokenInfo.getRefreshToken() != null) { + try { + tokenInfo = wps365OAuthService.refreshAccessToken(tokenInfo.getRefreshToken()); + wps365OAuthService.saveToken(userId, tokenInfo); + } catch (Exception e) { + log.error("刷新Token失败", e); + return AjaxResult.error("Token已过期且刷新失败,请重新授权"); + } + } + + Map params = new java.util.HashMap<>(); + params.put("page", page); + params.put("page_size", pageSize); + + JSONObject fileList = wps365ApiService.getFileList(tokenInfo.getAccessToken(), params); + return AjaxResult.success("获取文件列表成功", fileList); + } catch (Exception e) { + log.error("获取文件列表失败", e); + return AjaxResult.error("获取文件列表失败: " + e.getMessage()); + } + } + + /** + * 获取文件信息 + */ + @GetMapping("/fileInfo") + public AjaxResult getFileInfo(@RequestParam String userId, + @RequestParam String fileToken) { + try { + WPS365TokenInfo tokenInfo = wps365OAuthServiceImpl.getTokenByUserId(userId); + if (tokenInfo == null) { + return AjaxResult.error("用户未授权,请先完成授权"); + } + + // 检查Token是否有效 + if (tokenInfo.isExpired() && tokenInfo.getRefreshToken() != null) { + try { + tokenInfo = wps365OAuthService.refreshAccessToken(tokenInfo.getRefreshToken()); + wps365OAuthService.saveToken(userId, tokenInfo); + } catch (Exception e) { + log.error("刷新Token失败", e); + return AjaxResult.error("Token已过期且刷新失败,请重新授权"); + } + } + + JSONObject fileInfo = wps365ApiService.getFileInfo(tokenInfo.getAccessToken(), fileToken); + return AjaxResult.success("获取文件信息成功", fileInfo); + } catch (Exception e) { + log.error("获取文件信息失败 - fileToken: {}", fileToken, e); + return AjaxResult.error("获取文件信息失败: " + e.getMessage()); + } + } + + /** + * 获取工作表列表 + */ + @GetMapping("/sheets") + public AjaxResult getSheetList(@RequestParam String userId, + @RequestParam String fileToken) { + try { + WPS365TokenInfo tokenInfo = wps365OAuthServiceImpl.getTokenByUserId(userId); + if (tokenInfo == null) { + return AjaxResult.error("用户未授权,请先完成授权"); + } + + // 检查Token是否有效 + if (tokenInfo.isExpired() && tokenInfo.getRefreshToken() != null) { + try { + tokenInfo = wps365OAuthService.refreshAccessToken(tokenInfo.getRefreshToken()); + wps365OAuthService.saveToken(userId, tokenInfo); + } catch (Exception e) { + log.error("刷新Token失败", e); + return AjaxResult.error("Token已过期且刷新失败,请重新授权"); + } + } + + JSONObject sheetList = wps365ApiService.getSheetList(tokenInfo.getAccessToken(), fileToken); + return AjaxResult.success("获取工作表列表成功", sheetList); + } catch (Exception e) { + log.error("获取工作表列表失败 - fileToken: {}", fileToken, e); + return AjaxResult.error("获取工作表列表失败: " + e.getMessage()); + } + } + + /** + * 读取单元格数据 + */ + @GetMapping("/readCells") + public AjaxResult readCells(@RequestParam String userId, + @RequestParam String fileToken, + @RequestParam(defaultValue = "0") int sheetIdx, + @RequestParam(required = false) String range) { + try { + WPS365TokenInfo tokenInfo = wps365OAuthServiceImpl.getTokenByUserId(userId); + if (tokenInfo == null) { + return AjaxResult.error("用户未授权,请先完成授权"); + } + + // 检查Token是否有效 + if (tokenInfo.isExpired() && tokenInfo.getRefreshToken() != null) { + try { + tokenInfo = wps365OAuthService.refreshAccessToken(tokenInfo.getRefreshToken()); + wps365OAuthService.saveToken(userId, tokenInfo); + } catch (Exception e) { + log.error("刷新Token失败", e); + return AjaxResult.error("Token已过期且刷新失败,请重新授权"); + } + } + + JSONObject result = wps365ApiService.readCells(tokenInfo.getAccessToken(), fileToken, sheetIdx, range); + return AjaxResult.success("读取单元格数据成功", result); + } catch (Exception e) { + log.error("读取单元格数据失败 - fileToken: {}, sheetIdx: {}, range: {}", fileToken, sheetIdx, range, e); + return AjaxResult.error("读取单元格数据失败: " + e.getMessage()); + } + } + + /** + * 更新单元格数据 + */ + @PostMapping("/updateCells") + public AjaxResult updateCells(@RequestBody Map params) { + try { + String userId = (String) params.get("userId"); + String fileToken = (String) params.get("fileToken"); + Integer sheetIdx = params.get("sheetIdx") != null ? + Integer.valueOf(params.get("sheetIdx").toString()) : 0; + String range = (String) params.get("range"); + @SuppressWarnings("unchecked") + List> values = (List>) params.get("values"); + + if (userId == null || userId.trim().isEmpty()) { + return AjaxResult.error("userId不能为空"); + } + if (fileToken == null || fileToken.trim().isEmpty()) { + return AjaxResult.error("fileToken不能为空"); + } + if (range == null || range.trim().isEmpty()) { + return AjaxResult.error("range不能为空"); + } + if (values == null || values.isEmpty()) { + return AjaxResult.error("values不能为空"); + } + + WPS365TokenInfo tokenInfo = wps365OAuthServiceImpl.getTokenByUserId(userId); + if (tokenInfo == null) { + return AjaxResult.error("用户未授权,请先完成授权"); + } + + // 检查Token是否有效 + if (tokenInfo.isExpired() && tokenInfo.getRefreshToken() != null) { + try { + tokenInfo = wps365OAuthService.refreshAccessToken(tokenInfo.getRefreshToken()); + wps365OAuthService.saveToken(userId, tokenInfo); + } catch (Exception e) { + log.error("刷新Token失败", e); + return AjaxResult.error("Token已过期且刷新失败,请重新授权"); + } + } + + JSONObject result = wps365ApiService.updateCells( + tokenInfo.getAccessToken(), + fileToken, + sheetIdx, + range, + values + ); + return AjaxResult.success("更新单元格数据成功", result); + } catch (Exception e) { + log.error("更新单元格数据失败", e); + return AjaxResult.error("更新单元格数据失败: " + e.getMessage()); + } + } + + /** + * 创建数据表 + */ + @PostMapping("/createSheet") + public AjaxResult createSheet(@RequestBody Map params) { + try { + String userId = (String) params.get("userId"); + String fileToken = (String) params.get("fileToken"); + String sheetName = (String) params.get("sheetName"); + + if (userId == null || userId.trim().isEmpty()) { + return AjaxResult.error("userId不能为空"); + } + if (fileToken == null || fileToken.trim().isEmpty()) { + return AjaxResult.error("fileToken不能为空"); + } + if (sheetName == null || sheetName.trim().isEmpty()) { + return AjaxResult.error("sheetName不能为空"); + } + + WPS365TokenInfo tokenInfo = wps365OAuthServiceImpl.getTokenByUserId(userId); + if (tokenInfo == null) { + return AjaxResult.error("用户未授权,请先完成授权"); + } + + // 检查Token是否有效 + if (tokenInfo.isExpired() && tokenInfo.getRefreshToken() != null) { + try { + tokenInfo = wps365OAuthService.refreshAccessToken(tokenInfo.getRefreshToken()); + wps365OAuthService.saveToken(userId, tokenInfo); + } catch (Exception e) { + log.error("刷新Token失败", e); + return AjaxResult.error("Token已过期且刷新失败,请重新授权"); + } + } + + JSONObject result = wps365ApiService.createSheet(tokenInfo.getAccessToken(), fileToken, sheetName); + return AjaxResult.success("创建数据表成功", result); + } catch (Exception e) { + log.error("创建数据表失败", e); + return AjaxResult.error("创建数据表失败: " + e.getMessage()); + } + } + + /** + * 批量更新单元格数据 + */ + @PostMapping("/batchUpdateCells") + public AjaxResult batchUpdateCells(@RequestBody Map params) { + try { + String userId = (String) params.get("userId"); + String fileToken = (String) params.get("fileToken"); + Integer sheetIdx = params.get("sheetIdx") != null ? + Integer.valueOf(params.get("sheetIdx").toString()) : 0; + @SuppressWarnings("unchecked") + List> updates = (List>) params.get("updates"); + + if (userId == null || userId.trim().isEmpty()) { + return AjaxResult.error("userId不能为空"); + } + if (fileToken == null || fileToken.trim().isEmpty()) { + return AjaxResult.error("fileToken不能为空"); + } + if (updates == null || updates.isEmpty()) { + return AjaxResult.error("updates不能为空"); + } + + WPS365TokenInfo tokenInfo = wps365OAuthServiceImpl.getTokenByUserId(userId); + if (tokenInfo == null) { + return AjaxResult.error("用户未授权,请先完成授权"); + } + + // 检查Token是否有效 + if (tokenInfo.isExpired() && tokenInfo.getRefreshToken() != null) { + try { + tokenInfo = wps365OAuthService.refreshAccessToken(tokenInfo.getRefreshToken()); + wps365OAuthService.saveToken(userId, tokenInfo); + } catch (Exception e) { + log.error("刷新Token失败", e); + return AjaxResult.error("Token已过期且刷新失败,请重新授权"); + } + } + + JSONObject result = wps365ApiService.batchUpdateCells( + tokenInfo.getAccessToken(), + fileToken, + sheetIdx, + updates + ); + return AjaxResult.success("批量更新单元格数据成功", result); + } catch (Exception e) { + log.error("批量更新单元格数据失败", e); + return AjaxResult.error("批量更新单元格数据失败: " + e.getMessage()); + } + } +} + diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 64d42df..9f66ffd 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -222,3 +222,21 @@ tencent: # 刷新Token地址(用于通过refresh_token刷新access_token) refresh-token-url: https://docs.qq.com/oauth/v2/token +# WPS365开放平台配置 +# 文档地址:https://open.wps.cn/ +wps365: + # 应用ID(AppId)- 需要在WPS365开放平台申请 + app-id: YOUR_APP_ID + # 应用密钥(AppKey)- 需要在WPS365开放平台申请,注意保密 + app-key: YOUR_APP_KEY + # 授权回调地址(需要在WPS365开放平台配置授权域名) + redirect-uri: https://jarvis.van333.cn/jarvis/wps365/oauth/callback + # API基础地址 + api-base-url: https://open.wps.cn/api/v1 + # OAuth授权地址 + oauth-url: https://open.wps.cn/oauth2/v1/authorize + # 获取Token地址 + token-url: https://open.wps.cn/oauth2/v1/token + # 刷新Token地址 + refresh-token-url: https://open.wps.cn/oauth2/v1/token + diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/config/WPS365Config.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/config/WPS365Config.java new file mode 100644 index 0000000..aa551c8 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/config/WPS365Config.java @@ -0,0 +1,121 @@ +package com.ruoyi.jarvis.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; + +/** + * WPS365开放平台配置 + * + * @author system + */ +@Configuration +@Component +@ConfigurationProperties(prefix = "wps365") +public class WPS365Config { + + private static final Logger log = LoggerFactory.getLogger(WPS365Config.class); + + /** 应用ID(AppId) */ + private String appId; + + /** 应用密钥(AppKey) */ + private String appKey; + + /** 授权回调地址 */ + private String redirectUri; + + /** API基础地址 */ + private String apiBaseUrl = "https://open.wps.cn/api/v1"; + + /** OAuth授权地址 */ + private String oauthUrl = "https://open.wps.cn/oauth2/v1/authorize"; + + /** 获取Token地址 */ + private String tokenUrl = "https://open.wps.cn/oauth2/v1/token"; + + /** 刷新Token地址 */ + private String refreshTokenUrl = "https://open.wps.cn/oauth2/v1/token"; + + /** + * 配置初始化后验证 + */ + @PostConstruct + public void init() { + log.info("WPS365配置加载 - appId: {}, redirectUri: {}, apiBaseUrl: {}", + appId != null && appId.length() > 10 ? appId.substring(0, 10) + "..." : (appId != null ? appId : "null"), + redirectUri != null ? redirectUri : "null", + apiBaseUrl); + + if (appId == null || appId.trim().isEmpty()) { + log.warn("WPS365应用ID未配置!请检查application.yml中的wps365.app-id"); + } + if (appKey == null || appKey.trim().isEmpty()) { + log.warn("WPS365应用密钥未配置!请检查application.yml中的wps365.app-key"); + } + if (redirectUri == null || redirectUri.trim().isEmpty()) { + log.warn("WPS365回调地址未配置!请检查application.yml中的wps365.redirect-uri"); + } + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getAppKey() { + return appKey; + } + + public void setAppKey(String appKey) { + this.appKey = appKey; + } + + 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; + } +} + diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/dto/WPS365TokenInfo.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/dto/WPS365TokenInfo.java new file mode 100644 index 0000000..50e571e --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/dto/WPS365TokenInfo.java @@ -0,0 +1,108 @@ +package com.ruoyi.jarvis.domain.dto; + +import java.io.Serializable; + +/** + * WPS365 Token信息 + * + * @author system + */ +public class WPS365TokenInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 访问令牌 */ + private String accessToken; + + /** 刷新令牌 */ + private String refreshToken; + + /** 令牌类型 */ + private String tokenType; + + /** 过期时间(秒) */ + private Integer expiresIn; + + /** 作用域 */ + private String scope; + + /** 用户ID */ + private String userId; + + /** 创建时间戳(毫秒) */ + private Long createTime; + + public WPS365TokenInfo() { + this.createTime = System.currentTimeMillis(); + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + public Integer getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public Long getCreateTime() { + return createTime; + } + + public void setCreateTime(Long createTime) { + this.createTime = createTime; + } + + /** + * 检查token是否过期 + */ + public boolean isExpired() { + if (expiresIn == null || createTime == null) { + return true; + } + long currentTime = System.currentTimeMillis(); + long expireTime = createTime + (expiresIn * 1000L); + // 提前5分钟认为过期,留出刷新时间 + return currentTime >= (expireTime - 5 * 60 * 1000); + } +} + diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWPS365ApiService.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWPS365ApiService.java new file mode 100644 index 0000000..669d93f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWPS365ApiService.java @@ -0,0 +1,94 @@ +package com.ruoyi.jarvis.service; + +import com.alibaba.fastjson2.JSONObject; + +import java.util.List; +import java.util.Map; + +/** + * WPS365 API服务接口 + * + * @author system + */ +public interface IWPS365ApiService { + + /** + * 获取用户信息 + * + * @param accessToken 访问令牌 + * @return 用户信息 + */ + JSONObject getUserInfo(String accessToken); + + /** + * 获取文件列表 + * + * @param accessToken 访问令牌 + * @param params 查询参数(page, page_size等) + * @return 文件列表 + */ + JSONObject getFileList(String accessToken, Map params); + + /** + * 获取文件信息 + * + * @param accessToken 访问令牌 + * @param fileToken 文件token + * @return 文件信息 + */ + JSONObject getFileInfo(String accessToken, String fileToken); + + /** + * 更新单元格数据(KSheet - 在线表格) + * + * @param accessToken 访问令牌 + * @param fileToken 文件token + * @param sheetIdx 工作表索引(从0开始) + * @param range 单元格范围(如:A1:B2) + * @param values 单元格值(二维数组,第一维是行,第二维是列) + * @return 更新结果 + */ + JSONObject updateCells(String accessToken, String fileToken, int sheetIdx, String range, List> values); + + /** + * 读取单元格数据 + * + * @param accessToken 访问令牌 + * @param fileToken 文件token + * @param sheetIdx 工作表索引 + * @param range 单元格范围 + * @return 单元格数据 + */ + JSONObject readCells(String accessToken, String fileToken, int sheetIdx, String range); + + /** + * 获取工作表列表 + * + * @param accessToken 访问令牌 + * @param fileToken 文件token + * @return 工作表列表 + */ + JSONObject getSheetList(String accessToken, String fileToken); + + /** + * 创建数据表 + * + * @param accessToken 访问令牌 + * @param fileToken 文件token + * @param sheetName 工作表名称 + * @return 创建结果 + */ + JSONObject createSheet(String accessToken, String fileToken, String sheetName); + + /** + * 批量更新单元格数据 + * + * @param accessToken 访问令牌 + * @param fileToken 文件token + * @param sheetIdx 工作表索引 + * @param updates 更新列表,每个元素包含range和values + * @return 更新结果 + */ + JSONObject batchUpdateCells(String accessToken, String fileToken, int sheetIdx, List> updates); +} + diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWPS365OAuthService.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWPS365OAuthService.java new file mode 100644 index 0000000..5897989 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/IWPS365OAuthService.java @@ -0,0 +1,66 @@ +package com.ruoyi.jarvis.service; + +import com.ruoyi.jarvis.domain.dto.WPS365TokenInfo; + +/** + * WPS365 OAuth授权服务接口 + * + * @author system + */ +public interface IWPS365OAuthService { + + /** + * 获取授权URL + * + * @param state 状态参数(可选,用于防止CSRF攻击) + * @return 授权URL + */ + String getAuthUrl(String state); + + /** + * 通过授权码获取访问令牌 + * + * @param code 授权码 + * @return Token信息 + */ + WPS365TokenInfo getAccessTokenByCode(String code); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 新的Token信息 + */ + WPS365TokenInfo refreshAccessToken(String refreshToken); + + /** + * 获取当前用户的访问令牌 + * + * @return Token信息,如果未授权则返回null + */ + WPS365TokenInfo getCurrentToken(); + + /** + * 保存用户令牌信息 + * + * @param userId 用户ID + * @param tokenInfo Token信息 + */ + void saveToken(String userId, WPS365TokenInfo tokenInfo); + + /** + * 清除用户令牌信息 + * + * @param userId 用户ID + */ + void clearToken(String userId); + + /** + * 检查令牌是否有效 + * + * @param tokenInfo Token信息 + * @return true表示有效,false表示需要刷新或重新授权 + */ + boolean isTokenValid(WPS365TokenInfo tokenInfo); +} + diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WPS365ApiServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WPS365ApiServiceImpl.java new file mode 100644 index 0000000..c28c526 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WPS365ApiServiceImpl.java @@ -0,0 +1,200 @@ +package com.ruoyi.jarvis.service.impl; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.jarvis.config.WPS365Config; +import com.ruoyi.jarvis.service.IWPS365ApiService; +import com.ruoyi.jarvis.util.WPS365ApiUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +/** + * WPS365 API服务实现类 + * + * @author system + */ +@Service +public class WPS365ApiServiceImpl implements IWPS365ApiService { + + private static final Logger log = LoggerFactory.getLogger(WPS365ApiServiceImpl.class); + + @Autowired + private WPS365Config wps365Config; + + @Override + public JSONObject getUserInfo(String accessToken) { + try { + String url = wps365Config.getApiBaseUrl() + "/user/info"; + return WPS365ApiUtil.httpRequest("GET", url, accessToken, null); + } catch (Exception e) { + log.error("获取用户信息失败", e); + throw new RuntimeException("获取用户信息失败: " + e.getMessage(), e); + } + } + + @Override + public JSONObject getFileList(String accessToken, Map params) { + try { + StringBuilder url = new StringBuilder(wps365Config.getApiBaseUrl() + "/files"); + + // 添加查询参数 + if (params != null && !params.isEmpty()) { + url.append("?"); + boolean first = true; + for (Map.Entry entry : params.entrySet()) { + if (!first) { + url.append("&"); + } + url.append(entry.getKey()).append("=").append(entry.getValue()); + first = false; + } + } + + return WPS365ApiUtil.httpRequest("GET", url.toString(), accessToken, null); + } catch (Exception e) { + log.error("获取文件列表失败", e); + throw new RuntimeException("获取文件列表失败: " + e.getMessage(), e); + } + } + + @Override + public JSONObject getFileInfo(String accessToken, String fileToken) { + try { + String url = wps365Config.getApiBaseUrl() + "/files/" + fileToken; + return WPS365ApiUtil.httpRequest("GET", url, accessToken, null); + } catch (Exception e) { + log.error("获取文件信息失败 - fileToken: {}", fileToken, e); + throw new RuntimeException("获取文件信息失败: " + e.getMessage(), e); + } + } + + @Override + public JSONObject updateCells(String accessToken, String fileToken, int sheetIdx, String range, List> values) { + try { + // WPS365 KSheet API: /api/v1/openapi/ksheet/:file_token/sheets/:sheet_idx/cells + String url = wps365Config.getApiBaseUrl() + "/openapi/ksheet/" + fileToken + "/sheets/" + sheetIdx + "/cells"; + + // 构建请求体 + JSONObject requestBody = new JSONObject(); + requestBody.put("range", range); + + // 将values转换为JSONArray + JSONArray valuesArray = new JSONArray(); + if (values != null) { + for (List row : values) { + JSONArray rowArray = new JSONArray(); + if (row != null) { + for (Object cell : row) { + rowArray.add(cell != null ? cell : ""); + } + } + valuesArray.add(rowArray); + } + } + requestBody.put("values", valuesArray); + + String bodyStr = requestBody.toJSONString(); + log.debug("更新单元格数据 - url: {}, range: {}, values: {}", url, range, bodyStr); + + return WPS365ApiUtil.httpRequest("POST", url, accessToken, bodyStr); + } catch (Exception e) { + log.error("更新单元格数据失败 - fileToken: {}, sheetIdx: {}, range: {}", fileToken, sheetIdx, range, e); + throw new RuntimeException("更新单元格数据失败: " + e.getMessage(), e); + } + } + + @Override + public JSONObject readCells(String accessToken, String fileToken, int sheetIdx, String range) { + try { + // WPS365 KSheet API: GET /api/v1/openapi/ksheet/:file_token/sheets/:sheet_idx/cells + String url = wps365Config.getApiBaseUrl() + "/openapi/ksheet/" + fileToken + "/sheets/" + sheetIdx + "/cells"; + if (range != null && !range.trim().isEmpty()) { + url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8"); + } + + return WPS365ApiUtil.httpRequest("GET", url, accessToken, null); + } catch (Exception e) { + log.error("读取单元格数据失败 - fileToken: {}, sheetIdx: {}, range: {}", fileToken, sheetIdx, range, e); + throw new RuntimeException("读取单元格数据失败: " + e.getMessage(), e); + } + } + + @Override + public JSONObject getSheetList(String accessToken, String fileToken) { + try { + // WPS365 KSheet API: GET /api/v1/openapi/ksheet/:file_token/sheets + String url = wps365Config.getApiBaseUrl() + "/openapi/ksheet/" + fileToken + "/sheets"; + return WPS365ApiUtil.httpRequest("GET", url, accessToken, null); + } catch (Exception e) { + log.error("获取工作表列表失败 - fileToken: {}", fileToken, e); + throw new RuntimeException("获取工作表列表失败: " + e.getMessage(), e); + } + } + + @Override + public JSONObject createSheet(String accessToken, String fileToken, String sheetName) { + try { + // WPS365 KSheet API: POST /api/v1/openapi/ksheet/:file_token/sheets + String url = wps365Config.getApiBaseUrl() + "/openapi/ksheet/" + fileToken + "/sheets"; + + JSONObject requestBody = new JSONObject(); + requestBody.put("name", sheetName); + + String bodyStr = requestBody.toJSONString(); + return WPS365ApiUtil.httpRequest("POST", url, accessToken, bodyStr); + } catch (Exception e) { + log.error("创建数据表失败 - fileToken: {}, sheetName: {}", fileToken, sheetName, e); + throw new RuntimeException("创建数据表失败: " + e.getMessage(), e); + } + } + + @Override + public JSONObject batchUpdateCells(String accessToken, String fileToken, int sheetIdx, List> updates) { + try { + // WPS365 KSheet API: POST /api/v1/openapi/ksheet/:file_token/sheets/:sheet_idx/cells/batch + String url = wps365Config.getApiBaseUrl() + "/openapi/ksheet/" + fileToken + "/sheets/" + sheetIdx + "/cells/batch"; + + JSONObject requestBody = new JSONObject(); + JSONArray updatesArray = new JSONArray(); + + if (updates != null) { + for (Map update : updates) { + JSONObject updateObj = new JSONObject(); + updateObj.put("range", update.get("range")); + + // 将values转换为JSONArray + @SuppressWarnings("unchecked") + List> values = (List>) update.get("values"); + JSONArray valuesArray = new JSONArray(); + if (values != null) { + for (List row : values) { + JSONArray rowArray = new JSONArray(); + if (row != null) { + for (Object cell : row) { + rowArray.add(cell != null ? cell : ""); + } + } + valuesArray.add(rowArray); + } + } + updateObj.put("values", valuesArray); + updatesArray.add(updateObj); + } + } + + requestBody.put("updates", updatesArray); + String bodyStr = requestBody.toJSONString(); + + return WPS365ApiUtil.httpRequest("POST", url, accessToken, bodyStr); + } catch (Exception e) { + log.error("批量更新单元格数据失败 - fileToken: {}, sheetIdx: {}", fileToken, sheetIdx, e); + throw new RuntimeException("批量更新单元格数据失败: " + e.getMessage(), e); + } + } +} + diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WPS365OAuthServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WPS365OAuthServiceImpl.java new file mode 100644 index 0000000..6b5540f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/WPS365OAuthServiceImpl.java @@ -0,0 +1,207 @@ +package com.ruoyi.jarvis.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.jarvis.config.WPS365Config; +import com.ruoyi.jarvis.domain.dto.WPS365TokenInfo; +import com.ruoyi.jarvis.service.IWPS365OAuthService; +import com.ruoyi.jarvis.util.WPS365ApiUtil; +import com.ruoyi.common.core.redis.RedisCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * WPS365 OAuth授权服务实现类 + * + * @author system + */ +@Service +public class WPS365OAuthServiceImpl implements IWPS365OAuthService { + + private static final Logger log = LoggerFactory.getLogger(WPS365OAuthServiceImpl.class); + + /** Redis key前缀,用于存储用户token */ + private static final String TOKEN_KEY_PREFIX = "wps365:token:"; + + /** Token过期时间(秒),默认30天 */ + private static final long TOKEN_EXPIRE_TIME = 30 * 24 * 60 * 60; + + @Autowired + private WPS365Config wps365Config; + + @Autowired + private RedisCache redisCache; + + @Override + public String getAuthUrl(String state) { + if (wps365Config == null) { + throw new RuntimeException("WPS365配置未加载,请检查WPS365Config是否正确注入"); + } + + String appId = wps365Config.getAppId(); + String redirectUri = wps365Config.getRedirectUri(); + String oauthUrl = wps365Config.getOauthUrl(); + + log.debug("获取授权URL - appId: {}, redirectUri: {}, oauthUrl: {}", appId, redirectUri, oauthUrl); + + // 验证配置参数 + if (appId == null || appId.trim().isEmpty()) { + throw new RuntimeException("WPS365应用ID未配置,请检查application.yml中的wps365.app-id配置"); + } + if (redirectUri == null || redirectUri.trim().isEmpty()) { + throw new RuntimeException("WPS365回调地址未配置,请检查application.yml中的wps365.redirect-uri配置"); + } + + // 构建授权URL + StringBuilder authUrl = new StringBuilder(); + authUrl.append(oauthUrl); + authUrl.append("?client_id=").append(appId); + try { + String encodedRedirectUri = java.net.URLEncoder.encode(redirectUri, "UTF-8"); + authUrl.append("&redirect_uri=").append(encodedRedirectUri); + } catch (java.io.UnsupportedEncodingException e) { + log.error("URL编码失败", e); + authUrl.append("&redirect_uri=").append(redirectUri); + } + authUrl.append("&response_type=code"); + // WPS365的scope,根据官方文档设置 + authUrl.append("&scope=file.readwrite,user.info"); + + // 添加state参数 + if (state == null || state.trim().isEmpty()) { + state = UUID.randomUUID().toString(); + } + authUrl.append("&state=").append(state); + + String result = authUrl.toString(); + log.info("生成授权URL: {}", result); + return result; + } + + @Override + public WPS365TokenInfo getAccessTokenByCode(String code) { + try { + JSONObject result = WPS365ApiUtil.getAccessToken( + wps365Config.getAppId(), + wps365Config.getAppKey(), + code, + wps365Config.getRedirectUri(), + wps365Config.getTokenUrl() + ); + + // 解析响应并创建TokenInfo对象 + WPS365TokenInfo tokenInfo = new WPS365TokenInfo(); + tokenInfo.setAccessToken(result.getString("access_token")); + tokenInfo.setRefreshToken(result.getString("refresh_token")); + tokenInfo.setTokenType(result.getString("token_type")); + tokenInfo.setExpiresIn(result.getInteger("expires_in")); + tokenInfo.setScope(result.getString("scope")); + tokenInfo.setUserId(result.getString("user_id")); + + log.info("成功获取访问令牌 - userId: {}", tokenInfo.getUserId()); + + return tokenInfo; + } catch (Exception e) { + log.error("通过授权码获取访问令牌失败", e); + throw new RuntimeException("获取访问令牌失败: " + e.getMessage(), e); + } + } + + @Override + public WPS365TokenInfo refreshAccessToken(String refreshToken) { + try { + JSONObject result = WPS365ApiUtil.refreshAccessToken( + wps365Config.getAppId(), + wps365Config.getAppKey(), + refreshToken, + wps365Config.getRefreshTokenUrl() + ); + + // 解析响应并创建TokenInfo对象 + WPS365TokenInfo tokenInfo = new WPS365TokenInfo(); + tokenInfo.setAccessToken(result.getString("access_token")); + tokenInfo.setRefreshToken(result.getString("refresh_token")); + tokenInfo.setTokenType(result.getString("token_type")); + tokenInfo.setExpiresIn(result.getInteger("expires_in")); + tokenInfo.setScope(result.getString("scope")); + tokenInfo.setUserId(result.getString("user_id")); + + log.info("成功刷新访问令牌 - userId: {}", tokenInfo.getUserId()); + + return tokenInfo; + } catch (Exception e) { + log.error("刷新访问令牌失败", e); + throw new RuntimeException("刷新访问令牌失败: " + e.getMessage(), e); + } + } + + @Override + public WPS365TokenInfo getCurrentToken() { + // 这里需要根据实际业务获取当前用户ID + // 暂时返回null,需要在Controller中处理用户ID获取逻辑 + return null; + } + + @Override + public void saveToken(String userId, WPS365TokenInfo tokenInfo) { + if (userId == null || userId.trim().isEmpty()) { + throw new IllegalArgumentException("用户ID不能为空"); + } + if (tokenInfo == null || tokenInfo.getAccessToken() == null) { + throw new IllegalArgumentException("Token信息不能为空"); + } + + String key = TOKEN_KEY_PREFIX + userId; + redisCache.setCacheObject(key, tokenInfo, (int) TOKEN_EXPIRE_TIME, TimeUnit.SECONDS); + log.info("保存用户Token - userId: {}", userId); + } + + @Override + public void clearToken(String userId) { + if (userId == null || userId.trim().isEmpty()) { + return; + } + + String key = TOKEN_KEY_PREFIX + userId; + redisCache.deleteObject(key); + log.info("清除用户Token - userId: {}", userId); + } + + @Override + public boolean isTokenValid(WPS365TokenInfo tokenInfo) { + if (tokenInfo == null) { + return false; + } + + // 检查是否过期 + if (tokenInfo.isExpired()) { + log.debug("Token已过期"); + return false; + } + + // 检查必要字段 + if (tokenInfo.getAccessToken() == null || tokenInfo.getAccessToken().trim().isEmpty()) { + log.debug("Token缺少accessToken"); + return false; + } + + return true; + } + + /** + * 根据用户ID获取Token(从Redis) + */ + public WPS365TokenInfo getTokenByUserId(String userId) { + if (userId == null || userId.trim().isEmpty()) { + return null; + } + + String key = TOKEN_KEY_PREFIX + userId; + return redisCache.getCacheObject(key); + } +} + diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/WPS365ApiUtil.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/WPS365ApiUtil.java new file mode 100644 index 0000000..bc816e0 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/WPS365ApiUtil.java @@ -0,0 +1,240 @@ +package com.ruoyi.jarvis.util; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +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; + +/** + * WPS365 API工具类 + * + * @author system + */ +public class WPS365ApiUtil { + + private static final Logger log = LoggerFactory.getLogger(WPS365ApiUtil.class); + + // 静态初始化块:禁用系统代理 + static { + System.setProperty("java.net.useSystemProxies", "false"); + System.clearProperty("http.proxyHost"); + System.clearProperty("http.proxyPort"); + System.clearProperty("https.proxyHost"); + System.clearProperty("https.proxyPort"); + log.info("已禁用系统代理设置,WPS365 API将直接连接"); + } + + /** + * 获取访问令牌 + * + * @param appId 应用ID + * @param appKey 应用密钥 + * @param code 授权码 + * @param redirectUri 回调地址 + * @param tokenUrl Token地址 + * @return 包含access_token和refresh_token的JSON对象 + */ + public static JSONObject getAccessToken(String appId, String appKey, 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(appKey); + params.append("&code=").append(code); + params.append("&redirect_uri=").append(java.net.URLEncoder.encode(redirectUri, "UTF-8")); + + // 使用HttpURLConnection,不使用代理 + URL url = new URL(tokenUrl); + java.net.Proxy proxy = java.net.Proxy.NO_PROXY; + HttpURLConnection conn = (HttpURLConnection) url.openConnection(proxy); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("Accept", "application/json"); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setConnectTimeout(10000); + conn.setReadTimeout(30000); + + // 写入请求参数 + try (OutputStream os = conn.getOutputStream(); + OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8)) { + osw.write(params.toString()); + 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.info("获取访问令牌响应: statusCode={}, response={}", statusCode, responseStr); + + if (statusCode < 200 || statusCode >= 300) { + throw new RuntimeException("获取访问令牌失败: HTTP " + statusCode + ", response=" + responseStr); + } + + return JSON.parseObject(responseStr); + } catch (Exception e) { + log.error("获取访问令牌失败", e); + throw new RuntimeException("获取访问令牌失败: " + e.getMessage(), e); + } + } + + /** + * 刷新访问令牌 + * + * @param appId 应用ID + * @param appKey 应用密钥 + * @param refreshToken 刷新令牌 + * @param refreshTokenUrl 刷新令牌地址 + * @return 包含新的access_token和refresh_token的JSON对象 + */ + public static JSONObject refreshAccessToken(String appId, String appKey, 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(appKey); + params.append("&refresh_token=").append(refreshToken); + + // 使用HttpURLConnection,不使用代理 + URL url = new URL(refreshTokenUrl); + java.net.Proxy proxy = java.net.Proxy.NO_PROXY; + HttpURLConnection conn = (HttpURLConnection) url.openConnection(proxy); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("Accept", "application/json"); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setConnectTimeout(10000); + conn.setReadTimeout(30000); + + // 写入请求参数 + try (OutputStream os = conn.getOutputStream(); + OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8)) { + osw.write(params.toString()); + 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.info("刷新访问令牌响应: statusCode={}, response={}", statusCode, responseStr); + + if (statusCode < 200 || statusCode >= 300) { + throw new RuntimeException("刷新访问令牌失败: HTTP " + statusCode + ", response=" + responseStr); + } + + return JSON.parseObject(responseStr); + } catch (Exception e) { + log.error("刷新访问令牌失败", e); + throw new RuntimeException("刷新访问令牌失败: " + e.getMessage(), e); + } + } + + /** + * 发送HTTP请求 + * + * @param method 请求方法(GET/POST/PUT/DELETE) + * @param url 请求URL + * @param accessToken 访问令牌 + * @param body 请求体(JSON字符串,GET请求可为null) + * @return 响应JSON对象 + */ + public static JSONObject httpRequest(String method, String url, String accessToken, String body) { + try { + log.debug("发送HTTP请求: method={}, url={}", method, url); + + URL requestUrl = new URL(url); + java.net.Proxy proxy = java.net.Proxy.NO_PROXY; + HttpURLConnection conn = (HttpURLConnection) requestUrl.openConnection(proxy); + conn.setRequestMethod(method); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("Authorization", "Bearer " + accessToken); + conn.setConnectTimeout(10000); + conn.setReadTimeout(30000); + + if ("POST".equals(method) || "PUT".equals(method) || "PATCH".equals(method)) { + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + + 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("HTTP响应: statusCode={}, response={}", statusCode, responseStr); + + if (statusCode < 200 || statusCode >= 300) { + throw new RuntimeException("HTTP请求失败: " + statusCode + ", response=" + responseStr); + } + + if (responseStr == null || responseStr.trim().isEmpty()) { + return new JSONObject(); + } + + return JSON.parseObject(responseStr); + } catch (Exception e) { + log.error("HTTP请求失败: method={}, url={}", method, url, e); + throw new RuntimeException("HTTP请求失败: " + e.getMessage(), e); + } + } +} +