Compare commits

...

122 Commits

Author SHA1 Message Date
Leo
72ff30567b 1 2026-01-26 22:31:46 +08:00
Leo
caa36c4966 1 2026-01-19 21:22:55 +08:00
Leo
269e8e48a7 1 2026-01-19 20:53:46 +08:00
Leo
dde274acba 1 2026-01-17 23:52:56 +08:00
Leo
0297c6e131 1 2026-01-17 23:47:19 +08:00
Leo
6394658a70 1 2026-01-17 23:40:10 +08:00
Leo
dafd63a9ec 1 2026-01-17 13:43:46 +08:00
Leo
5a8e1198cf 1 2026-01-17 13:40:48 +08:00
Leo
6257d816e9 1 2026-01-17 13:23:32 +08:00
Leo
3336eeb6aa 1 2026-01-16 18:07:24 +08:00
Leo
18f541fdf7 1 2026-01-16 18:02:54 +08:00
Leo
9cb5e6a488 1 2026-01-15 22:00:09 +08:00
Leo
2ce45e5ccc 1 2026-01-15 21:49:38 +08:00
Leo
c837917be3 1 2026-01-15 21:07:53 +08:00
Leo
4379277a08 WPS365 2026-01-15 21:03:42 +08:00
Leo
ff9ab96833 WPS365 2026-01-15 20:51:40 +08:00
Leo
2b74f77419 WPS365 2026-01-15 20:48:39 +08:00
Leo
76500642eb WPS365 2026-01-15 20:42:07 +08:00
Leo
a41c9ceaf9 1 2026-01-15 20:38:45 +08:00
Leo
7f7bec8d29 1 2026-01-15 20:37:49 +08:00
Leo
8802e68106 1 2026-01-15 20:21:13 +08:00
Leo
2fb283c3f3 1 2026-01-15 20:13:15 +08:00
Leo
f044417d8d 1 2026-01-15 16:22:00 +08:00
Leo
ba1e025326 1 2026-01-15 16:13:16 +08:00
Leo
61f66d90b4 1 2026-01-15 16:12:42 +08:00
Leo
e93e7cec46 1 2026-01-15 16:10:41 +08:00
Leo
d5df4108a6 1 2026-01-15 16:02:04 +08:00
Leo
c58adef068 1 2026-01-15 15:28:10 +08:00
Leo
b43daf2965 1 2026-01-14 22:55:36 +08:00
Leo
ab062b3b5a 1 2026-01-13 23:05:20 +08:00
Leo
01b19602b6 1 2026-01-09 20:08:13 +08:00
Leo
01e5312ccc 1 2026-01-07 15:15:55 +08:00
Leo
00f0c38672 1 2026-01-06 22:52:24 +08:00
Leo
ba6f250914 1 2026-01-06 22:07:52 +08:00
Leo
fb00ecbcb6 1 2026-01-06 19:06:37 +08:00
Leo
9483ebf1f5 1 2026-01-06 18:51:38 +08:00
Leo
440df7d538 1 2026-01-06 18:50:24 +08:00
Leo
2fe78ec192 1 2026-01-06 18:44:16 +08:00
Leo
ccf8298e17 1 2026-01-06 18:22:11 +08:00
Leo
46d2a209c0 1 2026-01-06 15:56:27 +08:00
Leo
9935c6c07e 1 2026-01-06 15:51:56 +08:00
Leo
23c59a5e52 1 2026-01-06 01:03:53 +08:00
Leo
0556f19e97 1 2026-01-06 00:41:09 +08:00
Leo
74d3579e1e 1 2026-01-06 00:37:57 +08:00
Leo
160c97eb5b 1 2026-01-06 00:10:37 +08:00
Leo
407640ff96 1 2026-01-05 22:13:46 +08:00
Leo
81203488c8 1 2026-01-05 22:02:15 +08:00
Leo
e9747e6af2 1 2026-01-03 12:20:09 +08:00
Leo
4ba1f6a572 1 2025-12-24 14:29:23 +08:00
Leo
d6ab231534 1 2025-12-24 14:22:37 +08:00
Leo
dfa6109788 1 2025-12-23 18:46:26 +08:00
Leo
c56b911db0 1 2025-12-23 18:18:06 +08:00
Leo
d98711f06a 1 2025-12-23 15:59:05 +08:00
Leo
40dd64482c 1 2025-12-22 22:56:19 +08:00
Leo
77dcc149c3 1 2025-12-14 16:35:20 +08:00
Leo
1a14830dac 1 2025-12-14 15:39:26 +08:00
Leo
30ca39a4b6 1 2025-12-14 15:04:50 +08:00
Leo
237f0c88ad 1 2025-12-14 14:56:06 +08:00
Leo
2fd371f2f4 1 2025-12-14 14:50:49 +08:00
Leo
c661e921df 1 2025-12-14 14:50:38 +08:00
Leo
f4c07859e4 1 2025-12-14 13:43:30 +08:00
Leo
d1c374ca99 Merge branch 'master' of https://git.van333.cn/CC/ruoyi-java 2025-12-14 00:00:54 +08:00
Leo
317ab03c7c 1 2025-12-14 00:00:49 +08:00
b86c4bea88 1 2025-12-13 13:48:42 +08:00
Leo
9a8c7b1039 1 2025-12-08 14:42:44 +08:00
Leo
632b9f7eb1 1 2025-12-06 17:08:38 +08:00
Leo
eb53915bcd 1 2025-12-05 22:48:22 +08:00
Leo
4dd3e9dd70 1 2025-12-05 22:35:55 +08:00
Leo
9206824efb 1 2025-12-05 22:20:17 +08:00
Leo
2524461ff4 1 2025-12-05 22:16:25 +08:00
Leo
7581cc02a9 1 2025-12-02 17:39:27 +08:00
Leo
1dc91a6bb0 1 2025-12-02 01:41:51 +08:00
Leo
6b3c2b17c8 1 2025-11-29 23:39:40 +08:00
Leo
e890b18e3e 1 2025-11-29 23:28:53 +08:00
Leo
9b2b770e29 1 2025-11-26 15:01:30 +08:00
Leo
047575ea42 1 2025-11-25 21:27:15 +08:00
Leo
702463b856 1 2025-11-25 18:56:37 +08:00
Leo
3aa3da8ade 1 2025-11-24 19:02:07 +08:00
Leo
20861d270a 1 2025-11-24 18:55:02 +08:00
Leo
e7c991ed9c 1 2025-11-21 23:26:29 +08:00
Leo
2ead103faa 1 2025-11-20 23:38:04 +08:00
Leo
c541beb413 1 2025-11-19 22:29:52 +08:00
Leo
083bcca270 1 2025-11-19 16:02:30 +08:00
Leo
35dcb20e4a 1 2025-11-19 15:58:40 +08:00
Leo
7648b934ed 1 2025-11-16 00:28:52 +08:00
Leo
01f0be6198 1 2025-11-16 00:12:07 +08:00
Leo
276fb49354 1 2025-11-15 23:59:36 +08:00
Leo
4f917dce10 1 2025-11-15 23:45:41 +08:00
Leo
98b56ab11b 1 2025-11-15 17:48:17 +08:00
Leo
b495431b7e 1 2025-11-15 17:42:56 +08:00
Leo
7f4b0dd986 1 2025-11-15 17:39:42 +08:00
Leo
79c5bf266f 1 2025-11-15 17:33:03 +08:00
Leo
04156492a6 1 2025-11-15 15:15:09 +08:00
Leo
f578b9b2c9 1 2025-11-15 15:08:02 +08:00
Leo
6b07fa1d75 1 2025-11-15 11:26:01 +08:00
Leo
978da7042d 1 2025-11-15 01:45:20 +08:00
Leo
66ac54ca70 1 2025-11-15 00:45:52 +08:00
Leo
026c6bf2a3 1 2025-11-14 23:55:59 +08:00
Leo
2b0587d4e1 1 2025-11-14 23:48:19 +08:00
Leo
0880628c93 1 2025-11-14 23:43:41 +08:00
Leo
2e59f49677 1 2025-11-14 23:42:26 +08:00
Leo
a54c8cc0cd 1 2025-11-14 00:13:18 +08:00
Leo
8a23c4d3f7 1 2025-11-14 00:02:40 +08:00
Leo
b8981ffc98 1 2025-11-13 23:55:25 +08:00
Leo
9e69230948 1 2025-11-13 23:54:14 +08:00
Leo
64ce923631 1 2025-11-13 23:51:44 +08:00
Leo
2cd3a0a798 1 2025-11-13 23:38:30 +08:00
Leo
8889791a83 1 2025-11-13 16:08:45 +08:00
Leo
e184c7926f 1 2025-11-13 11:38:26 +08:00
Leo
d73c7b6560 1 2025-11-11 18:18:19 +08:00
Leo
9d8f2ded0c 1 2025-11-11 14:13:11 +08:00
Leo
7294748ae9 1 2025-11-11 14:06:34 +08:00
Leo
142b395dbe 1 2025-11-11 12:41:21 +08:00
Leo
c8b15275a4 Revert "1"
This reverts commit e79e7081ee.
2025-11-11 12:30:45 +08:00
Leo
a61003fb7c 1 2025-11-11 00:42:05 +08:00
Leo
939d03e192 1 2025-11-11 00:24:09 +08:00
Leo
e2facc3099 1 2025-11-10 23:32:04 +08:00
Leo
af68b529b0 1 2025-11-10 21:21:01 +08:00
Leo
185483dace 1 2025-11-10 21:02:44 +08:00
Leo
e79e7081ee 1 2025-11-10 19:07:24 +08:00
Leo
3176e45057 1 2025-11-10 18:55:03 +08:00
Leo
72b3458ef9 1 2025-11-09 23:54:38 +08:00
73 changed files with 11651 additions and 702 deletions

View File

@@ -0,0 +1,119 @@
# WPS365 回调地址配置说明
## 错误信息
如果遇到以下错误:
```json
{
"code": 40000001,
"msg": "invalid_request",
"debug": {
"desc": "The 'redirect_uri' parameter does not match any of the OAuth 2.0 Client's pre-registered redirect urls."
}
}
```
**原因**`redirect_uri` 参数与WPS365开放平台配置的回调地址不一致。
## 解决方案
### 1. 检查当前配置的回调地址
查看 `application.yml` 中的配置:
```yaml
wps365:
redirect-uri: https://jarvis.van333.cn/wps365-callback
```
### 2. 在WPS365开放平台配置回调地址
1. **登录WPS365开放平台**
- 访问https://open.wps.cn/
- 进入你的应用管理页面
2. **找到"安全设置"或"回调配置"**
- 通常在"开发配置" > "安全设置" 或 "事件与回调" 中
3. **配置授权回调地址**
- 回调地址必须与配置文件中的 `redirect-uri` **完全一致**
- 包括:
- ✅ 协议:`https://`(不能是 `http://`
- ✅ 域名:`jarvis.van333.cn`(不能有 `www` 或其他前缀)
- ✅ 路径:`/wps365-callback`(不能有多余的斜杠)
- ✅ 端口如果使用默认端口443不需要写端口号
4. **正确的配置示例**
```
https://jarvis.van333.cn/wps365-callback
```
5. **错误的配置示例(会导致错误)**
```
❌ http://jarvis.van333.cn/wps365-callback (协议错误)
❌ https://www.jarvis.van333.cn/wps365-callback (域名不一致)
❌ https://jarvis.van333.cn/wps365-callback/ (末尾多斜杠)
❌ https://jarvis.van333.cn:443/wps365-callback (端口号多余)
❌ https://jarvis.van333.cn/jarvis/wps365/oauth/callback (路径不一致)
```
### 3. 验证回调地址
配置完成后WPS365平台会发送一个challenge验证请求到你的回调地址。确保
- ✅ 回调接口支持GET和POST请求
- ✅ 能正确处理challenge参数并返回
- ✅ 接口可以正常访问(不被防火墙拦截)
### 4. 常见问题排查
#### 问题1配置了但验证失败
- 检查回调接口是否支持POST请求
- 检查是否能正确处理challenge参数
- 查看后端日志确认是否收到challenge请求
#### 问题2授权时提示redirect_uri不匹配
- 检查WPS365平台配置的回调地址
- 检查配置文件中的redirect-uri
- 确保两者完全一致(包括大小写、斜杠等)
#### 问题3回调地址访问失败
- 检查nginx配置是否正确代理
- 检查前端白名单是否已配置
- 检查域名DNS解析是否正常
### 5. 调试步骤
1. **查看后端日志**
```
生成授权URL: https://openapi.wps.cn/oauth2/auth?client_id=xxx&redirect_uri=...
使用回调地址: https://jarvis.van333.cn/wps365-callback
```
2. **复制日志中的回调地址**
- 确保WPS365平台配置的地址与日志中的地址完全一致
3. **测试回调地址**
- 直接在浏览器访问:`https://jarvis.van333.cn/wps365-callback`
- 应该能看到授权页面或提示信息
4. **重新授权**
- 修改配置后,重新点击"立即授权"
- 如果还是失败检查WPS365平台的配置
## 配置检查清单
- [ ] WPS365开放平台已配置回调地址
- [ ] 回调地址与配置文件中的redirect-uri完全一致
- [ ] 回调接口支持GET和POST请求
- [ ] 回调接口能正确处理challenge验证
- [ ] 前端白名单已配置 `/wps365-callback`
- [ ] nginx已配置 `/wps365-callback` 路由
- [ ] 域名DNS解析正常
- [ ] SSL证书有效
## 注意事项
1. **回调地址必须使用HTTPS**(生产环境)
2. **路径不能有末尾斜杠**(除非是根路径)
3. **域名必须完全匹配**不能有www前缀等
4. **配置后需要等待几分钟生效**WPS365平台可能有缓存

View File

@@ -0,0 +1,259 @@
# WPS365 授权错误排查指南
## 常见错误类型
### 1. invalid_request (40000001) - redirect_uri不匹配
```json
{
"code": 40000001,
"msg": "invalid_request",
"debug": {
"desc": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. The 'redirect_uri' parameter does not match any of the OAuth 2.0 Client's pre-registered redirect urls."
}
}
```
**错误含义**redirect_uri参数值与WPS365平台配置的回调地址不一致
### 2. invalid_scope (40000005) - scope权限无效 ⚠️
```json
{
"code": 40000005,
"msg": "invalid_scope",
"debug": {
"desc": "The requested scope is invalid, unknown, or malformed. The OAuth 2.0 Client is not allowed to request scope 'file.read,ksheet.read,user.info'."
}
}
```
**错误含义**请求的scope权限格式不正确或者应用未申请这些权限
## 错误含义总览
授权错误可能由以下原因导致:
1. **缺少必需参数** - 授权请求中缺少某个必需的参数
2. **参数值无效** - 某个参数的值格式不正确
3. **参数重复** - 某个参数在请求中出现了多次
4. **redirect_uri不匹配** - redirect_uri参数值与WPS365平台配置的回调地址不一致
5. **scope无效** - scope权限格式不正确或未申请
## 排查步骤
### 1. 查看后端日志
查看后端日志中的授权URL和参数清单例如
```
生成授权URL: https://openapi.wps.cn/oauth2/auth?client_id=xxx&redirect_uri=...
📋 授权请求参数清单:
- client_id: AK20260114NNQJKV
- redirect_uri: https://jarvis.van333.cn/wps365-callback
- response_type: code
- scope: file.read,ksheet.read,user.info
- state: xxxxx
```
### 2. 检查参数名是否正确
**问题**WPS365可能使用 `app_id` 而不是标准的 `client_id`
**解决方案**
- 如果使用 `client_id` 报错,尝试改为 `app_id`
- 查看WPS365官方文档确认正确的参数名
**当前代码使用**`client_id`标准OAuth2参数名
### 3. 检查必需参数是否齐全
授权请求必须包含以下参数:
| 参数名 | 是否必需 | 说明 | 当前值 |
|--------|---------|------|--------|
| `client_id``app_id` | ✅ 必需 | 应用ID | 从配置读取 |
| `redirect_uri` | ✅ 必需 | 回调地址 | 从配置读取 |
| `response_type` | ✅ 必需 | 固定值 `code` | `code` |
| `scope` | ✅ 必需 | 权限范围 | `file.read,ksheet.read,user.info` |
| `state` | ⚠️ 推荐 | 防CSRF攻击 | 自动生成 |
### 4. 检查redirect_uri是否匹配
这是最常见的错误原因。必须确保:
1. **协议一致**:必须是 `https://`(不能是 `http://`
2. **域名一致**:必须是 `jarvis.van333.cn`(不能有 `www` 前缀)
3. **路径一致**:必须是 `/wps365-callback`(不能有末尾斜杠)
4. **端口一致**使用默认443端口不需要写端口号
**在WPS365平台配置的回调地址必须与日志中的redirect_uri完全一致**
### 5. 检查scope权限是否已申请重要
**如果遇到 `invalid_scope` 错误,请按以下步骤排查:**
#### 5.1 检查WPS365平台后台的scope格式
1. 登录WPS365开放平台
2. 进入"开发配置" > "权限管理"
3. 查看已申请的权限列表,**注意权限的格式**
- 必须以 `kso.` 开头,如:`kso.file.read``kso.file.readwrite`
- 不是 `file.read``ksheet.read`(这些格式不存在)
- 根据官方文档https://open.wps.cn/documents/app-integration-dev/wps365/server/
#### 5.2 检查scope分隔符
**根据WPS365官方文档必须使用英文逗号分隔**(不是空格):
| 分隔符 | 示例 | 说明 |
|--------|------|------|
| **逗号(正确)** | `kso.file.readwrite,kso.doclib.readwrite` | ✅ WPS365官方要求 |
| **空格(错误)** | `kso.file.readwrite kso.doclib.readwrite` | ❌ 会导致invalid_scope |
| **逗号+空格** | `kso.file.readwrite, kso.doclib.readwrite` | ⚠️ 可能支持,但不推荐 |
**重要**WPS365官方文档明确要求使用英文逗号 `,` 分隔,不能使用空格。
#### 5.3 在配置文件中指定scope
`application.yml` 中添加 `scope` 配置,**必须使用英文逗号分隔**
```yaml
wps365:
# 根据WPS365平台后台"权限管理"中显示的实际权限名称配置
# 使用英文逗号分隔WPS365官方要求
# 权限名称必须以 kso. 开头
scope: kso.file.readwrite
```
多个权限示例:
```yaml
wps365:
# 多个权限用英文逗号分隔,不能有空格
# 权限名称必须以 kso. 开头
scope: kso.file.read,kso.file.readwrite
```
#### 5.4 确认权限已申请且名称正确
**关键步骤**
1. **登录WPS365开放平台**
- 访问https://open.wps.cn/
- 进入你的应用管理页面
2. **查看已申请的权限**
- 进入"开发配置" > "权限管理"
- 查看已申请权限的**准确名称**(注意大小写、分隔符、命名空间)
3. **对比权限名称**
- 权限名称必须与后台显示的**完全一致**
- 例如:如果后台显示的是 `kso.doclib.readwrite`,不能写成 `kso.doclib.readWrite``kso_doclib_readwrite`
4. **常见权限名称示例**(仅供参考,以平台后台实际显示为准):
- `kso.doclib.readwrite` - 文档库读写权限
- `kso.doclib.read` - 文档库读取权限
- `ksheet.read` - 在线表格读取权限(如果支持)
**重要提示**
- ⚠️ `file.read``ksheet.read``user.info` 这些权限名称可能不存在
- ✅ 必须查看WPS365平台后台实际显示的权限名称
- ✅ 权限名称必须完全匹配(包括大小写、分隔符、命名空间)
- ✅ 必须使用英文逗号分隔多个权限
### 6. 尝试修改参数名
如果确认所有参数都正确,但仍然报错,可能是参数名问题:
**测试方案1**:将 `client_id` 改为 `app_id`
修改 `WPS365OAuthServiceImpl.java` 中的代码:
```java
// 原代码
authUrl.append("?client_id=").append(appId);
// 改为
authUrl.append("?app_id=").append(appId);
```
**测试方案2**同时提供两个参数如果WPS365支持
```java
authUrl.append("?app_id=").append(appId);
authUrl.append("&client_id=").append(appId);
```
### 7. 检查URL编码
确保 `redirect_uri` 参数正确进行了URL编码
- 空格应该编码为 `%20`
- 斜杠 `/` 应该编码为 `%2F`
- 冒号 `:` 应该编码为 `%3A`
查看日志中的"编码后"值,确认编码是否正确。
### 8. 验证WPS365平台配置
登录WPS365开放平台检查
1. **应用IDAppId** 是否与配置文件中的 `app-id` 一致
2. **回调地址配置** 是否与日志中的 `redirect_uri` 完全一致
3. **权限配置** 是否已申请所需的scope权限
4. **应用状态** 是否为"已上线"或"测试中"
## 常见问题
### Q1: 参数名应该用 `client_id` 还是 `app_id`
**A**: 根据WPS365官方文档确认。标准OAuth2使用 `client_id`,但某些平台可能使用 `app_id`。如果 `client_id` 不工作,尝试 `app_id`
### Q2: 为什么redirect_uri明明配置了还是报错
**A**: 最常见的原因是:
- 平台配置的回调地址与代码中的不完全一致(多了/少了斜杠、协议不同等)
- 平台配置未保存或未生效
- 使用了错误的配置环境(开发/生产)
### Q3: scope权限在哪里申请
**A**: 在WPS365开放平台的"开发配置" > "权限管理"中申请。
### Q4: 如何确认参数是否正确?
**A**: 查看后端日志会打印完整的授权URL和参数清单。对比WPS365平台配置确保完全一致。
### Q5: 遇到 `invalid_scope` 错误怎么办? ⚠️
**A**: 这是scope格式或权限问题按以下步骤排查
1. **查看WPS365平台后台的权限格式**
- 进入"开发配置" > "权限管理"
- 查看已申请权限的**确切格式**(包括分隔符、大小写)
2. **尝试不同的scope格式**
- 空格分隔:`file.read ksheet.read user.info`(当前默认)
- 逗号分隔:`file.read,ksheet.read,user.info`
-`application.yml` 中配置 `scope` 参数进行测试
3. **确认权限已申请且已审核通过**
- 权限必须处于"已通过"或"可用"状态
- 如果权限未申请,需要先申请并等待审核
4. **查看后端日志中的scope值**
- 日志会显示实际使用的scope格式
- 对比WPS365平台显示的格式确保完全一致
## 调试建议
1. **启用DEBUG日志**:在 `application.yml` 中设置日志级别为DEBUG
2. **查看完整授权URL**复制日志中的授权URL在浏览器中访问查看具体错误
3. **对比官方文档**查看WPS365官方OAuth文档确认参数名和格式
4. **联系WPS365技术支持**:如果所有参数都正确但仍报错,可能是平台问题
## 下一步
如果按照以上步骤排查后仍然报错,请提供:
1. 后端日志中的完整授权URL和参数清单
2. WPS365平台配置的回调地址截图
3. WPS365平台的应用配置截图隐藏敏感信息

View File

@@ -0,0 +1,271 @@
# WPS365 读取在线表格内容配置指南
## 概述
本指南详细说明如何配置WPS365应用以实现读取自己创建的在线表格内容。
## 一、WPS365开放平台配置步骤
### 1. 应用基础配置
1. **登录WPS365开放平台**
- 访问https://open.wps.cn/
- 使用WPS账号登录
2. **创建应用**
- 进入"应用管理"
- 点击"创建应用"
- 填写应用名称Jarvis同步WPS
- 选择应用类型:**服务端应用**
3. **获取应用凭证**
- 记录 `AppID`应用ID
- 记录 `AppKey`(应用密钥,注意保密)
### 2. 配置回调地址
1. **进入"开发配置" > "事件与回调"**
2. **配置回调地址**
- 回调地址:`https://jarvis.van333.cn/wps365-callback`
- 点击"验证"按钮,确保验证通过
- 如果验证失败,检查:
- 回调接口是否支持GET和POST请求
- 是否能正确处理challenge参数
- 前端白名单是否已配置
### 3. 配置权限(关键步骤)
#### 3.1 进入权限管理
1. 在左侧导航栏选择 **"开发配置" > "权限管理"**
2. 查看当前应用的权限列表
#### 3.2 添加必要权限
需要添加以下权限才能读取在线表格内容:
**基础权限:**
-`file.read` - 读取文件权限
-`file.readwrite` - 读取和写入文件权限如果只需要读取可以只选read
-`user.info` - 获取用户信息权限
**在线表格KSheet权限**
-`ksheet:read` - 读取在线表格数据
-`ksheet:write` - 写入在线表格数据(如果需要编辑功能)
**权限说明:**
- 这些权限决定了你的应用能访问哪些资源
- 用户授权时,会看到这些权限的说明
- 只有用户同意授权后,应用才能访问对应的资源
#### 3.3 权限申请流程
1. 在权限管理页面,点击"添加权限"
2. 搜索并选择上述权限
3. 提交权限申请(部分权限可能需要审核)
4. 等待审核通过
### 4. 应用能力配置(可选)
根据图片显示WPS365开放平台提供了多种应用能力
**协作与会话:**
- WPS协作机器人
- 工作台小组件
- WPS协作网页应用
**多维表格:**
- 记录卡片插件
- 字段插件
- 视图插件
- 仪表盘插件
- 数据连接器插件
- 自动化插件
**注意:**
- 如果只是读取在线表格内容,通常不需要开启这些插件能力
- 这些能力主要用于扩展表格功能,不是读取数据的必要条件
- 读取数据主要依赖API权限而不是这些插件能力
## 二、后端配置
### 1. 更新配置文件
`application-dev.yml` 中添加配置:
```yaml
wps365:
# 应用ID从WPS365开放平台获取
app-id: YOUR_APP_ID
# 应用密钥从WPS365开放平台获取
app-key: YOUR_APP_KEY
# 授权回调地址
redirect-uri: https://jarvis.van333.cn/wps365-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
```
### 2. 确认授权范围Scope
当前代码中使用的授权范围:
```java
scope=file.readwrite,user.info
```
如果需要更细粒度的权限控制,可以修改为:
```java
scope=file.read,ksheet:read,user.info // 只读权限
// 或
scope=file.readwrite,ksheet:readwrite,user.info // 读写权限
```
## 三、使用流程
### 1. 用户授权
1. 调用 `/jarvis/wps365/authUrl` 获取授权URL
2. 用户访问授权URL同意授权
3. WPS365回调到 `/wps365-callback`自动保存Token
### 2. 获取文件列表
```javascript
// 调用接口获取文件列表
GET /jarvis/wps365/files?userId=xxx&page=1&pageSize=20
```
### 3. 获取工作表列表
```javascript
// 获取指定文件的工作表列表
GET /jarvis/wps365/sheets?userId=xxx&fileToken=xxx
```
### 4. 读取单元格数据
```javascript
// 读取指定范围的单元格数据
GET /jarvis/wps365/readCells?userId=xxx&fileToken=xxx&sheetIdx=0&range=A1:Z100
```
**参数说明:**
- `userId`: 用户ID授权后获取
- `fileToken`: 文件Token从文件列表中获取
- `sheetIdx`: 工作表索引从0开始
- `range`: 单元格范围A1:Z100可选不填则读取整个工作表
## 四、常见问题
### Q1: 提示"权限不足"或"无权限访问"
**解决方案:**
1. 检查WPS365开放平台的权限配置
2. 确认已添加 `file.read``ksheet:read` 权限
3. 重新授权删除旧Token重新获取授权
### Q2: 无法读取自己创建的表格
**可能原因:**
1. 文件Token不正确
2. 工作表索引错误
3. 用户对文件没有读取权限(即使是自己创建的,也需要授权给应用)
**解决方案:**
1. 确认文件Token是从文件列表中正确获取的
2. 使用 `/jarvis/wps365/sheets` 接口确认工作表索引
3. 确保用户已授权应用访问该文件
### Q3: 读取返回空数据
**可能原因:**
1. 指定的单元格范围没有数据
2. 工作表索引错误
3. API返回格式与预期不符
**解决方案:**
1. 尝试读取更大的范围A1:Z1000
2. 检查工作表索引是否正确
3. 查看后端日志确认API返回的具体内容
## 五、API接口说明
### 读取单元格数据接口
**接口地址:** `GET /jarvis/wps365/readCells`
**请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| userId | String | 是 | 用户ID |
| fileToken | String | 是 | 文件Token |
| sheetIdx | Integer | 否 | 工作表索引默认0 |
| range | String | 否 | 单元格范围A1:B10 |
**返回示例:**
```json
{
"code": 200,
"msg": "读取单元格数据成功",
"data": {
"values": [
["列1", "列2", "列3"],
["数据1", "数据2", "数据3"],
["数据4", "数据5", "数据6"]
]
}
}
```
## 六、完整示例
### 前端调用示例
```javascript
import {
getWPS365AuthUrl,
getWPS365FileList,
getWPS365SheetList,
readWPS365Cells
} from '@/api/jarvis/wps365'
// 1. 获取授权URL
const authResponse = await getWPS365AuthUrl()
window.open(authResponse.data, '_blank')
// 2. 等待授权完成后,获取文件列表
const fileListResponse = await getWPS365FileList({
userId: 'your_user_id',
page: 1,
pageSize: 20
})
// 3. 选择要读取的文件,获取工作表列表
const fileToken = fileListResponse.data.files[0].file_token
const sheetListResponse = await getWPS365SheetList('your_user_id', fileToken)
// 4. 读取第一个工作表的数据
const dataResponse = await readWPS365Cells({
userId: 'your_user_id',
fileToken: fileToken,
sheetIdx: 0,
range: 'A1:Z100'
})
console.log('表格数据:', dataResponse.data.values)
```
## 七、注意事项
1. **权限范围**确保在WPS365开放平台配置了正确的权限
2. **文件Token**使用fileToken而不是fileId来访问文件
3. **工作表索引**工作表索引从0开始不是从1开始
4. **单元格范围**:范围格式为 `A1:B10`,不区分大小写
5. **Token有效期**Token会过期需要定期刷新或重新授权
6. **数据量限制**:一次读取的数据量不要太大,建议分批读取

View File

@@ -0,0 +1,270 @@
# 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` 一致)
### 1.1 配置应用权限(重要!)
在WPS365开放平台的应用配置中需要确保开启以下权限
#### 基础权限
- **文件读取权限** (`file.read``file.readwrite`)
- **用户信息权限** (`user.info`)
#### 在线表格KSheet相关权限
根据图片中的应用能力配置页面,如果需要读取在线表格内容,建议:
1. **进入"应用能力"配置页面**
- 在左侧导航栏选择"应用能力" > "应用能力"
2. **确保相关能力已开启**
- 虽然图片中显示的是"多维表格"插件相关能力,但读取在线表格内容主要依赖:
- **文件读取权限**(在"权限管理"中配置)
- **API调用权限**确保应用有调用OpenAPI的权限
3. **权限管理配置**
- 进入"开发配置" > "权限管理"
- 确保添加了以下权限:
- `ksheet:read` - 读取在线表格数据
- `ksheet:write` - 写入在线表格数据(如果需要编辑)
- `file:read` - 读取文件
- `file:write` - 写入文件(如果需要编辑)
4. **事件与回调配置**
- 进入"开发配置" > "事件与回调"
- 配置回调地址:`https://your-domain.com/wps365-callback`
- 确保回调地址验证通过支持GET和POST请求
### 2. 更新配置文件
编辑 `application-dev.yml`或对应的环境配置文件添加WPS365配置
```yaml
wps365:
# 应用ID从WPS365开放平台获取
app-id: YOUR_APP_ID
# 应用密钥从WPS365开放平台获取
app-key: YOUR_APP_KEY
# 授权回调地址需要在WPS365开放平台配置
# 注意:使用 /wps365-callback 路径,避免前端路由拦截
redirect-uri: https://your-domain.com/wps365-callback
# API基础地址一般不需要修改
api-base-url: https://openapi.wps.cn/api/v1
# OAuth授权地址一般不需要修改
oauth-url: https://openapi.wps.cn/oauth2/auth
# 获取Token地址一般不需要修改
token-url: https://openapi.wps.cn/oauth2/token
# 刷新Token地址一般不需要修改
refresh-token-url: https://openapi.wps.cn/oauth2/token
```
**重要提示**
- 回调地址使用 `/wps365-callback` 而不是 `/jarvis/wps365/oauth/callback`
- 这样可以避免前端路由拦截确保OAuth回调能正常访问
- 在WPS365开放平台配置时只需配置域名`jarvis.van333.cn`回调路径会自动使用配置中的完整URL
## 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 /wps365-callback`
**参数**:
- `code`: 授权码由WPS365回调时自动传入
- `state`: 状态参数(可选)
- `error`: 错误码(授权失败时)
- `error_description`: 错误描述(授权失败时)
**说明**:
- 此接口处理WPS365的授权回调自动获取并保存Token
- 返回HTML页面显示授权结果
- 使用 `/wps365-callback` 路径避免前端路由拦截
- 授权成功后会显示成功页面3秒后自动关闭窗口
### 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 + RedisToken存储
- **前端**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/)

View File

@@ -16,6 +16,7 @@ import com.ruoyi.erp.request.ProductCategoryListQueryRequest;
import com.ruoyi.erp.request.ProductPropertyListQueryRequest;
import com.ruoyi.erp.request.AuthorizeListQueryRequest;
import com.ruoyi.erp.request.ProductPublishRequest;
import com.ruoyi.erp.request.ProductDownShelfRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -550,6 +551,217 @@ public class ProductController extends BaseController {
public String getAppid() { return appid; }
public void setAppid(String appid) { this.appid = appid; }
}
/**
* 下架商品(单个)
*/
@PostMapping("/downShelf")
public R<?> downShelf(@RequestBody @Validated DownShelfRequest req) {
try {
ERPAccount account = resolveAccount(req.getAppid());
ProductDownShelfRequest downShelfRequest = new ProductDownShelfRequest(account);
downShelfRequest.setProductId(req.getProductId());
String resp = downShelfRequest.getResponseBody();
JSONObject jo = JSONObject.parseObject(resp);
return R.ok(jo);
} catch (Exception e) {
log.error("下架商品失败: productId={}", req.getProductId(), e);
return R.fail("下架失败: " + e.getMessage());
}
}
/**
* 批量上架商品
*/
@PostMapping("/batchPublish")
public R<?> batchPublish(@RequestBody @Validated BatchPublishRequest req) {
try {
ERPAccount account = resolveAccount(req.getAppid());
List<Long> productIds = req.getProductIds();
if (productIds == null || productIds.isEmpty()) {
return R.fail("商品ID列表不能为空");
}
if (req.getUserName() == null || req.getUserName().isEmpty()) {
return R.fail("闲鱼会员名不能为空");
}
List<HashMap<String, Object>> results = new ArrayList<>();
int successCount = 0;
int failCount = 0;
for (Long productId : productIds) {
HashMap<String, Object> result = new HashMap<>();
result.put("productId", productId);
try {
ProductPublishRequest publishRequest = new ProductPublishRequest(account);
publishRequest.setProductId(productId);
publishRequest.setUserName(req.getUserName());
if (req.getSpecifyPublishTime() != null) {
publishRequest.setSpecifyPublishTime(req.getSpecifyPublishTime());
}
String resp = publishRequest.getResponseBody();
JSONObject jo = JSONObject.parseObject(resp);
if (jo != null && jo.getInteger("code") != null && jo.getInteger("code") == 0) {
result.put("success", true);
result.put("msg", "上架成功");
result.put("response", jo);
successCount++;
} else {
result.put("success", false);
result.put("msg", jo != null ? jo.getString("msg") : "上架失败");
result.put("response", jo);
failCount++;
}
} catch (Exception e) {
log.error("批量上架商品失败: productId={}", productId, e);
result.put("success", false);
result.put("msg", "上架异常: " + e.getMessage());
result.put("response", null);
failCount++;
}
results.add(result);
}
HashMap<String, Object> summary = new HashMap<>();
summary.put("total", productIds.size());
summary.put("success", successCount);
summary.put("fail", failCount);
summary.put("results", results);
JSONObject response = new JSONObject();
response.put("code", failCount == 0 ? 0 : 500);
response.put("msg", failCount == 0 ? "全部上架成功" : String.format("成功: %d, 失败: %d", successCount, failCount));
response.put("data", summary);
return R.ok(response);
} catch (Exception e) {
log.error("批量上架商品异常", e);
return R.fail("批量上架失败: " + e.getMessage());
}
}
/**
* 批量下架商品
*/
@PostMapping("/batchDownShelf")
public R<?> batchDownShelf(@RequestBody @Validated BatchDownShelfRequest req) {
try {
ERPAccount account = resolveAccount(req.getAppid());
List<Long> productIds = req.getProductIds();
if (productIds == null || productIds.isEmpty()) {
return R.fail("商品ID列表不能为空");
}
List<HashMap<String, Object>> results = new ArrayList<>();
int successCount = 0;
int failCount = 0;
for (Long productId : productIds) {
HashMap<String, Object> result = new HashMap<>();
result.put("productId", productId);
try {
ProductDownShelfRequest downShelfRequest = new ProductDownShelfRequest(account);
downShelfRequest.setProductId(productId);
String resp = downShelfRequest.getResponseBody();
JSONObject jo = JSONObject.parseObject(resp);
if (jo != null && jo.getInteger("code") != null && jo.getInteger("code") == 0) {
result.put("success", true);
result.put("msg", "下架成功");
result.put("response", jo);
successCount++;
} else {
result.put("success", false);
result.put("msg", jo != null ? jo.getString("msg") : "下架失败");
result.put("response", jo);
failCount++;
}
} catch (Exception e) {
log.error("批量下架商品失败: productId={}", productId, e);
result.put("success", false);
result.put("msg", "下架异常: " + e.getMessage());
result.put("response", null);
failCount++;
}
results.add(result);
}
HashMap<String, Object> summary = new HashMap<>();
summary.put("total", productIds.size());
summary.put("success", successCount);
summary.put("fail", failCount);
summary.put("results", results);
JSONObject response = new JSONObject();
response.put("code", failCount == 0 ? 0 : 500);
response.put("msg", failCount == 0 ? "全部下架成功" : String.format("成功: %d, 失败: %d", successCount, failCount));
response.put("data", summary);
return R.ok(response);
} catch (Exception e) {
log.error("批量下架商品异常", e);
return R.fail("批量下架失败: " + e.getMessage());
}
}
/**
* 下架请求体
*/
public static class DownShelfRequest {
@NotNull
private Long productId;
private String appid;
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }
public String getAppid() { return appid; }
public void setAppid(String appid) { this.appid = appid; }
}
/**
* 批量上架请求体
*/
public static class BatchPublishRequest {
@NotNull
@Size(min = 1, message = "商品ID列表不能为空")
private List<Long> productIds;
@NotBlank(message = "闲鱼会员名不能为空")
private String userName;
private String specifyPublishTime;
private String appid;
public List<Long> getProductIds() { return productIds; }
public void setProductIds(List<Long> productIds) { this.productIds = productIds; }
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public String getSpecifyPublishTime() { return specifyPublishTime; }
public void setSpecifyPublishTime(String specifyPublishTime) { this.specifyPublishTime = specifyPublishTime; }
public String getAppid() { return appid; }
public void setAppid(String appid) { this.appid = appid; }
}
/**
* 批量下架请求体
*/
public static class BatchDownShelfRequest {
@NotNull
@Size(min = 1, message = "商品ID列表不能为空")
private List<Long> productIds;
private String appid;
public List<Long> getProductIds() { return productIds; }
public void setProductIds(List<Long> productIds) { this.productIds = productIds; }
public String getAppid() { return appid; }
public void setAppid(String appid) { this.appid = appid; }
}
}

