Compare commits
124 Commits
c0908690b4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72ff30567b | ||
|
|
caa36c4966 | ||
|
|
269e8e48a7 | ||
|
|
dde274acba | ||
|
|
0297c6e131 | ||
|
|
6394658a70 | ||
|
|
dafd63a9ec | ||
|
|
5a8e1198cf | ||
|
|
6257d816e9 | ||
|
|
3336eeb6aa | ||
|
|
18f541fdf7 | ||
|
|
9cb5e6a488 | ||
|
|
2ce45e5ccc | ||
|
|
c837917be3 | ||
|
|
4379277a08 | ||
|
|
ff9ab96833 | ||
|
|
2b74f77419 | ||
|
|
76500642eb | ||
|
|
a41c9ceaf9 | ||
|
|
7f7bec8d29 | ||
|
|
8802e68106 | ||
|
|
2fb283c3f3 | ||
|
|
f044417d8d | ||
|
|
ba1e025326 | ||
|
|
61f66d90b4 | ||
|
|
e93e7cec46 | ||
|
|
d5df4108a6 | ||
|
|
c58adef068 | ||
|
|
b43daf2965 | ||
|
|
ab062b3b5a | ||
|
|
01b19602b6 | ||
|
|
01e5312ccc | ||
|
|
00f0c38672 | ||
|
|
ba6f250914 | ||
|
|
fb00ecbcb6 | ||
|
|
9483ebf1f5 | ||
|
|
440df7d538 | ||
|
|
2fe78ec192 | ||
|
|
ccf8298e17 | ||
|
|
46d2a209c0 | ||
|
|
9935c6c07e | ||
|
|
23c59a5e52 | ||
|
|
0556f19e97 | ||
|
|
74d3579e1e | ||
|
|
160c97eb5b | ||
|
|
407640ff96 | ||
|
|
81203488c8 | ||
|
|
e9747e6af2 | ||
|
|
4ba1f6a572 | ||
|
|
d6ab231534 | ||
|
|
dfa6109788 | ||
|
|
c56b911db0 | ||
|
|
d98711f06a | ||
|
|
40dd64482c | ||
|
|
77dcc149c3 | ||
|
|
1a14830dac | ||
|
|
30ca39a4b6 | ||
|
|
237f0c88ad | ||
|
|
2fd371f2f4 | ||
|
|
c661e921df | ||
|
|
f4c07859e4 | ||
|
|
d1c374ca99 | ||
|
|
317ab03c7c | ||
| b86c4bea88 | |||
|
|
9a8c7b1039 | ||
|
|
632b9f7eb1 | ||
|
|
eb53915bcd | ||
|
|
4dd3e9dd70 | ||
|
|
9206824efb | ||
|
|
2524461ff4 | ||
|
|
7581cc02a9 | ||
|
|
1dc91a6bb0 | ||
|
|
6b3c2b17c8 | ||
|
|
e890b18e3e | ||
|
|
9b2b770e29 | ||
|
|
047575ea42 | ||
|
|
702463b856 | ||
|
|
3aa3da8ade | ||
|
|
20861d270a | ||
|
|
e7c991ed9c | ||
|
|
2ead103faa | ||
|
|
c541beb413 | ||
|
|
083bcca270 | ||
|
|
35dcb20e4a | ||
|
|
7648b934ed | ||
|
|
01f0be6198 | ||
|
|
276fb49354 | ||
|
|
4f917dce10 | ||
|
|
98b56ab11b | ||
|
|
b495431b7e | ||
|
|
7f4b0dd986 | ||
|
|
79c5bf266f | ||
|
|
04156492a6 | ||
|
|
f578b9b2c9 | ||
|
|
6b07fa1d75 | ||
|
|
978da7042d | ||
|
|
66ac54ca70 | ||
|
|
026c6bf2a3 | ||
|
|
2b0587d4e1 | ||
|
|
0880628c93 | ||
|
|
2e59f49677 | ||
|
|
a54c8cc0cd | ||
|
|
8a23c4d3f7 | ||
|
|
b8981ffc98 | ||
|
|
9e69230948 | ||
|
|
64ce923631 | ||
|
|
2cd3a0a798 | ||
|
|
8889791a83 | ||
|
|
e184c7926f | ||
|
|
d73c7b6560 | ||
|
|
9d8f2ded0c | ||
|
|
7294748ae9 | ||
|
|
142b395dbe | ||
|
|
c8b15275a4 | ||
|
|
a61003fb7c | ||
|
|
939d03e192 | ||
|
|
e2facc3099 | ||
|
|
af68b529b0 | ||
|
|
185483dace | ||
|
|
e79e7081ee | ||
|
|
3176e45057 | ||
|
|
72b3458ef9 | ||
|
|
00149dc198 | ||
|
|
10020e6d52 |
119
doc/WPS365回调地址配置说明.md
Normal file
119
doc/WPS365回调地址配置说明.md
Normal 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平台可能有缓存)
|
||||
|
||||
259
doc/WPS365授权错误排查指南.md
Normal file
259
doc/WPS365授权错误排查指南.md
Normal 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. **应用ID(AppId)** 是否与配置文件中的 `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平台的应用配置截图(隐藏敏感信息)
|
||||
|
||||
271
doc/WPS365读取在线表格配置指南.md
Normal file
271
doc/WPS365读取在线表格配置指南.md
Normal 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. **数据量限制**:一次读取的数据量不要太大,建议分批读取
|
||||
|
||||
270
doc/WPS365集成使用说明.md
Normal file
270
doc/WPS365集成使用说明.md
Normal 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 + Redis(Token存储)
|
||||
- **前端**:Vue.js + Element UI
|
||||
- **HTTP客户端**:HttpURLConnection(不使用代理,直接连接)
|
||||
|
||||
## 参考文档
|
||||
|
||||
- [WPS365开放平台文档](https://open.wps.cn/)
|
||||
- [用户授权流程](https://open.wps.cn/documents/app-integration-dev/wps365/server/certification-authorization/user-authorization/flow)
|
||||
- [KSheet API文档](https://developer.kdocs.cn/server/ksheet/)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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参数会自动使用配置中的完整URL:https://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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("订单指令执行完成");
|
||||
|
||||
// 记录执行结果
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -8,4 +8,44 @@ tencent:
|
||||
delayed:
|
||||
push:
|
||||
# 延迟时间(分钟),默认10分钟
|
||||
minutes: 30
|
||||
minutes: 10
|
||||
# WPS365开放平台配置
|
||||
# 文档地址:https://open.wps.cn/
|
||||
wps365:
|
||||
# 应用ID(AppId)- 需要在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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
/** 应用ID(AppId) */
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(响应)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -98,9 +110,18 @@ private String cleanForbiddenPhrases(String text) {
|
||||
return text;
|
||||
}
|
||||
String cleaned = text;
|
||||
// 新增:清理【】符号(包括单独出现或成对出现的情况)
|
||||
cleaned = cleaned.replaceAll("【", ""); // 移除左括号【
|
||||
cleaned = cleaned.replaceAll("】", ""); // 移除右括号】
|
||||
// 新增:清理"咨询客服立减""咨询客服""客服"及变体(含空格)
|
||||
// 优先处理长组合,避免被拆分后遗漏
|
||||
cleaned = cleaned.replaceAll("咨询\\s*客服\\s*立减", ""); // 匹配"咨询客服立减""咨询 客服 立减"等
|
||||
cleaned = cleaned.replaceAll("咨询\\s*客服", ""); // 匹配"咨询客服""咨询 客服"等
|
||||
cleaned = cleaned.replaceAll("客\\s*服", ""); // 匹配"客服""客 服"等
|
||||
|
||||
// 一、政策补贴及特殊渠道类(长组合优先)
|
||||
cleaned = cleaned.replaceAll("咨询客服领\\s*国补", "");
|
||||
cleaned = cleaned.replaceAll("政府\\s*补贴", ""); // 匹配"政府 补贴"等带空格的情况
|
||||
cleaned = cleaned.replaceAll("政府\\s*补贴", "");
|
||||
cleaned = cleaned.replaceAll("购车\\s*补贴", "");
|
||||
cleaned = cleaned.replaceAll("家电\\s*下乡", "");
|
||||
cleaned = cleaned.replaceAll("内部\\s*渠道", "");
|
||||
@@ -128,15 +149,15 @@ private String cleanForbiddenPhrases(String text) {
|
||||
cleaned = cleaned.replaceAll("原单", "");
|
||||
cleaned = cleaned.replaceAll("尾单", "");
|
||||
cleaned = cleaned.replaceAll("工厂\\s*货", "");
|
||||
cleaned = cleaned.replaceAll("专柜\\s*验货", ""); // 无授权时违规
|
||||
cleaned = cleaned.replaceAll("专柜\\s*验货", "");
|
||||
|
||||
// 四、线下导流及规避监管类(多变体覆盖)
|
||||
cleaned = cleaned.replaceAll("微\\s*信", ""); // 匹配"微信""微 信"
|
||||
cleaned = cleaned.replaceAll("薇\\s*信", ""); // 谐音变体
|
||||
cleaned = cleaned.replaceAll("微\\s*信", "");
|
||||
cleaned = cleaned.replaceAll("薇\\s*信", "");
|
||||
cleaned = cleaned.replaceAll("V我", "");
|
||||
cleaned = cleaned.replaceAll("加\\s*卫星", "");
|
||||
cleaned = cleaned.replaceAll("QQ", "");
|
||||
cleaned = cleaned.replaceAll("扣扣", ""); // 谐音
|
||||
cleaned = cleaned.replaceAll("扣扣", "");
|
||||
cleaned = cleaned.replaceAll("手机\\s*号", "");
|
||||
cleaned = cleaned.replaceAll("淘宝\\s*链接", "");
|
||||
cleaned = cleaned.replaceAll("拼多\\s*多", "");
|
||||
@@ -146,7 +167,7 @@ private String cleanForbiddenPhrases(String text) {
|
||||
cleaned = cleaned.replaceAll("滚", "");
|
||||
cleaned = cleaned.replaceAll("垃圾", "");
|
||||
cleaned = cleaned.replaceAll("笨蛋", "");
|
||||
cleaned = cleaned.replaceAll("SB", ""); // 单独出现时清理(避免误判可后续加上下文校验)
|
||||
cleaned = cleaned.replaceAll("SB", "");
|
||||
cleaned = cleaned.replaceAll("原味", "");
|
||||
cleaned = cleaned.replaceAll("情趣", "");
|
||||
|
||||
@@ -174,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"));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; // 默认账号
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 返回 null,Access Token 可能无效");
|
||||
}
|
||||
|
||||
|
||||
JSONObject data = userInfo.getJSONObject("data");
|
||||
if (data == null) {
|
||||
log.error("用户信息响应中没有 data 字段,完整响应: {}", userInfo.toJSONString());
|
||||
throw new RuntimeException("无法获取用户数据,请检查Access Token是否有效。响应: " + userInfo.toJSONString());
|
||||
}
|
||||
|
||||
|
||||
String openId = data.getString("openID"); // 注意:官方返回的字段名是 openID(大写ID)
|
||||
if (openId == null || openId.isEmpty()) {
|
||||
log.error("data 对象中没有 openID 字段,data内容: {}", data.toJSONString());
|
||||
throw new RuntimeException("无法获取Open-Id,请检查Access Token是否有效。data: " + data.toJSONString());
|
||||
}
|
||||
|
||||
|
||||
log.info("成功获取 Open ID: {}", openId);
|
||||
log.info("准备调用API - appId: {}, apiBaseUrl: {}", tencentDocConfig.getAppId(), tencentDocConfig.getApiBaseUrl());
|
||||
|
||||
|
||||
JSONObject result = TencentDocApiUtil.readSheetData(
|
||||
accessToken,
|
||||
tencentDocConfig.getAppId(),
|
||||
openId,
|
||||
fileId,
|
||||
sheetId,
|
||||
range,
|
||||
accessToken,
|
||||
tencentDocConfig.getAppId(),
|
||||
openId,
|
||||
fileId,
|
||||
sheetId,
|
||||
range,
|
||||
tencentDocConfig.getApiBaseUrl()
|
||||
);
|
||||
|
||||
log.info("API调用成功,原始返回结果: {}", result != null ? result.toJSONString() : "null");
|
||||
|
||||
|
||||
//log.info("API调用成功,原始返回结果: {}", result != null ? result.toJSONString() : "null");
|
||||
|
||||
// 检查API响应中的错误码
|
||||
// 根据官方文档,成功响应包含 ret=0,错误响应包含 code!=0
|
||||
// 参考:https://docs.qq.com/open/document/app/openapi/v3/sheet/get/get_range.html
|
||||
@@ -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(),
|
||||
|
||||
@@ -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是整数索引
|
||||
// 如果用户只提供了一个ID(fileId),则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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 >= #{params.beginTime}
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
and created_at <= #{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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) >= #{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) >= #{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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 >= #{params.beginTime}
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
and created_at <= #{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>
|
||||
|
||||
4
sql/add_super_admin_touser_field.sql
Normal file
4
sql/add_super_admin_touser_field.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- 为超级管理员表添加接收人字段
|
||||
-- 字段说明:touser 存储企业微信用户ID,多个用逗号分隔
|
||||
ALTER TABLE super_admin ADD COLUMN touser VARCHAR(500) DEFAULT NULL COMMENT '接收人(企业微信用户ID,多个用逗号分隔)';
|
||||
|
||||
15
sql/jd_order_new_status_fields.sql
Normal file
15
sql/jd_order_new_status_fields.sql
Normal 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);
|
||||
16
sql/jd_order_refund_fields.sql
Normal file
16
sql/jd_order_refund_fields.sql
Normal 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);
|
||||
|
||||
37
sql/闲鱼商品菜单.sql
Normal file
37
sql/闲鱼商品菜单.sql
Normal 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
31
sql/闲鱼商品表.sql
Normal 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='闲鱼商品表';
|
||||
|
||||
Reference in New Issue
Block a user