View File

@@ -0,0 +1,137 @@
package com.ruoyi.web.controller.jarvis;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.jarvis.domain.Comment;
import com.ruoyi.jarvis.domain.dto.CommentApiStatistics;
import com.ruoyi.jarvis.domain.dto.CommentStatistics;
import com.ruoyi.jarvis.service.ICommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
/**
* 评论管理 Controller
*/
@RestController
@RequestMapping("/jarvis/comment")
public class CommentController extends BaseController {
@Autowired
private ICommentService commentService;
/**
* 查询京东评论列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/jd/list")
public TableDataInfo list(Comment comment) {
startPage();
List<Comment> list = commentService.selectCommentList(comment);
return getDataTable(list);
}
/**
* 导出京东评论列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:export')")
@Log(title = "京东评论", businessType = BusinessType.EXPORT)
@PostMapping("/jd/export")
public void export(HttpServletResponse response, Comment comment) {
List<Comment> list = commentService.selectCommentList(comment);
ExcelUtil<Comment> util = new ExcelUtil<Comment>(Comment.class);
util.exportExcel(response, list, "京东评论数据");
}
/**
* 获取京东评论详细信息
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:query')")
@GetMapping("/jd/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) {
return success(commentService.selectCommentById(id));
}
/**
* 修改评论使用状态
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:edit')")
@Log(title = "评论管理", businessType = BusinessType.UPDATE)
@PutMapping("/jd")
public AjaxResult edit(@RequestBody Comment comment) {
return toAjax(commentService.updateCommentIsUse(comment));
}
/**
* 删除评论
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:remove')")
@Log(title = "评论管理", businessType = BusinessType.DELETE)
@DeleteMapping("/jd/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(commentService.deleteCommentByIds(ids));
}
/**
* 重置评论使用状态按商品ID
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:edit')")
@Log(title = "评论管理", businessType = BusinessType.UPDATE)
@PutMapping("/jd/reset/{productId}")
public AjaxResult resetByProductId(@PathVariable String productId) {
return toAjax(commentService.resetCommentIsUseByProductId(productId));
}
/**
* 获取评论统计信息
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/statistics")
public AjaxResult getStatistics(@RequestParam(required = false) String source) {
List<CommentStatistics> statistics = commentService.getCommentStatistics(source);
return success(statistics);
}
/**
* 获取接口调用统计
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/api/statistics")
public AjaxResult getApiStatistics(
@RequestParam(required = false) String apiType,
@RequestParam(required = false) String productType,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
List<CommentApiStatistics> statistics = commentService.getApiStatistics(apiType, productType, startDate, endDate);
return success(statistics);
}
/**
* 获取Redis产品类型映射京东
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/redis/jd/map")
public AjaxResult getJdProductTypeMap() {
Map<String, String> map = commentService.getJdProductTypeMap();
return success(map);
}
/**
* 获取Redis产品类型映射淘宝
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/redis/tb/map")
public AjaxResult getTbProductTypeMap() {
Map<String, String> map = commentService.getTbProductTypeMap();
return success(map);
}
}

View File

@@ -0,0 +1,234 @@
package com.ruoyi.web.controller.jarvis;
import java.util.List;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.jarvis.domain.ErpProduct;
import com.ruoyi.jarvis.service.IErpProductService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
/**
* 闲鱼商品Controller
*
* @author ruoyi
* @date 2024-01-01
*/
@RestController
@RequestMapping("/jarvis/erpProduct")
public class ErpProductController extends BaseController
{
@Autowired
private IErpProductService erpProductService;
/**
* 查询闲鱼商品列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:list')")
@GetMapping("/list")
public TableDataInfo list(ErpProduct erpProduct)
{
startPage();
List<ErpProduct> list = erpProductService.selectErpProductList(erpProduct);
return getDataTable(list);
}
/**
* 导出闲鱼商品列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:export')")
@Log(title = "闲鱼商品", businessType = BusinessType.EXPORT)
@GetMapping("/export")
public AjaxResult export(ErpProduct erpProduct)
{
List<ErpProduct> list = erpProductService.selectErpProductList(erpProduct);
ExcelUtil<ErpProduct> util = new ExcelUtil<ErpProduct>(ErpProduct.class);
return util.exportExcel(list, "闲鱼商品数据");
}
/**
* 获取闲鱼商品详细信息
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(erpProductService.selectErpProductById(id));
}
/**
* 新增闲鱼商品
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:add')")
@Log(title = "闲鱼商品", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody ErpProduct erpProduct)
{
return toAjax(erpProductService.insertErpProduct(erpProduct));
}
/**
* 修改闲鱼商品
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:edit')")
@Log(title = "闲鱼商品", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody ErpProduct erpProduct)
{
return toAjax(erpProductService.updateErpProduct(erpProduct));
}
/**
* 删除闲鱼商品
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:remove')")
@Log(title = "闲鱼商品", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(erpProductService.deleteErpProductByIds(ids));
}
/**
* 从闲鱼ERP拉取商品列表并保存单页保留用于兼容
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:pull')")
@Log(title = "拉取闲鱼商品", businessType = BusinessType.INSERT)
@PostMapping("/pull")
public AjaxResult pullProductList(
@RequestParam(required = false) String appid,
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "50") Integer pageSize,
@RequestParam(required = false) Integer productStatus)
{
try {
int count = erpProductService.pullAndSaveProductList(appid, pageNo, pageSize, productStatus);
if (count > 0) {
return success("成功拉取并保存 " + count + " 个商品");
} else {
String statusText = getStatusText(productStatus);
String message = "拉取完成,但没有获取到商品数据";
if (productStatus != null) {
message += "(筛选条件:状态=" + statusText + "";
}
message += "。建议:使用全量同步功能自动遍历所有页码";
return success(message);
}
} catch (Exception e) {
return error("拉取商品列表失败: " + e.getMessage());
}
}
/**
* 全量同步商品(自动遍历所有页码,同步更新和删除)
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:pull')")
@Log(title = "全量同步闲鱼商品", businessType = BusinessType.UPDATE)
@PostMapping("/syncAll")
public AjaxResult syncAllProducts(
@RequestParam(required = false) String appid,
@RequestParam(required = false) Integer productStatus)
{
try {
IErpProductService.SyncResult result = erpProductService.syncAllProducts(appid, productStatus);
return success(result.getMessage());
} catch (Exception e) {
return error("全量同步失败: " + e.getMessage());
}
}
/**
* 获取状态文本(用于提示信息)
*/
private String getStatusText(Integer status) {
if (status == null) {
return "全部";
}
switch (status) {
case -1:
return "删除";
case 21:
return "待发布";
case 22:
return "销售中";
case 23:
return "已售罄";
case 31:
return "手动下架";
case 33:
return "售出下架";
case 36:
return "自动下架";
default:
return String.valueOf(status);
}
}
/**
* 批量上架商品
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:publish')")
@Log(title = "批量上架商品", businessType = BusinessType.UPDATE)
@PostMapping("/batchPublish")
public AjaxResult batchPublish(@RequestBody BatchOperationRequest request)
{
try {
return success("批量上架功能请调用 /erp/product/batchPublish 接口");
} catch (Exception e) {
return error("批量上架失败: " + e.getMessage());
}
}
/**
* 批量下架商品
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:downShelf')")
@Log(title = "批量下架商品", businessType = BusinessType.UPDATE)
@PostMapping("/batchDownShelf")
public AjaxResult batchDownShelf(@RequestBody BatchOperationRequest request)
{
try {
return success("批量下架功能请调用 /erp/product/batchDownShelf 接口");
} catch (Exception e) {
return error("批量下架失败: " + e.getMessage());
}
}
/**
* 批量操作请求体
*/
public static class BatchOperationRequest {
private java.util.List<Long> productIds;
private String appid;
public java.util.List<Long> getProductIds() {
return productIds;
}
public void setProductIds(java.util.List<Long> productIds) {
this.productIds = productIds;
}
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
}
}

View File

@@ -21,13 +21,15 @@ public class InstructionController extends BaseController {
}
/**
* 执行文本指令
* body: { command: "京今日统计" }
* 执行文本指令(控制台入口,需要权限)
* body: { command: "京今日统计", forceGenerate: false }
*/
@PostMapping("/execute")
public AjaxResult execute(@RequestBody Map<String, String> body) {
String cmd = body != null ? body.get("command") : null;
java.util.List<String> result = instructionService.execute(cmd);
public AjaxResult execute(@RequestBody Map<String, Object> body) {
String cmd = body != null ? (body.get("command") != null ? String.valueOf(body.get("command")) : null) : null;
boolean forceGenerate = body != null && body.get("forceGenerate") != null && Boolean.parseBoolean(String.valueOf(body.get("forceGenerate")));
// 控制台入口,传递 isFromConsole=true跳过订单查询校验
java.util.List<String> result = instructionService.execute(cmd, forceGenerate, true);
return AjaxResult.success(result);
}

View File

@@ -13,10 +13,13 @@ import com.ruoyi.jarvis.service.IJDOrderService;
import com.ruoyi.jarvis.service.IOrderRowsService;
import com.ruoyi.jarvis.service.IGiftCouponService;
import com.ruoyi.jarvis.domain.GiftCoupon;
import com.ruoyi.system.service.ISysConfigService;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,15 +40,46 @@ public class JDOrderController extends BaseController {
private final IJDOrderService jdOrderService;
private final IOrderRowsService orderRowsService;
private final IGiftCouponService giftCouponService;
private final ISysConfigService sysConfigService;
private static final String CONFIG_KEY_PREFIX = "logistics.push.touser.";
private static final java.util.regex.Pattern URL_DETECT_PATTERN = java.util.regex.Pattern.compile(
"(https?://[^\\s]+)|(u\\.jd\\.com/[^\\s]+)",
java.util.regex.Pattern.CASE_INSENSITIVE);
private static final java.util.regex.Pattern UJD_LINK_PATTERN = java.util.regex.Pattern.compile(
"^https?://u\\.jd\\.com/[A-Za-z0-9]+[A-Za-z0-9_-]*$",
java.util.regex.Pattern.CASE_INSENSITIVE);
private static final java.util.regex.Pattern JINGFEN_LINK_PATTERN = java.util.regex.Pattern.compile(
"^https?://jingfen\\.jd\\.com/detail/[A-Za-z0-9]+\\.html$",
java.util.regex.Pattern.CASE_INSENSITIVE);
public JDOrderController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService, IGiftCouponService giftCouponService) {
public JDOrderController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService,
IGiftCouponService giftCouponService, ISysConfigService sysConfigService) {
this.jdOrderService = jdOrderService;
this.orderRowsService = orderRowsService;
this.giftCouponService = giftCouponService;
this.sysConfigService = sysConfigService;
}
private final static String skey = "2192057370ef8140c201079969c956a3";
private final static String requestUrl = "http://192.168.8.88:6666/jd/";
@Value("${jarvis.server.jarvis-java.base-url:http://127.0.0.1:6666}")
private String jarvisJavaBaseUrl;
@Value("${jarvis.server.jarvis-java.jd-api-path:/jd}")
private String jdApiPath;
@Value("${jarvis.server.logistics.base-url:http://127.0.0.1:5001}")
private String logisticsBaseUrl;
@Value("${jarvis.server.logistics.fetch-path:/fetch_logistics}")
private String logisticsFetchPath;
/**
* 获取JD接口请求URL
*/
private String getRequestUrl() {
return jarvisJavaBaseUrl + jdApiPath + "/";
}
@@ -55,7 +89,7 @@ public class JDOrderController extends BaseController {
String promotionContent = requestBody.get("promotionContent");
String result = "";
try {
String url = requestUrl + "generatePromotionContent";
String url = getRequestUrl() + "generatePromotionContent";
JSONObject param = new JSONObject();
param.put("skey", skey);
param.put("promotionContent", promotionContent);
@@ -134,10 +168,16 @@ public class JDOrderController extends BaseController {
if (w instanceof JSONObject) {
JSONObject wj = (JSONObject) w;
Object content = wj.get("content");
if (content instanceof String) {
String cleaned = stripUrls((String) content);
wj.put("content", cleaned);
if (!(content instanceof String)) {
continue;
}
String type = wj.getString("type");
if ("通用文案".equals(type)) {
// 通用文案需要保留链接,用于后续替换/复制
continue;
}
String cleaned = stripUrls((String) content);
wj.put("content", cleaned);
}
}
}
@@ -170,7 +210,7 @@ public class JDOrderController extends BaseController {
body.get("materialUrl"), body.get("skuId"), body.get("amount"),
body.get("quantity"), body.get("owner"), body.get("skuName"));
String url = requestUrl + "createGiftCoupon";
String url = getRequestUrl() + "createGiftCoupon";
JSONObject param = new JSONObject();
param.put("skey", skey);
// 透传必要参数
@@ -289,7 +329,7 @@ public class JDOrderController extends BaseController {
@PostMapping("/transfer")
public AjaxResult transfer(@RequestBody Map<String, Object> body) {
try {
String url = requestUrl + "transfer";
String url = getRequestUrl() + "transfer";
JSONObject param = new JSONObject();
param.put("skey", skey);
if (body.get("materialUrl") != null) param.put("materialUrl", body.get("materialUrl"));
@@ -319,7 +359,7 @@ public class JDOrderController extends BaseController {
body.get("materialUrl"), body.get("skuId"), body.get("amount"),
body.get("quantity"), body.get("batchSize"), body.get("owner"), body.get("skuName"));
String url = requestUrl + "batchCreateGiftCoupons";
String url = getRequestUrl() + "batchCreateGiftCoupons";
JSONObject param = new JSONObject();
param.put("skey", skey);
if (body.get("materialUrl") != null) param.put("materialUrl", body.get("materialUrl"));
@@ -475,7 +515,7 @@ public class JDOrderController extends BaseController {
for (String url : urls) {
try {
logger.info("查询商品信息 - URL: {}", url);
String queryUrl = requestUrl + "generatePromotionContent";
String queryUrl = getRequestUrl() + "generatePromotionContent";
JSONObject param = new JSONObject();
param.put("skey", skey);
param.put("promotionContent", url);
@@ -566,22 +606,20 @@ public class JDOrderController extends BaseController {
content.length(), amount, quantity, owner);
// 提取文本中的所有URL京东链接
java.util.List<String> urls = new java.util.ArrayList<>();
java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile(
"(https?://[^\\s]+)|(u\\.jd\\.com/[^\\s]+)",
java.util.regex.Pattern.CASE_INSENSITIVE);
java.util.regex.Matcher matcher = urlPattern.matcher(content);
java.util.List<UrlSegment> urlSegments = new java.util.ArrayList<>();
java.util.regex.Matcher matcher = URL_DETECT_PATTERN.matcher(content);
while (matcher.find()) {
String url = matcher.group(0);
if (url != null && !url.trim().isEmpty()) {
urls.add(url.trim());
String segment = matcher.group(0);
UrlSegment urlInfo = parseUrlSegment(segment);
if (urlInfo != null) {
urlSegments.add(urlInfo);
}
}
logger.info("文本URL替换 - 提取到{}个URL", urls.size());
logger.info("文本URL替换 - 提取到{}个URL", urlSegments.size());
if (urls.isEmpty()) {
if (urlSegments.isEmpty()) {
return AjaxResult.success(new JSONObject()
.fluentPut("replacedContent", content)
.fluentPut("originalContent", content)
@@ -596,17 +634,29 @@ public class JDOrderController extends BaseController {
JSONArray replacementInfo = new JSONArray();
int successCount = 0;
for (int i = 0; i < urls.size(); i++) {
String originalUrl = urls.get(i);
logger.info("处理第{}/{}个URL: {}", i + 1, urls.size(), originalUrl);
for (int i = 0; i < urlSegments.size(); i++) {
UrlSegment urlSegment = urlSegments.get(i);
logger.info("处理第{}/{}个URL: {}", i + 1, urlSegments.size(), urlSegment.urlPart);
JSONObject product = null; // 在try外部声明以便在catch中使用
try {
if (urlSegment.normalizedJdUrl == null) {
logger.warn("URL{}不是京东推广链接,跳过: {}", i + 1, urlSegment.original);
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", "非京东推广链接或格式不支持");
replacementInfo.add(info);
continue;
}
// 1. 查询该URL的商品信息
String queryUrl = requestUrl + "generatePromotionContent";
String queryUrl = getRequestUrl() + "generatePromotionContent";
JSONObject queryParam = new JSONObject();
queryParam.put("skey", skey);
queryParam.put("promotionContent", originalUrl);
queryParam.put("promotionContent", urlSegment.normalizedJdUrl);
String queryResult = HttpUtils.sendJsonPost(queryUrl, queryParam.toJSONString());
logger.debug("商品查询响应: {}", queryResult);
@@ -643,11 +693,11 @@ public class JDOrderController extends BaseController {
errorMsg = apiError;
}
}
logger.warn("URL{}商品信息查询失败: {}", originalUrl, errorMsg);
logger.warn("URL{}商品信息查询失败: {}", urlSegment.urlPart, errorMsg);
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", originalUrl);
info.put("newUrl", originalUrl);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", errorMsg);
replacementInfo.add(info);
@@ -655,7 +705,7 @@ public class JDOrderController extends BaseController {
}
// 2. 为该商品创建礼金券
String createUrl = requestUrl + "createGiftCoupon";
String createUrl = getRequestUrl() + "createGiftCoupon";
JSONObject createParam = new JSONObject();
createParam.put("skey", skey);
createParam.put("amount", amount);
@@ -704,8 +754,8 @@ public class JDOrderController extends BaseController {
logger.warn("POP商品{}缺少有效的oriItemId/materialUrl/skuId", product.getString("skuName"));
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", originalUrl);
info.put("newUrl", originalUrl);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", "POP商品信息不完整");
replacementInfo.add(info);
@@ -723,8 +773,8 @@ public class JDOrderController extends BaseController {
logger.warn("自营商品{}缺少有效的skuId/materialUrl", product.getString("skuName"));
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", originalUrl);
info.put("newUrl", originalUrl);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", "自营商品信息不完整");
replacementInfo.add(info);
@@ -749,11 +799,11 @@ public class JDOrderController extends BaseController {
errorMsg = apiError;
}
}
logger.warn("URL{}礼金创建失败: {}", originalUrl, errorMsg);
logger.warn("URL{}礼金创建失败: {}", urlSegment.urlPart, errorMsg);
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", originalUrl);
info.put("newUrl", originalUrl);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", errorMsg);
if (product != null && product.getString("skuName") != null) {
@@ -766,10 +816,10 @@ public class JDOrderController extends BaseController {
String giftCouponKey = createData.getString("giftCouponKey");
// 3. 转链(带礼金)
String transferUrl = requestUrl + "transfer";
String transferUrl = getRequestUrl() + "transfer";
JSONObject transferParam = new JSONObject();
transferParam.put("skey", skey);
transferParam.put("materialUrl", originalUrl);
transferParam.put("materialUrl", urlSegment.normalizedJdUrl);
transferParam.put("giftCouponKey", giftCouponKey);
String transferResult = HttpUtils.sendJsonPost(transferUrl, transferParam.toJSONString());
@@ -789,11 +839,11 @@ public class JDOrderController extends BaseController {
errorMsg = apiError;
}
}
logger.warn("URL{}转链失败: {}", originalUrl, errorMsg);
logger.warn("URL{}转链失败: {}", urlSegment.urlPart, errorMsg);
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", originalUrl);
info.put("newUrl", originalUrl);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("giftCouponKey", giftCouponKey);
info.put("error", errorMsg);
@@ -807,11 +857,11 @@ public class JDOrderController extends BaseController {
String shortURL = transferData.getString("shortURL");
// 4. 替换文本中的URL
replacedContent = replacedContent.replace(originalUrl, shortURL);
replacedContent = replaceUrlInContent(replacedContent, urlSegment, shortURL);
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", originalUrl);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", shortURL);
info.put("success", true);
info.put("giftCouponKey", giftCouponKey);
@@ -819,18 +869,18 @@ public class JDOrderController extends BaseController {
replacementInfo.add(info);
successCount++;
logger.info("URL{}处理成功: {} -> {}", i + 1, originalUrl, shortURL);
logger.info("URL{}处理成功: {} -> {}", i + 1, urlSegment.urlPart, shortURL);
} catch (Exception e) {
logger.error("处理URL{}失败: {}", originalUrl, e.getMessage(), e);
logger.error("处理URL{}失败: {}", urlSegment.urlPart, e.getMessage(), e);
String errorMsg = e.getMessage();
if (errorMsg == null || errorMsg.trim().isEmpty()) {
errorMsg = "处理失败: " + e.getClass().getSimpleName();
}
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", originalUrl);
info.put("newUrl", originalUrl);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", errorMsg);
if (product != null && product.getString("skuName") != null) {
@@ -840,13 +890,13 @@ public class JDOrderController extends BaseController {
}
}
logger.info("文本URL替换完成 - 成功{}/{}", successCount, urls.size());
logger.info("文本URL替换完成 - 成功{}/{}", successCount, urlSegments.size());
JSONObject response = new JSONObject();
response.put("replacedContent", replacedContent);
response.put("originalContent", content);
response.put("replacements", replacementInfo);
response.put("totalUrls", urls.size());
response.put("totalUrls", urlSegments.size());
response.put("replacedCount", successCount);
return AjaxResult.success(response);
@@ -886,9 +936,7 @@ public class JDOrderController extends BaseController {
// 检查分销标识
String distributionMark = order.getDistributionMark();
if (distributionMark == null || (!distributionMark.equals("F") && !distributionMark.equals("PDD"))) {
return AjaxResult.error("该订单的分销标识不是F或PDD无需处理。当前分销标识: " + distributionMark);
}
// 检查物流链接
String logisticsLink = order.getLogisticsLink();
@@ -900,7 +948,7 @@ public class JDOrderController extends BaseController {
orderId, order.getOrderId(), distributionMark, logisticsLink);
// 构建外部接口URL
String externalUrl = "http://192.168.8.88:5001/fetch_logistics?tracking_url=" +
String externalUrl = logisticsBaseUrl + logisticsFetchPath + "?tracking_url=" +
java.net.URLEncoder.encode(logisticsLink, "UTF-8");
logger.info("准备调用外部接口 - URL: {}", externalUrl);
@@ -994,6 +1042,43 @@ public class JDOrderController extends BaseController {
}
}
/**
* 根据分销标识获取接收人列表
* 从系统配置中读取配置键名格式logistics.push.touser.{分销标识}
* 配置值格式接收人1,接收人2,接收人3逗号分隔
*
* @param distributionMark 分销标识
* @return 接收人列表逗号分隔如果未配置则返回null
*/
private String getTouserByDistributionMark(String distributionMark) {
if (!StringUtils.hasText(distributionMark)) {
logger.warn("分销标识为空,无法获取接收人配置");
return null;
}
try {
// 构建配置键名
String configKey = CONFIG_KEY_PREFIX + distributionMark.trim();
// 从系统配置中获取接收人列表
String configValue = sysConfigService.selectConfigByKey(configKey);
if (StringUtils.hasText(configValue)) {
// 清理配置值(去除空格)
String touser = configValue.trim().replaceAll(",\\s+", ",");
logger.info("从配置获取接收人列表 - 分销标识: {}, 配置键: {}, 接收人: {}",
distributionMark, configKey, touser);
return touser;
} else {
logger.debug("未找到接收人配置 - 分销标识: {}, 配置键: {}", distributionMark, configKey);
return null;
}
} catch (Exception e) {
logger.error("获取接收人配置失败 - 分销标识: {}, 错误: {}", distributionMark, e.getMessage(), e);
return null;
}
}
/**
* 调用企业应用推送逻辑
* @param order 订单信息
@@ -1022,7 +1107,7 @@ public class JDOrderController extends BaseController {
// 收货地址
pushContent.append("收货地址:").append(order.getAddress() != null ? order.getAddress() : "").append("\n");
// 运单号
pushContent.append("运单号:").append(waybillNo).append("\n");
pushContent.append("运单号:").append("\n").append("\n").append("\n").append("\n").append(waybillNo).append("\n");
// 调用企业微信推送接口参考WxtsUtil的实现
String pushUrl = "https://wxts.van333.cn/wx/send/pdd";
@@ -1032,8 +1117,23 @@ public class JDOrderController extends BaseController {
String content = pushContent.toString();
pushParam.put("text", content);
// 根据分销标识获取接收人列表
String touser = getTouserByDistributionMark(distributionMark);
if (StringUtils.hasText(touser)) {
pushParam.put("touser", touser);
logger.info("企业微信推送设置接收人 - 订单ID: {}, 分销标识: {}, 接收人: {}",
order.getId(), distributionMark, touser);
} else {
logger.warn("未找到分销标识对应的接收人配置 - 订单ID: {}, 分销标识: {}",
order.getId(), distributionMark);
}
// 记录完整的推送参数(用于调试)
String jsonBody = pushParam.toJSONString();
logger.info("企业微信推送完整参数 - 订单ID: {}, JSON: {}", order.getId(), jsonBody);
// 使用支持自定义header的HTTP请求
String pushResult = sendPostWithHeaders(pushUrl, pushParam.toJSONString(), token);
String pushResult = sendPostWithHeaders(pushUrl, jsonBody, token);
logger.info("企业应用推送调用结果 - 订单ID: {}, waybill_no: {}, 推送结果: {}",
order.getId(), waybillNo, pushResult);
@@ -1110,4 +1210,80 @@ public class JDOrderController extends BaseController {
}
return result.toString();
}
private static class UrlSegment {
private final String original;
private final String urlPart;
private final String suffix;
private final String normalizedJdUrl;
private UrlSegment(String original, String urlPart, String suffix, String normalizedJdUrl) {
this.original = original;
this.urlPart = urlPart;
this.suffix = suffix;
this.normalizedJdUrl = normalizedJdUrl;
}
}
private static UrlSegment parseUrlSegment(String segment) {
if (segment == null || segment.trim().isEmpty()) {
return null;
}
String original = segment.trim();
int urlLength = calculateUrlLength(original);
if (urlLength <= 0) {
return null;
}
String urlPart = original.substring(0, urlLength);
String suffix = original.substring(urlLength);
String normalized = normalizeJdUrl(urlPart);
return new UrlSegment(original, urlPart, suffix, normalized);
}
private static int calculateUrlLength(String text) {
int length = 0;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (isAllowedUrlChar(c)) {
length++;
} else {
break;
}
}
return length;
}
private static boolean isAllowedUrlChar(char c) {
if (c <= 0x7F) {
return Character.isLetterOrDigit(c) || "-._~:/?#[]@!$&'()*+,;=%".indexOf(c) >= 0;
}
return false;
}
private static String normalizeJdUrl(String urlPart) {
if (urlPart == null || urlPart.isEmpty()) {
return null;
}
String normalized = urlPart;
if (!normalized.startsWith("http://") && !normalized.startsWith("https://")) {
normalized = "https://" + normalized;
}
if (UJD_LINK_PATTERN.matcher(normalized).matches() || JINGFEN_LINK_PATTERN.matcher(normalized).matches()) {
return normalized;
}
return null;
}
private static String replaceUrlInContent(String content, UrlSegment segment, String newUrl) {
if (content == null || segment == null || newUrl == null || newUrl.trim().isEmpty()) {
return content;
}
String suffix = segment.suffix != null ? segment.suffix : "";
String replacementWithSuffix = newUrl + suffix;
String result = content.replace(segment.original, replacementWithSuffix);
if (result.equals(content) && !segment.original.equals(segment.urlPart)) {
result = result.replace(segment.urlPart, newUrl);
}
return result;
}
}

View File

@@ -0,0 +1,70 @@
package com.ruoyi.web.controller.jarvis;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.jarvis.service.IPhoneReplaceConfigService;
/**
* 手机号替换配置Controller
*
* @author ruoyi
*/
@RestController
@RequestMapping("/jarvis/phoneReplaceConfig")
public class PhoneReplaceConfigController extends BaseController
{
@Autowired
private IPhoneReplaceConfigService phoneReplaceConfigService;
/**
* 获取指定类型的手机号列表
*/
@GetMapping("/{type}")
public AjaxResult getPhoneList(@PathVariable("type") String type)
{
List<String> phoneList = phoneReplaceConfigService.getPhoneList(type);
return AjaxResult.success(phoneList);
}
/**
* 设置指定类型的手机号列表
*/
@Log(title = "手机号替换配置", businessType = BusinessType.UPDATE)
@PutMapping("/{type}")
public AjaxResult setPhoneList(@PathVariable("type") String type, @RequestBody List<String> phoneList)
{
return toAjax(phoneReplaceConfigService.setPhoneList(type, phoneList));
}
/**
* 添加手机号到指定类型
*/
@Log(title = "手机号替换配置", businessType = BusinessType.UPDATE)
@PostMapping("/{type}/add")
public AjaxResult addPhone(@PathVariable("type") String type, @RequestBody String phone)
{
return toAjax(phoneReplaceConfigService.addPhone(type, phone));
}
/**
* 从指定类型删除手机号
*/
@Log(title = "手机号替换配置", businessType = BusinessType.UPDATE)
@PostMapping("/{type}/remove")
public AjaxResult removePhone(@PathVariable("type") String type, @RequestBody String phone)
{
return toAjax(phoneReplaceConfigService.removePhone(type, phone));
}
}

View File

@@ -0,0 +1,159 @@
package com.ruoyi.web.controller.jarvis;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.jarvis.service.ISocialMediaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 小红书/抖音内容生成Controller
*
* @author ruoyi
* @date 2025-01-XX
*/
@RestController
@RequestMapping("/jarvis/social-media")
public class SocialMediaController extends BaseController
{
@Autowired
private ISocialMediaService socialMediaService;
/**
* 提取关键词
*/
@PostMapping("/extract-keywords")
public AjaxResult extractKeywords(@RequestBody Map<String, Object> request)
{
try {
String productName = (String) request.get("productName");
if (productName == null || productName.trim().isEmpty()) {
return AjaxResult.error("商品名称不能为空");
}
Map<String, Object> result = socialMediaService.extractKeywords(productName);
return AjaxResult.success(result);
} catch (Exception e) {
logger.error("提取关键词失败", e);
return AjaxResult.error("提取关键词失败: " + e.getMessage());
}
}
/**
* 生成文案
*/
@PostMapping("/generate-content")
public AjaxResult generateContent(@RequestBody Map<String, Object> request)
{
try {
String productName = (String) request.get("productName");
if (productName == null || productName.trim().isEmpty()) {
return AjaxResult.error("商品名称不能为空");
}
Object originalPriceObj = request.get("originalPrice");
Object finalPriceObj = request.get("finalPrice");
String keywords = (String) request.get("keywords");
String style = (String) request.getOrDefault("style", "both");
Map<String, Object> result = socialMediaService.generateContent(
productName, originalPriceObj, finalPriceObj, keywords, style
);
return AjaxResult.success(result);
} catch (Exception e) {
logger.error("生成文案失败", e);
return AjaxResult.error("生成文案失败: " + e.getMessage());
}
}
/**
* 一键生成完整内容(关键词 + 文案 + 图片)
*/
@Log(title = "小红书/抖音内容生成", businessType = BusinessType.OTHER)
@PostMapping("/generate-complete")
public AjaxResult generateComplete(@RequestBody Map<String, Object> request)
{
try {
String productImageUrl = (String) request.get("productImageUrl");
String productName = (String) request.get("productName");
if (productName == null || productName.trim().isEmpty()) {
return AjaxResult.error("商品名称不能为空");
}
Object originalPriceObj = request.get("originalPrice");
Object finalPriceObj = request.get("finalPrice");
String style = (String) request.getOrDefault("style", "both");
Map<String, Object> result = socialMediaService.generateCompleteContent(
productImageUrl, productName, originalPriceObj, finalPriceObj, style
);
return AjaxResult.success(result);
} catch (Exception e) {
logger.error("生成完整内容失败", e);
return AjaxResult.error("生成完整内容失败: " + e.getMessage());
}
}
/**
* 获取提示词模板列表
*/
@GetMapping("/prompt/list")
public AjaxResult listPromptTemplates()
{
try {
return socialMediaService.listPromptTemplates();
} catch (Exception e) {
logger.error("获取提示词模板列表失败", e);
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取单个提示词模板
*/
@GetMapping("/prompt/{key}")
public AjaxResult getPromptTemplate(@PathVariable String key)
{
try {
return socialMediaService.getPromptTemplate(key);
} catch (Exception e) {
logger.error("获取提示词模板失败", e);
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 保存提示词模板
*/
@Log(title = "保存提示词模板", businessType = BusinessType.UPDATE)
@PostMapping("/prompt/save")
public AjaxResult savePromptTemplate(@RequestBody Map<String, Object> request)
{
try {
return socialMediaService.savePromptTemplate(request);
} catch (Exception e) {
logger.error("保存提示词模板失败", e);
return AjaxResult.error("保存失败: " + e.getMessage());
}
}
/**
* 删除提示词模板(恢复默认)
*/
@Log(title = "删除提示词模板", businessType = BusinessType.DELETE)
@DeleteMapping("/prompt/{key}")
public AjaxResult deletePromptTemplate(@PathVariable String key)
{
try {
return socialMediaService.deletePromptTemplate(key);
} catch (Exception e) {
logger.error("删除提示词模板失败", e);
return AjaxResult.error("删除失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,90 @@
package com.ruoyi.web.controller.jarvis;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.jarvis.domain.TaobaoComment;
import com.ruoyi.jarvis.service.ITaobaoCommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* 淘宝评论管理 Controller
*/
@RestController
@RequestMapping("/jarvis/taobaoComment")
public class TaobaoCommentController extends BaseController {
@Autowired
private ITaobaoCommentService taobaoCommentService;
/**
* 查询淘宝评论列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/list")
public TableDataInfo list(TaobaoComment taobaoComment) {
startPage();
List<TaobaoComment> list = taobaoCommentService.selectTaobaoCommentList(taobaoComment);
return getDataTable(list);
}
/**
* 导出淘宝评论列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:export')")
@Log(title = "淘宝评论", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, TaobaoComment taobaoComment) {
List<TaobaoComment> list = taobaoCommentService.selectTaobaoCommentList(taobaoComment);
ExcelUtil<TaobaoComment> util = new ExcelUtil<TaobaoComment>(TaobaoComment.class);
util.exportExcel(response, list, "淘宝评论数据");
}
/**
* 获取淘宝评论详细信息
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable("id") Integer id) {
return success(taobaoCommentService.selectTaobaoCommentById(id));
}
/**
* 修改评论使用状态
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:edit')")
@Log(title = "评论管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody TaobaoComment taobaoComment) {
return toAjax(taobaoCommentService.updateTaobaoCommentIsUse(taobaoComment));
}
/**
* 删除评论
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:remove')")
@Log(title = "评论管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Integer[] ids) {
return toAjax(taobaoCommentService.deleteTaobaoCommentByIds(ids));
}
/**
* 重置评论使用状态按商品ID
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:edit')")
@Log(title = "评论管理", businessType = BusinessType.UPDATE)
@PutMapping("/reset/{productId}")
public AjaxResult resetByProductId(@PathVariable String productId) {
return toAjax(taobaoCommentService.resetTaobaoCommentIsUseByProductId(productId));
}
}

View File

@@ -96,9 +96,11 @@ public class TencentDocConfigController extends BaseController {
config.put("apiBaseUrl", tencentDocConfig.getApiBaseUrl());
// 获取当前同步进度(如果有配置)
// 注意:使用与 TencentDocController 相同的 Redis key 前缀
if (fileId != null && !fileId.isEmpty() && sheetId != null && !sheetId.isEmpty()) {
String syncProgressKey = "tencent:doc:sync:last_row:" + fileId + ":" + sheetId;
String syncProgressKey = "tendoc:last_row:" + fileId + ":" + sheetId;
Integer currentProgress = redisCache.getCacheObject(syncProgressKey);
log.debug("读取同步进度 - key: {}, value: {}", syncProgressKey, currentProgress);
if (currentProgress != null) {
config.put("currentProgress", currentProgress);
@@ -208,11 +210,12 @@ public class TencentDocConfigController extends BaseController {
redisCache.setCacheObject(REDIS_KEY_PREFIX + "startRow", startRow, 180, TimeUnit.DAYS);
// 清除该文档的同步进度配置更新时重置进度从新的startRow重新开始
String syncProgressKey = "tencent:doc:sync:last_row:" + fileId.trim() + ":" + sheetId.trim();
// 注意:使用与 TencentDocController 相同的 Redis key 前缀
String syncProgressKey = "tendoc:last_row:" + fileId.trim() + ":" + sheetId.trim();
String configVersionKey = "tencent:doc:sync:config_version:" + fileId.trim() + ":" + sheetId.trim();
redisCache.deleteObject(syncProgressKey);
redisCache.deleteObject(configVersionKey);
log.info("配置已更新,已清除同步进度将从第 {} 行重新开始同步", startRow);
log.info("配置已更新,已清除同步进度 - key: {}, 将从第 {} 行重新开始同步", syncProgressKey, startRow);
// 同时更新TencentDocConfig对象内存中
tencentDocConfig.setFileId(fileId.trim());

View File

@@ -0,0 +1,275 @@
package com.ruoyi.web.controller.jarvis;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.jarvis.domain.dto.WPS365TokenInfo;
import com.ruoyi.jarvis.service.IWPS365OAuthService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* WPS365 OAuth回调控制器
* 用于处理OAuth回调避免前端路由拦截
*
* @author system
*/
@RestController
@RequestMapping("/wps365-callback")
public class WPS365CallbackController extends BaseController {
private static final Logger log = LoggerFactory.getLogger(WPS365CallbackController.class);
@Autowired
private IWPS365OAuthService wps365OAuthService;
/**
* OAuth回调 - 通过授权码获取访问令牌GET请求
* 路径:/wps365-callback
* 注意在WPS365开放平台只需配置域名jarvis.van333.cn不能包含路径
* 授权URL中的redirect_uri参数会自动使用配置中的完整URLhttps://jarvis.van333.cn/wps365-callback
*/
@Anonymous
@GetMapping
public ResponseEntity<?> oauthCallbackGet(@RequestParam(value = "code", required = false) String code,
@RequestParam(value = "state", required = false) String state,
@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "error_description", required = false) String errorDescription,
@RequestParam(value = "challenge", required = false) String challenge) {
return handleOAuthCallback(code, state, error, errorDescription, challenge, null);
}
/**
* OAuth回调 - 通过授权码获取访问令牌POST请求
* WPS365在验证回调URL时会发送POST请求进行challenge验证
* 支持application/json和application/x-www-form-urlencoded两种格式
*/
@Anonymous
@PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.TEXT_PLAIN_VALUE})
public ResponseEntity<?> oauthCallbackPost(@RequestParam(value = "code", required = false) String code,
@RequestParam(value = "state", required = false) String state,
@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "error_description", required = false) String errorDescription,
@RequestParam(value = "challenge", required = false) String challenge,
@RequestBody(required = false) String requestBody) {
log.info("收到WPS365 POST回调请求 - code: {}, challenge: {}, requestBody: {}",
code != null ? "" : "",
challenge != null ? challenge : "",
requestBody != null && requestBody.length() > 0 ? requestBody.substring(0, Math.min(100, requestBody.length())) : "");
// 如果challenge在URL参数中直接使用
// 如果不在URL参数中尝试从请求体中解析可能是JSON或form-data
if (challenge == null && requestBody != null && !requestBody.trim().isEmpty()) {
String bodyTrimmed = requestBody.trim();
// 尝试解析JSON格式
if (bodyTrimmed.startsWith("{")) {
try {
com.alibaba.fastjson2.JSONObject json = com.alibaba.fastjson2.JSON.parseObject(requestBody);
if (json.containsKey("challenge")) {
challenge = json.getString("challenge");
log.info("从JSON请求体中解析到challenge: {}", challenge);
}
} catch (Exception e) {
log.debug("解析JSON请求体失败", e);
}
}
// 尝试解析form-urlencoded格式 (challenge=xxx)
else if (bodyTrimmed.contains("challenge=")) {
try {
String[] pairs = bodyTrimmed.split("&");
for (String pair : pairs) {
if (pair.startsWith("challenge=")) {
challenge = java.net.URLDecoder.decode(pair.substring("challenge=".length()), "UTF-8");
log.info("从form-urlencoded请求体中解析到challenge: {}", challenge);
break;
}
}
} catch (Exception e) {
log.debug("解析form-urlencoded请求体失败", e);
}
}
// 如果请求体就是challenge值本身纯文本
else if (bodyTrimmed.length() < 200) {
challenge = bodyTrimmed;
log.info("将请求体作为challenge值: {}", challenge);
}
}
return handleOAuthCallback(code, state, error, errorDescription, challenge, requestBody);
}
/**
* 处理OAuth回调的核心逻辑
*/
private ResponseEntity<?> handleOAuthCallback(String code, String state, String error,
String errorDescription, String challenge, String requestBody) {
try {
// 处理challenge验证WPS365后台配置时用于验证回调URL
if (challenge != null && !challenge.trim().isEmpty()) {
log.info("收到WPS365 challenge验证请求 - challenge: {}", challenge);
// 尝试返回JSON格式符合OAuth标准
// 格式:{"challenge": "xxx"} 或直接返回challenge值
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String jsonResponse = "{\"challenge\":\"" + challenge + "\"}";
return new ResponseEntity<>(jsonResponse, headers, HttpStatus.OK);
} catch (Exception e) {
log.warn("返回JSON格式失败尝试纯文本格式", e);
// 如果JSON失败尝试纯文本
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
return new ResponseEntity<>(challenge, headers, HttpStatus.OK);
}
}
// 处理授权错误
if (error != null) {
log.error("WPS365授权失败 - error: {}, error_description: {}", error, errorDescription);
String errorMsg = errorDescription != null ? errorDescription : error;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_HTML);
return new ResponseEntity<>(generateCallbackHtml(false, "授权失败: " + errorMsg, null), headers, HttpStatus.OK);
}
// 如果没有code也没有challenge可能是直接访问显示提示信息
if (code == null || code.trim().isEmpty()) {
log.warn("访问回调地址但没有授权码,可能是测试访问");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_HTML);
return new ResponseEntity<>(generateCallbackHtml(false, "等待授权回调... 如果没有授权码,请通过授权流程访问", null), headers, HttpStatus.OK);
}
log.info("收到WPS365授权回调 - code: {}, state: {}", code, state);
// 使用授权码换取access_token
WPS365TokenInfo tokenInfo = wps365OAuthService.getAccessTokenByCode(code);
// 验证返回的token信息
if (tokenInfo == null || tokenInfo.getAccessToken() == null) {
log.error("获取访问令牌失败 - tokenInfo: {}", tokenInfo);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_HTML);
return new ResponseEntity<>(generateCallbackHtml(false, "获取访问令牌失败,响应数据格式不正确", null), headers, HttpStatus.OK);
}
log.info("成功获取访问令牌 - userId: {}, access_token: {}",
tokenInfo.getUserId(),
tokenInfo.getAccessToken() != null ? tokenInfo.getAccessToken().substring(0, 20) + "..." : "null");
// 自动保存token到后端
try {
if (tokenInfo.getUserId() != null) {
wps365OAuthService.saveToken(tokenInfo.getUserId(), tokenInfo);
log.info("访问令牌已自动保存到后端缓存 - userId: {}", tokenInfo.getUserId());
} else {
log.warn("userId为空无法保存Token");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_HTML);
return new ResponseEntity<>(generateCallbackHtml(false, "获取用户ID失败无法保存Token", null), headers, HttpStatus.OK);
}
} catch (Exception e) {
log.error("保存访问令牌失败", e);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_HTML);
return new ResponseEntity<>(generateCallbackHtml(false, "保存访问令牌失败: " + e.getMessage(), null), headers, HttpStatus.OK);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_HTML);
return new ResponseEntity<>(generateCallbackHtml(true, "授权成功,访问令牌已自动保存", tokenInfo), headers, HttpStatus.OK);
} catch (Exception e) {
log.error("OAuth回调处理失败", e);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_HTML);
return new ResponseEntity<>(generateCallbackHtml(false, "授权失败: " + e.getMessage(), null), headers, HttpStatus.OK);
}
}
/**
* 生成回调HTML页面
*/
private String generateCallbackHtml(boolean success, String message, WPS365TokenInfo tokenInfo) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>");
html.append("<html lang='zh-CN'>");
html.append("<head>");
html.append("<meta charset='UTF-8'>");
html.append("<meta name='viewport' content='width=device-width, initial-scale=1.0'>");
html.append("<title>WPS365授权").append(success ? "成功" : "失败").append("</title>");
html.append("<style>");
html.append("body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; ");
html.append("display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; ");
html.append("background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }");
html.append(".container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); text-align: center; max-width: 500px; }");
html.append(".icon { font-size: 64px; margin-bottom: 20px; }");
html.append(".success { color: #52c41a; }");
html.append(".error { color: #ff4d4f; }");
html.append(".message { font-size: 16px; color: #333; margin-bottom: 20px; line-height: 1.6; }");
html.append(".info { font-size: 14px; color: #666; margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 5px; text-align: left; }");
html.append("</style>");
html.append("</head>");
html.append("<body>");
html.append("<div class='container'>");
if (success) {
html.append("<div class='icon success'>✓</div>");
html.append("<h2 style='color: #52c41a; margin-bottom: 10px;'>授权成功</h2>");
} else {
html.append("<div class='icon error'>✗</div>");
html.append("<h2 style='color: #ff4d4f; margin-bottom: 10px;'>授权失败</h2>");
}
html.append("<div class='message'>").append(message).append("</div>");
// 显示Token信息仅成功时
if (success && tokenInfo != null) {
html.append("<div class='info'>");
html.append("<strong>授权信息:</strong><br>");
if (tokenInfo.getUserId() != null) {
html.append("用户ID: ").append(tokenInfo.getUserId()).append("<br>");
}
if (tokenInfo.getExpiresIn() != null) {
html.append("有效期: ").append(tokenInfo.getExpiresIn()).append(" 秒<br>");
}
html.append("</div>");
}
html.append("<p style='color: #999; font-size: 14px; margin-top: 20px;'>窗口将在3秒后自动关闭...</p>");
html.append("</div>");
html.append("<script>");
html.append("// 通知父窗口授权结果");
html.append("if (window.opener) {");
html.append(" window.opener.postMessage({");
html.append(" type: 'wps365_oauth_callback',");
html.append(" success: ").append(success).append(",");
html.append(" message: '").append(message.replace("'", "\\'").replace("\n", "\\n").replace("\r", "\\r")).append("'");
if (success && tokenInfo != null && tokenInfo.getUserId() != null) {
html.append(",");
html.append(" userId: '").append(tokenInfo.getUserId()).append("'");
}
html.append(" }, '*');");
html.append("}");
html.append("// 3秒后自动关闭窗口");
html.append("setTimeout(function() {");
html.append(" if (window.opener) {");
html.append(" window.close();");
html.append(" } else {");
html.append(" window.location.href = '/';");
html.append(" }");
html.append("}, 3000);");
html.append("</script>");
html.append("</body>");
html.append("</html>");
return html.toString();
}
}

View File

@@ -0,0 +1,572 @@
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回调处理已废弃请使用 /wps365-callback
* 保留此接口用于兼容,实际回调请使用 WPS365CallbackController
*/
@Anonymous
@GetMapping("/oauth/callback")
public AjaxResult oauthCallback(@RequestParam String code,
@RequestParam(required = false) String state) {
try {
log.warn("使用已废弃的回调接口 /jarvis/wps365/oauth/callback建议使用 /wps365-callback");
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<String, Object> 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 {
WPS365TokenInfo tokenInfo = null;
// 如果提供了userId查询指定用户的token
if (userId != null && !userId.trim().isEmpty()) {
tokenInfo = wps365OAuthServiceImpl.getTokenByUserId(userId);
} else {
// 如果没有提供userId尝试查找所有token通常只有一个
// 这里使用getCurrentToken方法它会尝试查找可用的token
tokenInfo = wps365OAuthService.getCurrentToken();
}
if (tokenInfo == null) {
JSONObject result = new JSONObject();
result.put("hasToken", false);
result.put("isValid", false);
return AjaxResult.success("未授权", result);
}
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());
if (tokenInfo.getExpiresIn() != null) {
result.put("expiresIn", tokenInfo.getExpiresIn());
}
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<String, Object> 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<String, Object> 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<String, Object> 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<List<Object>> values = (List<List<Object>>) 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<String, Object> 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<String, Object> 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<Map<String, Object>> updates = (List<Map<String, Object>>) 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());
}
}
/**
* 读取AirSheet工作表数据
*/
@GetMapping("/readAirSheetCells")
public AjaxResult readAirSheetCells(@RequestParam String userId,
@RequestParam String fileId,
@RequestParam(required = false, defaultValue = "0") String worksheetId,
@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.readAirSheetCells(tokenInfo.getAccessToken(), fileId, worksheetId, range);
return AjaxResult.success("读取AirSheet数据成功", result);
} catch (Exception e) {
log.error("读取AirSheet数据失败 - fileId: {}, worksheetId: {}, range: {}", fileId, worksheetId, range, e);
return AjaxResult.error("读取AirSheet数据失败: " + e.getMessage());
}
}
/**
* 更新AirSheet工作表数据
*/
@PostMapping("/updateAirSheetCells")
public AjaxResult updateAirSheetCells(@RequestBody Map<String, Object> params) {
try {
String userId = (String) params.get("userId");
String fileId = (String) params.get("fileId");
String worksheetId = params.get("worksheetId") != null ? params.get("worksheetId").toString() : "0";
String range = (String) params.get("range");
@SuppressWarnings("unchecked")
List<List<Object>> values = (List<List<Object>>) params.get("values");
if (userId == null || fileId == null) {
return AjaxResult.error("userId和fileId不能为空");
}
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.updateAirSheetCells(tokenInfo.getAccessToken(), fileId, worksheetId, range, values);
return AjaxResult.success("更新AirSheet数据成功", result);
} catch (Exception e) {
log.error("更新AirSheet数据失败", e);
return AjaxResult.error("更新AirSheet数据失败: " + e.getMessage());
}
}
}

View File

@@ -6,6 +6,12 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.framework.web.domain.Server;
import com.ruoyi.jarvis.service.ILogisticsService;
import com.ruoyi.jarvis.service.IWxSendService;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* 服务器监控
@@ -16,6 +22,12 @@ import com.ruoyi.framework.web.domain.Server;
@RequestMapping("/monitor/server")
public class ServerController
{
@Resource
private ILogisticsService logisticsService;
@Resource
private IWxSendService wxSendService;
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
@GetMapping()
public AjaxResult getInfo() throws Exception
@@ -24,4 +36,52 @@ public class ServerController
server.copyTo();
return AjaxResult.success(server);
}
/**
* 获取服务健康度检测
*/
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
@GetMapping("/health")
public AjaxResult getHealth() throws Exception
{
Map<String, Object> healthMap = new HashMap<>();
// 物流服务健康检测
try {
ILogisticsService.HealthCheckResult logisticsHealth = logisticsService.checkHealth();
Map<String, Object> logisticsMap = new HashMap<>();
logisticsMap.put("healthy", logisticsHealth.isHealthy());
logisticsMap.put("status", logisticsHealth.getStatus());
logisticsMap.put("message", logisticsHealth.getMessage());
logisticsMap.put("serviceUrl", logisticsHealth.getServiceUrl());
healthMap.put("logistics", logisticsMap);
} catch (Exception e) {
Map<String, Object> logisticsMap = new HashMap<>();
logisticsMap.put("healthy", false);
logisticsMap.put("status", "异常");
logisticsMap.put("message", "健康检测异常: " + e.getMessage());
logisticsMap.put("serviceUrl", "");
healthMap.put("logistics", logisticsMap);
}
// 微信推送服务健康检测
try {
IWxSendService.HealthCheckResult wxSendHealth = wxSendService.checkHealth();
Map<String, Object> wxSendMap = new HashMap<>();
wxSendMap.put("healthy", wxSendHealth.isHealthy());
wxSendMap.put("status", wxSendHealth.getStatus());
wxSendMap.put("message", wxSendHealth.getMessage());
wxSendMap.put("serviceUrl", wxSendHealth.getServiceUrl());
healthMap.put("wxSend", wxSendMap);
} catch (Exception e) {
Map<String, Object> wxSendMap = new HashMap<>();
wxSendMap.put("healthy", false);
wxSendMap.put("status", "异常");
wxSendMap.put("message", "健康检测异常: " + e.getMessage());
wxSendMap.put("serviceUrl", "");
healthMap.put("wxSend", wxSendMap);
}
return AjaxResult.success(healthMap);
}
}

View File

@@ -111,11 +111,14 @@ public class PublicOrderController extends BaseController {
log.info("日期验证通过: 订单日期[{}]", orderDate);
// 获取forceGenerate参数默认为false
boolean forceGenerate = body != null && body.get("forceGenerate") != null && Boolean.parseBoolean(String.valueOf(body.get("forceGenerate")));
// 执行指令
List<String> result;
try {
log.info("开始执行订单指令...");
result = instructionService.execute(trimmedCmd);
log.info("开始执行订单指令... forceGenerate={}", forceGenerate);
result = instructionService.execute(trimmedCmd, forceGenerate);
log.info("订单指令执行完成");
// 记录执行结果

View File

@@ -3,11 +3,17 @@ package com.ruoyi.web.controller.publicapi;
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.page.TableDataInfo;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.jarvis.domain.dto.CommentCallHistory;
import com.ruoyi.jarvis.service.ICommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
@@ -18,22 +24,43 @@ import java.util.*;
@RequestMapping("/public/comment")
public class CommentPublicController extends BaseController {
// TODO: 可改为读取配置
private static final String JD_BASE = "http://192.168.8.88:6666/jd";
private static final String SKEY = "2192057370ef8140c201079969c956a3";
@Value("${jarvis.server.jarvis-java.base-url:http://127.0.0.1:6666}")
private String jarvisJavaBaseUrl;
@Value("${jarvis.server.jarvis-java.jd-api-path:/jd}")
private String jdApiPath;
@Autowired(required = false)
private ICommentService commentService;
/**
* 获取JD接口基础URL
*/
private String getJdBase() {
return jarvisJavaBaseUrl + jdApiPath;
}
/**
* 获取可选型号/类型(示例)
*/
@GetMapping("/types")
public AjaxResult types() {
boolean success = false;
try {
String url = JD_BASE + "/comment/types?skey=" + SKEY;
String url = getJdBase() + "/comment/types?skey=" + SKEY;
String result = HttpUtils.sendGet(url);
Object parsed = JSON.parse(result);
success = true;
return AjaxResult.success(parsed);
} catch (Exception e) {
return AjaxResult.error("types failed: " + e.getMessage());
} finally {
// 记录接口调用统计
if (commentService != null) {
commentService.recordApiCall("jd", "types", success);
}
}
}
@@ -42,22 +69,129 @@ public class CommentPublicController extends BaseController {
* 入参productType型号/类型)
*/
@PostMapping("/generate")
public AjaxResult generate(@RequestBody Map<String, String> body) {
public AjaxResult generate(@RequestBody Map<String, String> body, HttpServletRequest request) {
boolean success = false;
String productType = null;
String clientIp = getClientIp(request);
try {
String url = JD_BASE + "/comment/generate";
String url = getJdBase() + "/comment/generate";
JSONObject param = new JSONObject();
param.put("skey", SKEY);
if (body != null && body.get("productType") != null) {
param.put("productType", body.get("productType"));
productType = body.get("productType");
param.put("productType", productType);
}
String result = HttpUtils.sendJsonPost(url, param.toJSONString());
Object parsed = JSON.parse(result);
success = true;
return AjaxResult.success(parsed);
} catch (Exception e) {
return AjaxResult.error("generate failed: " + e.getMessage());
} finally {
// 记录接口调用统计和历史
if (commentService != null && productType != null) {
commentService.recordApiCall("jd", productType, success);
commentService.recordApiCallHistory(productType, clientIp);
}
}
}
/**
* 获取当前IP地址
*/
@GetMapping("/ip")
public AjaxResult getIp(HttpServletRequest request) {
try {
String ip = getClientIp(request);
Map<String, String> result = new HashMap<>();
result.put("ip", ip);
return AjaxResult.success(result);
} catch (Exception e) {
return AjaxResult.error("获取IP失败: " + e.getMessage());
}
}
/**
* 获取使用统计(今天/7天/30天/累计)
*/
@GetMapping("/usage-statistics")
public AjaxResult getUsageStatistics() {
try {
if (commentService != null) {
Map<String, Long> statistics = commentService.getUsageStatistics();
return AjaxResult.success(statistics);
} else {
Map<String, Long> statistics = new HashMap<>();
statistics.put("today", 0L);
statistics.put("last7Days", 0L);
statistics.put("last30Days", 0L);
statistics.put("total", 0L);
return AjaxResult.success(statistics);
}
} catch (Exception e) {
return AjaxResult.error("获取使用统计失败: " + e.getMessage());
}
}
/**
* 获取历史记录
*/
@GetMapping("/history")
public TableDataInfo getHistory(@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
try {
if (commentService != null) {
List<CommentCallHistory> historyList = commentService.getApiCallHistory(pageNum, pageSize);
TableDataInfo dataTable = new TableDataInfo();
dataTable.setCode(200);
dataTable.setMsg("查询成功");
dataTable.setRows(historyList);
dataTable.setTotal(historyList.size()); // 注意:这里返回的是当前页的数量,实际总数可能需要单独查询
return dataTable;
} else {
TableDataInfo dataTable = new TableDataInfo();
dataTable.setCode(200);
dataTable.setMsg("查询成功");
dataTable.setRows(new ArrayList<>());
dataTable.setTotal(0);
return dataTable;
}
} catch (Exception e) {
TableDataInfo dataTable = new TableDataInfo();
dataTable.setCode(500);
dataTable.setMsg("获取历史记录失败: " + e.getMessage());
return dataTable;
}
}
/**
* 获取客户端真实IP地址
* 考虑代理和负载均衡的情况
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 对于通过多个代理的情况第一个IP为客户端真实IP
if (ip != null && ip.contains(",")) {
ip = ip.substring(0, ip.indexOf(",")).trim();
}
return ip;
}
}

View File

@@ -10,11 +10,14 @@ import com.ruoyi.jarvis.service.IOrderRowsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.jarvis.domain.JDOrder;
import com.ruoyi.jarvis.domain.dto.JDOrderSimpleDTO;
import com.ruoyi.jarvis.service.IJDOrderService;
import com.ruoyi.jarvis.service.IInstructionService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
@@ -30,10 +33,12 @@ public class JDOrderListController extends BaseController
private final IJDOrderService jdOrderService;
private final IOrderRowsService orderRowsService;
private final IInstructionService instructionService;
public JDOrderListController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService) {
public JDOrderListController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService, IInstructionService instructionService) {
this.jdOrderService = jdOrderService;
this.orderRowsService = orderRowsService;
this.instructionService = instructionService;
}
/**
@@ -64,6 +69,12 @@ public class JDOrderListController extends BaseController
query.getParams().put("hasFinishTime", true);
}
// 处理混合搜索参数(订单号/第三方单号/分销标识)
String orderSearch = request.getParameter("orderSearch");
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
query.getParams().put("orderSearch", orderSearch.trim());
}
java.util.List<JDOrder> list;
if (orderBy != null && !orderBy.isEmpty()) {
// 设置排序参数
@@ -85,9 +96,11 @@ public class JDOrderListController extends BaseController
if (orderRows != null) {
jdOrder.setProPriceAmount(orderRows.getProPriceAmount());
jdOrder.setFinishTime(orderRows.getFinishTime());
jdOrder.setOrderStatus(orderRows.getValidCode());
} else {
jdOrder.setProPriceAmount(0.0);
jdOrder.setFinishTime(null);
jdOrder.setOrderStatus(null);
}
}
// 过滤掉完成时间为空的订单
@@ -101,8 +114,10 @@ public class JDOrderListController extends BaseController
if (orderRows != null) {
jdOrder.setProPriceAmount(orderRows.getProPriceAmount());
jdOrder.setFinishTime(orderRows.getFinishTime());
jdOrder.setOrderStatus(orderRows.getValidCode());
} else {
jdOrder.setProPriceAmount(0.0);
jdOrder.setOrderStatus(null);
}
}
}
@@ -171,4 +186,251 @@ public class JDOrderListController extends BaseController
{
return toAjax(jdOrderService.deleteJDOrderByIds(ids));
}
/**
* 订单搜索工具接口(返回简易字段)
*/
@Anonymous
@GetMapping("/tools/search")
public TableDataInfo searchOrders(
@RequestParam(required = false) String orderSearch,
@RequestParam(required = false) String address,
HttpServletRequest request)
{
// startPage会从request中读取pageNum和pageSize参数
startPage();
JDOrder query = new JDOrder();
// 处理单号搜索过滤TF、H、F、PDD等关键词
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
String searchKeyword = orderSearch.trim().toUpperCase();
// 过滤掉TF、H、F、PDD等关键词
if (searchKeyword.contains("TF") || searchKeyword.contains("H") ||
searchKeyword.contains("F") || searchKeyword.contains("PDD")) {
// 如果包含过滤关键词,返回空结果
return getDataTable(new java.util.ArrayList<>());
}
// 至少5个字符
if (searchKeyword.length() >= 5) {
query.getParams().put("orderSearch", orderSearch.trim());
}
}
// 处理地址搜索至少3个字符
if (address != null && !address.trim().isEmpty()) {
if (address.trim().length() >= 3) {
query.setAddress(address.trim());
}
}
// 如果没有有效的搜索条件,返回空结果
if ((orderSearch == null || orderSearch.trim().isEmpty() || orderSearch.trim().length() < 5) &&
(address == null || address.trim().isEmpty() || address.trim().length() < 3)) {
return getDataTable(new java.util.ArrayList<>());
}
java.util.List<JDOrder> list = jdOrderService.selectJDOrderList(query);
// 转换为简易DTO只返回前端需要的字段其他字段脱敏
java.util.List<JDOrderSimpleDTO> simpleList = new java.util.ArrayList<>();
for (JDOrder jdOrder : list) {
JDOrderSimpleDTO dto = new JDOrderSimpleDTO();
// 只设置前端需要的字段
dto.setRemark(jdOrder.getRemark());
dto.setOrderId(jdOrder.getOrderId());
dto.setThirdPartyOrderNo(jdOrder.getThirdPartyOrderNo());
dto.setModelNumber(jdOrder.getModelNumber());
dto.setAddress(jdOrder.getAddress());
dto.setIsRefunded(jdOrder.getIsRefunded() != null ? jdOrder.getIsRefunded() : 0);
dto.setIsRebateReceived(jdOrder.getIsRebateReceived() != null ? jdOrder.getIsRebateReceived() : 0);
dto.setStatus(jdOrder.getStatus());
dto.setCreateTime(jdOrder.getCreateTime());
// 关联查询订单状态和赔付金额
OrderRows orderRows = orderRowsService.selectOrderRowsByOrderId(jdOrder.getOrderId());
if (orderRows != null) {
dto.setProPriceAmount(orderRows.getProPriceAmount());
dto.setOrderStatus(orderRows.getValidCode());
} else {
dto.setProPriceAmount(0.0);
dto.setOrderStatus(null);
}
simpleList.add(dto);
}
return getDataTable(simpleList);
}
/**
* 一次性批量更新历史订单将赔付金额大于0的订单标记为后返到账
* 此方法只应执行一次,用于处理历史数据
*/
@Log(title = "批量标记后返到账", businessType = BusinessType.UPDATE)
@RequestMapping(value = "/tools/batch-mark-rebate-received", method = {RequestMethod.POST, RequestMethod.GET})
public AjaxResult batchMarkRebateReceivedForCompensation() {
try {
// 调用批量更新方法
if (instructionService instanceof com.ruoyi.jarvis.service.impl.InstructionServiceImpl) {
((com.ruoyi.jarvis.service.impl.InstructionServiceImpl) instructionService)
.batchMarkRebateReceivedForCompensation();
return AjaxResult.success("批量标记后返到账完成,请查看控制台日志");
} else {
return AjaxResult.error("无法执行批量更新操作");
}
} catch (Exception e) {
return AjaxResult.error("批量标记失败: " + e.getMessage());
}
}
/**
* 生成录单格式文本Excel可粘贴格式
* 根据当前查询条件生成Tab分隔的文本可以直接粘贴到Excel
*/
@GetMapping("/generateExcelText")
public AjaxResult generateExcelText(JDOrder query, HttpServletRequest request) {
try {
// 处理时间筛选参数
String beginTimeStr = request.getParameter("beginTime");
String endTimeStr = request.getParameter("endTime");
if (beginTimeStr != null && !beginTimeStr.isEmpty()) {
query.getParams().put("beginTime", beginTimeStr);
}
if (endTimeStr != null && !endTimeStr.isEmpty()) {
query.getParams().put("endTime", endTimeStr);
}
// 处理混合搜索参数
String orderSearch = request.getParameter("orderSearch");
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
query.getParams().put("orderSearch", orderSearch.trim());
}
// 处理其他查询参数
if (query.getRemark() != null && !query.getRemark().trim().isEmpty()) {
query.setRemark(query.getRemark().trim());
}
if (query.getDistributionMark() != null && !query.getDistributionMark().trim().isEmpty()) {
query.setDistributionMark(query.getDistributionMark().trim());
}
if (query.getModelNumber() != null && !query.getModelNumber().trim().isEmpty()) {
query.setModelNumber(query.getModelNumber().trim());
}
if (query.getBuyer() != null && !query.getBuyer().trim().isEmpty()) {
query.setBuyer(query.getBuyer().trim());
}
if (query.getAddress() != null && !query.getAddress().trim().isEmpty()) {
query.setAddress(query.getAddress().trim());
}
if (query.getStatus() != null && !query.getStatus().trim().isEmpty()) {
query.setStatus(query.getStatus().trim());
}
// 获取订单列表(不分页,获取所有符合条件的订单)
List<JDOrder> list = jdOrderService.selectJDOrderList(query);
if (list == null || list.isEmpty()) {
return AjaxResult.success("暂无订单数据");
}
// 关联查询订单状态和赔付金额
for (JDOrder jdOrder : list) {
OrderRows orderRows = orderRowsService.selectOrderRowsByOrderId(jdOrder.getOrderId());
if (orderRows != null) {
jdOrder.setProPriceAmount(orderRows.getProPriceAmount());
// estimateCosPrice 是京粉实际价格
if (orderRows.getEstimateCosPrice() != null) {
jdOrder.setJingfenActualPrice(orderRows.getEstimateCosPrice());
}
}
}
// 按 remark 排序
list.sort((o1, o2) -> {
String r1 = o1.getRemark() != null ? o1.getRemark() : "";
String r2 = o2.getRemark() != null ? o2.getRemark() : "";
return r1.compareTo(r2);
});
// 生成Excel格式文本Tab分隔
StringBuilder sb = new StringBuilder();
for (JDOrder o : list) {
// 日期格式yyyy/MM/dd
String dateStr = "";
if (o.getOrderTime() != null) {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy/MM/dd");
dateStr = sdf.format(o.getOrderTime());
}
// 多多单号(第三方单号,如果没有则使用内部单号)
String duoduoOrderNo = o.getThirdPartyOrderNo() != null && !o.getThirdPartyOrderNo().trim().isEmpty()
? o.getThirdPartyOrderNo() : (o.getRemark() != null ? o.getRemark() : "");
// 型号
String modelNumber = o.getModelNumber() != null ? o.getModelNumber() : "";
// 数量固定为1
String quantity = "1";
// 地址
String address = o.getAddress() != null ? o.getAddress() : "";
// 姓名(从地址中提取,地址格式通常是"姓名 电话 详细地址"
String buyer = "";
if (address != null && !address.trim().isEmpty()) {
String[] addressParts = address.trim().split("\\s+");
if (addressParts.length > 0) {
buyer = addressParts[0];
}
}
// 售价固定为0
String sellingPriceStr = "0";
// 成本售价是0成本也设为空
String costStr = "";
// 利润(后返金额)
Double rebate = o.getRebateAmount() != null ? o.getRebateAmount() : 0.0;
String profitStr = rebate > 0
? String.format(java.util.Locale.ROOT, "%.2f", rebate) : "";
// 京东单号
String orderId = o.getOrderId() != null ? o.getOrderId() : "";
// 物流链接
String logisticsLink = o.getLogisticsLink() != null ? o.getLogisticsLink() : "";
// 下单付款
String paymentAmountStr = o.getPaymentAmount() != null
? String.format(java.util.Locale.ROOT, "%.2f", o.getPaymentAmount()) : "";
// 后返
String rebateAmountStr = o.getRebateAmount() != null
? String.format(java.util.Locale.ROOT, "%.2f", o.getRebateAmount()) : "";
// 按顺序拼接:日期、多多单号、型号、数量、姓名、地址、售价、成本、利润、京东单号、物流、下单付款、后返
sb.append(dateStr).append('\t')
.append(duoduoOrderNo).append('\t')
.append(modelNumber).append('\t')
.append(quantity).append('\t')
.append(buyer).append('\t')
.append(address).append('\t')
.append(sellingPriceStr).append('\t')
.append(costStr).append('\t')
.append(profitStr).append('\t')
.append(orderId).append('\t')
.append(logisticsLink).append('\t')
.append(paymentAmountStr).append('\t')
.append(rebateAmountStr).append('\n');
}
return AjaxResult.success(sb.toString());
} catch (Exception e) {
return AjaxResult.error("生成失败: " + e.getMessage());
}
}
}

View File

@@ -185,7 +185,21 @@ xss:
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# 服务地址配置(用于服务器迁移)
jarvis:
# 服务器基础地址如果所有服务都在同一台服务器可以使用127.0.0.1
# 开发环境:根据实际情况配置
server:
host: 192.168.8.88
# Jarvis Java服务地址JD相关接口
jarvis-java:
base-url: http://192.168.8.88:6666
jd-api-path: /jd
# 物流接口服务地址
logistics:
base-url: http://192.168.8.88:5001
fetch-path: /fetch_logistics
health-path: /health
# 腾讯文档开放平台配置
# 文档地址https://docs.qq.com/open/document/app/openapi/v3/sheet/model/spreadsheet.html
tencent:
@@ -208,3 +222,5 @@ tencent:
# 刷新Token地址用于通过refresh_token刷新access_token
refresh-token-url: https://docs.qq.com/oauth/v2/token

View File

@@ -66,7 +66,7 @@ spring:
# redis 配置
redis:
# 地址
host: 192.168.8.88
host: 127.0.0.1
# 端口默认为6379
port: 6379
# 数据库索引
@@ -92,7 +92,7 @@ spring:
druid:
# 主库数据源
master:
url: jdbc:mysql://192.168.8.88:3306/jd?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
url: jdbc:mysql://127.0.0.1:3306/jd?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: mysql_7sjTXH
# 从库数据源
@@ -185,6 +185,21 @@ xss:
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# 服务地址配置(用于服务器迁移)
jarvis:
# 服务器基础地址如果所有服务都在同一台服务器可以使用127.0.0.1
# 生产环境192.168.8.88 或 127.0.0.1
server:
host: 127.0.0.1
# Jarvis Java服务地址JD相关接口
jarvis-java:
base-url: http://127.0.0.1:6666
jd-api-path: /jd
# 物流接口服务地址
logistics:
base-url: http://127.0.0.1:5001
fetch-path: /fetch_logistics
health-path: /health
# 腾讯文档开放平台配置
# 文档地址https://docs.qq.com/open/document/app/openapi/v3/sheet/model/spreadsheet.html
tencent:

View File

@@ -8,4 +8,44 @@ tencent:
delayed:
push:
# 延迟时间分钟默认10分钟
minutes: 30
minutes: 10
# WPS365开放平台配置
# 文档地址https://open.wps.cn/
wps365:
# 应用IDAppId- 需要在WPS365开放平台申请
app-id: AK20260114NNQJKV
# 应用密钥AppKey- 需要在WPS365开放平台申请注意保密
app-key: 4c58bc1642e5e8fa731f75af9370496a
# 授权回调地址需要在WPS365开放平台配置授权域名
# 注意:使用 /wps365-callback 路径,避免前端路由拦截
redirect-uri: https://jarvis.van333.cn/wps365-callback
# API基础地址
api-base-url: https://openapi.wps.cn/api/v1
# OAuth授权地址正确格式https://openapi.wps.cn/oauth2/auth
oauth-url: https://openapi.wps.cn/oauth2/auth
# 获取Token地址
token-url: https://openapi.wps.cn/oauth2/token
# 刷新Token地址
refresh-token-url: https://openapi.wps.cn/oauth2/token
# OAuth授权请求的scope权限可选
# 如果不配置默认使用kso.file.readwrite文件读写权限支持在线表格操作
#
# ⚠️ 重要如果报错invalid_scope必须按以下步骤操作
# 1. 登录WPS365开放平台https://open.wps.cn/
# 2. 进入"开发配置" > "权限管理"
# 3. 查看已申请权限的准确名称(必须以 kso. 开头)
# 4. 在下方配置scope使用英文逗号分隔WPS365官方要求
#
# 根据WPS365官方文档https://open.wps.cn/documents/app-integration-dev/wps365/server/
# - 必须使用英文逗号分隔(不是空格)
# - 权限名称必须以 kso. 开头格式如kso.file.read, kso.file.readwrite
# - 常见权限名称:
# * kso.file.read (文件读取)
# * kso.file.readwrite (文件读写,支持在线表格操作)
# * kso.doclib.readwrite (文档库读写)
# * kso.wiki.readwrite (知识库读写)
#
# 示例配置(根据平台后台实际显示的权限名称修改):
# scope: kso.file.readwrite
# scope: kso.file.read,kso.file.readwrite
# scope: kso.doclib.readwrite

View File

@@ -75,6 +75,17 @@
<logger name="com.ruoyi" level="info" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
<!-- MyBatis SQL日志级别控制 - 关闭SQL打印 -->
<logger name="org.apache.ibatis" level="warn" />
<logger name="java.sql" level="warn" />
<logger name="java.sql.Connection" level="warn" />
<logger name="java.sql.Statement" level="warn" />
<logger name="java.sql.PreparedStatement" level="warn" />
<logger name="java.sql.ResultSet" level="warn" />
<!-- MyBatis Mapper接口日志级别控制 - 关闭Mapper SQL打印 -->
<logger name="com.ruoyi.jarvis.mapper" level="warn" />
<logger name="com.ruoyi.system.mapper" level="warn" />
<logger name="com.ruoyi.erp.mapper" level="warn" />
<root level="info">
<appender-ref ref="console" />

View File

@@ -217,6 +217,79 @@ public class HttpUtils
return result.toString();
}
/**
* 向指定 URL 发送DELETE方法的请求
*
* @param url 发送请求的 URL
* @return 所代表远程资源的响应结果
*/
public static String sendDelete(String url)
{
StringBuilder result = new StringBuilder();
BufferedReader in = null;
try
{
log.info("sendDelete - {}", url);
URL realUrl = new URL(url);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) realUrl.openConnection();
conn.setRequestMethod("DELETE");
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setConnectTimeout(10000);
conn.setReadTimeout(20000);
conn.connect();
int responseCode = conn.getResponseCode();
InputStream inputStream = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream()
: conn.getErrorStream();
if (inputStream != null)
{
in = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line;
while ((line = in.readLine()) != null)
{
result.append(line);
}
}
log.info("recv - {}", result);
}
catch (ConnectException e)
{
log.error("调用HttpUtils.sendDelete ConnectException, url=" + url, e);
}
catch (SocketTimeoutException e)
{
log.error("调用HttpUtils.sendDelete SocketTimeoutException, url=" + url, e);
}
catch (IOException e)
{
log.error("调用HttpUtils.sendDelete IOException, url=" + url, e);
}
catch (Exception e)
{
log.error("调用HttpUtils.sendDelete Exception, url=" + url, e);
}
finally
{
try
{
if (in != null)
{
in.close();
}
}
catch (Exception ex)
{
log.error("调用in.close Exception, url=" + url, ex);
}
}
return result.toString();
}
public static String sendSSLPost(String url, String param)
{
return sendSSLPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE);

View File

@@ -0,0 +1,132 @@
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);
/** 应用IDAppId */
private String appId;
/** 应用密钥AppKey */
private String appKey;
/** 授权回调地址 */
private String redirectUri;
/** API基础地址 */
private String apiBaseUrl = "https://openapi.wps.cn/api/v1";
/** OAuth授权地址 */
private String oauthUrl = "https://openapi.wps.cn/oauth2/auth";
/** 获取Token地址 */
private String tokenUrl = "https://openapi.wps.cn/oauth2/token";
/** 刷新Token地址 */
private String refreshTokenUrl = "https://openapi.wps.cn/oauth2/token";
/** OAuth授权请求的scope权限可选如果不配置则使用默认值 */
private String scope;
/**
* 配置初始化后验证
*/
@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;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
}

View File

@@ -0,0 +1,60 @@
package com.ruoyi.jarvis.domain;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 京东评论对象 comments
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Comment extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 主键ID */
@Excel(name = "ID")
private Long id;
/** 商品ID */
@Excel(name = "商品ID")
private String productId;
/** 用户名 */
@Excel(name = "用户名")
private String userName;
/** 评论内容 */
@Excel(name = "评论内容")
private String commentText;
/** 评论ID */
@Excel(name = "评论ID")
private String commentId;
/** 图片URLs */
@Excel(name = "图片URLs")
private String pictureUrls;
/** 创建时间 */
@Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date createdAt;
/** 评论日期 */
@Excel(name = "评论日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date commentDate;
/** 是否已使用 0-未使用 1-已使用 */
@Excel(name = "使用状态", readConverterExp = "0=未使用,1=已使用")
private Integer isUse;
/** 产品类型从Redis映射获取 */
private String productType;
/** Redis映射的产品ID */
private String mappedProductId;
}

View File

@@ -0,0 +1,282 @@
package com.ruoyi.jarvis.domain;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
/**
* 闲鱼商品对象 erp_product
*
* @author ruoyi
* @date 2024-01-01
*/
public class ErpProduct extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 主键ID */
private Long id;
/** 管家商品ID */
@Excel(name = "管家商品ID")
private Long productId;
/** 商品标题 */
@Excel(name = "商品标题")
private String title;
/** 商品图片(主图) */
@Excel(name = "商品图片")
private String mainImage;
/** 商品价格(分) */
@Excel(name = "商品价格")
private Long price;
/** 商品库存 */
@Excel(name = "商品库存")
private Integer stock;
/** 商品状态 -1:删除 21:待发布 22:销售中 23:已售罄 31:手动下架 33:售出下架 36:自动下架 */
@Excel(name = "商品状态", readConverterExp = "-1=删除,21=待发布,22=销售中,23=已售罄,31=手动下架,33=售出下架,36=自动下架")
private Integer productStatus;
/** 销售状态 */
@Excel(name = "销售状态")
private Integer saleStatus;
/** 闲鱼会员名 */
@Excel(name = "闲鱼会员名")
private String userName;
/** 上架时间 */
@Excel(name = "上架时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Long onlineTime;
/** 下架时间 */
@Excel(name = "下架时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Long offlineTime;
/** 售出时间 */
@Excel(name = "售出时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Long soldTime;
/** 创建时间(闲鱼) */
@Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Long createTimeXy;
/** 更新时间(闲鱼) */
@Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Long updateTimeXy;
/** ERP应用ID */
@Excel(name = "ERP应用ID")
private String appid;
/** 商品链接 */
@Excel(name = "商品链接")
private String productUrl;
/** 备注 */
@Excel(name = "备注")
private String remark;
public void setId(Long id)
{
this.id = id;
}
public Long getId()
{
return id;
}
public void setProductId(Long productId)
{
this.productId = productId;
}
public Long getProductId()
{
return productId;
}
public void setTitle(String title)
{
this.title = title;
}
public String getTitle()
{
return title;
}
public void setMainImage(String mainImage)
{
this.mainImage = mainImage;
}
public String getMainImage()
{
return mainImage;
}
public void setPrice(Long price)
{
this.price = price;
}
public Long getPrice()
{
return price;
}
public void setStock(Integer stock)
{
this.stock = stock;
}
public Integer getStock()
{
return stock;
}
public void setProductStatus(Integer productStatus)
{
this.productStatus = productStatus;
}
public Integer getProductStatus()
{
return productStatus;
}
public void setSaleStatus(Integer saleStatus)
{
this.saleStatus = saleStatus;
}
public Integer getSaleStatus()
{
return saleStatus;
}
public void setUserName(String userName)
{
this.userName = userName;
}
public String getUserName()
{
return userName;
}
public void setOnlineTime(Long onlineTime)
{
this.onlineTime = onlineTime;
}
public Long getOnlineTime()
{
return onlineTime;
}
public void setOfflineTime(Long offlineTime)
{
this.offlineTime = offlineTime;
}
public Long getOfflineTime()
{
return offlineTime;
}
public void setSoldTime(Long soldTime)
{
this.soldTime = soldTime;
}
public Long getSoldTime()
{
return soldTime;
}
public void setCreateTimeXy(Long createTimeXy)
{
this.createTimeXy = createTimeXy;
}
public Long getCreateTimeXy()
{
return createTimeXy;
}
public void setUpdateTimeXy(Long updateTimeXy)
{
this.updateTimeXy = updateTimeXy;
}
public Long getUpdateTimeXy()
{
return updateTimeXy;
}
public void setAppid(String appid)
{
this.appid = appid;
}
public String getAppid()
{
return appid;
}
public void setProductUrl(String productUrl)
{
this.productUrl = productUrl;
}
public String getProductUrl()
{
return productUrl;
}
@Override
public void setRemark(String remark)
{
this.remark = remark;
}
@Override
public String getRemark()
{
return remark;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("productId", getProductId())
.append("title", getTitle())
.append("mainImage", getMainImage())
.append("price", getPrice())
.append("stock", getStock())
.append("productStatus", getProductStatus())
.append("saleStatus", getSaleStatus())
.append("userName", getUserName())
.append("onlineTime", getOnlineTime())
.append("offlineTime", getOfflineTime())
.append("soldTime", getSoldTime())
.append("createTimeXy", getCreateTimeXy())
.append("updateTimeXy", getUpdateTimeXy())
.append("appid", getAppid())
.append("productUrl", getProductUrl())
.append("remark", getRemark())
.append("createTime", getCreateTime())
.append("updateTime", getUpdateTime())
.toString();
}
}

View File

@@ -80,6 +80,11 @@ public class JDOrder extends BaseEntity {
@Excel(name = "完成时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date finishTime;
/** 订单状态从order_rows表查询 */
@Transient
@Excel(name = "订单状态")
private Integer orderStatus;
/** 是否参与统计0否 1是 */
@Excel(name = "参与统计")
private Integer isCountEnabled;
@@ -92,6 +97,54 @@ public class JDOrder extends BaseEntity {
@Excel(name = "京粉实际价格")
private Double jingfenActualPrice;
/** 是否退款0否 1是 */
@Excel(name = "是否退款")
private Integer isRefunded;
/** 退款日期 */
@Excel(name = "退款日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date refundDate;
/** 是否退款到账0否 1是 */
@Excel(name = "是否退款到账")
private Integer isRefundReceived;
/** 退款到账日期 */
@Excel(name = "退款到账日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date refundReceivedDate;
/** 后返到账0否 1是 */
@Excel(name = "后返到账")
private Integer isRebateReceived;
/** 后返到账日期 */
@Excel(name = "后返到账日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date rebateReceivedDate;
/** 点过价保0否 1是 */
@Excel(name = "点过价保")
private Integer isPriceProtected;
/** 价保日期 */
@Excel(name = "价保日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date priceProtectedDate;
/** 开过专票0否 1是 */
@Excel(name = "开过专票")
private Integer isInvoiceOpened;
/** 开票日期 */
@Excel(name = "开票日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date invoiceOpenedDate;
/** 晒过评价0否 1是 */
@Excel(name = "晒过评价")
private Integer isReviewPosted;
/** 评价日期 */
@Excel(name = "评价日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date reviewPostedDate;
}

View File

@@ -44,6 +44,10 @@ public class SuperAdmin extends BaseEntity
@Excel(name = "是否参与订单统计", readConverterExp = "0=否,1=是")
private Integer isCount;
/** 接收人企业微信用户ID多个用逗号分隔 */
@Excel(name = "接收人")
private String touser;
/** 创建时间 */
@Excel(name = "创建时间")
private Date createdAt;
@@ -151,4 +155,14 @@ public class SuperAdmin extends BaseEntity
{
this.isCount = isCount;
}
public String getTouser()
{
return touser;
}
public void setTouser(String touser)
{
this.touser = touser;
}
}

View File

@@ -0,0 +1,58 @@
package com.ruoyi.jarvis.domain;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
import java.util.Date;
/**
* 淘宝评论对象 taobao_comments
*/
@Data
public class TaobaoComment extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 主键ID */
@Excel(name = "ID")
private Integer id;
/** 商品ID */
@Excel(name = "商品ID")
private String productId;
/** 用户名 */
@Excel(name = "用户名")
private String userName;
/** 评论内容 */
@Excel(name = "评论内容")
private String commentText;
/** 评论ID */
@Excel(name = "评论ID")
private String commentId;
/** 图片URLs */
@Excel(name = "图片URLs")
private String pictureUrls;
/** 创建时间 */
@Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date createdAt;
/** 评论日期 */
@Excel(name = "评论日期")
private String commentDate;
/** 是否已使用 0-未使用 1-已使用 */
@Excel(name = "使用状态", readConverterExp = "0=未使用,1=已使用")
private Integer isUse;
/** 产品类型从Redis映射获取 */
private String productType;
/** Redis映射的产品ID */
private String mappedProductId;
}

View File

@@ -0,0 +1,30 @@
package com.ruoyi.jarvis.domain.dto;
import lombok.Data;
import java.util.Date;
/**
* 评论接口调用统计
*/
@Data
public class CommentApiStatistics {
/** 统计日期 */
private Date statDate;
/** 接口类型jd-京东tb-淘宝 */
private String apiType;
/** 产品类型 */
private String productType;
/** 调用次数 */
private Long callCount;
/** 成功次数 */
private Long successCount;
/** 失败次数 */
private Long failCount;
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.jarvis.domain.dto;
import lombok.Data;
import java.util.Date;
/**
* 评论接口调用历史记录
*/
@Data
public class CommentCallHistory {
/** 产品类型 */
private String productType;
/** IP地址 */
private String ip;
/** 创建时间 */
private Date createTime;
}

View File

@@ -0,0 +1,38 @@
package com.ruoyi.jarvis.domain.dto;
import lombok.Data;
import java.util.Date;
/**
* 评论统计信息
*/
@Data
public class CommentStatistics {
/** 评论来源jd-京东tb-淘宝 */
private String source;
/** 产品类型 */
private String productType;
/** 产品ID */
private String productId;
/** 总评论数 */
private Long totalCount;
/** 可用评论数(未使用) */
private Long availableCount;
/** 已使用评论数 */
private Long usedCount;
/** 接口调用次数 */
private Long apiCallCount;
/** 今日调用次数 */
private Long todayCallCount;
/** 最后一条评论的创建时间(作为更新日期) */
private Date lastCommentUpdateTime;
}

View File

@@ -0,0 +1,132 @@
package com.ruoyi.jarvis.domain.dto;
import java.util.Date;
/**
* 订单搜索工具返回的简易DTO
* 只包含前端展示需要的字段,其他字段脱敏
*/
public class JDOrderSimpleDTO {
/** 内部单号 */
private String remark;
/** 京东单号 */
private String orderId;
/** 第三方单号 */
private String thirdPartyOrderNo;
/** 型号 */
private String modelNumber;
/** 地址 */
private String address;
/** 退款状态0否 1是 */
private Integer isRefunded;
/** 后返到账0否 1是 */
private Integer isRebateReceived;
/** 赔付金额 */
private Double proPriceAmount;
/** 订单状态 */
private Integer orderStatus;
/** 备注/状态 */
private String status;
/** 创建时间 */
private Date createTime;
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getThirdPartyOrderNo() {
return thirdPartyOrderNo;
}
public void setThirdPartyOrderNo(String thirdPartyOrderNo) {
this.thirdPartyOrderNo = thirdPartyOrderNo;
}
public String getModelNumber() {
return modelNumber;
}
public void setModelNumber(String modelNumber) {
this.modelNumber = modelNumber;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getIsRefunded() {
return isRefunded;
}
public void setIsRefunded(Integer isRefunded) {
this.isRefunded = isRefunded;
}
public Integer getIsRebateReceived() {
return isRebateReceived;
}
public void setIsRebateReceived(Integer isRebateReceived) {
this.isRebateReceived = isRebateReceived;
}
public Double getProPriceAmount() {
return proPriceAmount;
}
public void setProPriceAmount(Double proPriceAmount) {
this.proPriceAmount = proPriceAmount;
}
public Integer getOrderStatus() {
return orderStatus;
}
public void setOrderStatus(Integer orderStatus) {
this.orderStatus = orderStatus;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,42 @@
package com.ruoyi.jarvis.mapper;
import com.ruoyi.jarvis.domain.Comment;
import java.util.List;
import java.util.Map;
/**
* 京东评论 Mapper 接口
*/
public interface CommentMapper {
/**
* 查询京东评论列表
*/
List<Comment> selectCommentList(Comment comment);
/**
* 根据ID查询京东评论
*/
Comment selectCommentById(Long id);
/**
* 根据商品ID查询评论统计
*/
Map<String, Object> selectCommentStatisticsByProductId(String productId);
/**
* 更新评论使用状态
*/
int updateCommentIsUse(Comment comment);
/**
* 批量删除评论
*/
int deleteCommentByIds(Long[] ids);
/**
* 重置评论使用状态(批量)
*/
int resetCommentIsUseByProductId(String productId);
}

View File

@@ -0,0 +1,80 @@
package com.ruoyi.jarvis.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.jarvis.domain.ErpProduct;
/**
* 闲鱼商品Mapper接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface ErpProductMapper
{
/**
* 查询闲鱼商品
*
* @param id 闲鱼商品主键
* @return 闲鱼商品
*/
public ErpProduct selectErpProductById(Long id);
/**
* 查询闲鱼商品列表
*
* @param erpProduct 闲鱼商品
* @return 闲鱼商品集合
*/
public List<ErpProduct> selectErpProductList(ErpProduct erpProduct);
/**
* 新增闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
public int insertErpProduct(ErpProduct erpProduct);
/**
* 修改闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
public int updateErpProduct(ErpProduct erpProduct);
/**
* 删除闲鱼商品
*
* @param id 闲鱼商品主键
* @return 结果
*/
public int deleteErpProductById(Long id);
/**
* 批量删除闲鱼商品
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
public int deleteErpProductByIds(Long[] ids);
/**
* 根据商品ID和appid查询
*
* @param productId 商品ID
* @param appid ERP应用ID
* @return 闲鱼商品
*/
public ErpProduct selectErpProductByProductIdAndAppid(@Param("productId") Long productId, @Param("appid") String appid);
/**
* 批量插入或更新闲鱼商品
*
* @param erpProducts 闲鱼商品列表
* @return 结果
*/
public int batchInsertOrUpdateErpProduct(List<ErpProduct> erpProducts);
}

View File

@@ -0,0 +1,42 @@
package com.ruoyi.jarvis.mapper;
import com.ruoyi.jarvis.domain.TaobaoComment;
import java.util.List;
import java.util.Map;
/**
* 淘宝评论 Mapper 接口
*/
public interface TaobaoCommentMapper {
/**
* 查询淘宝评论列表
*/
List<TaobaoComment> selectTaobaoCommentList(TaobaoComment taobaoComment);
/**
* 根据ID查询淘宝评论
*/
TaobaoComment selectTaobaoCommentById(Integer id);
/**
* 根据商品ID查询评论统计
*/
Map<String, Object> selectTaobaoCommentStatisticsByProductId(String productId);
/**
* 更新评论使用状态
*/
int updateTaobaoCommentIsUse(TaobaoComment taobaoComment);
/**
* 批量删除评论
*/
int deleteTaobaoCommentByIds(Integer[] ids);
/**
* 重置评论使用状态(批量)
*/
int resetTaobaoCommentIsUseByProductId(String productId);
}

View File

@@ -0,0 +1,81 @@
package com.ruoyi.jarvis.service;
import com.ruoyi.jarvis.domain.Comment;
import com.ruoyi.jarvis.domain.dto.CommentStatistics;
import com.ruoyi.jarvis.domain.dto.CommentApiStatistics;
import com.ruoyi.jarvis.domain.dto.CommentCallHistory;
import java.util.List;
import java.util.Map;
/**
* 评论管理 Service 接口
*/
public interface ICommentService {
/**
* 查询京东评论列表
*/
List<Comment> selectCommentList(Comment comment);
/**
* 根据ID查询京东评论
*/
Comment selectCommentById(Long id);
/**
* 更新评论使用状态
*/
int updateCommentIsUse(Comment comment);
/**
* 批量删除评论
*/
int deleteCommentByIds(Long[] ids);
/**
* 重置评论使用状态(批量)
*/
int resetCommentIsUseByProductId(String productId);
/**
* 获取评论统计信息包含Redis映射
*/
List<CommentStatistics> getCommentStatistics(String source);
/**
* 记录接口调用统计
*/
void recordApiCall(String apiType, String productType, boolean success);
/**
* 记录接口调用历史带IP
*/
void recordApiCallHistory(String productType, String ip);
/**
* 获取接口调用历史记录
*/
List<CommentCallHistory> getApiCallHistory(int pageNum, int pageSize);
/**
* 获取使用统计(今天/7天/30天/累计)
*/
Map<String, Long> getUsageStatistics();
/**
* 获取接口调用统计
*/
List<CommentApiStatistics> getApiStatistics(String apiType, String productType, String startDate, String endDate);
/**
* 获取Redis产品类型映射京东
*/
Map<String, String> getJdProductTypeMap();
/**
* 获取Redis产品类型映射淘宝
*/
Map<String, String> getTbProductTypeMap();
}

View File

@@ -0,0 +1,107 @@
package com.ruoyi.jarvis.service;
import java.util.List;
import com.ruoyi.jarvis.domain.ErpProduct;
/**
* 闲鱼商品Service接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface IErpProductService
{
/**
* 查询闲鱼商品
*
* @param id 闲鱼商品主键
* @return 闲鱼商品
*/
public ErpProduct selectErpProductById(Long id);
/**
* 查询闲鱼商品列表
*
* @param erpProduct 闲鱼商品
* @return 闲鱼商品集合
*/
public List<ErpProduct> selectErpProductList(ErpProduct erpProduct);
/**
* 新增闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
public int insertErpProduct(ErpProduct erpProduct);
/**
* 修改闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
public int updateErpProduct(ErpProduct erpProduct);
/**
* 批量删除闲鱼商品
*
* @param ids 需要删除的闲鱼商品主键集合
* @return 结果
*/
public int deleteErpProductByIds(Long[] ids);
/**
* 删除闲鱼商品信息
*
* @param id 闲鱼商品主键
* @return 结果
*/
public int deleteErpProductById(Long id);
/**
* 从闲鱼ERP拉取商品列表并保存
*
* @param appid ERP应用ID
* @param pageNo 页码
* @param pageSize 每页大小
* @param productStatus 商品状态
* @return 拉取结果
*/
public int pullAndSaveProductList(String appid, Integer pageNo, Integer pageSize, Integer productStatus);
/**
* 全量同步商品列表(自动遍历所有页码,同步更新和删除)
*
* @param appid ERP应用ID
* @param productStatus 商品状态null表示全部状态
* @return 同步结果
*/
public SyncResult syncAllProducts(String appid, Integer productStatus);
/**
* 同步结果
*/
public static class SyncResult {
private int totalPulled; // 拉取总数
private int added; // 新增数量
private int updated; // 更新数量
private int deleted; // 删除数量
private int failed; // 失败数量
private String message; // 结果消息
public int getTotalPulled() { return totalPulled; }
public void setTotalPulled(int totalPulled) { this.totalPulled = totalPulled; }
public int getAdded() { return added; }
public void setAdded(int added) { this.added = added; }
public int getUpdated() { return updated; }
public void setUpdated(int updated) { this.updated = updated; }
public int getDeleted() { return deleted; }
public void setDeleted(int deleted) { this.deleted = deleted; }
public int getFailed() { return failed; }
public void setFailed(int failed) { this.failed = failed; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
}
}

View File

@@ -11,6 +11,23 @@ public interface IInstructionService {
*/
java.util.List<String> execute(String command);
/**
* 执行文本指令,返回结果文本(支持强制生成参数)
* @param command 指令内容
* @param forceGenerate 是否强制生成表单(跳过地址重复检查)
* @return 执行结果文本列表(可能为单条或多条)
*/
java.util.List<String> execute(String command, boolean forceGenerate);
/**
* 执行文本指令,返回结果文本(支持强制生成参数和控制台入口标识)
* @param command 指令内容
* @param forceGenerate 是否强制生成表单(跳过地址重复检查)
* @param isFromConsole 是否来自控制台入口(控制台入口跳过订单查询校验)
* @return 执行结果文本列表(可能为单条或多条)
*/
java.util.List<String> execute(String command, boolean forceGenerate, boolean isFromConsole);
/**
* 获取历史消息记录
* @param type 消息类型request(请求) 或 response(响应)

View File

@@ -19,5 +19,60 @@ public interface ILogisticsService {
* @return 如果已处理返回true否则返回false
*/
boolean isOrderProcessed(Long orderId);
/**
* 检查物流服务健康状态
* @return 健康状态信息,包含是否健康、状态描述等
*/
HealthCheckResult checkHealth();
/**
* 健康检测结果
*/
class HealthCheckResult {
private boolean healthy;
private String status;
private String message;
private String serviceUrl;
public HealthCheckResult(boolean healthy, String status, String message, String serviceUrl) {
this.healthy = healthy;
this.status = status;
this.message = message;
this.serviceUrl = serviceUrl;
}
public boolean isHealthy() {
return healthy;
}
public void setHealthy(boolean healthy) {
this.healthy = healthy;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getServiceUrl() {
return serviceUrl;
}
public void setServiceUrl(String serviceUrl) {
this.serviceUrl = serviceUrl;
}
}
}

View File

@@ -0,0 +1,56 @@
package com.ruoyi.jarvis.service;
import java.util.List;
/**
* 手机号替换配置Service接口
*
* @author ruoyi
*/
public interface IPhoneReplaceConfigService
{
/**
* 获取指定类型的手机号列表
*
* @param type 类型(腾锋或昭迎)
* @return 手机号列表
*/
public List<String> getPhoneList(String type);
/**
* 设置指定类型的手机号列表
*
* @param type 类型(腾锋或昭迎)
* @param phoneList 手机号列表
* @return 结果
*/
public int setPhoneList(String type, List<String> phoneList);
/**
* 添加手机号到指定类型
*
* @param type 类型(腾锋或昭迎)
* @param phone 手机号
* @return 结果
*/
public int addPhone(String type, String phone);
/**
* 从指定类型删除手机号
*
* @param type 类型(腾锋或昭迎)
* @param phone 手机号
* @return 结果
*/
public int removePhone(String type, String phone);
/**
* 获取下一个循环使用的手机号
*
* @param type 类型(腾锋或昭迎)
* @param originalPhone 原始手机号
* @return 替换后的手机号
*/
public String getNextPhone(String type, String originalPhone);
}

View File

@@ -0,0 +1,67 @@
package com.ruoyi.jarvis.service;
import java.util.Map;
/**
* 小红书/抖音内容生成Service接口
*
* @author ruoyi
* @date 2025-01-XX
*/
public interface ISocialMediaService
{
/**
* 提取商品标题关键词
*
* @param productName 商品名称
* @return 关键词结果
*/
Map<String, Object> extractKeywords(String productName);
/**
* 生成文案
*
* @param productName 商品名称
* @param originalPrice 原价
* @param finalPrice 到手价
* @param keywords 关键词
* @param style 文案风格
* @return 生成的文案
*/
Map<String, Object> generateContent(String productName, Object originalPrice,
Object finalPrice, String keywords, String style);
/**
* 一键生成完整内容(关键词 + 文案 + 图片)
*
* @param productImageUrl 商品主图URL
* @param productName 商品名称
* @param originalPrice 原价
* @param finalPrice 到手价
* @param style 文案风格
* @return 完整内容
*/
Map<String, Object> generateCompleteContent(String productImageUrl, String productName,
Object originalPrice, Object finalPrice, String style);
/**
* 获取提示词模板列表
*/
com.ruoyi.common.core.domain.AjaxResult listPromptTemplates();
/**
* 获取单个提示词模板
*/
com.ruoyi.common.core.domain.AjaxResult getPromptTemplate(String key);
/**
* 保存提示词模板
*/
com.ruoyi.common.core.domain.AjaxResult savePromptTemplate(Map<String, Object> request);
/**
* 删除提示词模板(恢复默认)
*/
com.ruoyi.common.core.domain.AjaxResult deletePromptTemplate(String key);
}

View File

@@ -0,0 +1,37 @@
package com.ruoyi.jarvis.service;
import com.ruoyi.jarvis.domain.TaobaoComment;
import java.util.List;
/**
* 淘宝评论管理 Service 接口
*/
public interface ITaobaoCommentService {
/**
* 查询淘宝评论列表
*/
List<TaobaoComment> selectTaobaoCommentList(TaobaoComment taobaoComment);
/**
* 根据ID查询淘宝评论
*/
TaobaoComment selectTaobaoCommentById(Integer id);
/**
* 更新评论使用状态
*/
int updateTaobaoCommentIsUse(TaobaoComment taobaoComment);
/**
* 批量删除评论
*/
int deleteTaobaoCommentByIds(Integer[] ids);
/**
* 重置评论使用状态(批量)
*/
int resetTaobaoCommentIsUseByProductId(String productId);
}

View File

@@ -0,0 +1,117 @@
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<String, Object> 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<List<Object>> 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<Map<String, Object>> updates);
/**
* 读取AirSheet工作表数据
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param worksheetId 工作表ID整数通常为0表示第一个工作表
* @param range 单元格范围A1:B10可选
* @return 单元格数据
*/
JSONObject readAirSheetCells(String accessToken, String fileId, String worksheetId, String range);
/**
* 更新AirSheet工作表数据
*
* @param accessToken 访问令牌
* @param fileId 文件ID
* @param worksheetId 工作表ID整数通常为0表示第一个工作表
* @param range 单元格范围A1:B2
* @param values 单元格值(二维数组,第一维是行,第二维是列)
* @return 更新结果
*/
JSONObject updateAirSheetCells(String accessToken, String fileId, String worksheetId, String range, List<List<Object>> values);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,62 @@
package com.ruoyi.jarvis.service;
/**
* 微信推送服务接口
*/
public interface IWxSendService {
/**
* 检查微信推送服务健康状态
* @return 健康状态信息,包含是否健康、状态描述等
*/
HealthCheckResult checkHealth();
/**
* 健康检测结果
*/
class HealthCheckResult {
private boolean healthy;
private String status;
private String message;
private String serviceUrl;
public HealthCheckResult(boolean healthy, String status, String message, String serviceUrl) {
this.healthy = healthy;
this.status = status;
this.message = message;
this.serviceUrl = serviceUrl;
}
public boolean isHealthy() {
return healthy;
}
public void setHealthy(boolean healthy) {
this.healthy = healthy;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getServiceUrl() {
return serviceUrl;
}
public void setServiceUrl(String serviceUrl) {
this.serviceUrl = serviceUrl;
}
}
}

View File

@@ -24,6 +24,7 @@ import com.ruoyi.common.utils.http.HttpUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -53,9 +54,20 @@ public class BatchPublishServiceImpl implements IBatchPublishService
@Autowired
private IJDOrderService jdOrderService;
// 京东接口配置
private final static String requestUrl = "http://192.168.8.88:6666/jd/";
@Value("${jarvis.server.jarvis-java.base-url:http://127.0.0.1:6666}")
private String jarvisJavaBaseUrl;
@Value("${jarvis.server.jarvis-java.jd-api-path:/jd}")
private String jdApiPath;
private final static String skey = "2192057370ef8140c201079969c956a3";
/**
* 获取JD接口请求URL
*/
private String getRequestUrl() {
return jarvisJavaBaseUrl + jdApiPath + "/";
}
@Autowired
private IOuterIdGeneratorService outerIdGeneratorService;
@@ -183,7 +195,7 @@ private String cleanForbiddenPhrases(String text) {
*/
private String generatePromotionContent(Map<String, String> requestBody) {
try {
String url = requestUrl + "generatePromotionContent";
String url = getRequestUrl() + "generatePromotionContent";
JSONObject param = new JSONObject();
param.put("skey", skey);
param.put("promotionContent", requestBody.get("promotionContent"));

View File

@@ -0,0 +1,443 @@
package com.ruoyi.jarvis.service.impl;
import com.ruoyi.jarvis.domain.Comment;
import com.ruoyi.jarvis.domain.dto.CommentApiStatistics;
import com.ruoyi.jarvis.domain.dto.CommentStatistics;
import com.ruoyi.jarvis.domain.dto.CommentCallHistory;
import com.ruoyi.jarvis.mapper.CommentMapper;
import com.ruoyi.jarvis.service.ICommentService;
import com.alibaba.fastjson2.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 评论管理 Service 实现
*/
@Service
public class CommentServiceImpl implements ICommentService {
private static final Logger log = LoggerFactory.getLogger(CommentServiceImpl.class);
private static final String PRODUCT_TYPE_MAP_PREFIX = "product_type_map";
private static final String PRODUCT_TYPE_MAP_PREFIX_TB = "product_type_map_tb";
private static final String API_CALL_STAT_PREFIX = "comment:api:stat:";
private static final String API_CALL_TODAY_PREFIX = "comment:api:today:";
private static final String API_CALL_HISTORY_KEY = "comment:api:history:list";
private static final int MAX_HISTORY_SIZE = 1000; // 最多保留1000条历史记录
@Autowired
private CommentMapper commentMapper;
@Autowired(required = false)
private com.ruoyi.jarvis.mapper.TaobaoCommentMapper taobaoCommentMapper;
@Autowired(required = false)
private StringRedisTemplate stringRedisTemplate;
@Override
public List<Comment> selectCommentList(Comment comment) {
List<Comment> list = commentMapper.selectCommentList(comment);
// 填充Redis映射的产品类型信息
if (stringRedisTemplate != null) {
Map<String, String> jdMap = getJdProductTypeMap();
for (Comment c : list) {
// 查找对应的产品类型
String productId = c.getProductId();
if (jdMap != null) {
for (Map.Entry<String, String> entry : jdMap.entrySet()) {
if (entry.getValue().equals(productId)) {
c.setProductType(entry.getKey());
c.setMappedProductId(productId);
break;
}
}
}
}
}
return list;
}
@Override
public Comment selectCommentById(Long id) {
Comment comment = commentMapper.selectCommentById(id);
if (comment != null && stringRedisTemplate != null) {
Map<String, String> jdMap = getJdProductTypeMap();
if (jdMap != null) {
String productId = comment.getProductId();
for (Map.Entry<String, String> entry : jdMap.entrySet()) {
if (entry.getValue().equals(productId)) {
comment.setProductType(entry.getKey());
comment.setMappedProductId(productId);
break;
}
}
}
}
return comment;
}
@Override
public int updateCommentIsUse(Comment comment) {
return commentMapper.updateCommentIsUse(comment);
}
@Override
public int deleteCommentByIds(Long[] ids) {
return commentMapper.deleteCommentByIds(ids);
}
@Override
public int resetCommentIsUseByProductId(String productId) {
return commentMapper.resetCommentIsUseByProductId(productId);
}
@Override
public List<CommentStatistics> getCommentStatistics(String source) {
List<CommentStatistics> statisticsList = new ArrayList<>();
Map<String, String> productTypeMap = null;
if ("jd".equals(source) || source == null) {
productTypeMap = getJdProductTypeMap();
} else if ("tb".equals(source)) {
productTypeMap = getTbProductTypeMap();
}
if (productTypeMap == null || productTypeMap.isEmpty()) {
return statisticsList;
}
for (Map.Entry<String, String> entry : productTypeMap.entrySet()) {
String productType = entry.getKey();
String productId = entry.getValue();
CommentStatistics stats = new CommentStatistics();
stats.setSource("jd".equals(source) ? "京东评论" : "淘宝评论");
stats.setProductType(productType);
stats.setProductId(productId);
// 查询评论统计
Map<String, Object> statMap = null;
if ("jd".equals(source) || source == null) {
statMap = commentMapper.selectCommentStatisticsByProductId(productId);
} else if ("tb".equals(source) && taobaoCommentMapper != null) {
statMap = taobaoCommentMapper.selectTaobaoCommentStatisticsByProductId(productId);
}
if (statMap != null) {
stats.setTotalCount(((Number) statMap.get("totalCount")).longValue());
stats.setAvailableCount(((Number) statMap.get("availableCount")).longValue());
stats.setUsedCount(((Number) statMap.get("usedCount")).longValue());
// 设置最后一条评论的创建时间
Object lastUpdateTime = statMap.get("lastCommentUpdateTime");
if (lastUpdateTime != null) {
if (lastUpdateTime instanceof Date) {
stats.setLastCommentUpdateTime((Date) lastUpdateTime);
} else if (lastUpdateTime instanceof java.sql.Timestamp) {
stats.setLastCommentUpdateTime(new Date(((java.sql.Timestamp) lastUpdateTime).getTime()));
}
}
}
// 获取接口调用统计
if (stringRedisTemplate != null) {
String todayKey = API_CALL_TODAY_PREFIX + source + ":" + productType + ":" + getTodayDate();
String todayCount = stringRedisTemplate.opsForValue().get(todayKey);
stats.setTodayCallCount(todayCount != null ? Long.parseLong(todayCount) : 0L);
// 获取总调用次数从Redis中统计
String statKey = API_CALL_STAT_PREFIX + source + ":" + productType;
String totalCount = stringRedisTemplate.opsForValue().get(statKey);
stats.setApiCallCount(totalCount != null ? Long.parseLong(totalCount) : 0L);
}
statisticsList.add(stats);
}
return statisticsList;
}
@Override
public void recordApiCall(String apiType, String productType, boolean success) {
if (stringRedisTemplate == null) {
return;
}
try {
String today = getTodayDate();
String todayKey = API_CALL_TODAY_PREFIX + apiType + ":" + productType + ":" + today;
stringRedisTemplate.opsForValue().increment(todayKey);
stringRedisTemplate.expire(todayKey, 7, TimeUnit.DAYS); // 保留7天
String statKey = API_CALL_STAT_PREFIX + apiType + ":" + productType;
stringRedisTemplate.opsForValue().increment(statKey);
// 记录成功/失败统计
String successKey = API_CALL_STAT_PREFIX + apiType + ":" + productType + ":success";
String failKey = API_CALL_STAT_PREFIX + apiType + ":" + productType + ":fail";
if (success) {
stringRedisTemplate.opsForValue().increment(successKey);
} else {
stringRedisTemplate.opsForValue().increment(failKey);
}
} catch (Exception e) {
log.error("记录接口调用统计失败", e);
}
}
@Override
public List<CommentApiStatistics> getApiStatistics(String apiType, String productType, String startDate, String endDate) {
List<CommentApiStatistics> statisticsList = new ArrayList<>();
if (stringRedisTemplate == null) {
return statisticsList;
}
try {
// 如果指定了产品类型,只查询该类型的统计
if (productType != null && !productType.isEmpty()) {
CommentApiStatistics stats = new CommentApiStatistics();
stats.setApiType(apiType);
stats.setProductType(productType);
String statKey = API_CALL_STAT_PREFIX + apiType + ":" + productType;
String totalCount = stringRedisTemplate.opsForValue().get(statKey);
stats.setCallCount(totalCount != null ? Long.parseLong(totalCount) : 0L);
String successKey = statKey + ":success";
String successCount = stringRedisTemplate.opsForValue().get(successKey);
stats.setSuccessCount(successCount != null ? Long.parseLong(successCount) : 0L);
String failKey = statKey + ":fail";
String failCount = stringRedisTemplate.opsForValue().get(failKey);
stats.setFailCount(failCount != null ? Long.parseLong(failCount) : 0L);
statisticsList.add(stats);
} else {
// 查询所有产品类型的统计
Map<String, String> productTypeMap = "jd".equals(apiType) ? getJdProductTypeMap() : getTbProductTypeMap();
if (productTypeMap != null) {
for (String pt : productTypeMap.keySet()) {
CommentApiStatistics stats = new CommentApiStatistics();
stats.setApiType(apiType);
stats.setProductType(pt);
String statKey = API_CALL_STAT_PREFIX + apiType + ":" + pt;
String totalCount = stringRedisTemplate.opsForValue().get(statKey);
stats.setCallCount(totalCount != null ? Long.parseLong(totalCount) : 0L);
String successKey = statKey + ":success";
String successCount = stringRedisTemplate.opsForValue().get(successKey);
stats.setSuccessCount(successCount != null ? Long.parseLong(successCount) : 0L);
String failKey = statKey + ":fail";
String failCount = stringRedisTemplate.opsForValue().get(failKey);
stats.setFailCount(failCount != null ? Long.parseLong(failCount) : 0L);
statisticsList.add(stats);
}
}
}
} catch (Exception e) {
log.error("获取接口调用统计失败", e);
}
return statisticsList;
}
@Override
public Map<String, String> getJdProductTypeMap() {
if (stringRedisTemplate == null) {
return new HashMap<>();
}
try {
Map<Object, Object> rawMap = stringRedisTemplate.opsForHash().entries(PRODUCT_TYPE_MAP_PREFIX);
Map<String, String> result = new LinkedHashMap<>();
for (Map.Entry<Object, Object> entry : rawMap.entrySet()) {
result.put(entry.getKey().toString(), entry.getValue().toString());
}
return result;
} catch (Exception e) {
log.error("获取京东产品类型映射失败", e);
return new HashMap<>();
}
}
@Override
public Map<String, String> getTbProductTypeMap() {
if (stringRedisTemplate == null) {
return new HashMap<>();
}
try {
Map<Object, Object> rawMap = stringRedisTemplate.opsForHash().entries(PRODUCT_TYPE_MAP_PREFIX_TB);
Map<String, String> result = new LinkedHashMap<>();
for (Map.Entry<Object, Object> entry : rawMap.entrySet()) {
result.put(entry.getKey().toString(), entry.getValue().toString());
}
return result;
} catch (Exception e) {
log.error("获取淘宝产品类型映射失败", e);
return new HashMap<>();
}
}
private String getTodayDate() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(new Date());
}
@Override
public void recordApiCallHistory(String productType, String ip) {
if (stringRedisTemplate == null) {
return;
}
try {
CommentCallHistory history = new CommentCallHistory();
history.setProductType(productType);
history.setIp(ip);
history.setCreateTime(new Date());
String historyJson = JSON.toJSONString(history);
// 使用List存储历史记录从左侧推入
stringRedisTemplate.opsForList().leftPush(API_CALL_HISTORY_KEY, historyJson);
// 限制列表大小只保留最近MAX_HISTORY_SIZE条
stringRedisTemplate.opsForList().trim(API_CALL_HISTORY_KEY, 0, MAX_HISTORY_SIZE - 1);
} catch (Exception e) {
log.error("记录接口调用历史失败", e);
}
}
@Override
public List<CommentCallHistory> getApiCallHistory(int pageNum, int pageSize) {
List<CommentCallHistory> historyList = new ArrayList<>();
if (stringRedisTemplate == null) {
return historyList;
}
try {
long start = (pageNum - 1) * pageSize;
long end = start + pageSize - 1;
List<String> jsonList = stringRedisTemplate.opsForList().range(API_CALL_HISTORY_KEY, start, end);
if (jsonList != null) {
for (String json : jsonList) {
try {
CommentCallHistory history = JSON.parseObject(json, CommentCallHistory.class);
historyList.add(history);
} catch (Exception e) {
log.warn("解析历史记录失败: " + json, e);
}
}
}
} catch (Exception e) {
log.error("获取接口调用历史失败", e);
}
return historyList;
}
@Override
public Map<String, Long> getUsageStatistics() {
Map<String, Long> statistics = new HashMap<>();
statistics.put("today", 0L);
statistics.put("last7Days", 0L);
statistics.put("last30Days", 0L);
statistics.put("total", 0L);
if (stringRedisTemplate == null) {
return statistics;
}
try {
String today = getTodayDate();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date todayDate = sdf.parse(today);
Calendar calendar = Calendar.getInstance();
// 统计今天
long todayCount = 0;
String todayPattern = API_CALL_TODAY_PREFIX + "jd:*:" + today;
Set<String> todayKeys = stringRedisTemplate.keys(todayPattern);
if (todayKeys != null) {
for (String key : todayKeys) {
String count = stringRedisTemplate.opsForValue().get(key);
if (count != null) {
todayCount += Long.parseLong(count);
}
}
}
statistics.put("today", todayCount);
// 统计近7天
long last7DaysCount = 0;
calendar.setTime(todayDate);
for (int i = 0; i < 7; i++) {
String dateStr = sdf.format(calendar.getTime());
String pattern = API_CALL_TODAY_PREFIX + "jd:*:" + dateStr;
Set<String> keys = stringRedisTemplate.keys(pattern);
if (keys != null) {
for (String key : keys) {
String count = stringRedisTemplate.opsForValue().get(key);
if (count != null) {
last7DaysCount += Long.parseLong(count);
}
}
}
calendar.add(Calendar.DAY_OF_MONTH, -1);
}
statistics.put("last7Days", last7DaysCount);
// 统计近30天
long last30DaysCount = 0;
calendar.setTime(todayDate);
for (int i = 0; i < 30; i++) {
String dateStr = sdf.format(calendar.getTime());
String pattern = API_CALL_TODAY_PREFIX + "jd:*:" + dateStr;
Set<String> keys = stringRedisTemplate.keys(pattern);
if (keys != null) {
for (String key : keys) {
String count = stringRedisTemplate.opsForValue().get(key);
if (count != null) {
last30DaysCount += Long.parseLong(count);
}
}
}
calendar.add(Calendar.DAY_OF_MONTH, -1);
}
statistics.put("last30Days", last30DaysCount);
// 统计累计从统计key获取
long totalCount = 0;
String totalPattern = API_CALL_STAT_PREFIX + "jd:*";
Set<String> totalKeys = stringRedisTemplate.keys(totalPattern);
if (totalKeys != null) {
for (String key : totalKeys) {
// 排除success和fail后缀的key
if (!key.endsWith(":success") && !key.endsWith(":fail")) {
String count = stringRedisTemplate.opsForValue().get(key);
if (count != null) {
totalCount += Long.parseLong(count);
}
}
}
}
statistics.put("total", totalCount);
} catch (Exception e) {
log.error("获取使用统计失败", e);
}
return statistics;
}
}

View File

@@ -0,0 +1,480 @@
package com.ruoyi.jarvis.service.impl;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.jarvis.mapper.ErpProductMapper;
import com.ruoyi.jarvis.domain.ErpProduct;
import com.ruoyi.jarvis.service.IErpProductService;
import com.ruoyi.erp.request.ERPAccount;
import com.ruoyi.erp.request.ProductListQueryRequest;
import com.ruoyi.erp.request.ProductDeleteRequest;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
import java.util.HashSet;
import java.util.stream.Collectors;
/**
* 闲鱼商品Service业务层处理
*
* @author ruoyi
* @date 2024-01-01
*/
@Service
public class ErpProductServiceImpl implements IErpProductService
{
private static final Logger log = LoggerFactory.getLogger(ErpProductServiceImpl.class);
@Autowired
private ErpProductMapper erpProductMapper;
/**
* 查询闲鱼商品
*
* @param id 闲鱼商品主键
* @return 闲鱼商品
*/
@Override
public ErpProduct selectErpProductById(Long id)
{
return erpProductMapper.selectErpProductById(id);
}
/**
* 查询闲鱼商品列表
*
* @param erpProduct 闲鱼商品
* @return 闲鱼商品
*/
@Override
public List<ErpProduct> selectErpProductList(ErpProduct erpProduct)
{
return erpProductMapper.selectErpProductList(erpProduct);
}
/**
* 新增闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
@Override
public int insertErpProduct(ErpProduct erpProduct)
{
// 检查是否已存在
ErpProduct existing = erpProductMapper.selectErpProductByProductIdAndAppid(
erpProduct.getProductId(), erpProduct.getAppid());
if (existing != null) {
// 更新已存在的商品
erpProduct.setId(existing.getId());
return erpProductMapper.updateErpProduct(erpProduct);
}
return erpProductMapper.insertErpProduct(erpProduct);
}
/**
* 修改闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
@Override
public int updateErpProduct(ErpProduct erpProduct)
{
return erpProductMapper.updateErpProduct(erpProduct);
}
/**
* 批量删除闲鱼商品
*
* @param ids 需要删除的闲鱼商品主键
* @return 结果
*/
@Override
public int deleteErpProductByIds(Long[] ids)
{
return erpProductMapper.deleteErpProductByIds(ids);
}
/**
* 删除闲鱼商品信息
*
* @param id 闲鱼商品主键
* @return 结果
*/
@Override
public int deleteErpProductById(Long id)
{
return erpProductMapper.deleteErpProductById(id);
}
/**
* 从闲鱼ERP拉取商品列表并保存
*
* @param appid ERP应用ID
* @param pageNo 页码
* @param pageSize 每页大小
* @param productStatus 商品状态
* @return 拉取并保存的商品数量
*/
@Override
public int pullAndSaveProductList(String appid, Integer pageNo, Integer pageSize, Integer productStatus)
{
try {
// 解析ERP账号
ERPAccount account = resolveAccount(appid);
// 创建查询请求
ProductListQueryRequest request = new ProductListQueryRequest(account);
if (pageNo != null) {
request.setPageNo(pageNo);
}
if (pageSize != null) {
request.setPageSize(pageSize);
}
if (productStatus != null) {
// API要求的状态值-1(全部), 10(上架), 21(下架), 22(草稿), 23(审核中), 31(已售), 33(已删除), 36(违规)
// 前端传入的简化状态值1(上架), 2(下架), 3(已售)
Integer apiStatus = convertProductStatus(productStatus);
if (apiStatus != null) {
request.setProductStatus(apiStatus);
}
}
// 调用接口获取商品列表
String responseBody = request.getResponseBody();
JSONObject response = JSONObject.parseObject(responseBody);
if (response == null || response.getInteger("code") == null || response.getInteger("code") != 0) {
String errorMsg = response != null ? response.getString("msg") : "未知错误";
log.error("拉取商品列表失败: code={}, msg={}, response={}",
response != null ? response.getInteger("code") : null, errorMsg, responseBody);
throw new RuntimeException("拉取商品列表失败: " + errorMsg);
}
// 解析商品列表
JSONObject data = response.getJSONObject("data");
if (data == null) {
log.warn("拉取商品列表返回数据为空");
return 0;
}
JSONArray productList = data.getJSONArray("list");
Integer totalCount = data.getInteger("count");
if (productList == null || productList.isEmpty()) {
String statusMsg = productStatus != null ? "(状态:" + productStatus + "" : "";
if (totalCount != null && totalCount > 0) {
log.info("拉取商品列表为空,但总数显示为 {},可能是分页问题", totalCount);
} else {
log.info("拉取商品列表为空{},该账号下没有符合条件的商品", statusMsg);
}
return 0;
}
log.info("拉取到 {} 个商品,开始保存", productList.size());
// 转换为实体对象并保存
List<ErpProduct> erpProducts = new ArrayList<>();
for (int i = 0; i < productList.size(); i++) {
JSONObject productJson = productList.getJSONObject(i);
ErpProduct erpProduct = parseProductJson(productJson, appid);
if (erpProduct != null) {
erpProducts.add(erpProduct);
}
}
// 批量保存或更新
if (!erpProducts.isEmpty()) {
// 逐个保存(兼容更新)
int savedCount = 0;
for (ErpProduct product : erpProducts) {
ErpProduct existing = erpProductMapper.selectErpProductByProductIdAndAppid(
product.getProductId(), product.getAppid());
if (existing != null) {
product.setId(existing.getId());
erpProductMapper.updateErpProduct(product);
} else {
erpProductMapper.insertErpProduct(product);
}
savedCount++;
}
log.info("成功拉取并保存 {} 个商品", savedCount);
return savedCount;
}
return 0;
} catch (Exception e) {
log.error("拉取商品列表异常", e);
throw new RuntimeException("拉取商品列表失败: " + e.getMessage(), e);
}
}
/**
* 解析商品JSON数据
*/
private ErpProduct parseProductJson(JSONObject productJson, String appid) {
try {
ErpProduct product = new ErpProduct();
// 管家商品ID
Long productId = productJson.getLong("product_id");
if (productId == null) {
log.warn("商品ID为空跳过: {}", productJson);
return null;
}
product.setProductId(productId);
// 商品标题
product.setTitle(productJson.getString("title"));
// 商品图片(取第一张)
JSONArray images = productJson.getJSONArray("images");
if (images != null && !images.isEmpty()) {
product.setMainImage(images.getString(0));
}
// 价格(分)
Long price = productJson.getLong("price");
if (price == null) {
// 尝试从price字段解析
Object priceObj = productJson.get("price");
if (priceObj instanceof Number) {
price = ((Number) priceObj).longValue();
}
}
product.setPrice(price);
// 库存
Integer stock = productJson.getInteger("stock");
product.setStock(stock);
// 商品状态
Integer productStatus = productJson.getInteger("product_status");
product.setProductStatus(productStatus);
// 销售状态
Integer saleStatus = productJson.getInteger("sale_status");
product.setSaleStatus(saleStatus);
// 闲鱼会员名
product.setUserName(productJson.getString("user_name"));
// 时间字段(时间戳,秒)
product.setOnlineTime(productJson.getLong("online_time"));
product.setOfflineTime(productJson.getLong("offline_time"));
product.setSoldTime(productJson.getLong("sold_time"));
product.setCreateTimeXy(productJson.getLong("create_time"));
product.setUpdateTimeXy(productJson.getLong("update_time"));
// ERP应用ID
product.setAppid(appid);
// 商品链接
product.setProductUrl(productJson.getString("product_url"));
return product;
} catch (Exception e) {
log.error("解析商品JSON失败: {}", productJson, e);
return null;
}
}
/**
* 转换商品状态值将前端状态值转换为API需要的状态值
* 实际状态值:-1(删除), 21(待发布), 22(销售中), 23(已售罄), 31(手动下架), 33(售出下架), 36(自动下架)
* API支持的状态值-1, 10, 21, 22, 23, 31, 33, 36
* 前端传入的状态值直接使用,不做转换
*/
private Integer convertProductStatus(Integer frontendStatus) {
if (frontendStatus == null) {
return null;
}
// 直接使用前端传入的状态值(-1, 21, 22, 23, 31, 33, 36
// API支持的状态值列表
if (frontendStatus == -1 || frontendStatus == 10 || frontendStatus == 21 ||
frontendStatus == 22 || frontendStatus == 23 || frontendStatus == 31 ||
frontendStatus == 33 || frontendStatus == 36) {
return frontendStatus;
}
log.warn("未知的商品状态值: {}, 将不设置状态筛选", frontendStatus);
return null;
}
/**
* 全量同步商品列表(自动遍历所有页码,同步更新和删除)
*/
@Override
public IErpProductService.SyncResult syncAllProducts(String appid, Integer productStatus) {
IErpProductService.SyncResult result = new IErpProductService.SyncResult();
Set<Long> remoteProductIds = new HashSet<>(); // 远程商品ID集合
int pageNo = 1;
int pageSize = 50; // 每页50条尽量少请求次数
int totalPulled = 0;
int added = 0;
int updated = 0;
try {
ERPAccount account = resolveAccount(appid);
// 第一步:遍历所有页码,拉取并保存所有商品
log.info("开始全量同步商品,账号:{}", appid);
while (true) {
ProductListQueryRequest request = new ProductListQueryRequest(account);
request.setPage(pageNo, pageSize);
if (productStatus != null) {
Integer apiStatus = convertProductStatus(productStatus);
if (apiStatus != null) {
request.setProductStatus(apiStatus);
}
}
String responseBody = request.getResponseBody();
JSONObject response = JSONObject.parseObject(responseBody);
if (response == null || response.getInteger("code") == null || response.getInteger("code") != 0) {
String errorMsg = response != null ? response.getString("msg") : "未知错误";
log.error("拉取商品列表失败(页码:{}: {}", pageNo, errorMsg);
result.setFailed(result.getFailed() + 1);
break;
}
JSONObject data = response.getJSONObject("data");
if (data == null) {
log.warn("拉取商品列表返回数据为空(页码:{}", pageNo);
break;
}
JSONArray productList = data.getJSONArray("list");
if (productList == null || productList.isEmpty()) {
log.info("第 {} 页数据为空,同步完成", pageNo);
break;
}
// 处理当前页商品
for (int i = 0; i < productList.size(); i++) {
JSONObject productJson = productList.getJSONObject(i);
ErpProduct erpProduct = parseProductJson(productJson, appid);
if (erpProduct != null && erpProduct.getProductId() != null) {
remoteProductIds.add(erpProduct.getProductId());
// 保存或更新商品
ErpProduct existing = erpProductMapper.selectErpProductByProductIdAndAppid(
erpProduct.getProductId(), erpProduct.getAppid());
if (existing != null) {
erpProduct.setId(existing.getId());
erpProductMapper.updateErpProduct(erpProduct);
updated++;
} else {
erpProductMapper.insertErpProduct(erpProduct);
added++;
}
totalPulled++;
}
}
log.info("已同步第 {} 页,共 {} 条商品", pageNo, productList.size());
// 判断是否还有下一页
if (productList.size() < pageSize) {
log.info("已拉取完所有页码,共 {} 页", pageNo);
break;
}
pageNo++;
}
result.setTotalPulled(totalPulled);
result.setAdded(added);
result.setUpdated(updated);
// 第二步:对比本地和远程,删除本地有但远程没有的商品
log.info("开始同步删除,远程商品数:{}", remoteProductIds.size());
// 查询本地该账号下的所有商品
ErpProduct queryParam = new ErpProduct();
queryParam.setAppid(appid);
List<ErpProduct> localProducts = erpProductMapper.selectErpProductList(queryParam);
// 找出需要删除的商品(本地有但远程没有的)
List<ErpProduct> toDelete = localProducts.stream()
.filter(p -> !remoteProductIds.contains(p.getProductId()))
.collect(Collectors.toList());
if (!toDelete.isEmpty()) {
log.info("发现 {} 个本地商品在远程已不存在,开始删除", toDelete.size());
for (ErpProduct product : toDelete) {
try {
// 先调用远程删除接口
ProductDeleteRequest deleteRequest = new ProductDeleteRequest(account);
deleteRequest.setProductId(product.getProductId());
String deleteResponse = deleteRequest.getResponseBody();
JSONObject deleteResult = JSONObject.parseObject(deleteResponse);
if (deleteResult != null && deleteResult.getInteger("code") != null &&
deleteResult.getInteger("code") == 0) {
// 远程删除成功,删除本地记录
erpProductMapper.deleteErpProductById(product.getId());
result.setDeleted(result.getDeleted() + 1);
log.debug("成功删除商品:{}", product.getProductId());
} else {
// 远程删除失败,记录日志但不删除本地(可能是远程已经删除了)
String errorMsg = deleteResult != null ? deleteResult.getString("msg") : "未知错误";
log.warn("远程删除商品失败(可能已不存在):{},错误:{}", product.getProductId(), errorMsg);
// 如果远程返回商品不存在的错误,也删除本地记录
if (errorMsg != null && (errorMsg.contains("不存在") || errorMsg.contains("not found"))) {
erpProductMapper.deleteErpProductById(product.getId());
result.setDeleted(result.getDeleted() + 1);
} else {
result.setFailed(result.getFailed() + 1);
}
}
} catch (Exception e) {
log.error("删除商品异常:{}", product.getProductId(), e);
result.setFailed(result.getFailed() + 1);
}
}
}
// 构建结果消息
StringBuilder msg = new StringBuilder();
msg.append(String.format("同步完成!拉取:%d个新增%d个更新%d个删除%d个",
totalPulled, added, updated, result.getDeleted()));
if (result.getFailed() > 0) {
msg.append(String.format(",失败:%d个", result.getFailed()));
}
result.setMessage(msg.toString());
log.info(result.getMessage());
return result;
} catch (Exception e) {
log.error("全量同步商品异常", e);
result.setMessage("同步失败: " + e.getMessage());
result.setFailed(result.getFailed() + 1);
throw new RuntimeException("全量同步商品失败: " + e.getMessage(), e);
}
}
/**
* 解析ERP账号
*/
private ERPAccount resolveAccount(String appid) {
if (appid != null && !appid.isEmpty()) {
for (ERPAccount account : ERPAccount.values()) {
if (account.getApiKey().equals(appid)) {
return account;
}
}
}
return ERPAccount.ACCOUNT_HUGE; // 默认账号
}
}

View File

@@ -5,13 +5,20 @@ import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.jarvis.domain.JDOrder;
import com.ruoyi.jarvis.service.ILogisticsService;
import com.ruoyi.jarvis.service.IJDOrderService;
import com.ruoyi.system.service.ISysConfigService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import javax.annotation.Resource;
import javax.annotation.PostConstruct;
import java.net.URLEncoder;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
@@ -22,13 +29,43 @@ public class LogisticsServiceImpl implements ILogisticsService {
private static final Logger logger = LoggerFactory.getLogger(LogisticsServiceImpl.class);
private static final String REDIS_WAYBILL_KEY_PREFIX = "logistics:waybill:order:";
private static final String EXTERNAL_API_URL = "http://192.168.8.88:5001/fetch_logistics?tracking_url=";
private static final String REDIS_LOCK_KEY_PREFIX = "logistics:lock:order:";
private static final String REDIS_HEALTH_CHECK_ALERT_KEY = "logistics:health:alert:";
private static final String PUSH_URL = "https://wxts.van333.cn/wx/send/pdd";
private static final String PUSH_TOKEN = "super_token_b62190c26";
private static final String CONFIG_KEY_PREFIX = "logistics.push.touser.";
private static final long LOCK_EXPIRE_SECONDS = 300; // 锁过期时间5分钟防止死锁
private static final long HEALTH_CHECK_ALERT_INTERVAL_MINUTES = 30; // 健康检查失败提醒间隔30分钟避免频繁推送
@Value("${jarvis.server.logistics.base-url:http://127.0.0.1:5001}")
private String logisticsBaseUrl;
@Value("${jarvis.server.logistics.fetch-path:/fetch_logistics}")
private String logisticsFetchPath;
@Value("${jarvis.server.logistics.health-path:/health}")
private String logisticsHealthPath;
private String externalApiUrlTemplate;
private String healthCheckUrl;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private ISysConfigService sysConfigService;
@Resource
private IJDOrderService jdOrderService;
@PostConstruct
public void init() {
externalApiUrlTemplate = logisticsBaseUrl + logisticsFetchPath + "?tracking_url=";
healthCheckUrl = logisticsBaseUrl + logisticsHealthPath;
logger.info("物流服务地址已初始化: {}", externalApiUrlTemplate);
logger.info("物流服务健康检查地址已初始化: {}", healthCheckUrl);
}
@Override
public boolean isOrderProcessed(Long orderId) {
if (orderId == null) {
@@ -38,6 +75,122 @@ public class LogisticsServiceImpl implements ILogisticsService {
return stringRedisTemplate.hasKey(redisKey);
}
@Override
public ILogisticsService.HealthCheckResult checkHealth() {
try {
logger.debug("开始检查物流服务健康状态 - URL: {}", healthCheckUrl);
String healthResult = HttpUtils.sendGet(healthCheckUrl);
if (healthResult == null || healthResult.trim().isEmpty()) {
logger.warn("物流服务健康检查返回空结果");
return new ILogisticsService.HealthCheckResult(false, "异常", "健康检查返回空结果", healthCheckUrl);
}
// 尝试解析JSON响应
try {
JSONObject response = JSON.parseObject(healthResult);
if (response != null) {
// 检查常见的健康状态字段
String status = response.getString("status");
Boolean healthy = response.getBoolean("healthy");
Integer code = response.getInteger("code");
if ("ok".equalsIgnoreCase(status) || "healthy".equalsIgnoreCase(status) ||
Boolean.TRUE.equals(healthy) || (code != null && code == 200)) {
logger.debug("物流服务健康检查通过");
return new ILogisticsService.HealthCheckResult(true, "正常", "服务运行正常", healthCheckUrl);
}
}
} catch (Exception e) {
// 如果不是JSON格式检查是否包含成功标识
String lowerResult = healthResult.toLowerCase();
if (lowerResult.contains("ok") || lowerResult.contains("healthy") || lowerResult.contains("success")) {
logger.debug("物流服务健康检查通过非JSON格式");
return new ILogisticsService.HealthCheckResult(true, "正常", "服务运行正常", healthCheckUrl);
}
}
logger.warn("物流服务健康检查失败 - 响应: {}", healthResult);
return new ILogisticsService.HealthCheckResult(false, "异常", "健康检查返回异常状态: " + healthResult, healthCheckUrl);
} catch (Exception e) {
logger.error("物流服务健康检查异常 - URL: {}, 错误: {}", healthCheckUrl, e.getMessage(), e);
return new ILogisticsService.HealthCheckResult(false, "异常", "健康检查异常: " + e.getMessage(), healthCheckUrl);
}
}
/**
* 检查物流服务健康状态(内部方法,用于业务逻辑)
* @return 是否健康
*/
@SuppressWarnings("unused")
private boolean checkLogisticsServiceHealth() {
ILogisticsService.HealthCheckResult result = checkHealth();
if (result.isHealthy()) {
// 清除健康检查失败标记
stringRedisTemplate.delete(REDIS_HEALTH_CHECK_ALERT_KEY);
} else {
handleHealthCheckFailure(result.getMessage());
}
return result.isHealthy();
}
/**
* 处理健康检查失败,推送提醒消息
* @param reason 失败原因
*/
@SuppressWarnings("null")
private void handleHealthCheckFailure(String reason) {
try {
// 检查是否在提醒间隔内已经推送过
String alertKey = REDIS_HEALTH_CHECK_ALERT_KEY;
String lastAlertTime = stringRedisTemplate.opsForValue().get(alertKey);
if (lastAlertTime != null) {
// 如果30分钟内已经推送过不再重复推送
logger.debug("健康检查失败提醒已在30分钟内推送过跳过本次推送");
return;
}
// 构建推送消息
StringBuilder pushContent = new StringBuilder();
pushContent.append("【物流服务异常提醒】\n");
pushContent.append("服务地址:").append(healthCheckUrl).append("\n");
pushContent.append("失败原因:").append(reason).append("\n");
pushContent.append("时间:").append(new Date()).append("\n");
pushContent.append("请及时检查服务状态!");
JSONObject pushParam = new JSONObject();
pushParam.put("title", "物流服务健康检查失败");
pushParam.put("text", pushContent.toString());
// 尝试获取系统管理员接收人配置,如果没有则使用默认接收人
String adminTouser = sysConfigService.selectConfigByKey("logistics.push.touser.ADMIN");
if (StringUtils.hasText(adminTouser)) {
pushParam.put("touser", adminTouser.trim());
logger.info("使用管理员接收人配置推送健康检查失败提醒");
} else {
logger.info("未配置管理员接收人,将使用远程接口默认接收人推送健康检查失败提醒");
}
String jsonBody = pushParam.toJSONString();
String pushResult = sendPostWithHeaders(PUSH_URL, jsonBody, PUSH_TOKEN);
if (pushResult != null && !pushResult.trim().isEmpty()) {
logger.info("健康检查失败提醒已推送 - 响应: {}", pushResult);
// 记录推送时间30分钟内不再重复推送
long currentTime = System.currentTimeMillis();
stringRedisTemplate.opsForValue().set(alertKey, Long.toString(currentTime),
HEALTH_CHECK_ALERT_INTERVAL_MINUTES, TimeUnit.MINUTES);
} else {
logger.warn("健康检查失败提醒推送失败 - 响应为空");
}
} catch (Exception e) {
logger.error("推送健康检查失败提醒异常", e);
}
}
@Override
public boolean fetchLogisticsAndPush(JDOrder order) {
if (order == null || order.getId() == null) {
@@ -45,27 +198,57 @@ public class LogisticsServiceImpl implements ILogisticsService {
return false;
}
// 检查物流链接
String logisticsLink = order.getLogisticsLink();
if (logisticsLink == null || logisticsLink.trim().isEmpty()) {
logger.info("订单暂无物流链接,跳过处理 - 订单ID: {}", order.getId());
return false;
Long orderId = order.getId();
// 双重检查:先检查是否已处理过
if (isOrderProcessed(orderId)) {
logger.info("订单已处理过,跳过 - 订单ID: {}", orderId);
return true; // 返回true表示已处理避免重复处理
}
// 获取分布式锁,防止并发处理同一订单
String lockKey = REDIS_LOCK_KEY_PREFIX + orderId;
Boolean lockAcquired = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "locked", LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(lockAcquired)) {
logger.warn("订单正在被其他线程处理,跳过 - 订单ID: {}", orderId);
return false; // 其他线程正在处理返回false让调用方稍后重试
}
try {
// 获取锁后再次检查是否已处理(双重检查锁定模式)
if (isOrderProcessed(orderId)) {
logger.info("订单在获取锁后检查发现已处理,跳过 - 订单ID: {}", orderId);
return true;
}
// 检查物流链接
String logisticsLink = order.getLogisticsLink();
if (logisticsLink == null || logisticsLink.trim().isEmpty()) {
logger.info("订单暂无物流链接,跳过处理 - 订单ID: {}", orderId);
return false;
}
// 先进行健康检查
ILogisticsService.HealthCheckResult healthResult = checkHealth();
if (!healthResult.isHealthy()) {
logger.error("物流服务健康检查失败,跳过处理订单 - 订单ID: {}, 原因: {}", orderId, healthResult.getMessage());
return false;
}
// 构建外部接口URL
String externalUrl = EXTERNAL_API_URL + URLEncoder.encode(logisticsLink, "UTF-8");
logger.info("调用外部接口获取物流信息 - 订单ID: {}, URL: {}", order.getId(), externalUrl);
String externalUrl = externalApiUrlTemplate + URLEncoder.encode(logisticsLink, "UTF-8");
logger.info("调用外部接口获取物流信息 - 订单ID: {}, URL: {}", orderId, externalUrl);
// 在服务端执行HTTP请求
String result = HttpUtils.sendGet(externalUrl);
if (result == null || result.trim().isEmpty()) {
logger.warn("外部接口返回空结果 - 订单ID: {}", order.getId());
logger.warn("外部接口返回空结果 - 订单ID: {}", orderId);
return false;
}
logger.info("外部接口调用成功 - 订单ID: {}, 返回数据长度: {}", order.getId(), result.length());
logger.info("外部接口调用成功 - 订单ID: {}, 返回数据长度: {}", orderId, result.length());
// 解析返回结果
JSONObject parsedData = null;
@@ -74,42 +257,165 @@ public class LogisticsServiceImpl implements ILogisticsService {
if (parsed instanceof JSONObject) {
parsedData = (JSONObject) parsed;
} else {
logger.warn("返回数据不是JSON对象格式 - 订单ID: {}", order.getId());
logger.warn("返回数据不是JSON对象格式 - 订单ID: {}", orderId);
return false;
}
} catch (Exception e) {
logger.warn("解析返回数据失败 - 订单ID: {}, 错误: {}", order.getId(), e.getMessage());
logger.warn("解析返回数据失败 - 订单ID: {}, 错误: {}", orderId, e.getMessage());
return false;
}
// 检查waybill_no
JSONObject dataObj = parsedData.getJSONObject("data");
if (dataObj == null) {
logger.info("返回数据中没有data字段 - 订单ID: {}", order.getId());
logger.info("返回数据中没有data字段 - 订单ID: {}", orderId);
return false;
}
String waybillNo = dataObj.getString("waybill_no");
if (waybillNo == null || waybillNo.trim().isEmpty()) {
logger.info("waybill_no为空无需处理 - 订单ID: {}", order.getId());
logger.info("waybill_no为空无需处理 - 订单ID: {}", orderId);
return false;
}
logger.info("检测到waybill_no: {} - 订单ID: {}", waybillNo, order.getId());
logger.info("检测到waybill_no: {} - 订单ID: {}", waybillNo, orderId);
// 保存运单号到Redis避免重复处理
String redisKey = REDIS_WAYBILL_KEY_PREFIX + order.getId();
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
// 标记物流链接是否更新
boolean logisticsLinkUpdated = false;
String oldLogisticsLink = null;
String newLogisticsLink = null;
// 调用企业应用推送
sendEnterprisePushNotification(order, waybillNo);
// 检查并更新物流链接(如果返回的数据中包含新的物流链接)
// 尝试多种可能的字段名
if (dataObj.containsKey("tracking_url")) {
newLogisticsLink = dataObj.getString("tracking_url");
} else if (dataObj.containsKey("logistics_link")) {
newLogisticsLink = dataObj.getString("logistics_link");
} else if (dataObj.containsKey("logisticsLink")) {
newLogisticsLink = dataObj.getString("logisticsLink");
} else if (dataObj.containsKey("trackingUrl")) {
newLogisticsLink = dataObj.getString("trackingUrl");
}
logger.info("物流信息获取并推送成功 - 订单ID: {}, waybill_no: {}", order.getId(), waybillNo);
// 如果获取到新的物流链接,与数据库中的进行比对
if (newLogisticsLink != null && !newLogisticsLink.trim().isEmpty()) {
String currentLogisticsLink = order.getLogisticsLink();
String trimmedNewLink = newLogisticsLink.trim();
String trimmedCurrentLink = (currentLogisticsLink != null) ? currentLogisticsLink.trim() : "";
oldLogisticsLink = trimmedCurrentLink;
// 比对物流链接,如果不一致则更新数据库
if (!trimmedNewLink.equals(trimmedCurrentLink)) {
logger.info("========== 检测到物流链接发生变化 - 订单ID: {}, 订单号: {}, 旧链接: {}, 新链接: {}, 开始更新数据库 ==========",
orderId, order.getOrderId(), trimmedCurrentLink, trimmedNewLink);
try {
// 重新查询订单,确保获取最新数据
JDOrder updateOrder = jdOrderService.selectJDOrderById(orderId);
if (updateOrder != null) {
updateOrder.setLogisticsLink(trimmedNewLink);
int updateResult = jdOrderService.updateJDOrder(updateOrder);
if (updateResult > 0) {
logisticsLinkUpdated = true;
logger.info("========== 物流链接更新成功 - 订单ID: {}, 订单号: {}, 旧链接: {}, 新链接: {} ==========",
orderId, order.getOrderId(), trimmedCurrentLink, trimmedNewLink);
// 更新内存中的order对象以便后续使用最新数据
order.setLogisticsLink(trimmedNewLink);
} else {
logger.warn("物流链接更新失败影响行数为0 - 订单ID: {}, 订单号: {}, 新链接: {}",
orderId, order.getOrderId(), trimmedNewLink);
}
} else {
logger.warn("未找到订单,无法更新物流链接 - 订单ID: {}, 订单号: {}", orderId, order.getOrderId());
}
} catch (Exception e) {
logger.error("更新物流链接时发生异常 - 订单ID: {}, 订单号: {}, 新链接: {}, 错误: {}",
orderId, order.getOrderId(), trimmedNewLink, e.getMessage(), e);
// 不中断主流程,继续处理
}
} else {
logger.debug("物流链接未变化,无需更新 - 订单ID: {}, 订单号: {}, 链接: {}",
orderId, order.getOrderId(), trimmedCurrentLink);
}
} else {
logger.debug("返回数据中未包含物流链接字段 - 订单ID: {}, 订单号: {}", orderId, order.getOrderId());
}
// 兼容处理检查Redis中是否已有该订单的运单号记录
// 如果存在且运单号一致,说明之前已经推送过了(可能是之前没有配置接收人但推送成功的情况)
String redisKey = REDIS_WAYBILL_KEY_PREFIX + orderId;
String existingWaybillNo = stringRedisTemplate.opsForValue().get(redisKey);
if (existingWaybillNo != null && existingWaybillNo.trim().equals(waybillNo.trim())) {
// 运单号一致,说明之前已经推送过了,直接标记为已处理,跳过推送
logger.info("订单运单号已存在且一致,说明之前已推送过,跳过重复推送 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
// 更新过期时间,确保记录不会过期
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
return true;
}
// 兼容处理如果Redis中没有记录但订单创建时间在30天之前且获取到了运单号
// 说明可能是之前推送过但没标记的情况(比如之前没有配置接收人但推送成功,响应解析失败)
// 这种情况下,直接标记为已处理,跳过推送,避免重复推送旧订单
if (existingWaybillNo == null && order.getCreateTime() != null) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, -30); // 30天前
Date thresholdDate = calendar.getTime();
if (order.getCreateTime().before(thresholdDate)) {
// 订单创建时间在30天之前且Redis中没有记录但获取到了运单号
// 视为之前已推送过但未标记,直接标记为已处理,跳过推送
logger.info("订单创建时间较早({}且Redis中无记录但已获取到运单号视为之前已推送过直接标记为已处理跳过推送 - 订单ID: {}, waybill_no: {}",
order.getCreateTime(), orderId, waybillNo);
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
return true;
}
}
// 如果Redis中有记录但运单号不一致记录警告
if (existingWaybillNo != null && !existingWaybillNo.trim().equals(waybillNo.trim())) {
logger.warn("订单运单号发生变化 - 订单ID: {}, 旧运单号: {}, 新运单号: {}, 将重新推送",
orderId, existingWaybillNo, waybillNo);
}
// 调用企业应用推送,只有推送成功才记录状态
boolean pushSuccess = sendEnterprisePushNotification(order, waybillNo, logisticsLinkUpdated, oldLogisticsLink, newLogisticsLink);
if (!pushSuccess) {
logger.warn("企业微信推送未确认成功,稍后将重试 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
return false;
}
// 保存运单号到Redis避免重复处理- 使用原子操作确保只写入一次
Boolean setSuccess = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, waybillNo, 30, TimeUnit.DAYS);
if (Boolean.FALSE.equals(setSuccess)) {
// 如果Redis中已存在说明可能被其他线程处理了记录警告但不算失败
logger.warn("订单运单号已存在(可能被并发处理),但推送已成功 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
// 更新过期时间,确保记录不会过期
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
}
// 记录最终处理结果
if (logisticsLinkUpdated) {
logger.info("========== 物流信息获取并推送成功(已更新物流链接) - 订单ID: {}, 订单号: {}, waybill_no: {}, 新链接: {} ==========",
orderId, order.getOrderId(), waybillNo, newLogisticsLink);
} else {
logger.info("物流信息获取并推送成功 - 订单ID: {}, 订单号: {}, waybill_no: {}",
orderId, order.getOrderId(), waybillNo);
}
return true;
} catch (Exception e) {
logger.error("获取物流信息失败 - 订单ID: {}, 错误: {}", order.getId(), e.getMessage(), e);
logger.error("获取物流信息失败 - 订单ID: {}, 错误: {}", orderId, e.getMessage(), e);
return false;
} finally {
// 释放分布式锁
try {
stringRedisTemplate.delete(lockKey);
logger.debug("释放订单处理锁 - 订单ID: {}", orderId);
} catch (Exception e) {
logger.warn("释放订单处理锁失败 - 订单ID: {}, 错误: {}", orderId, e.getMessage());
}
}
}
@@ -117,8 +423,11 @@ public class LogisticsServiceImpl implements ILogisticsService {
* 调用企业应用推送逻辑
* @param order 订单信息
* @param waybillNo 运单号
* @param logisticsLinkUpdated 物流链接是否已更新
* @param oldLogisticsLink 旧的物流链接(如果更新了)
* @param newLogisticsLink 新的物流链接(如果更新了)
*/
private void sendEnterprisePushNotification(JDOrder order, String waybillNo) {
private boolean sendEnterprisePushNotification(JDOrder order, String waybillNo, boolean logisticsLinkUpdated, String oldLogisticsLink, String newLogisticsLink) {
try {
// 构建推送消息内容
StringBuilder pushContent = new StringBuilder();
@@ -127,35 +436,214 @@ public class LogisticsServiceImpl implements ILogisticsService {
String distributionMark = order.getDistributionMark() != null ? order.getDistributionMark() : "未知";
pushContent.append(distributionMark).append("\n");
// PDD订单包含第三方单号F订单不包含
if ("PDD".equals(distributionMark)) {
String thirdPartyOrderNo = order.getThirdPartyOrderNo();
if (thirdPartyOrderNo != null && !thirdPartyOrderNo.trim().isEmpty()) {
pushContent.append("第三方单号:").append(thirdPartyOrderNo).append("\n");
}
String thirdPartyOrderNo = order.getThirdPartyOrderNo();
if (thirdPartyOrderNo != null && !thirdPartyOrderNo.trim().isEmpty()) {
pushContent.append("第三方单号:").append(thirdPartyOrderNo).append("\n");
}
// 型号
pushContent.append("型号:").append(order.getModelNumber() != null ? order.getModelNumber() : "").append("\n");
// 收货地址
pushContent.append("收货地址:").append(order.getAddress() != null ? order.getAddress() : "").append("\n");
// 如果物流链接已更新,在推送消息中说明
if (logisticsLinkUpdated && newLogisticsLink != null && !newLogisticsLink.trim().isEmpty()) {
pushContent.append("【物流链接已更新】").append("\n");
pushContent.append("新物流链接:").append(newLogisticsLink.trim()).append("\n");
if (oldLogisticsLink != null && !oldLogisticsLink.trim().isEmpty()) {
pushContent.append("旧物流链接:").append(oldLogisticsLink.trim()).append("\n");
}
pushContent.append("\n");
}
// 运单号
pushContent.append("运单号:").append(waybillNo).append("\n");
pushContent.append("运单号:").append("\n").append("\n").append("\n").append("\n").append(waybillNo).append("\n");
// 调用企业微信推送接口
JSONObject pushParam = new JSONObject();
pushParam.put("title", "JD物流信息推送");
pushParam.put("text", pushContent.toString());
// 根据分销标识获取接收人列表
String touser = getTouserByDistributionMark(distributionMark);
if (StringUtils.hasText(touser)) {
pushParam.put("touser", touser);
logger.info("企业微信推送设置接收人 - 订单ID: {}, 分销标识: {}, 接收人: {}",
order.getId(), distributionMark, touser);
} else {
// 未配置接收人时,使用远程接口的默认接收人,这是正常情况
logger.info("未找到分销标识对应的接收人配置,将使用远程接口默认接收人 - 订单ID: {}, 分销标识: {}",
order.getId(), distributionMark);
}
// 记录完整的推送参数(用于调试)
String jsonBody = pushParam.toJSONString();
if (logisticsLinkUpdated) {
logger.info("企业微信推送完整参数(已更新物流链接) - 订单ID: {}, 订单号: {}, 旧链接: {}, 新链接: {}, JSON: {}",
order.getId(), order.getOrderId(), oldLogisticsLink, newLogisticsLink, jsonBody);
} else {
logger.info("企业微信推送完整参数 - 订单ID: {}, 订单号: {}, JSON: {}",
order.getId(), order.getOrderId(), jsonBody);
}
// 使用支持自定义header的HTTP请求
String pushResult = sendPostWithHeaders(PUSH_URL, pushParam.toJSONString(), PUSH_TOKEN);
logger.info("企业应用推送调用结果 - 订单ID: {}, waybill_no: {}, 推送结果: {}",
order.getId(), waybillNo, pushResult);
String pushResult = sendPostWithHeaders(PUSH_URL, jsonBody, PUSH_TOKEN);
if (pushResult == null || pushResult.trim().isEmpty()) {
logger.warn("企业应用推送响应为空 - 订单ID: {}, waybill_no: {}", order.getId(), waybillNo);
return false;
}
boolean success = isPushResponseSuccess(pushResult);
if (success) {
if (logisticsLinkUpdated) {
logger.info("企业应用推送成功(已更新物流链接) - 订单ID: {}, 订单号: {}, waybill_no: {}, 新链接: {}, 推送结果: {}",
order.getId(), order.getOrderId(), waybillNo, newLogisticsLink, pushResult);
} else {
logger.info("企业应用推送成功 - 订单ID: {}, 订单号: {}, waybill_no: {}, 推送结果: {}",
order.getId(), order.getOrderId(), waybillNo, pushResult);
}
} else {
logger.warn("企业应用推送响应未确认成功 - 订单ID: {}, 订单号: {}, waybill_no: {}, 响应: {}",
order.getId(), order.getOrderId(), waybillNo, pushResult);
}
return success;
} catch (Exception e) {
logger.error("调用企业应用推送失败 - 订单ID: {}, waybill_no: {}, 错误: {}",
order.getId(), waybillNo, e.getMessage(), e);
// 不抛出异常,避免影响主流程
// 不抛出异常,主流程根据返回值决定是否重试
return false;
}
}
/**
* 根据分销标识获取接收人列表
* 从系统配置中读取配置键名格式logistics.push.touser.{分销标识}
* 配置值格式接收人1,接收人2,接收人3逗号分隔
*
* @param distributionMark 分销标识
* @return 接收人列表逗号分隔如果未配置则返回null
*/
private String getTouserByDistributionMark(String distributionMark) {
if (!StringUtils.hasText(distributionMark)) {
logger.warn("分销标识为空,无法获取接收人配置");
return null;
}
try {
// 构建配置键名
String configKey = CONFIG_KEY_PREFIX + distributionMark.trim();
// 从系统配置中获取接收人列表
String configValue = sysConfigService.selectConfigByKey(configKey);
if (StringUtils.hasText(configValue)) {
// 清理配置值(去除空格)
String touser = configValue.trim().replaceAll(",\\s+", ",");
logger.info("从配置获取接收人列表 - 分销标识: {}, 配置键: {}, 接收人: {}",
distributionMark, configKey, touser);
return touser;
} else {
logger.debug("未找到接收人配置 - 分销标识: {}, 配置键: {}", distributionMark, configKey);
return null;
}
} catch (Exception e) {
logger.error("获取接收人配置失败 - 分销标识: {}, 错误: {}", distributionMark, e.getMessage(), e);
return null;
}
}
/**
* 判断推送返回值是否为成功状态
* @param pushResult 推送接口返回结果
* @return 是否成功
*/
private boolean isPushResponseSuccess(String pushResult) {
if (pushResult == null || pushResult.trim().isEmpty()) {
logger.warn("推送响应为空,视为失败");
return false;
}
try {
JSONObject response = JSON.parseObject(pushResult);
if (response == null) {
logger.warn("推送响应解析为null视为失败。原始响应: {}", pushResult);
return false;
}
Integer code = response.getInteger("code");
Boolean successFlag = response.getBoolean("success");
String status = response.getString("status");
String message = response.getString("msg");
String errcode = response.getString("errcode");
// 记录完整的响应信息用于调试
logger.debug("推送响应解析 - code: {}, success: {}, status: {}, msg: {}, errcode: {}",
code, successFlag, status, message, errcode);
// 检查错误码errcode为0表示成功
if (errcode != null && "0".equals(errcode)) {
logger.info("推送成功通过errcode=0判断");
return true;
}
// 检查code字段0或200表示成功
if (code != null && (code == 0 || code == 200)) {
logger.info("推送成功通过code={}判断)", code);
return true;
}
// 检查success字段
if (Boolean.TRUE.equals(successFlag)) {
logger.info("推送成功通过success=true判断");
return true;
}
// 检查status字段
if (status != null && ("success".equalsIgnoreCase(status) || "ok".equalsIgnoreCase(status))) {
logger.info("推送成功通过status={}判断)", status);
return true;
}
// 检查message字段某些接口可能用message表示成功
if (message != null && ("success".equalsIgnoreCase(message) || "ok".equalsIgnoreCase(message))) {
logger.info("推送成功通过message={}判断)", message);
return true;
}
// 检查是否包含明确的错误标识
String responseStr = pushResult.toLowerCase();
boolean hasErrorKeyword = responseStr.contains("\"error\"") || responseStr.contains("\"fail\"") ||
responseStr.contains("\"failed\"") || responseStr.contains("\"errmsg\"");
// 如果包含错误标识,检查是否有明确的错误码
if (hasErrorKeyword) {
if (code != null && code < 0) {
logger.warn("推送失败(检测到错误标识和负错误码) - code: {}, 完整响应: {}", code, pushResult);
return false;
}
if (errcode != null && !"0".equals(errcode) && !errcode.isEmpty()) {
logger.warn("推送失败(检测到错误标识和非零错误码) - errcode: {}, 完整响应: {}", errcode, pushResult);
return false;
}
}
// 如果响应是有效的JSON且没有明确的错误标识视为成功
// 因为远程接口有默认接收人,即使没有配置接收人,推送也应该成功
// 如果响应格式特殊(不是标准格式),只要没有错误,也视为成功
if (!hasErrorKeyword) {
logger.info("推送成功(响应格式有效且无错误标识,使用默认接收人) - 完整响应: {}", pushResult);
return true;
}
// 如果所有判断都失败,记录详细信息
logger.warn("推送响应未确认成功 - code: {}, success: {}, status: {}, msg: {}, errcode: {}, 完整响应: {}",
code, successFlag, status, message, errcode, pushResult);
return false;
} catch (Exception e) {
logger.error("解析企业应用推送响应失败,将视为未成功。原始响应: {}, 错误: {}", pushResult, e.getMessage(), e);
return false;
}
}

View File

@@ -0,0 +1,119 @@
package com.ruoyi.jarvis.service.impl;
import com.ruoyi.jarvis.service.IPhoneReplaceConfigService;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson2.JSON;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* 手机号替换配置Service业务层处理
*
* @author ruoyi
*/
@Service
public class PhoneReplaceConfigServiceImpl implements IPhoneReplaceConfigService
{
@Resource
private StringRedisTemplate stringRedisTemplate;
// Redis key前缀
private static final String REDIS_KEY_PREFIX = "phone_replace_config:";
// 循环索引key前缀
private static final String CYCLE_INDEX_KEY_PREFIX = "phone_cycle_index:";
@Override
public List<String> getPhoneList(String type) {
if (stringRedisTemplate == null) {
return new ArrayList<>();
}
try {
String key = REDIS_KEY_PREFIX + type;
String value = stringRedisTemplate.opsForValue().get(key);
if (value == null || value.isEmpty()) {
return new ArrayList<>();
}
return JSON.parseArray(value, String.class);
} catch (Exception e) {
return new ArrayList<>();
}
}
@Override
public int setPhoneList(String type, List<String> phoneList) {
if (stringRedisTemplate == null) {
return 0;
}
try {
String key = REDIS_KEY_PREFIX + type;
if (phoneList == null || phoneList.isEmpty()) {
stringRedisTemplate.delete(key);
} else {
String value = JSON.toJSONString(phoneList);
stringRedisTemplate.opsForValue().set(key, value);
}
return 1;
} catch (Exception e) {
return 0;
}
}
@Override
public int addPhone(String type, String phone) {
List<String> phoneList = getPhoneList(type);
if (!phoneList.contains(phone)) {
phoneList.add(phone);
return setPhoneList(type, phoneList);
}
return 1;
}
@Override
public int removePhone(String type, String phone) {
List<String> phoneList = getPhoneList(type);
phoneList.remove(phone);
return setPhoneList(type, phoneList);
}
@Override
public String getNextPhone(String type, String originalPhone) {
List<String> phoneList = getPhoneList(type);
if (phoneList == null || phoneList.isEmpty()) {
return originalPhone;
}
if (stringRedisTemplate == null) {
// 如果没有Redis使用简单的轮询基于时间戳
int index = (int) (System.currentTimeMillis() % phoneList.size());
return phoneList.get(index);
}
try {
String sequenceKey = CYCLE_INDEX_KEY_PREFIX + type;
Long index = stringRedisTemplate.opsForValue().increment(sequenceKey);
if (index == null) {
// 如果获取失败,使用时间戳
int idx = (int) (System.currentTimeMillis() % phoneList.size());
return phoneList.get(idx);
}
int phoneIndex = (int) ((index - 1) % phoneList.size());
String phone = phoneList.get(phoneIndex) != null ? phoneList.get(phoneIndex) : originalPhone;
// 当完成一轮循环后重置索引为0
if (index % phoneList.size() == 0) {
stringRedisTemplate.opsForValue().set(sequenceKey, "0");
}
return phone;
} catch (Exception e) {
// 异常时使用时间戳方式
int idx = (int) (System.currentTimeMillis() % phoneList.size());
return phoneList.get(idx);
}
}
}

View File

@@ -0,0 +1,422 @@
package com.ruoyi.jarvis.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.jarvis.service.ISocialMediaService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* 小红书/抖音内容生成Service业务层处理
*
* @author ruoyi
* @date 2025-01-XX
*/
@Service
public class SocialMediaServiceImpl implements ISocialMediaService
{
private static final Logger log = LoggerFactory.getLogger(SocialMediaServiceImpl.class);
@Autowired(required = false)
private StringRedisTemplate redisTemplate;
@Value("${jarvis.server.jarvis-java.base-url:http://127.0.0.1:6666}")
private String jarvisBaseUrl;
// Redis Key 前缀
private static final String REDIS_KEY_PREFIX = "social_media:prompt:";
// 模板键名列表
private static final String[] TEMPLATE_KEYS = {
"keywords",
"content:xhs",
"content:douyin",
"content:both"
};
// 模板说明
private static final Map<String, String> TEMPLATE_DESCRIPTIONS = new HashMap<String, String>() {{
put("keywords", "关键词提取提示词模板\n占位符%s - 商品名称");
put("content:xhs", "小红书文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
put("content:douyin", "抖音文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
put("content:both", "通用文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
}};
/**
* 提取商品标题关键词
*/
@Override
public Map<String, Object> extractKeywords(String productName) {
Map<String, Object> result = new HashMap<>();
if (StringUtils.isEmpty(productName)) {
result.put("success", false);
result.put("error", "商品名称不能为空");
return result;
}
try {
// 调用 jarvis_java 的接口
String url = jarvisBaseUrl + "/jarvis/social-media/extract-keywords";
JSONObject requestBody = new JSONObject();
requestBody.put("productName", productName);
log.info("调用jarvis_java提取关键词接口URL: {}, 参数: {}", url, requestBody.toJSONString());
String response = HttpUtils.sendJsonPost(url, requestBody.toJSONString());
log.info("jarvis_java响应: {}", response);
if (StringUtils.isEmpty(response)) {
throw new Exception("jarvis_java返回空结果");
}
// 解析响应
Object parsed = JSON.parse(response);
if (parsed instanceof JSONObject) {
JSONObject jsonResponse = (JSONObject) parsed;
if (jsonResponse.getInteger("code") == 200) {
Object data = jsonResponse.get("data");
if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> dataMap = (Map<String, Object>) data;
return dataMap;
}
} else {
String msg = jsonResponse.getString("msg");
result.put("success", false);
result.put("error", msg != null ? msg : "提取关键词失败");
return result;
}
}
result.put("success", false);
result.put("error", "响应格式错误");
return result;
} catch (Exception e) {
log.error("提取关键词失败", e);
result.put("success", false);
result.put("error", "提取关键词失败: " + e.getMessage());
return result;
}
}
/**
* 生成文案
*/
@Override
public Map<String, Object> generateContent(String productName, Object originalPrice,
Object finalPrice, String keywords, String style) {
Map<String, Object> result = new HashMap<>();
if (StringUtils.isEmpty(productName)) {
result.put("success", false);
result.put("error", "商品名称不能为空");
return result;
}
try {
// 调用 jarvis_java 的接口
String url = jarvisBaseUrl + "/jarvis/social-media/generate-content";
JSONObject requestBody = new JSONObject();
requestBody.put("productName", productName);
if (originalPrice != null) {
requestBody.put("originalPrice", parseDouble(originalPrice));
}
if (finalPrice != null) {
requestBody.put("finalPrice", parseDouble(finalPrice));
}
if (StringUtils.isNotEmpty(keywords)) {
requestBody.put("keywords", keywords);
}
if (StringUtils.isNotEmpty(style)) {
requestBody.put("style", style);
}
log.info("调用jarvis_java生成文案接口URL: {}, 参数: {}", url, requestBody.toJSONString());
String response = HttpUtils.sendJsonPost(url, requestBody.toJSONString());
log.info("jarvis_java响应: {}", response);
if (StringUtils.isEmpty(response)) {
throw new Exception("jarvis_java返回空结果");
}
// 解析响应
Object parsed = JSON.parse(response);
if (parsed instanceof JSONObject) {
JSONObject jsonResponse = (JSONObject) parsed;
if (jsonResponse.getInteger("code") == 200) {
Object data = jsonResponse.get("data");
if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> dataMap = (Map<String, Object>) data;
return dataMap;
}
} else {
String msg = jsonResponse.getString("msg");
result.put("success", false);
result.put("error", msg != null ? msg : "生成文案失败");
return result;
}
}
result.put("success", false);
result.put("error", "响应格式错误");
return result;
} catch (Exception e) {
log.error("生成文案失败", e);
result.put("success", false);
result.put("error", "生成文案失败: " + e.getMessage());
return result;
}
}
/**
* 一键生成完整内容(关键词 + 文案 + 图片)
*/
@Override
public Map<String, Object> generateCompleteContent(String productImageUrl, String productName,
Object originalPrice, Object finalPrice, String style) {
Map<String, Object> result = new HashMap<>();
if (StringUtils.isEmpty(productName)) {
result.put("success", false);
result.put("error", "商品名称不能为空");
return result;
}
try {
// 调用 jarvis_java 的接口
String url = jarvisBaseUrl + "/jarvis/social-media/generate-complete";
JSONObject requestBody = new JSONObject();
if (StringUtils.isNotEmpty(productImageUrl)) {
requestBody.put("productImageUrl", productImageUrl);
}
requestBody.put("productName", productName);
if (originalPrice != null) {
requestBody.put("originalPrice", parseDouble(originalPrice));
}
if (finalPrice != null) {
requestBody.put("finalPrice", parseDouble(finalPrice));
}
if (StringUtils.isNotEmpty(style)) {
requestBody.put("style", style);
}
log.info("调用jarvis_java生成完整内容接口URL: {}, 参数: {}", url, requestBody.toJSONString());
String response = HttpUtils.sendJsonPost(url, requestBody.toJSONString());
log.info("jarvis_java响应: {}", response);
if (StringUtils.isEmpty(response)) {
throw new Exception("jarvis_java返回空结果");
}
// 解析响应
Object parsed = JSON.parse(response);
if (parsed instanceof JSONObject) {
JSONObject jsonResponse = (JSONObject) parsed;
if (jsonResponse.getInteger("code") == 200) {
Object data = jsonResponse.get("data");
if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> dataMap = (Map<String, Object>) data;
return dataMap;
}
} else {
String msg = jsonResponse.getString("msg");
result.put("success", false);
result.put("error", msg != null ? msg : "生成完整内容失败");
return result;
}
}
result.put("success", false);
result.put("error", "响应格式错误");
return result;
} catch (Exception e) {
log.error("生成完整内容失败", e);
result.put("success", false);
result.put("error", "生成完整内容失败: " + e.getMessage());
return result;
}
}
/**
* 获取提示词模板列表
*/
@Override
public AjaxResult listPromptTemplates() {
try {
Map<String, Object> templates = new HashMap<>();
for (String key : TEMPLATE_KEYS) {
Map<String, Object> templateInfo = new HashMap<>();
templateInfo.put("key", key);
templateInfo.put("description", TEMPLATE_DESCRIPTIONS.get(key));
String template = getTemplateFromRedis(key);
templateInfo.put("template", template);
templateInfo.put("isDefault", template == null);
templates.put(key, templateInfo);
}
return AjaxResult.success(templates);
} catch (Exception e) {
log.error("获取提示词模板列表失败", e);
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取单个提示词模板
*/
@Override
public AjaxResult getPromptTemplate(String key) {
try {
if (!isValidKey(key)) {
return AjaxResult.error("无效的模板键名");
}
String template = getTemplateFromRedis(key);
Map<String, Object> data = new HashMap<>();
data.put("key", key);
data.put("description", TEMPLATE_DESCRIPTIONS.get(key));
data.put("template", template);
data.put("isDefault", template == null);
return AjaxResult.success(data);
} catch (Exception e) {
log.error("获取提示词模板失败", e);
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 保存提示词模板
*/
@Override
public AjaxResult savePromptTemplate(Map<String, Object> request) {
try {
String key = (String) request.get("key");
String template = (String) request.get("template");
if (!isValidKey(key)) {
return AjaxResult.error("无效的模板键名");
}
if (StringUtils.isEmpty(template)) {
return AjaxResult.error("模板内容不能为空");
}
if (redisTemplate == null) {
return AjaxResult.error("Redis未配置无法保存模板");
}
String redisKey = REDIS_KEY_PREFIX + key;
String templateValue = template.trim();
if (StringUtils.isEmpty(templateValue)) {
return AjaxResult.error("模板内容不能为空");
}
redisTemplate.opsForValue().set(redisKey, templateValue);
log.info("保存提示词模板成功: {}", key);
return AjaxResult.success("保存成功");
} catch (Exception e) {
log.error("保存提示词模板失败", e);
return AjaxResult.error("保存失败: " + e.getMessage());
}
}
/**
* 删除提示词模板(恢复默认)
*/
@Override
public AjaxResult deletePromptTemplate(String key) {
try {
if (!isValidKey(key)) {
return AjaxResult.error("无效的模板键名");
}
if (redisTemplate == null) {
return AjaxResult.error("Redis未配置无法删除模板");
}
String redisKey = REDIS_KEY_PREFIX + key;
redisTemplate.delete(redisKey);
log.info("删除提示词模板成功: {}", key);
return AjaxResult.success("删除成功,已恢复默认模板");
} catch (Exception e) {
log.error("删除提示词模板失败", e);
return AjaxResult.error("删除失败: " + e.getMessage());
}
}
/**
* 从 Redis 获取模板
*/
private String getTemplateFromRedis(String key) {
if (redisTemplate == null) {
return null;
}
try {
String redisKey = REDIS_KEY_PREFIX + key;
String template = redisTemplate.opsForValue().get(redisKey);
return StringUtils.isNotEmpty(template) ? template : null;
} catch (Exception e) {
log.warn("读取Redis模板失败: {}", key, e);
return null;
}
}
/**
* 验证模板键名是否有效
*/
private boolean isValidKey(String key) {
if (StringUtils.isEmpty(key)) {
return false;
}
for (String validKey : TEMPLATE_KEYS) {
if (validKey.equals(key)) {
return true;
}
}
return false;
}
/**
* 解析Double值
*/
private Double parseDouble(Object value) {
if (value == null) {
return null;
}
if (value instanceof Double) {
return (Double) value;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
try {
return Double.parseDouble(value.toString());
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,100 @@
package com.ruoyi.jarvis.service.impl;
import com.ruoyi.jarvis.domain.TaobaoComment;
import com.ruoyi.jarvis.mapper.TaobaoCommentMapper;
import com.ruoyi.jarvis.service.ITaobaoCommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* 淘宝评论管理 Service 实现
*/
@Service
public class TaobaoCommentServiceImpl implements ITaobaoCommentService {
private static final String PRODUCT_TYPE_MAP_PREFIX_TB = "product_type_map_tb";
@Autowired
private TaobaoCommentMapper taobaoCommentMapper;
@Autowired(required = false)
private StringRedisTemplate stringRedisTemplate;
@Override
public List<TaobaoComment> selectTaobaoCommentList(TaobaoComment taobaoComment) {
List<TaobaoComment> list = taobaoCommentMapper.selectTaobaoCommentList(taobaoComment);
// 填充Redis映射的产品类型信息
if (stringRedisTemplate != null) {
Map<String, String> tbMap = getTbProductTypeMap();
for (TaobaoComment c : list) {
// 查找对应的产品类型
String productId = c.getProductId();
if (tbMap != null) {
for (Map.Entry<String, String> entry : tbMap.entrySet()) {
if (entry.getValue().equals(productId)) {
c.setProductType(entry.getKey());
c.setMappedProductId(productId);
break;
}
}
}
}
}
return list;
}
@Override
public TaobaoComment selectTaobaoCommentById(Integer id) {
TaobaoComment comment = taobaoCommentMapper.selectTaobaoCommentById(id);
if (comment != null && stringRedisTemplate != null) {
Map<String, String> tbMap = getTbProductTypeMap();
if (tbMap != null) {
String productId = comment.getProductId();
for (Map.Entry<String, String> entry : tbMap.entrySet()) {
if (entry.getValue().equals(productId)) {
comment.setProductType(entry.getKey());
comment.setMappedProductId(productId);
break;
}
}
}
}
return comment;
}
@Override
public int updateTaobaoCommentIsUse(TaobaoComment taobaoComment) {
return taobaoCommentMapper.updateTaobaoCommentIsUse(taobaoComment);
}
@Override
public int deleteTaobaoCommentByIds(Integer[] ids) {
return taobaoCommentMapper.deleteTaobaoCommentByIds(ids);
}
@Override
public int resetTaobaoCommentIsUseByProductId(String productId) {
return taobaoCommentMapper.resetTaobaoCommentIsUseByProductId(productId);
}
private Map<String, String> getTbProductTypeMap() {
if (stringRedisTemplate == null) {
return null;
}
try {
Map<Object, Object> rawMap = stringRedisTemplate.opsForHash().entries(PRODUCT_TYPE_MAP_PREFIX_TB);
Map<String, String> result = new java.util.LinkedHashMap<>();
for (Map.Entry<Object, Object> entry : rawMap.entrySet()) {
result.put(entry.getKey().toString(), entry.getValue().toString());
}
return result;
} catch (Exception e) {
return null;
}
}
}

View File

@@ -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;
@@ -212,8 +212,9 @@ public class TencentDocServiceImpl implements ITencentDocService {
Integer addressColumn = null;
Integer priceColumn = null;
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) {
@@ -227,27 +228,28 @@ public class TencentDocServiceImpl implements ITencentDocService {
else if (cellText.contains("地址")) addressColumn = i;
else if (cellText.contains("价格")) priceColumn = i;
else if (cellText.contains("备注")) remarkColumn = i;
else if (cellText.contains("是否安排") || cellText.contains("安排")) arrangedColumn = i;
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
@@ -265,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));
@@ -296,68 +298,73 @@ 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));
}
if (priceColumn != null && order.getPaymentAmount() != null) {
requests.add(buildUpdateRequest(sheetId, rowIndex, priceColumn, order.getPaymentAmount().toString(), false));
}
if (remarkColumn != null && order.getRemark() != null) {
requests.add(buildUpdateRequest(sheetId, rowIndex, remarkColumn, order.getRemark(), false));
// 备注列固定填写"有货"
if (remarkColumn != null) {
requests.add(buildUpdateRequest(sheetId, rowIndex, remarkColumn, "有货", false));
}
// 是否安排列固定填写"2"
if (arrangedColumn != null) {
requests.add(buildUpdateRequest(sheetId, rowIndex, arrangedColumn, "2", false));
}
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);
@@ -366,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 返回 nullAccess 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
@@ -447,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 {
@@ -482,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) {
@@ -498,7 +505,7 @@ public class TencentDocServiceImpl implements ITencentDocService {
throw new RuntimeException("写入表格数据失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject getFileInfo(String accessToken, String fileId) {
try {
@@ -513,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) {
@@ -526,7 +533,7 @@ public class TencentDocServiceImpl implements ITencentDocService {
throw new RuntimeException("获取文件信息失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject getSheetList(String accessToken, String fileId) {
try {
@@ -541,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) {
@@ -554,7 +561,7 @@ public class TencentDocServiceImpl implements ITencentDocService {
throw new RuntimeException("获取工作表列表失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject getUserInfo(String accessToken) {
try {
@@ -564,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 {
@@ -578,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(),

View File

@@ -0,0 +1,483 @@
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 {
// WPS365用户信息API: GET /api/v1/user/info
// 注意如果此API不存在或需要不同的路径请查看WPS365官方文档
String url = wps365Config.getApiBaseUrl() + "/user/info";
log.debug("调用用户信息API: {}", url);
JSONObject result = WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
log.debug("用户信息API响应: {}", result);
return result;
} catch (Exception e) {
log.error("获取用户信息失败 - url: {}, error: {}",
wps365Config.getApiBaseUrl() + "/user/info", e.getMessage(), e);
// 不抛出异常,让调用方处理(使用降级方案)
throw new RuntimeException("获取用户信息失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject getFileList(String accessToken, Map<String, Object> params) {
try {
// WPS365文件列表API路径可能是 /yundoc/files 而不是 /files
// 根据WPS365 API文档文件相关API通常在 /yundoc 路径下
StringBuilder url = new StringBuilder(wps365Config.getApiBaseUrl() + "/yundoc/files");
// 添加查询参数
if (params != null && !params.isEmpty()) {
url.append("?");
boolean first = true;
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (!first) {
url.append("&");
}
// URL编码参数值
try {
String key = entry.getKey();
String value = entry.getValue() != null ? entry.getValue().toString() : "";
url.append(key).append("=").append(java.net.URLEncoder.encode(value, "UTF-8"));
} catch (java.io.UnsupportedEncodingException e) {
url.append(entry.getKey()).append("=").append(entry.getValue());
}
first = false;
}
}
log.debug("调用文件列表API: {}", url.toString());
return WPS365ApiUtil.httpRequest("GET", url.toString(), accessToken, null);
} catch (Exception e) {
log.error("获取文件列表失败 - url: {}", wps365Config.getApiBaseUrl() + "/yundoc/files", e);
throw new RuntimeException("获取文件列表失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject getFileInfo(String accessToken, String fileToken) {
try {
// WPS365文件信息API路径/yundoc/files/{fileToken}
String url = wps365Config.getApiBaseUrl() + "/yundoc/files/" + fileToken;
log.debug("调用文件信息API: {}", url);
return WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
} catch (Exception e) {
log.error("获取文件信息失败 - fileToken: {}, url: {}", fileToken,
wps365Config.getApiBaseUrl() + "/yundoc/files/" + fileToken, e);
throw new RuntimeException("获取文件信息失败: " + e.getMessage(), e);
}
}
@Override
public JSONObject updateCells(String accessToken, String fileToken, int sheetIdx, String range, List<List<Object>> 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<Object> 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<Map<String, Object>> 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<String, Object> update : updates) {
JSONObject updateObj = new JSONObject();
updateObj.put("range", update.get("range"));
// 将values转换为JSONArray
@SuppressWarnings("unchecked")
List<List<Object>> values = (List<List<Object>>) update.get("values");
JSONArray valuesArray = new JSONArray();
if (values != null) {
for (List<Object> 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);
}
}
@Override
public JSONObject readAirSheetCells(String accessToken, String fileId, String worksheetId, String range) {
try {
// WPS365 AirSheet API路径格式
// 根据文档https://open.wps.cn/documents/app-integration-dev/wps365/server/airsheet/worksheets/VbHZwButmh
// 注意AirSheet中fileId和worksheetId可能是同一个值或者worksheetId是整数索引
// 如果用户只提供了一个IDfileId则fileId和worksheetId使用同一个值
String baseUrl = "https://openapi.wps.cn/v7";
// 根据WPS365官方文档AirSheet的worksheet_id必须是整数
// 如果worksheetId为空或"0"使用0第一个工作表
// 如果worksheetId与fileId相同说明用户只配置了一个ID尝试使用fileId作为worksheetId
String wsId;
if (worksheetId == null || worksheetId.trim().isEmpty() || "0".equals(worksheetId)) {
// 默认使用0第一个工作表
wsId = "0";
} else if (worksheetId.equals(fileId)) {
// 如果worksheetId与fileId相同说明用户只配置了一个ID类似腾讯文档
// 在AirSheet中这个ID可能既是file_id也是worksheet_id
wsId = fileId;
} else {
// 使用提供的worksheetId应该是整数
wsId = worksheetId;
}
// 根据官方文档https://open.wps.cn/documents/app-integration-dev/wps365/server/airsheet/worksheets/VbHZwButmh
// 正确路径https://openapi.wps.cn/v7/airsheet/{file_id}/worksheets
// 注意:路径中不需要 worksheet_id只需要 file_id
try {
String url = baseUrl + "/airsheet/" + fileId + "/worksheets";
// 如果指定了range添加range参数
if (range != null && !range.trim().isEmpty()) {
url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8");
}
// 如果指定了worksheetId也可以作为查询参数传递如果API支持
if (worksheetId != null && !worksheetId.trim().isEmpty() && !worksheetId.equals("0") && !worksheetId.equals(fileId)) {
if (url.contains("?")) {
url += "&worksheet_id=" + java.net.URLEncoder.encode(worksheetId, "UTF-8");
} else {
url += "?worksheet_id=" + java.net.URLEncoder.encode(worksheetId, "UTF-8");
}
}
log.debug("使用官方文档路径 - url: {}, fileId: {}, worksheetId: {}, range: {}", url, fileId, worksheetId, range);
return WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
} catch (Exception e) {
log.debug("官方文档路径失败,尝试其他方案", e);
}
// 尝试多种API路径格式降级方案
// 方案1: 尝试使用fileId作为worksheetId如果用户只配置了一个ID
if (wsId.equals("0") && !fileId.equals(worksheetId)) {
try {
String url = baseUrl + "/airsheet/" + fileId + "/worksheets/" + fileId;
if (range != null && !range.trim().isEmpty()) {
url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8");
}
log.debug("尝试方案1 - 使用fileId作为worksheetId - url: {}", url);
return WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
} catch (Exception e) {
log.debug("方案1失败尝试其他方案", e);
}
}
// 方案2: 使用 /range_data 子路径(根据官方文档,这是读取区域数据的标准路径)
// 注意range_data接口需要使用 row_from, row_to, col_from, col_to 参数,而不是 range=A1:B5
try {
String url = baseUrl + "/airsheet/" + fileId + "/worksheets/" + wsId + "/range_data";
if (range != null && !range.trim().isEmpty()) {
// 尝试解析A1:B5格式的range转换为行列参数
int[] rangeParams = parseRangeToRowCol(range);
if (rangeParams != null && rangeParams.length == 4) {
// 使用行列参数格式row_from, row_to, col_from, col_to
// 注意WPS365的行列索引可能从0开始或从1开始需要测试确认
url += "?row_from=" + rangeParams[0] + "&row_to=" + rangeParams[1]
+ "&col_from=" + rangeParams[2] + "&col_to=" + rangeParams[3];
} else {
// 如果解析失败尝试作为range参数传递
url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8");
}
}
log.debug("尝试方案2 - 使用/range_data子路径 - url: {}", url);
return WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
} catch (Exception e) {
log.debug("方案2失败尝试其他方案", e);
}
// 方案3: 使用 /values 子路径
try {
String url = baseUrl + "/airsheet/" + fileId + "/worksheets/" + wsId + "/values";
if (range != null && !range.trim().isEmpty()) {
url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8");
}
log.debug("尝试方案3 - 使用/values子路径 - url: {}", url);
return WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
} catch (Exception e) {
log.debug("方案3失败尝试其他方案", e);
}
// 方案4: 基础路径(不带子路径)
String url = baseUrl + "/airsheet/" + fileId + "/worksheets/" + wsId;
if (range != null && !range.trim().isEmpty()) {
url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8");
}
log.debug("尝试方案4 - 基础路径 - url: {}", url);
JSONObject result = WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
return result;
} catch (Exception e) {
log.error("读取AirSheet数据失败 - fileId: {}, worksheetId: {}, range: {}", fileId, worksheetId, range, e);
// 如果失败,尝试使用 /values 子路径
try {
String baseUrl = "https://openapi.wps.cn/v7";
String wsId = (worksheetId != null && !worksheetId.trim().isEmpty() && !worksheetId.equals(fileId)) ? worksheetId : fileId;
String url = baseUrl + "/airsheet/" + fileId + "/worksheets/" + wsId + "/values";
if (range != null && !range.trim().isEmpty()) {
url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8");
}
log.debug("尝试使用/values子路径 - url: {}", url);
return WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
} catch (Exception e2) {
log.error("使用/values子路径也失败", e2);
throw new RuntimeException("读取AirSheet数据失败: " + e.getMessage(), e);
}
}
}
@Override
public JSONObject updateAirSheetCells(String accessToken, String fileId, String worksheetId, String range, List<List<Object>> values) {
try {
// WPS365 AirSheet API路径格式
// 根据文档https://open.wps.cn/documents/app-integration-dev/wps365/server/airsheet/worksheets/VbHZwButmh
// 正确路径https://openapi.wps.cn/v7/airsheet/{file_id}/worksheets
// 注意:路径中不需要 worksheet_id只需要 file_id
String baseUrl = "https://openapi.wps.cn/v7";
// 使用官方文档中的正确路径格式
String url = baseUrl + "/airsheet/" + fileId + "/worksheets";
// 构建请求体
JSONObject requestBody = new JSONObject();
if (range != null && !range.trim().isEmpty()) {
requestBody.put("range", range);
}
// 构建values数组
JSONArray valuesArray = new JSONArray();
if (values != null) {
for (List<Object> row : values) {
JSONArray rowArray = new JSONArray();
if (row != null) {
for (Object cell : row) {
rowArray.add(cell);
}
}
valuesArray.add(rowArray);
}
}
requestBody.put("values", valuesArray);
// 如果指定了worksheetId也可以添加到请求体中如果API支持
if (worksheetId != null && !worksheetId.trim().isEmpty() && !worksheetId.equals("0") && !worksheetId.equals(fileId)) {
requestBody.put("worksheet_id", worksheetId);
}
String bodyStr = requestBody.toJSONString();
log.debug("更新AirSheet数据 - url: {}, fileId: {}, worksheetId: {}, range: {}, values: {}",
url, fileId, worksheetId, range, bodyStr);
return WPS365ApiUtil.httpRequest("PUT", url, accessToken, bodyStr);
} catch (Exception e) {
log.error("更新AirSheet数据失败 - fileId: {}, worksheetId: {}, range: {}", fileId, worksheetId, range, e);
throw new RuntimeException("更新AirSheet数据失败: " + e.getMessage(), e);
}
}
/**
* 解析A1:B5格式的range转换为行列参数
* 返回数组:[row_from, row_to, col_from, col_to]
* 注意WPS365的行列索引可能从0开始或从1开始这里假设从1开始Excel标准
*
* @param range 单元格范围,如 "A1:B5"
* @return 行列参数数组如果解析失败返回null
*/
private int[] parseRangeToRowCol(String range) {
if (range == null || range.trim().isEmpty()) {
return null;
}
try {
// 解析A1:B5格式
String[] parts = range.split(":");
if (parts.length != 2) {
return null;
}
String startCell = parts[0].trim();
String endCell = parts[1].trim();
// 解析起始单元格,如 "A1" -> row=1, col=1
int[] start = parseCellAddress(startCell);
int[] end = parseCellAddress(endCell);
if (start == null || end == null) {
return null;
}
// 返回 [row_from, row_to, col_from, col_to]
// 注意WPS365可能从0开始索引这里先使用从1开始的索引Excel标准
// 如果API要求从0开始需要减1
return new int[]{start[0], end[0], start[1], end[1]};
} catch (Exception e) {
log.warn("解析range失败: {}", range, e);
return null;
}
}
/**
* 解析单元格地址,如 "A1" -> [row=1, col=1]
*
* @param cellAddress 单元格地址,如 "A1", "B5"
* @return [row, col] 数组如果解析失败返回null
*/
private int[] parseCellAddress(String cellAddress) {
if (cellAddress == null || cellAddress.trim().isEmpty()) {
return null;
}
try {
// 分离字母部分(列)和数字部分(行)
// 例如 "A1" -> col="A", row="1"
String colStr = "";
String rowStr = "";
for (char c : cellAddress.toCharArray()) {
if (Character.isLetter(c)) {
colStr += c;
} else if (Character.isDigit(c)) {
rowStr += c;
}
}
if (colStr.isEmpty() || rowStr.isEmpty()) {
return null;
}
// 转换列字母为数字A=1, B=2, ..., Z=26, AA=27, ...
int col = 0;
for (char c : colStr.toUpperCase().toCharArray()) {
col = col * 26 + (c - 'A' + 1);
}
// 转换行号为整数
int row = Integer.parseInt(rowStr);
return new int[]{row, col};
} catch (Exception e) {
log.warn("解析单元格地址失败: {}", cellAddress, e);
return null;
}
}
}

View File

@@ -0,0 +1,364 @@
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.service.IWPS365ApiService;
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;
@Autowired
private IWPS365ApiService wps365ApiService;
@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);
// WPS365可能使用 app_id 而不是 client_id先尝试 client_id标准OAuth2参数
// 如果失败,可能需要改为 app_id
authUrl.append("?client_id=").append(appId);
log.debug("授权URL参数 - client_id: {}", appId);
// 重要redirect_uri必须与WPS365开放平台配置的回调地址完全一致
// 包括协议(https)、域名、路径,不能有多余的斜杠
String finalRedirectUri = redirectUri.trim();
// 确保URL末尾没有斜杠除非是根路径
if (finalRedirectUri.endsWith("/") && !finalRedirectUri.equals("https://")) {
finalRedirectUri = finalRedirectUri.substring(0, finalRedirectUri.length() - 1);
}
// 验证redirect_uri不为空
if (finalRedirectUri == null || finalRedirectUri.isEmpty()) {
throw new RuntimeException("redirect_uri不能为空请检查application.yml中的wps365.redirect-uri配置");
}
try {
String encodedRedirectUri = java.net.URLEncoder.encode(finalRedirectUri, "UTF-8");
authUrl.append("&redirect_uri=").append(encodedRedirectUri);
log.info("使用回调地址: {} (编码后: {})", finalRedirectUri, encodedRedirectUri);
} catch (java.io.UnsupportedEncodingException e) {
log.error("URL编码失败", e);
authUrl.append("&redirect_uri=").append(finalRedirectUri);
}
// response_type参数必需
authUrl.append("&response_type=code");
log.debug("授权URL参数 - response_type: code");
// scope参数必需根据WPS365官方文档
// 优先使用配置文件中指定的scope如果没有配置则使用默认值
// 重要WPS365官方文档明确要求使用英文逗号分隔且权限名称必须与后台注册的完全一致
// 根据官方文档https://open.wps.cn/documents/app-integration-dev/wps365/server/
// 权限名称格式为kso.xxx.read 或 kso.xxx.readwrite不是 file.read
String scope = wps365Config.getScope();
if (scope == null || scope.trim().isEmpty()) {
// 默认scope根据WPS365官方文档
// 1. 必须使用英文逗号分隔(不是空格)
// 2. 权限名称必须以 kso. 开头格式如kso.file.read, kso.file.readwrite
// 3. 常见权限名称(根据官方文档):
// - kso.file.read (文件读取)
// - kso.file.readwrite (文件读写)
// - kso.doclib.readwrite (文档库读写)
// - kso.wiki.readwrite (知识库读写)
// - 对于在线表格AirSheet/KSheet可能需要 kso.file.readwrite
//
// 如果报错invalid_scope
// 1. 登录WPS365开放平台https://open.wps.cn/
// 2. 进入"开发配置" > "权限管理"
// 3. 查看已申请权限的准确名称(必须以 kso. 开头)
// 4. 在application.yml中配置scope参数使用逗号分隔
scope = "kso.file.readwrite"; // 默认使用文件读写权限(支持在线表格操作)
}
scope = scope.trim();
// URL编码scope参数
try {
String encodedScope = java.net.URLEncoder.encode(scope, "UTF-8");
authUrl.append("&scope=").append(encodedScope);
log.debug("授权URL参数 - scope: {} (编码后: {})", scope, encodedScope);
} catch (java.io.UnsupportedEncodingException e) {
log.error("Scope URL编码失败", e);
authUrl.append("&scope=").append(scope);
}
// state参数推荐用于防止CSRF攻击
if (state == null || state.trim().isEmpty()) {
state = UUID.randomUUID().toString();
}
authUrl.append("&state=").append(state);
log.debug("授权URL参数 - state: {}", state);
// prompt参数可选用于控制授权页面显示
// prompt=consent: 强制显示授权确认页面,即使用户已授权过
// prompt=login: 强制显示登录页面
// 如果不添加此参数,已登录且已授权的用户会直接跳过授权页面
// 注意WPS365可能不支持此参数如果不支持会被忽略
authUrl.append("&prompt=consent");
log.debug("授权URL参数 - prompt: consent (强制显示授权确认页面)");
String result = authUrl.toString();
log.info("生成授权URL: {}", result);
log.warn("⚠️ 请确保WPS365开放平台配置的回调地址与以下地址完全一致包括协议、域名、路径:");
log.warn("⚠️ 回调地址: {}", finalRedirectUri);
log.info("📋 授权请求参数清单:");
log.info(" - client_id: {}", appId);
log.info(" - redirect_uri: {}", finalRedirectUri);
log.info(" - response_type: code");
log.info(" - scope: {}", scope);
log.info(" - state: {}", state);
log.info(" - prompt: consent (强制显示授权确认页面)");
log.info("💡 说明:已添加 prompt=consent 参数,强制显示授权确认页面");
log.info(" 如果用户已登录且已授权过WPS365可能会跳过授权页面直接返回code");
log.info(" 这是正常的OAuth2行为不是安全问题");
log.info("如果仍然报错,请检查:");
log.info(" 1. WPS365平台配置的回调地址是否与上述redirect_uri完全一致");
log.info(" 2. 参数名是否正确WPS365可能使用app_id而不是client_id");
log.info(" 3. scope权限是否已在WPS365平台申请");
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"));
// WPS365的token响应中可能不包含user_id需要调用用户信息API获取
String userId = result.getString("user_id");
if (userId == null || userId.trim().isEmpty()) {
// 尝试通过用户信息API获取用户ID
try {
JSONObject userInfo = wps365ApiService.getUserInfo(tokenInfo.getAccessToken());
if (userInfo != null) {
// 尝试多种可能的用户ID字段名
userId = userInfo.getString("id");
if (userId == null || userId.trim().isEmpty()) {
userId = userInfo.getString("user_id");
}
if (userId == null || userId.trim().isEmpty()) {
userId = userInfo.getString("open_id");
}
if (userId == null || userId.trim().isEmpty()) {
userId = userInfo.getString("uid");
}
// 如果还是获取不到使用access_token的前16位作为标识临时方案
if (userId == null || userId.trim().isEmpty()) {
String accessToken = tokenInfo.getAccessToken();
if (accessToken != null && accessToken.length() > 16) {
userId = "wps365_" + accessToken.substring(0, 16);
log.warn("无法从用户信息API获取用户ID使用access_token前16位作为标识: {}", userId);
} else {
userId = "wps365_default";
log.warn("无法获取用户ID使用默认值: {}", userId);
}
} else {
log.info("通过用户信息API获取到用户ID: {}", userId);
}
} else {
userId = "wps365_default";
log.warn("用户信息API返回为空使用默认用户ID: {}", userId);
}
} catch (Exception e) {
log.warn("调用用户信息API失败使用默认用户ID: {}", e.getMessage());
// 使用access_token的前16位作为标识临时方案
String accessToken = tokenInfo.getAccessToken();
if (accessToken != null && accessToken.length() > 16) {
userId = "wps365_" + accessToken.substring(0, 16);
} else {
userId = "wps365_default";
}
}
}
tokenInfo.setUserId(userId);
log.info("成功获取访问令牌 - userId: {}", userId);
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() {
// 尝试查找所有WPS365 token通常只有一个
// 使用Redis的keys命令查找所有匹配的token key
try {
String pattern = TOKEN_KEY_PREFIX + "*";
// 注意keys命令在生产环境可能性能较差但这里token数量通常很少
java.util.Collection<String> keys = redisCache.keys(pattern);
if (keys != null && !keys.isEmpty()) {
// 返回第一个找到的有效token
for (String key : keys) {
WPS365TokenInfo tokenInfo = redisCache.getCacheObject(key);
if (tokenInfo != null && isTokenValid(tokenInfo)) {
log.debug("找到有效的WPS365 token: {}", key);
return tokenInfo;
}
}
// 如果没有有效的token返回第一个即使过期
for (String key : keys) {
WPS365TokenInfo tokenInfo = redisCache.getCacheObject(key);
if (tokenInfo != null) {
log.debug("找到WPS365 token可能已过期: {}", key);
return tokenInfo;
}
}
}
} catch (Exception e) {
log.warn("查找WPS365 token失败", e);
}
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);
}
}

View File

@@ -0,0 +1,167 @@
package com.ruoyi.jarvis.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.jarvis.service.IWxSendService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
/**
* 微信推送服务实现类
*/
@Service
public class WxSendServiceImpl implements IWxSendService {
private static final Logger logger = LoggerFactory.getLogger(WxSendServiceImpl.class);
private static final String PUSH_TOKEN = "super_token_b62190c26";
@Value("${jarvis.server.wxsend.base-url:https://wxts.van333.cn}")
private String wxSendBaseUrl;
@Value("${jarvis.server.wxsend.health-path:/wx/send/jd}")
private String wxSendHealthPath;
private String healthCheckUrl;
@PostConstruct
public void init() {
healthCheckUrl = wxSendBaseUrl + wxSendHealthPath;
logger.info("微信推送服务健康检查地址已初始化: {}", healthCheckUrl);
}
@Override
public IWxSendService.HealthCheckResult checkHealth() {
try {
logger.debug("开始检查微信推送服务健康状态 - URL: {}", healthCheckUrl);
// 构建健康检查请求,发送一条真实的消息
// /send/jd 接口只需要 text 字段vanToken 在 header 中传递
JSONObject testRequest = new JSONObject();
testRequest.put("text", "【系统健康检查】微信推送服务运行正常 - " + new java.util.Date());
String jsonBody = testRequest.toJSONString();
// 发送POST请求进行健康检查
String healthResult = sendPostWithHeaders(healthCheckUrl, jsonBody, PUSH_TOKEN);
if (healthResult == null || healthResult.trim().isEmpty()) {
logger.warn("微信推送服务健康检查返回空结果");
return new IWxSendService.HealthCheckResult(false, "异常", "健康检查返回空结果", healthCheckUrl);
}
// 尝试解析JSON响应
try {
JSONObject response = JSON.parseObject(healthResult);
if (response != null) {
Integer code = response.getInteger("code");
Boolean success = response.getBoolean("success");
String msg = response.getString("msg");
// 检查是否成功code为200或0或者success为true
if ((code != null && (code == 200 || code == 0)) || Boolean.TRUE.equals(success)) {
logger.debug("微信推送服务健康检查通过,消息已发送");
return new IWxSendService.HealthCheckResult(true, "正常", "服务运行正常,健康检查消息已发送", healthCheckUrl);
} else {
// 检查是否是token验证失败说明服务可用只是token问题
if (msg != null && (msg.contains("vanToken") || msg.contains("token"))) {
logger.debug("微信推送服务健康检查通过服务可用token验证失败");
return new IWxSendService.HealthCheckResult(true, "正常", "服务运行正常token验证失败", healthCheckUrl);
}
logger.warn("微信推送服务健康检查失败 - 响应: {}", healthResult);
return new IWxSendService.HealthCheckResult(false, "异常", "健康检查返回异常状态: " + (msg != null ? msg : healthResult), healthCheckUrl);
}
}
} catch (Exception e) {
// 如果不是JSON格式检查是否包含成功标识
String lowerResult = healthResult.toLowerCase();
if (lowerResult.contains("ok") || lowerResult.contains("success") || lowerResult.contains("正常")) {
logger.debug("微信推送服务健康检查通过非JSON格式");
return new IWxSendService.HealthCheckResult(true, "正常", "服务运行正常", healthCheckUrl);
}
}
logger.warn("微信推送服务健康检查失败 - 响应: {}", healthResult);
return new IWxSendService.HealthCheckResult(false, "异常", "健康检查返回异常状态: " + healthResult, healthCheckUrl);
} catch (Exception e) {
logger.error("微信推送服务健康检查异常 - URL: {}, 错误: {}", healthCheckUrl, e.getMessage(), e);
return new IWxSendService.HealthCheckResult(false, "异常", "健康检查异常: " + e.getMessage(), healthCheckUrl);
}
}
/**
* 发送POST请求支持自定义header用于健康检查
* @param url 请求URL
* @param jsonBody JSON请求体
* @param token 认证token
* @return 响应结果
*/
private String sendPostWithHeaders(String url, String jsonBody, String token) {
java.io.BufferedReader in = null;
java.io.PrintWriter out = null;
StringBuilder result = new StringBuilder();
try {
java.net.URL realUrl = new java.net.URL(url);
java.net.URLConnection conn = realUrl.openConnection();
// 设置请求头
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("vanToken", token);
conn.setRequestProperty("source", "XZJ_UBUNTU");
conn.setDoOutput(true);
conn.setDoInput(true);
// 设置超时时间(健康检查需要快速响应)
conn.setConnectTimeout(5000); // 5秒连接超时
conn.setReadTimeout(5000); // 5秒读取超时
// 发送请求体
out = new java.io.PrintWriter(conn.getOutputStream());
out.print(jsonBody);
out.flush();
// 读取响应
in = new java.io.BufferedReader(new java.io.InputStreamReader(conn.getInputStream(), java.nio.charset.StandardCharsets.UTF_8));
String line;
while ((line = in.readLine()) != null) {
result.append(line);
}
logger.debug("微信推送服务健康检查请求成功 - URL: {}, 响应: {}", url, result.toString());
} catch (java.net.ConnectException e) {
logger.error("微信推送服务健康检查连接失败 - URL: {}", url, e);
throw new RuntimeException("健康检查连接失败: " + e.getMessage(), e);
} catch (java.net.SocketTimeoutException e) {
logger.error("微信推送服务健康检查超时 - URL: {}", url, e);
throw new RuntimeException("健康检查请求超时: " + e.getMessage(), e);
} catch (java.io.IOException e) {
logger.error("微信推送服务健康检查IO异常 - URL: {}", url, e);
throw new RuntimeException("健康检查IO异常: " + e.getMessage(), e);
} catch (Exception e) {
logger.error("微信推送服务健康检查异常 - URL: {}", url, e);
throw new RuntimeException("健康检查异常: " + e.getMessage(), e);
} finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (java.io.IOException ex) {
logger.error("关闭流异常", ex);
}
}
return result.toString();
}
}

View File

@@ -13,7 +13,7 @@ import java.util.List;
/**
* 物流信息扫描定时任务
* 每1小时扫描一次分销标记为F或PDD的订单获取物流信息并推送
* 每20分钟扫描一次分销标记为F或PDD的订单最近30天,获取物流信息并推送
*/
@Component
public class LogisticsScanTask {
@@ -26,15 +26,16 @@ public class LogisticsScanTask {
private ILogisticsService logisticsService;
/**
* 定时任务:每1小时执行一次
* Cron表达式0 0 * * * ? 表示每小时的第0分钟执行
* 定时任务:每20分钟执行一次
* Cron表达式格式0 每N分钟 * * * ? 表示每N分钟执行一次
* 只扫描最近30天的订单
*/
@Scheduled(cron = "0 */30 * * * ?")
@Scheduled(cron = "0 */10 * * * ?")
public void scanAndFetchLogistics() {
logger.info("========== 开始执行物流信息扫描定时任务 ==========");
logger.info("========== 开始执行物流信息扫描定时任务最近30天订单 ==========");
try {
// 查询分销标记为F或PDD且有物流链接的订单
// 查询分销标记为F或PDD且有物流链接的订单最近30天
List<JDOrder> orders = jdOrderService.selectJDOrderListByDistributionMarkFOrPDD();
if (orders == null || orders.isEmpty()) {
@@ -54,7 +55,7 @@ public class LogisticsScanTask {
try {
// 检查Redis中是否已处理过避免重复处理
if (logisticsService.isOrderProcessed(order.getId())) {
logger.debug("订单已处理过,跳过 - 订单ID: {}", order.getId());
//logger.debug("订单已处理过,跳过 - 订单ID: {}", order.getId());
skippedCount++;
continue;
}

View File

@@ -0,0 +1,284 @@
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.InputStream;
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();
StringBuilder response = new StringBuilder();
try {
BufferedReader reader;
InputStream inputStream;
if (statusCode >= 200 && statusCode < 300) {
inputStream = conn.getInputStream();
} else {
// 对于错误响应,尝试读取错误流
inputStream = conn.getErrorStream();
// 如果错误流为null尝试读取正常流某些服务器可能将错误信息放在正常流中
if (inputStream == null) {
inputStream = conn.getInputStream();
}
}
// 如果输入流仍然为null说明无法读取响应
if (inputStream == null) {
log.warn("无法读取HTTP响应流状态码: {}", statusCode);
response.append("无法读取响应内容");
} else {
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
}
} catch (Exception e) {
log.error("读取HTTP响应失败", e);
response.append("读取响应失败: ").append(e.getMessage());
}
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();
StringBuilder response = new StringBuilder();
try {
BufferedReader reader;
InputStream inputStream;
if (statusCode >= 200 && statusCode < 300) {
inputStream = conn.getInputStream();
} else {
// 对于错误响应,尝试读取错误流
inputStream = conn.getErrorStream();
// 如果错误流为null尝试读取正常流某些服务器可能将错误信息放在正常流中
if (inputStream == null) {
inputStream = conn.getInputStream();
}
}
// 如果输入流仍然为null说明无法读取响应
if (inputStream == null) {
log.warn("无法读取HTTP响应流状态码: {}", statusCode);
response.append("无法读取响应内容");
} else {
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
}
} catch (Exception e) {
log.error("读取HTTP响应失败", e);
response.append("读取响应失败: ").append(e.getMessage());
}
String responseStr = response.toString();
log.info("HTTP响应: statusCode={}, url={}, response={}", statusCode, url, responseStr);
if (statusCode < 200 || statusCode >= 300) {
String errorMsg = String.format("HTTP请求失败: statusCode=%d, url=%s, response=%s",
statusCode, url, responseStr);
log.error(errorMsg);
throw new RuntimeException(errorMsg);
}
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);
}
}
}

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.jarvis.mapper.CommentMapper">
<resultMap id="CommentResult" type="Comment">
<result property="id" column="id"/>
<result property="productId" column="product_id"/>
<result property="userName" column="user_name"/>
<result property="commentText" column="comment_text"/>
<result property="commentId" column="comment_id"/>
<result property="pictureUrls" column="picture_urls"/>
<result property="createdAt" column="created_at"/>
<result property="commentDate" column="comment_date"/>
<result property="isUse" column="is_use"/>
</resultMap>
<sql id="selectCommentBase">
select id, product_id, user_name, comment_text, comment_id, picture_urls,
created_at, comment_date, is_use
from comments
</sql>
<select id="selectCommentList" parameterType="Comment" resultMap="CommentResult">
<include refid="selectCommentBase"/>
<where>
<if test="productId != null and productId != ''"> and product_id = #{productId}</if>
<if test="userName != null and userName != ''"> and user_name like concat('%', #{userName}, '%')</if>
<if test="commentText != null and commentText != ''"> and comment_text like concat('%', #{commentText}, '%')</if>
<if test="isUse != null"> and is_use = #{isUse}</if>
<if test="params.beginTime != null and params.beginTime != ''">
and created_at &gt;= #{params.beginTime}
</if>
<if test="params.endTime != null and params.endTime != ''">
and created_at &lt;= #{params.endTime}
</if>
</where>
order by created_at desc
</select>
<select id="selectCommentById" parameterType="Long" resultMap="CommentResult">
<include refid="selectCommentBase"/>
where id = #{id}
</select>
<select id="selectCommentStatisticsByProductId" parameterType="String" resultType="java.util.Map">
select
count(*) as totalCount,
sum(case when is_use = 0 then 1 else 0 end) as availableCount,
sum(case when is_use = 1 then 1 else 0 end) as usedCount,
(select max(created_at) from comments where product_id = #{productId}) as lastCommentUpdateTime
from comments
where product_id = #{productId}
</select>
<update id="updateCommentIsUse" parameterType="Comment">
update comments
<set>
<if test="isUse != null">is_use = #{isUse},</if>
</set>
where id = #{id}
</update>
<update id="resetCommentIsUseByProductId" parameterType="String">
update comments
set is_use = 0
where product_id = #{productId}
</update>
<delete id="deleteCommentByIds" parameterType="String">
delete from comments where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>

View File

@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.jarvis.mapper.ErpProductMapper">
<resultMap type="ErpProduct" id="ErpProductResult">
<result property="id" column="id" />
<result property="productId" column="product_id" />
<result property="title" column="title" />
<result property="mainImage" column="main_image" />
<result property="price" column="price" />
<result property="stock" column="stock" />
<result property="productStatus" column="product_status" />
<result property="saleStatus" column="sale_status" />
<result property="userName" column="user_name" />
<result property="onlineTime" column="online_time" />
<result property="offlineTime" column="offline_time" />
<result property="soldTime" column="sold_time" />
<result property="createTimeXy" column="create_time_xy" />
<result property="updateTimeXy" column="update_time_xy" />
<result property="appid" column="appid" />
<result property="productUrl" column="product_url" />
<result property="remark" column="remark" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectErpProductVo">
select id, product_id, title, main_image, price, stock, product_status, sale_status,
user_name, online_time, offline_time, sold_time, create_time_xy, update_time_xy,
appid, product_url, remark, create_time, update_time
from erp_product
</sql>
<select id="selectErpProductList" parameterType="ErpProduct" resultMap="ErpProductResult">
<include refid="selectErpProductVo"/>
<where>
<if test="productId != null "> and product_id = #{productId}</if>
<if test="title != null and title != ''"> and title like concat('%', #{title}, '%')</if>
<if test="productStatus != null "> and product_status = #{productStatus}</if>
<if test="saleStatus != null "> and sale_status = #{saleStatus}</if>
<if test="userName != null and userName != ''"> and user_name = #{userName}</if>
<if test="appid != null and appid != ''"> and appid = #{appid}</if>
</where>
order by update_time_xy desc, id desc
</select>
<select id="selectErpProductById" parameterType="Long" resultMap="ErpProductResult">
<include refid="selectErpProductVo"/>
where id = #{id}
</select>
<select id="selectErpProductByProductIdAndAppid" resultMap="ErpProductResult">
<include refid="selectErpProductVo"/>
where product_id = #{productId} and appid = #{appid}
</select>
<insert id="insertErpProduct" parameterType="ErpProduct" useGeneratedKeys="true" keyProperty="id">
insert into erp_product
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="productId != null">product_id,</if>
<if test="title != null">title,</if>
<if test="mainImage != null">main_image,</if>
<if test="price != null">price,</if>
<if test="stock != null">stock,</if>
<if test="productStatus != null">product_status,</if>
<if test="saleStatus != null">sale_status,</if>
<if test="userName != null">user_name,</if>
<if test="onlineTime != null">online_time,</if>
<if test="offlineTime != null">offline_time,</if>
<if test="soldTime != null">sold_time,</if>
<if test="createTimeXy != null">create_time_xy,</if>
<if test="updateTimeXy != null">update_time_xy,</if>
<if test="appid != null">appid,</if>
<if test="productUrl != null">product_url,</if>
<if test="remark != null">remark,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="productId != null">#{productId},</if>
<if test="title != null">#{title},</if>
<if test="mainImage != null">#{mainImage},</if>
<if test="price != null">#{price},</if>
<if test="stock != null">#{stock},</if>
<if test="productStatus != null">#{productStatus},</if>
<if test="saleStatus != null">#{saleStatus},</if>
<if test="userName != null">#{userName},</if>
<if test="onlineTime != null">#{onlineTime},</if>
<if test="offlineTime != null">#{offlineTime},</if>
<if test="soldTime != null">#{soldTime},</if>
<if test="createTimeXy != null">#{createTimeXy},</if>
<if test="updateTimeXy != null">#{updateTimeXy},</if>
<if test="appid != null">#{appid},</if>
<if test="productUrl != null">#{productUrl},</if>
<if test="remark != null">#{remark},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
</trim>
</insert>
<update id="updateErpProduct" parameterType="ErpProduct">
update erp_product
<trim prefix="SET" suffixOverrides=",">
<if test="productId != null">product_id = #{productId},</if>
<if test="title != null">title = #{title},</if>
<if test="mainImage != null">main_image = #{mainImage},</if>
<if test="price != null">price = #{price},</if>
<if test="stock != null">stock = #{stock},</if>
<if test="productStatus != null">product_status = #{productStatus},</if>
<if test="saleStatus != null">sale_status = #{saleStatus},</if>
<if test="userName != null">user_name = #{userName},</if>
<if test="onlineTime != null">online_time = #{onlineTime},</if>
<if test="offlineTime != null">offline_time = #{offlineTime},</if>
<if test="soldTime != null">sold_time = #{soldTime},</if>
<if test="createTimeXy != null">create_time_xy = #{createTimeXy},</if>
<if test="updateTimeXy != null">update_time_xy = #{updateTimeXy},</if>
<if test="appid != null">appid = #{appid},</if>
<if test="productUrl != null">product_url = #{productUrl},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
</trim>
where id = #{id}
</update>
<delete id="deleteErpProductById" parameterType="Long">
delete from erp_product where id = #{id}
</delete>
<delete id="deleteErpProductByIds" parameterType="String">
delete from erp_product where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
<insert id="batchInsertOrUpdateErpProduct" parameterType="java.util.List">
insert into erp_product
(product_id, title, main_image, price, stock, product_status, sale_status,
user_name, online_time, offline_time, sold_time, create_time_xy, update_time_xy,
appid, product_url, remark)
values
<foreach collection="list" item="item" separator=",">
(#{item.productId}, #{item.title}, #{item.mainImage}, #{item.price}, #{item.stock},
#{item.productStatus}, #{item.saleStatus}, #{item.userName}, #{item.onlineTime},
#{item.offlineTime}, #{item.soldTime}, #{item.createTimeXy}, #{item.updateTimeXy},
#{item.appid}, #{item.productUrl}, #{item.remark})
</foreach>
ON DUPLICATE KEY UPDATE
title = VALUES(title),
main_image = VALUES(main_image),
price = VALUES(price),
stock = VALUES(stock),
product_status = VALUES(product_status),
sale_status = VALUES(sale_status),
user_name = VALUES(user_name),
online_time = VALUES(online_time),
offline_time = VALUES(offline_time),
sold_time = VALUES(sold_time),
create_time_xy = VALUES(create_time_xy),
update_time_xy = VALUES(update_time_xy),
product_url = VALUES(product_url),
remark = VALUES(remark),
update_time = NOW()
</insert>
</mapper>

View File

@@ -23,11 +23,25 @@
<result property="isCountEnabled" column="is_count_enabled"/>
<result property="thirdPartyOrderNo" column="third_party_order_no"/>
<result property="jingfenActualPrice" column="jingfen_actual_price"/>
<result property="isRefunded" column="is_refunded"/>
<result property="refundDate" column="refund_date"/>
<result property="isRefundReceived" column="is_refund_received"/>
<result property="refundReceivedDate" column="refund_received_date"/>
<result property="isRebateReceived" column="is_rebate_received"/>
<result property="rebateReceivedDate" column="rebate_received_date"/>
<result property="isPriceProtected" column="is_price_protected"/>
<result property="priceProtectedDate" column="price_protected_date"/>
<result property="isInvoiceOpened" column="is_invoice_opened"/>
<result property="invoiceOpenedDate" column="invoice_opened_date"/>
<result property="isReviewPosted" column="is_review_posted"/>
<result property="reviewPostedDate" column="review_posted_date"/>
</resultMap>
<sql id="selectJDOrderBase">
select id, remark, distribution_mark, model_number, link, payment_amount, rebate_amount,
address, logistics_link, order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price
address, logistics_link, order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price,
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date,
is_price_protected, price_protected_date, is_invoice_opened, invoice_opened_date, is_review_posted, review_posted_date
from jd_order
</sql>
@@ -35,18 +49,29 @@
<include refid="selectJDOrderBase"/>
<where>
<if test="remark != null and remark != ''"> and remark like concat('%', #{remark}, '%')</if>
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark like concat('%', #{distributionMark}, '%')</if>
<if test="params.orderSearch != null and params.orderSearch != ''">
and (
order_id like concat('%', #{params.orderSearch}, '%')
or third_party_order_no like concat('%', #{params.orderSearch}, '%')
or distribution_mark like concat('%', #{params.orderSearch}, '%')
)
</if>
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
<if test="modelNumber != null and modelNumber != ''"> and model_number like concat('%', #{modelNumber}, '%')</if>
<if test="link != null and link != ''"> and link like concat('%', #{link}, '%')</if>
<if test="paymentAmount != null"> and payment_amount = #{paymentAmount}</if>
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
<if test="address != null and address != ''"> and address like concat('%', #{address}, '%')</if>
<if test="logisticsLink != null and logisticsLink != ''"> and logistics_link like concat('%', #{logisticsLink}, '%')</if>
<if test="orderId != null and orderId != ''"> and order_id like concat('%', #{orderId}, '%')</if>
<if test="thirdPartyOrderNo != null and thirdPartyOrderNo != ''"> and third_party_order_no like concat('%', #{thirdPartyOrderNo}, '%')</if>
<if test="buyer != null and buyer != ''"> and buyer like concat('%', #{buyer}, '%')</if>
<if test="orderTime != null"> and order_time = #{orderTime}</if>
<if test="status != null and status != ''"> and status like concat('%', #{status}, '%')</if>
<if test="isRefunded != null"> and is_refunded = #{isRefunded}</if>
<if test="isRefundReceived != null"> and is_refund_received = #{isRefundReceived}</if>
<if test="isRebateReceived != null"> and is_rebate_received = #{isRebateReceived}</if>
<if test="isPriceProtected != null"> and is_price_protected = #{isPriceProtected}</if>
<if test="isInvoiceOpened != null"> and is_invoice_opened = #{isInvoiceOpened}</if>
<if test="isReviewPosted != null"> and is_review_posted = #{isReviewPosted}</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
and date(order_time) &gt;= #{params.beginTime}
</if>
@@ -61,18 +86,29 @@
<include refid="selectJDOrderBase"/>
<where>
<if test="remark != null and remark != ''"> and remark like concat('%', #{remark}, '%')</if>
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark like concat('%', #{distributionMark}, '%')</if>
<if test="params.orderSearch != null and params.orderSearch != ''">
and (
order_id like concat('%', #{params.orderSearch}, '%')
or third_party_order_no like concat('%', #{params.orderSearch}, '%')
or distribution_mark like concat('%', #{params.orderSearch}, '%')
)
</if>
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
<if test="modelNumber != null and modelNumber != ''"> and model_number like concat('%', #{modelNumber}, '%')</if>
<if test="link != null and link != ''"> and link like concat('%', #{link}, '%')</if>
<if test="paymentAmount != null"> and payment_amount = #{paymentAmount}</if>
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
<if test="address != null and address != ''"> and address like concat('%', #{address}, '%')</if>
<if test="logisticsLink != null and logisticsLink != ''"> and logistics_link like concat('%', #{logisticsLink}, '%')</if>
<if test="orderId != null and orderId != ''"> and order_id like concat('%', #{orderId}, '%')</if>
<if test="thirdPartyOrderNo != null and thirdPartyOrderNo != ''"> and third_party_order_no like concat('%', #{thirdPartyOrderNo}, '%')</if>
<if test="buyer != null and buyer != ''"> and buyer like concat('%', #{buyer}, '%')</if>
<if test="orderTime != null"> and order_time = #{orderTime}</if>
<if test="status != null and status != ''"> and status like concat('%', #{status}, '%')</if>
<if test="isRefunded != null"> and is_refunded = #{isRefunded}</if>
<if test="isRefundReceived != null"> and is_refund_received = #{isRefundReceived}</if>
<if test="isRebateReceived != null"> and is_rebate_received = #{isRebateReceived}</if>
<if test="isPriceProtected != null"> and is_price_protected = #{isPriceProtected}</if>
<if test="isInvoiceOpened != null"> and is_invoice_opened = #{isInvoiceOpened}</if>
<if test="isReviewPosted != null"> and is_review_posted = #{isReviewPosted}</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
and date(order_time) &gt;= #{params.beginTime}
</if>
@@ -102,12 +138,16 @@
remark, distribution_mark, model_number, link,
payment_amount, rebate_amount, address, logistics_link,
tencent_doc_pushed, tencent_doc_push_time,
order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price
order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price,
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date,
is_price_protected, price_protected_date, is_invoice_opened, invoice_opened_date, is_review_posted, review_posted_date
) values (
#{remark}, #{distributionMark}, #{modelNumber}, #{link},
#{paymentAmount}, #{rebateAmount}, #{address}, #{logisticsLink},
0, null,
#{orderId}, #{buyer}, #{orderTime}, now(), now(), #{status}, #{isCountEnabled}, #{thirdPartyOrderNo}, #{jingfenActualPrice}
#{orderId}, #{buyer}, #{orderTime}, now(), now(), #{status}, #{isCountEnabled}, #{thirdPartyOrderNo}, #{jingfenActualPrice},
#{isRefunded}, #{refundDate}, #{isRefundReceived}, #{refundReceivedDate}, #{isRebateReceived}, #{rebateReceivedDate},
#{isPriceProtected}, #{priceProtectedDate}, #{isInvoiceOpened}, #{invoiceOpenedDate}, #{isReviewPosted}, #{reviewPostedDate}
)
</insert>
@@ -131,6 +171,18 @@
<if test="isCountEnabled != null"> is_count_enabled = #{isCountEnabled},</if>
<if test="thirdPartyOrderNo != null"> third_party_order_no = #{thirdPartyOrderNo},</if>
<if test="jingfenActualPrice != null"> jingfen_actual_price = #{jingfenActualPrice},</if>
<if test="isRefunded != null"> is_refunded = #{isRefunded},</if>
<if test="refundDate != null"> refund_date = #{refundDate},</if>
<if test="isRefundReceived != null"> is_refund_received = #{isRefundReceived},</if>
<if test="refundReceivedDate != null"> refund_received_date = #{refundReceivedDate},</if>
<if test="isRebateReceived != null"> is_rebate_received = #{isRebateReceived},</if>
<if test="rebateReceivedDate != null"> rebate_received_date = #{rebateReceivedDate},</if>
<if test="isPriceProtected != null"> is_price_protected = #{isPriceProtected},</if>
<if test="priceProtectedDate != null"> price_protected_date = #{priceProtectedDate},</if>
<if test="isInvoiceOpened != null"> is_invoice_opened = #{isInvoiceOpened},</if>
<if test="invoiceOpenedDate != null"> invoice_opened_date = #{invoiceOpenedDate},</if>
<if test="isReviewPosted != null"> is_review_posted = #{isReviewPosted},</if>
<if test="reviewPostedDate != null"> review_posted_date = #{reviewPostedDate},</if>
update_time = now()
</set>
where id = #{id}
@@ -161,7 +213,7 @@
order by order_time desc
limit 1
</select>
<select id="selectJDOrderByThirdPartyOrderNo" parameterType="string" resultMap="JDOrderResult">
<include refid="selectJDOrderBase"/>
where third_party_order_no = #{thirdPartyOrderNo}
@@ -178,9 +230,10 @@
<select id="selectJDOrderListByDistributionMarkFOrPDD" resultMap="JDOrderResult">
<include refid="selectJDOrderBase"/>
<where>
(distribution_mark = 'F' OR distribution_mark = 'PDD')
(distribution_mark = 'F' OR distribution_mark = 'PDD' OR distribution_mark = 'H' OR distribution_mark = 'W' OR distribution_mark = 'PDD-W')
AND logistics_link IS NOT NULL
AND logistics_link != ''
AND create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
</where>
ORDER BY create_time DESC
</select>

View File

@@ -11,12 +11,13 @@
<result property="secretKey" column="secret_key"/>
<result property="isActive" column="is_active"/>
<result property="isCount" column="is_count"/>
<result property="touser" column="touser"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>
<sql id="selectSuperAdminVo">
select id, wxid, name, union_id, app_key, secret_key, is_active, is_count, created_at, updated_at from super_admin
select id, wxid, name, union_id, app_key, secret_key, is_active, is_count, touser, created_at, updated_at from super_admin
</sql>
<select id="selectSuperAdminList" parameterType="SuperAdmin" resultMap="SuperAdminResult">
@@ -51,6 +52,8 @@
<if test="appKey != null and appKey != ''">app_key,</if>
<if test="secretKey != null and secretKey != ''">secret_key,</if>
<if test="isActive != null">is_active,</if>
<if test="isCount != null">is_count,</if>
<if test="touser != null and touser != ''">touser,</if>
created_at,
updated_at,
</trim>
@@ -61,6 +64,8 @@
<if test="appKey != null and appKey != ''">#{appKey},</if>
<if test="secretKey != null and secretKey != ''">#{secretKey},</if>
<if test="isActive != null">#{isActive},</if>
<if test="isCount != null">#{isCount},</if>
<if test="touser != null and touser != ''">#{touser},</if>
now(),
now(),
</trim>
@@ -76,6 +81,7 @@
<if test="secretKey != null and secretKey != ''">secret_key = #{secretKey},</if>
<if test="isActive != null">is_active = #{isActive},</if>
<if test="isCount != null">is_count = #{isCount},</if>
<if test="touser != null">touser = #{touser},</if>
updated_at = now(),
</trim>
where id = #{id}

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.jarvis.mapper.TaobaoCommentMapper">
<resultMap id="TaobaoCommentResult" type="TaobaoComment">
<result property="id" column="id"/>
<result property="productId" column="product_id"/>
<result property="userName" column="user_name"/>
<result property="commentText" column="comment_text"/>
<result property="commentId" column="comment_id"/>
<result property="pictureUrls" column="picture_urls"/>
<result property="createdAt" column="created_at"/>
<result property="commentDate" column="comment_date"/>
<result property="isUse" column="is_use"/>
</resultMap>
<sql id="selectTaobaoCommentBase">
select id, product_id, user_name, comment_text, comment_id, picture_urls,
created_at, comment_date, is_use
from taobao_comments
</sql>
<select id="selectTaobaoCommentList" parameterType="TaobaoComment" resultMap="TaobaoCommentResult">
<include refid="selectTaobaoCommentBase"/>
<where>
<if test="productId != null and productId != ''"> and product_id = #{productId}</if>
<if test="userName != null and userName != ''"> and user_name like concat('%', #{userName}, '%')</if>
<if test="commentText != null and commentText != ''"> and comment_text like concat('%', #{commentText}, '%')</if>
<if test="isUse != null"> and is_use = #{isUse}</if>
<if test="params.beginTime != null and params.beginTime != ''">
and created_at &gt;= #{params.beginTime}
</if>
<if test="params.endTime != null and params.endTime != ''">
and created_at &lt;= #{params.endTime}
</if>
</where>
order by created_at desc
</select>
<select id="selectTaobaoCommentById" parameterType="Integer" resultMap="TaobaoCommentResult">
<include refid="selectTaobaoCommentBase"/>
where id = #{id}
</select>
<select id="selectTaobaoCommentStatisticsByProductId" parameterType="String" resultType="java.util.Map">
select
count(*) as totalCount,
sum(case when is_use = 0 then 1 else 0 end) as availableCount,
sum(case when is_use = 1 then 1 else 0 end) as usedCount,
(select max(created_at) from taobao_comments where product_id = #{productId}) as lastCommentUpdateTime
from taobao_comments
where product_id = #{productId}
</select>
<update id="updateTaobaoCommentIsUse" parameterType="TaobaoComment">
update taobao_comments
<set>
<if test="isUse != null">is_use = #{isUse},</if>
</set>
where id = #{id}
</update>
<update id="resetTaobaoCommentIsUseByProductId" parameterType="String">
update taobao_comments
set is_use = 0
where product_id = #{productId}
</update>
<delete id="deleteTaobaoCommentByIds" parameterType="String">
delete from taobao_comments where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>

View File

@@ -0,0 +1,4 @@
-- 为超级管理员表添加接收人字段
-- 字段说明touser 存储企业微信用户ID多个用逗号分隔
ALTER TABLE super_admin ADD COLUMN touser VARCHAR(500) DEFAULT NULL COMMENT '接收人企业微信用户ID多个用逗号分隔';

View File

@@ -0,0 +1,15 @@
-- 为jd_order表添加新的状态字段点过价保、开过专票、晒过评价
-- 执行日期2025-01-26
ALTER TABLE jd_order
ADD COLUMN is_price_protected INT DEFAULT 0 COMMENT '点过价保0否 1是',
ADD COLUMN price_protected_date DATETIME NULL COMMENT '价保日期',
ADD COLUMN is_invoice_opened INT DEFAULT 0 COMMENT '开过专票0否 1是',
ADD COLUMN invoice_opened_date DATETIME NULL COMMENT '开票日期',
ADD COLUMN is_review_posted INT DEFAULT 0 COMMENT '晒过评价0否 1是',
ADD COLUMN review_posted_date DATETIME NULL COMMENT '评价日期';
-- 添加索引(可选,根据查询需求)
-- CREATE INDEX idx_is_price_protected ON jd_order(is_price_protected);
-- CREATE INDEX idx_is_invoice_opened ON jd_order(is_invoice_opened);
-- CREATE INDEX idx_is_review_posted ON jd_order(is_review_posted);

View File

@@ -0,0 +1,16 @@
-- 为jd_order表添加退款相关字段
-- 执行日期2025-01-XX
ALTER TABLE jd_order
ADD COLUMN is_refunded INT DEFAULT 0 COMMENT '是否退款0否 1是',
ADD COLUMN refund_date DATETIME NULL COMMENT '退款日期',
ADD COLUMN is_refund_received INT DEFAULT 0 COMMENT '是否退款到账0否 1是',
ADD COLUMN refund_received_date DATETIME NULL COMMENT '退款到账日期',
ADD COLUMN is_rebate_received INT DEFAULT 0 COMMENT '后返到账0否 1是',
ADD COLUMN rebate_received_date DATETIME NULL COMMENT '后返到账日期';
-- 添加索引(可选,根据查询需求)
-- CREATE INDEX idx_is_refunded ON jd_order(is_refunded);
-- CREATE INDEX idx_is_refund_received ON jd_order(is_refund_received);
-- CREATE INDEX idx_is_rebate_received ON jd_order(is_rebate_received);

View File

@@ -0,0 +1,37 @@
-- 闲鱼商品菜单配置
-- 菜单类型M=目录 C=菜单 F=按钮
-- 1. 主菜单如果是放在系统管理下parent_id为1如果是独立菜单需要先创建一个jarvis目录
-- 假设放在系统管理parent_id=1可以根据实际情况调整
-- 闲鱼商品管理菜单(主菜单)
insert into sys_menu values(2000, '闲鱼商品管理', 1, 10, 'erpProduct', 'system/erpProduct/index', '', '', 1, 0, 'C', '0', '0', 'jarvis:erpProduct:list', 'shopping', 'admin', sysdate(), '', null, '闲鱼商品管理菜单');
-- 闲鱼商品管理按钮权限
insert into sys_menu values(2001, '商品查询', 2000, 1, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2002, '商品新增', 2000, 2, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2003, '商品修改', 2000, 3, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2004, '商品删除', 2000, 4, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2005, '商品导出', 2000, 5, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:export', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2006, '拉取商品', 2000, 6, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:pull', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2007, '批量上架', 2000, 7, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:publish', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2008, '批量下架', 2000, 8, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:downShelf', '#', 'admin', sysdate(), '', null, '');
-- 给管理员角色role_id=1添加闲鱼商品菜单权限
insert into sys_role_menu(role_id, menu_id) values(1, 2000);
insert into sys_role_menu(role_id, menu_id) values(1, 2001);
insert into sys_role_menu(role_id, menu_id) values(1, 2002);
insert into sys_role_menu(role_id, menu_id) values(1, 2003);
insert into sys_role_menu(role_id, menu_id) values(1, 2004);
insert into sys_role_menu(role_id, menu_id) values(1, 2005);
insert into sys_role_menu(role_id, menu_id) values(1, 2006);
insert into sys_role_menu(role_id, menu_id) values(1, 2007);
insert into sys_role_menu(role_id, menu_id) values(1, 2008);
-- 注意:
-- 1. 如果菜单需要放在其他目录下比如jarvis目录请修改parent_id
-- 2. order_num 是显示顺序,可以根据需要调整(值越大越靠后)
-- 3. 如果管理员角色ID不是1请修改上面的role_id值
-- 4. 如果需要给其他角色添加权限可以复制上面的insert语句并修改role_id
-- 5. 执行完此SQL后需要清除Redis缓存或重启系统才能看到菜单

31
sql/闲鱼商品表.sql Normal file
View File

@@ -0,0 +1,31 @@
-- 闲鱼商品表
CREATE TABLE `erp_product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`product_id` bigint(20) NOT NULL COMMENT '管家商品ID',
`title` varchar(500) DEFAULT NULL COMMENT '商品标题',
`main_image` varchar(1000) DEFAULT NULL COMMENT '商品图片(主图)',
`price` bigint(20) DEFAULT NULL COMMENT '商品价格(分)',
`stock` int(11) DEFAULT NULL COMMENT '商品库存',
`product_status` int(11) DEFAULT NULL COMMENT '商品状态 -1:删除 21:待发布 22:销售中 23:已售罄 31:手动下架 33:售出下架 36:自动下架',
`sale_status` int(11) DEFAULT NULL COMMENT '销售状态',
`user_name` varchar(100) DEFAULT NULL COMMENT '闲鱼会员名',
`online_time` bigint(20) DEFAULT NULL COMMENT '上架时间(时间戳)',
`offline_time` bigint(20) DEFAULT NULL COMMENT '下架时间(时间戳)',
`sold_time` bigint(20) DEFAULT NULL COMMENT '售出时间(时间戳)',
`create_time_xy` bigint(20) DEFAULT NULL COMMENT '创建时间(闲鱼,时间戳)',
`update_time_xy` bigint(20) DEFAULT NULL COMMENT '更新时间(闲鱼,时间戳)',
`appid` varchar(100) DEFAULT NULL COMMENT 'ERP应用ID',
`product_url` varchar(1000) DEFAULT NULL COMMENT '商品链接',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_id_appid` (`product_id`, `appid`),
KEY `idx_product_id` (`product_id`),
KEY `idx_appid` (`appid`),
KEY `idx_product_status` (`product_status`),
KEY `idx_user_name` (`user_name`),
KEY `idx_online_time` (`online_time`),
KEY `idx_update_time_xy` (`update_time_xy`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闲鱼商品表';