Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6647b07b1 | ||
|
|
8b16d70259 |
+249
@@ -0,0 +1,249 @@
|
|||||||
|
# License 授权保护使用说明
|
||||||
|
|
||||||
|
## 一、功能说明
|
||||||
|
|
||||||
|
本系统已集成 **MAC 地址绑定授权保护**,防止软件被甲方转移到其他服务器或出售给第三方。
|
||||||
|
|
||||||
|
**授权原理:**
|
||||||
|
1. 程序启动时自动采集服务器所有物理网卡的 MAC 地址作为机器指纹
|
||||||
|
2. 使用内置密钥对机器指纹进行 HMAC-SHA256 签名校验
|
||||||
|
3. 与 `license.lic` 文件中的授权码比对,不匹配则拒绝启动
|
||||||
|
4. 签名密钥硬编码在 jar 包内,甲方无法自行伪造授权文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、部署流程(给甲方部署时)
|
||||||
|
|
||||||
|
### 步骤 1:打包项目
|
||||||
|
|
||||||
|
在开发机上执行 Maven 打包:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean package -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
打包完成后,将 `ruoyi-admin/target/ruoyi-admin.jar` 上传到甲方服务器。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 步骤 2:在甲方服务器上生成 License
|
||||||
|
|
||||||
|
**重要:License 必须在目标服务器上生成,不能在开发机上生成后拷贝。**
|
||||||
|
|
||||||
|
在甲方服务器上执行以下命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -cp ruoyi-admin.jar com.ruoyi.common.utils.license.LicenseGenerator
|
||||||
|
```
|
||||||
|
|
||||||
|
**交互示例:**
|
||||||
|
|
||||||
|
```
|
||||||
|
========================================
|
||||||
|
License 授权文件生成工具
|
||||||
|
========================================
|
||||||
|
|
||||||
|
当前服务器机器指纹(MAC 地址):
|
||||||
|
08:00:27:A3:B4:C5|0A:00:27:00:00:00
|
||||||
|
|
||||||
|
生成的授权码:
|
||||||
|
a3f5e8d9c2b1a4f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0
|
||||||
|
|
||||||
|
请输入 license.lic 文件保存路径(如 D:\license\license.lic 或 /opt/license/license.lic):
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows 示例:**
|
||||||
|
```
|
||||||
|
D:\license\license.lic
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux 示例:**
|
||||||
|
```
|
||||||
|
/opt/license/license.lic
|
||||||
|
```
|
||||||
|
|
||||||
|
生成成功后会显示:
|
||||||
|
|
||||||
|
```
|
||||||
|
========================================
|
||||||
|
License 文件生成成功!
|
||||||
|
========================================
|
||||||
|
文件路径:/opt/license/license.lic
|
||||||
|
|
||||||
|
下一步操作:
|
||||||
|
1. 将此路径配置到 application.yml 的 af.license.path 参数中
|
||||||
|
2. 启动应用程序,系统会自动校验 License
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 步骤 3:配置 License 路径
|
||||||
|
|
||||||
|
编辑 `application.yml`,修改 `af.license.path` 为实际生成的文件路径:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
af:
|
||||||
|
license:
|
||||||
|
path: /opt/license/license.lic # Linux 路径
|
||||||
|
# path: D:\license\license.lic # Windows 路径
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意:**
|
||||||
|
- Windows 路径使用反斜杠 `\` 或双反斜杠 `\\`
|
||||||
|
- Linux 路径使用正斜杠 `/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 步骤 4:启动程序
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar ruoyi-admin.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
**启动成功示例:**
|
||||||
|
|
||||||
|
```
|
||||||
|
2026-05-17 10:30:15.123 INFO --- License 校验开始,文件路径:/opt/license/license.lic
|
||||||
|
2026-05-17 10:30:15.234 INFO --- 当前机器指纹:08:00:27:A3:B4:C5|0A:00:27:00:00:00
|
||||||
|
2026-05-17 10:30:15.345 INFO --- License 校验通过
|
||||||
|
|
||||||
|
(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙
|
||||||
|
```
|
||||||
|
|
||||||
|
**启动失败示例(License 不匹配):**
|
||||||
|
|
||||||
|
```
|
||||||
|
========================================
|
||||||
|
License 授权校验失败
|
||||||
|
========================================
|
||||||
|
错误:License 授权校验失败:当前服务器未获得授权,程序拒绝启动
|
||||||
|
|
||||||
|
解决方法:
|
||||||
|
1. 在目标服务器上运行 License 生成工具:
|
||||||
|
java -cp ruoyi.jar com.ruoyi.common.utils.license.LicenseGenerator
|
||||||
|
2. 将生成的 license.lic 文件路径配置到 application.yml 的 af.license.path
|
||||||
|
========================================
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、常见问题
|
||||||
|
|
||||||
|
### Q1:甲方更换了网卡或服务器,程序无法启动怎么办?
|
||||||
|
|
||||||
|
**A:** 需要重新生成 License。
|
||||||
|
|
||||||
|
1. 在新服务器上运行 License 生成工具
|
||||||
|
2. 将新生成的 `license.lic` 替换旧文件
|
||||||
|
3. 重启程序
|
||||||
|
|
||||||
|
### Q2:甲方能否自己修改 License 文件?
|
||||||
|
|
||||||
|
**A:** 不能。License 文件是用内置密钥签名的,甲方无法伪造。任何手动修改都会导致校验失败。
|
||||||
|
|
||||||
|
### Q3:如何修改签名密钥?
|
||||||
|
|
||||||
|
**A:** 修改 `LicenseUtils.java` 中的 `HMAC_SECRET` 常量,重新打包即可。
|
||||||
|
|
||||||
|
**注意:** 修改密钥后,所有已颁发的 License 都会失效,需要重新生成。
|
||||||
|
|
||||||
|
```java
|
||||||
|
private static final String HMAC_SECRET = "你的新密钥";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q4:开发环境如何跳过 License 校验?
|
||||||
|
|
||||||
|
**方案 1:** 在开发机上生成 License 并配置路径。
|
||||||
|
|
||||||
|
**方案 2:** 修改 `application.yml`,将 `af.license.path` 留空或注释掉(会导致启动失败,需要临时注释 `RuoYiApplication` 中的校验代码)。
|
||||||
|
|
||||||
|
**推荐方案 1**,保持代码一致性。
|
||||||
|
|
||||||
|
### Q5:如何查看当前服务器的机器指纹?
|
||||||
|
|
||||||
|
运行 License 生成工具即可看到:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -cp ruoyi-admin.jar com.ruoyi.common.utils.license.LicenseGenerator
|
||||||
|
```
|
||||||
|
|
||||||
|
工具会显示当前服务器的 MAC 地址指纹。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、安全建议
|
||||||
|
|
||||||
|
1. **保护签名密钥:** 不要将 `HMAC_SECRET` 泄露给甲方或第三方
|
||||||
|
2. **定期更换密钥:** 每次交付新客户时,建议使用不同的密钥版本
|
||||||
|
3. **加固 jar 包:** 可使用混淆工具(如 ProGuard)进一步保护代码
|
||||||
|
4. **License 文件权限:** 建议将 `license.lic` 设置为只读权限
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、技术实现
|
||||||
|
|
||||||
|
### 核心类说明
|
||||||
|
|
||||||
|
| 类名 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `LicenseUtils` | `com.ruoyi.common.utils.license.LicenseUtils` | License 工具类,提供指纹采集、签名、校验功能 |
|
||||||
|
| `LicenseException` | `com.ruoyi.common.utils.license.LicenseException` | License 异常类 |
|
||||||
|
| `LicenseGenerator` | `com.ruoyi.common.utils.license.LicenseGenerator` | License 生成工具(独立 main 方法) |
|
||||||
|
| `RuoYiApplication` | `com.ruoyi.RuoYiApplication` | 启动类,集成 License 校验逻辑 |
|
||||||
|
|
||||||
|
### 配置参数
|
||||||
|
|
||||||
|
| 参数 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `af.license.path` | `application.yml` | License 文件的绝对路径 |
|
||||||
|
| `HMAC_SECRET` | `LicenseUtils.java` | 签名密钥(硬编码) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、授权流程图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 开发者打包项目 │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 上传 jar 到甲方 │
|
||||||
|
│ 服务器 │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 运行 License │
|
||||||
|
│ 生成工具 │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 采集 MAC 地址 │
|
||||||
|
│ 生成 license.lic│
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 配置 License │
|
||||||
|
│ 文件路径 │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 启动程序 │
|
||||||
|
│ 自动校验 License│
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────┴────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
校验通过 校验失败
|
||||||
|
正常启动 拒绝启动
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**如有疑问,请联系开发团队。**
|
||||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
|||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为远程机器生成 License 授权码(在开发机上运行)
|
||||||
|
*/
|
||||||
|
public class GenerateLicenseForRemote
|
||||||
|
{
|
||||||
|
private static final String HMAC_SECRET = "D0uY!n@R3ch@rge#S3cr3tK3y$2026";
|
||||||
|
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception
|
||||||
|
{
|
||||||
|
// 远程机器的 MAC 地址指纹
|
||||||
|
String remoteFingerprint = "5C:DE:34:E6:40:4C";
|
||||||
|
|
||||||
|
System.out.println("远程机器指纹:" + remoteFingerprint);
|
||||||
|
|
||||||
|
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(
|
||||||
|
HMAC_SECRET.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
|
||||||
|
mac.init(keySpec);
|
||||||
|
byte[] raw = mac.doFinal(remoteFingerprint.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : raw)
|
||||||
|
{
|
||||||
|
sb.append(byteToHex(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
String licenseCode = sb.toString();
|
||||||
|
System.out.println("生成的授权码:");
|
||||||
|
System.out.println(licenseCode);
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("请将上面的授权码保存到远程机器的 D:\\douyinrecharge\\license\\license.lic 文件中");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String byteToHex(byte b)
|
||||||
|
{
|
||||||
|
final char[] HEX = "0123456789abcdef".toCharArray();
|
||||||
|
return "" + HEX[(b >> 4) & 0xF] + HEX[b & 0xF];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* License 独立生成工具,无任何第三方依赖,直接用 JDK 编译运行。
|
||||||
|
*
|
||||||
|
* 编译:javac LicenseTool.java
|
||||||
|
* 运行:java LicenseTool D:\license\license.lic
|
||||||
|
*
|
||||||
|
* 注意:HMAC_SECRET 必须与主程序 LicenseUtils.java 中的值完全一致。
|
||||||
|
*/
|
||||||
|
public class LicenseTool
|
||||||
|
{
|
||||||
|
private static final String HMAC_SECRET = "D0uY!n@R3ch@rge#S3cr3tK3y$2026";
|
||||||
|
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception
|
||||||
|
{
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println(" License 授权文件生成工具");
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
String fingerprint = collectMachineFingerprint();
|
||||||
|
System.out.println("当前机器指纹(MAC 地址):");
|
||||||
|
System.out.println(fingerprint);
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
String licenseCode = sign(fingerprint);
|
||||||
|
System.out.println("生成的授权码:");
|
||||||
|
System.out.println(licenseCode);
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
String outputPath;
|
||||||
|
if (args != null && args.length > 0 && !args[0].trim().isEmpty())
|
||||||
|
{
|
||||||
|
outputPath = args[0].trim();
|
||||||
|
System.out.println("保存路径:" + outputPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
System.out.print("请输入 license.lic 保存路径(如 D:\\license\\license.lic):");
|
||||||
|
java.util.Scanner scanner = new java.util.Scanner(System.in);
|
||||||
|
outputPath = scanner.nextLine().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputPath.isEmpty())
|
||||||
|
{
|
||||||
|
System.err.println("错误:路径不能为空");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
File outputFile = new File(outputPath);
|
||||||
|
File parentDir = outputFile.getParentFile();
|
||||||
|
if (parentDir != null && !parentDir.exists())
|
||||||
|
{
|
||||||
|
parentDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.write(Paths.get(outputPath), licenseCode.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println(" License 文件生成成功!");
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println("文件路径:" + outputFile.getAbsolutePath());
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("下一步:将此路径配置到 application.yml 的 af.license.path,然后启动程序。");
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String collectMachineFingerprint() throws Exception
|
||||||
|
{
|
||||||
|
List<String> macList = new ArrayList<>();
|
||||||
|
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||||
|
if (interfaces == null)
|
||||||
|
{
|
||||||
|
throw new RuntimeException("无法枚举网络接口");
|
||||||
|
}
|
||||||
|
while (interfaces.hasMoreElements())
|
||||||
|
{
|
||||||
|
NetworkInterface ni = interfaces.nextElement();
|
||||||
|
if (ni.isLoopback() || ni.isVirtual() || !ni.isUp())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byte[] mac = ni.getHardwareAddress();
|
||||||
|
if (mac == null || mac.length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < mac.length; i++)
|
||||||
|
{
|
||||||
|
if (i > 0) sb.append(":");
|
||||||
|
sb.append(byteToHex(mac[i]).toUpperCase());
|
||||||
|
}
|
||||||
|
macList.add(sb.toString());
|
||||||
|
}
|
||||||
|
if (macList.isEmpty())
|
||||||
|
{
|
||||||
|
throw new RuntimeException("未找到任何物理网卡 MAC 地址");
|
||||||
|
}
|
||||||
|
Collections.sort(macList);
|
||||||
|
String result = "";
|
||||||
|
for (int i = 0; i < macList.size(); i++)
|
||||||
|
{
|
||||||
|
if (i > 0) result += "|";
|
||||||
|
result += macList.get(i);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String sign(String fingerprint) throws Exception
|
||||||
|
{
|
||||||
|
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(
|
||||||
|
HMAC_SECRET.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
|
||||||
|
mac.init(keySpec);
|
||||||
|
byte[] raw = mac.doFinal(fingerprint.getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : raw)
|
||||||
|
{
|
||||||
|
sb.append(byteToHex(b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String byteToHex(byte b)
|
||||||
|
{
|
||||||
|
final char[] HEX = "0123456789abcdef".toCharArray();
|
||||||
|
return "" + HEX[(b >> 4) & 0xF] + HEX[b & 0xF];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.ruoyi;
|
package com.ruoyi;
|
||||||
|
|
||||||
|
import com.ruoyi.common.utils.license.LicenseException;
|
||||||
|
import com.ruoyi.common.utils.license.LicenseUtils;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||||
@@ -20,9 +22,49 @@ public class RuoYiApplication
|
|||||||
{
|
{
|
||||||
// System.setProperty("spring.devtools.restart.enabled", "false");
|
// System.setProperty("spring.devtools.restart.enabled", "false");
|
||||||
ConfigurableApplicationContext application = SpringApplication.run(RuoYiApplication.class, args);
|
ConfigurableApplicationContext application = SpringApplication.run(RuoYiApplication.class, args);
|
||||||
|
|
||||||
|
verifyLicense(application.getEnvironment());
|
||||||
|
|
||||||
printStartupMessage(application.getEnvironment());
|
printStartupMessage(application.getEnvironment());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 License 授权,校验失败则退出程序。
|
||||||
|
*/
|
||||||
|
private static void verifyLicense(Environment env)
|
||||||
|
{
|
||||||
|
String licensePath = env.getProperty("af.license.path");
|
||||||
|
if (licensePath == null || licensePath.trim().isEmpty())
|
||||||
|
{
|
||||||
|
System.err.println("\n========================================");
|
||||||
|
System.err.println(" License 授权校验失败");
|
||||||
|
System.err.println("========================================");
|
||||||
|
System.err.println("错误:未配置 License 文件路径(af.license.path)");
|
||||||
|
System.err.println("请在 application.yml 中配置 af.license.path 参数");
|
||||||
|
System.err.println("========================================\n");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LicenseUtils.verify(licensePath);
|
||||||
|
}
|
||||||
|
catch (LicenseException e)
|
||||||
|
{
|
||||||
|
System.err.println("\n========================================");
|
||||||
|
System.err.println(" License 授权校验失败");
|
||||||
|
System.err.println("========================================");
|
||||||
|
System.err.println("错误:" + e.getMessage());
|
||||||
|
System.err.println();
|
||||||
|
System.err.println("解决方法:");
|
||||||
|
System.err.println("1. 在目标服务器上运行 License 生成工具:");
|
||||||
|
System.err.println(" java -cp ruoyi.jar com.ruoyi.common.utils.license.LicenseGenerator");
|
||||||
|
System.err.println("2. 将生成的 license.lic 文件路径配置到 application.yml 的 af.license.path");
|
||||||
|
System.err.println("========================================\n");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打印启动成功提示,包含端口与接口文档地址。
|
* 打印启动成功提示,包含端口与接口文档地址。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.PathVariable;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import com.ruoyi.common.core.controller.BaseController;
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
@@ -122,4 +123,37 @@ public class AfOrderController extends BaseController
|
|||||||
Long orderId = Convert.toLong(body.get("orderId"));
|
Long orderId = Convert.toLong(body.get("orderId"));
|
||||||
return toAjax(orderService.retryOrder(orderId, getUsername()));
|
return toAjax(orderService.retryOrder(orderId, getUsername()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从微店拉取订单并同步入库。
|
||||||
|
*
|
||||||
|
* @param body 请求体,pageNum 为页码(从 0 开始,不传默认 0)
|
||||||
|
* @return 同步结果,包含 total(接口返回总数)、inserted(本次新增数)
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('af:order:sync')")
|
||||||
|
@PostMapping("/sync/weidian")
|
||||||
|
public AjaxResult syncWeidian(@RequestBody(required = false) Map<String, Object> body)
|
||||||
|
{
|
||||||
|
int pageNum = body == null ? 0 : Convert.toInt(body.get("pageNum"), 0);
|
||||||
|
logger.info("syncWeidian 入参:pageNum={}, operator={}", pageNum, getUsername());
|
||||||
|
Map<String, Object> syncResult = orderService.syncWeidianOrders(pageNum, getUsername());
|
||||||
|
AjaxResult result = success();
|
||||||
|
result.putAll(syncResult);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知微店指定订单已发货(虚拟发货)。
|
||||||
|
*
|
||||||
|
* @param sourceOrderNo 微店订单号
|
||||||
|
* @return 发货结果
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('af:order:deliver')")
|
||||||
|
@PostMapping("/deliver/weidian")
|
||||||
|
public AjaxResult deliverWeidian(@RequestParam String sourceOrderNo)
|
||||||
|
{
|
||||||
|
logger.info("deliverWeidian 入参:sourceOrderNo={}, operator={}", sourceOrderNo, getUsername());
|
||||||
|
orderService.deliverWeidianOrder(sourceOrderNo, getUsername());
|
||||||
|
return success("发货成功");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,15 @@ referer:
|
|||||||
|
|
||||||
# 自动充值业务配置
|
# 自动充值业务配置
|
||||||
af:
|
af:
|
||||||
|
license:
|
||||||
|
path: D:\license\license.lic
|
||||||
rechargeUrl: https://www.douyin.com/pay
|
rechargeUrl: https://www.douyin.com/pay
|
||||||
|
weidian:
|
||||||
|
wdToken: e96014f3
|
||||||
|
cookie: __spider__visitorid=884e5806506186bd; is_login=true; login_type=LOGIN_USER_TYPE_MASTER; login_source=LOGIN_USER_SOURCE_MASTER; uid=1774188581; duid=1774188581; sid=1619718241; smart_login_type=0; hi_dxh=; hold=; cn_merchant=; wdtoken=e96014f3; __spider__sessionid=d7756539bef26b5b; Hm_lvt_f3b91484e26c0d850ada494bff4b469b=1777446699,1778986671; HMACCOUNT=2383A5C40E664A02; login_token=_EwWqqVIQEKuI-Phw6hSEGFcEi9mlSZQq9CGPxfyUsL_A0Fnptw_8BdSEQvLn8r8oOo73sACMqrfJj3n0ichiWNcQwt0cS2Yxh1UgqGCs8bE5zqWBCXV2KZIxKzMApQbuf98L9hOdI1xlZH0pIJ6MT7WXbckhMUaS0juJhzzDpn1YnXxUAB9jy2G7fb2f15UK6_Thhr-eLwj54zYDhTghIixzfpTsmAVvlE_45xetReIESH7SYsHeO1JGiRIEey3yDnlo_-jJ; Hm_lpvt_f3b91484e26c0d850ada494bff4b469b=1778988001; cashier_token=558d8084586ea5a1ecf74c1d372296d5
|
||||||
|
orderListUrl: https://thor.weidian.com/tradeview/seller.getOrderListForPC/1.0
|
||||||
|
deliverOrderUrl: https://thor.weidian.com/tradeview/seller.deliverOrder/1.0
|
||||||
|
pageSize: 20
|
||||||
|
|
||||||
# 防止XSS攻击
|
# 防止XSS攻击
|
||||||
xss:
|
xss:
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import java.io.InputStream;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.net.ConnectException;
|
import java.net.ConnectException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
|
import java.util.Map;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import javax.net.ssl.HostnameVerifier;
|
import javax.net.ssl.HostnameVerifier;
|
||||||
@@ -203,6 +205,84 @@ public class HttpUtils
|
|||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向指定 URL 发送POST方法的请求,支持自定义请求头。
|
||||||
|
*
|
||||||
|
* @param url 发送请求的 URL
|
||||||
|
* @param param 请求参数,application/x-www-form-urlencoded 格式
|
||||||
|
* @param headers 自定义请求头,key 为 Header 名称,value 为 Header 值
|
||||||
|
* @return 响应结果字符串,请求失败时返回空字符串
|
||||||
|
*/
|
||||||
|
public static String sendPostWithHeaders(String url, String param, Map<String, String> headers)
|
||||||
|
{
|
||||||
|
PrintWriter out = null;
|
||||||
|
BufferedReader in = null;
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.info("sendPostWithHeaders - {}", url);
|
||||||
|
URL realUrl = new URL(url);
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setConnectTimeout(10000);
|
||||||
|
conn.setReadTimeout(10000);
|
||||||
|
if (headers != null)
|
||||||
|
{
|
||||||
|
for (Map.Entry<String, String> entry : headers.entrySet())
|
||||||
|
{
|
||||||
|
conn.setRequestProperty(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setDoInput(true);
|
||||||
|
out = new PrintWriter(conn.getOutputStream());
|
||||||
|
out.print(param);
|
||||||
|
out.flush();
|
||||||
|
in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
|
||||||
|
String line;
|
||||||
|
while ((line = in.readLine()) != null)
|
||||||
|
{
|
||||||
|
result.append(line);
|
||||||
|
}
|
||||||
|
log.info("recv - {}", result);
|
||||||
|
}
|
||||||
|
catch (ConnectException e)
|
||||||
|
{
|
||||||
|
log.error("调用HttpUtils.sendPostWithHeaders ConnectException, url=" + url, e);
|
||||||
|
}
|
||||||
|
catch (SocketTimeoutException e)
|
||||||
|
{
|
||||||
|
log.error("调用HttpUtils.sendPostWithHeaders SocketTimeoutException, url=" + url, e);
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
log.error("调用HttpUtils.sendPostWithHeaders IOException, url=" + url, e);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.error("调用HttpUtils.sendPostWithHeaders Exception, url=" + url, e);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (out != null)
|
||||||
|
{
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
if (in != null)
|
||||||
|
{
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
log.error("调用in.close Exception, url=" + url, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
public static String sendSSLPost(String url, String param)
|
public static String sendSSLPost(String url, String param)
|
||||||
{
|
{
|
||||||
return sendSSLPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
return sendSSLPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ruoyi.common.utils.license;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* License 授权异常。
|
||||||
|
*/
|
||||||
|
public class LicenseException extends Exception
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public LicenseException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LicenseException(String message, Throwable cause)
|
||||||
|
{
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.ruoyi.common.utils.license;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* License 生成工具(独立运行)。
|
||||||
|
*
|
||||||
|
* <p>使用方法:
|
||||||
|
* <ol>
|
||||||
|
* <li>IDEA 开发环境:右键 main 方法 → Run,在 Program arguments 中填写路径,如 D:\license\license.lic</li>
|
||||||
|
* <li>服务器环境:java -cp ruoyi.jar com.ruoyi.common.utils.license.LicenseGenerator [路径]</li>
|
||||||
|
* <li>无参数运行:交互式输入路径</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>注意:此工具必须在目标服务器上运行,不能在开发机上生成后拷贝。
|
||||||
|
*/
|
||||||
|
public class LicenseGenerator
|
||||||
|
{
|
||||||
|
public static void main(String[] args)
|
||||||
|
{
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println(" License 授权文件生成工具");
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
String fingerprint = LicenseUtils.collectMachineFingerprint();
|
||||||
|
System.out.println("当前服务器机器指纹(MAC 地址):");
|
||||||
|
System.out.println(fingerprint);
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
String licenseCode = LicenseUtils.sign(fingerprint);
|
||||||
|
System.out.println("生成的授权码:");
|
||||||
|
System.out.println(licenseCode);
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
String outputPath;
|
||||||
|
if (args != null && args.length > 0 && !args[0].trim().isEmpty())
|
||||||
|
{
|
||||||
|
outputPath = args[0].trim();
|
||||||
|
System.out.println("使用参数指定路径:" + outputPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Scanner scanner = new Scanner(System.in);
|
||||||
|
System.out.print("请输入 license.lic 文件保存路径(如 D:\\license\\license.lic 或 /opt/license/license.lic):");
|
||||||
|
outputPath = scanner.nextLine().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputPath.isEmpty())
|
||||||
|
{
|
||||||
|
System.err.println("错误:路径不能为空");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
File outputFile = new File(outputPath);
|
||||||
|
File parentDir = outputFile.getParentFile();
|
||||||
|
if (parentDir != null && !parentDir.exists())
|
||||||
|
{
|
||||||
|
if (!parentDir.mkdirs())
|
||||||
|
{
|
||||||
|
System.err.println("错误:无法创建目录 " + parentDir.getAbsolutePath());
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.write(Paths.get(outputPath), licenseCode.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println(" License 文件生成成功!");
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println("文件路径:" + outputFile.getAbsolutePath());
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("下一步操作:");
|
||||||
|
System.out.println("1. 将此路径配置到 application.yml 的 af.license.path 参数中");
|
||||||
|
System.out.println("2. 启动应用程序,系统会自动校验 License");
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
catch (LicenseException e)
|
||||||
|
{
|
||||||
|
System.err.println("错误:" + e.getMessage());
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
System.err.println("生成 License 文件失败:" + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package com.ruoyi.common.utils.license;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* License 授权工具类。
|
||||||
|
*
|
||||||
|
* <p>授权原理:
|
||||||
|
* <ol>
|
||||||
|
* <li>采集当前服务器所有物理网卡的 MAC 地址,排序后拼接为指纹字符串。</li>
|
||||||
|
* <li>用内置密钥对指纹做 HMAC-SHA256 签名,得到授权码。</li>
|
||||||
|
* <li>将授权码写入 license.lic 文件(由开发者在目标机器上生成后交付)。</li>
|
||||||
|
* <li>程序启动时重新采集 MAC 指纹,重新计算签名,与 license.lic 内容比对。</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>防篡改:签名密钥硬编码在 jar 内,甲方无法自行伪造 license.lic。
|
||||||
|
*/
|
||||||
|
public class LicenseUtils
|
||||||
|
{
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LicenseUtils.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HMAC 签名密钥,打包后不可见,修改此值会使所有已颁发的 License 失效。
|
||||||
|
* 生产部署前请替换为你自己的随机字符串。
|
||||||
|
*/
|
||||||
|
private static final String HMAC_SECRET = "D0uY!n@R3ch@rge#S3cr3tK3y$2026";
|
||||||
|
|
||||||
|
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 采集当前机器所有物理网卡的 MAC 地址,排序后拼接为机器指纹。
|
||||||
|
*
|
||||||
|
* @return 机器指纹字符串,格式如 "AA:BB:CC:DD:EE:FF|11:22:33:44:55:66"
|
||||||
|
* @throws LicenseException 无法获取任何 MAC 地址时抛出
|
||||||
|
*/
|
||||||
|
public static String collectMachineFingerprint() throws LicenseException
|
||||||
|
{
|
||||||
|
List<String> macList = new ArrayList<>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||||
|
if (interfaces == null)
|
||||||
|
{
|
||||||
|
throw new LicenseException("无法枚举网络接口");
|
||||||
|
}
|
||||||
|
while (interfaces.hasMoreElements())
|
||||||
|
{
|
||||||
|
NetworkInterface ni = interfaces.nextElement();
|
||||||
|
if (ni.isLoopback() || ni.isVirtual() || !ni.isUp())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byte[] mac = ni.getHardwareAddress();
|
||||||
|
if (mac == null || mac.length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < mac.length; i++)
|
||||||
|
{
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
sb.append(":");
|
||||||
|
}
|
||||||
|
sb.append(byteToHex(mac[i]).toUpperCase());
|
||||||
|
}
|
||||||
|
macList.add(sb.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (LicenseException e)
|
||||||
|
{
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new LicenseException("采集 MAC 地址失败:" + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (macList.isEmpty())
|
||||||
|
{
|
||||||
|
throw new LicenseException("未找到任何物理网卡 MAC 地址,无法生成机器指纹");
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(macList);
|
||||||
|
return String.join("|", macList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对机器指纹做 HMAC-SHA256 签名,生成授权码。
|
||||||
|
*
|
||||||
|
* @param fingerprint 机器指纹
|
||||||
|
* @return 十六进制授权码字符串
|
||||||
|
* @throws LicenseException 签名失败时抛出
|
||||||
|
*/
|
||||||
|
public static String sign(String fingerprint) throws LicenseException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(
|
||||||
|
HMAC_SECRET.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
|
||||||
|
mac.init(keySpec);
|
||||||
|
byte[] raw = mac.doFinal(fingerprint.getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : raw)
|
||||||
|
{
|
||||||
|
sb.append(byteToHex(b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new LicenseException("License 签名失败:" + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将单个字节转为两位小写十六进制字符串,不依赖 String.format。
|
||||||
|
*/
|
||||||
|
private static String byteToHex(byte b)
|
||||||
|
{
|
||||||
|
final char[] HEX = "0123456789abcdef".toCharArray();
|
||||||
|
return "" + HEX[(b >> 4) & 0xF] + HEX[b & 0xF];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 License 文件是否与当前机器匹配。
|
||||||
|
*
|
||||||
|
* <p>校验逻辑:读取 licenseFilePath 文件内容,与当前机器指纹的签名比对。
|
||||||
|
*
|
||||||
|
* @param licenseFilePath license.lic 文件的绝对路径
|
||||||
|
* @throws LicenseException 文件不存在、内容为空或签名不匹配时抛出
|
||||||
|
*/
|
||||||
|
public static void verify(String licenseFilePath) throws LicenseException
|
||||||
|
{
|
||||||
|
log.info("License 校验开始,文件路径:{}", licenseFilePath);
|
||||||
|
|
||||||
|
if (licenseFilePath == null || licenseFilePath.trim().isEmpty())
|
||||||
|
{
|
||||||
|
throw new LicenseException("License 文件路径未配置(af.license.path)");
|
||||||
|
}
|
||||||
|
|
||||||
|
String licenseContent;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
licenseContent = new String(
|
||||||
|
Files.readAllBytes(Paths.get(licenseFilePath.trim())),
|
||||||
|
StandardCharsets.UTF_8).trim();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new LicenseException("读取 License 文件失败,路径:" + licenseFilePath + ",原因:" + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (licenseContent.isEmpty())
|
||||||
|
{
|
||||||
|
throw new LicenseException("License 文件内容为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String fingerprint = collectMachineFingerprint();
|
||||||
|
log.info("当前机器指纹:{}", fingerprint);
|
||||||
|
|
||||||
|
String expected = sign(fingerprint);
|
||||||
|
|
||||||
|
if (!expected.equalsIgnoreCase(licenseContent))
|
||||||
|
{
|
||||||
|
log.error("License 校验失败,当前机器指纹与授权不匹配");
|
||||||
|
throw new LicenseException("License 授权校验失败:当前服务器未获得授权,程序拒绝启动");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("License 校验通过");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.ruoyi.system.domain.af;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微店账号配置属性。
|
||||||
|
* 对应 application.yml 中 af.weidian.* 配置项。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "af.weidian")
|
||||||
|
public class AfWeidianProperties
|
||||||
|
{
|
||||||
|
/** 微店 wdToken */
|
||||||
|
private String wdToken;
|
||||||
|
|
||||||
|
/** 微店登录 Cookie */
|
||||||
|
private String cookie;
|
||||||
|
|
||||||
|
/** 微店订单列表接口地址 */
|
||||||
|
private String orderListUrl;
|
||||||
|
|
||||||
|
/** 微店发货接口地址 */
|
||||||
|
private String deliverOrderUrl;
|
||||||
|
|
||||||
|
/** 每次拉取的页大小 */
|
||||||
|
private int pageSize = 20;
|
||||||
|
|
||||||
|
public String getWdToken()
|
||||||
|
{
|
||||||
|
return wdToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWdToken(String wdToken)
|
||||||
|
{
|
||||||
|
this.wdToken = wdToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCookie()
|
||||||
|
{
|
||||||
|
return cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCookie(String cookie)
|
||||||
|
{
|
||||||
|
this.cookie = cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrderListUrl()
|
||||||
|
{
|
||||||
|
return orderListUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrderListUrl(String orderListUrl)
|
||||||
|
{
|
||||||
|
this.orderListUrl = orderListUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDeliverOrderUrl()
|
||||||
|
{
|
||||||
|
return deliverOrderUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeliverOrderUrl(String deliverOrderUrl)
|
||||||
|
{
|
||||||
|
this.deliverOrderUrl = deliverOrderUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPageSize()
|
||||||
|
{
|
||||||
|
return pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPageSize(int pageSize)
|
||||||
|
{
|
||||||
|
this.pageSize = pageSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -115,4 +115,12 @@ public interface AfOrderMapper
|
|||||||
public int updateOrderFailReason(@Param("id") Long id,
|
public int updateOrderFailReason(@Param("id") Long id,
|
||||||
@Param("failReason") String failReason,
|
@Param("failReason") String failReason,
|
||||||
@Param("updateBy") String updateBy);
|
@Param("updateBy") String updateBy);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入订单,若 source+source_order_no 已存在则忽略(幂等写入)
|
||||||
|
*
|
||||||
|
* @param order 订单对象
|
||||||
|
* @return 影响行数,0 表示已存在被忽略,1 表示新插入
|
||||||
|
*/
|
||||||
|
public int insertAfOrderIgnoreDuplicate(AfOrder order);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.system.service.af;
|
package com.ruoyi.system.service.af;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import com.ruoyi.system.domain.af.AfOrder;
|
import com.ruoyi.system.domain.af.AfOrder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,4 +45,23 @@ public interface IAfOrderService
|
|||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
public int retryOrder(Long orderId, String operator);
|
public int retryOrder(Long orderId, String operator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从微店拉取待充值订单并同步入库。
|
||||||
|
* 使用 INSERT IGNORE 幂等写入,已存在的订单不会重复插入。
|
||||||
|
*
|
||||||
|
* @param pageNum 页码(从 0 开始)
|
||||||
|
* @param operator 操作人
|
||||||
|
* @return 同步结果摘要,包含 total(接口返回总数)、inserted(本次新增数)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> syncWeidianOrders(int pageNum, String operator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用微店发货接口,通知微店该订单已完成充值(虚拟发货)。
|
||||||
|
* 仅对来源为 WEIDIAN 的订单执行,其他来源直接忽略。
|
||||||
|
*
|
||||||
|
* @param sourceOrderNo 微店订单号(orderId)
|
||||||
|
* @param operator 操作人
|
||||||
|
*/
|
||||||
|
public void deliverWeidianOrder(String sourceOrderNo, String operator);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.ruoyi.system.service.af.impl;
|
package com.ruoyi.system.service.af.impl;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
@@ -7,11 +9,14 @@ import org.springframework.stereotype.Component;
|
|||||||
import com.ruoyi.system.domain.af.AfDispatchPolicy;
|
import com.ruoyi.system.domain.af.AfDispatchPolicy;
|
||||||
import com.ruoyi.system.service.af.IAfDispatchPolicyService;
|
import com.ruoyi.system.service.af.IAfDispatchPolicyService;
|
||||||
import com.ruoyi.system.service.af.IAfDispatchService;
|
import com.ruoyi.system.service.af.IAfDispatchService;
|
||||||
|
import com.ruoyi.system.service.af.IAfOrderService;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
public class AfDispatchScheduler
|
public class AfDispatchScheduler
|
||||||
{
|
{
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AfDispatchScheduler.class);
|
||||||
|
|
||||||
private volatile long lastDispatchTimestamp = 0L;
|
private volatile long lastDispatchTimestamp = 0L;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -20,6 +25,9 @@ public class AfDispatchScheduler
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IAfDispatchService dispatchService;
|
private IAfDispatchService dispatchService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IAfOrderService orderService;
|
||||||
|
|
||||||
@Scheduled(fixedDelay = 10000L, initialDelay = 15000L)
|
@Scheduled(fixedDelay = 10000L, initialDelay = 15000L)
|
||||||
public void scanPendingOrders()
|
public void scanPendingOrders()
|
||||||
{
|
{
|
||||||
@@ -35,6 +43,14 @@ public class AfDispatchScheduler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastDispatchTimestamp = now;
|
lastDispatchTimestamp = now;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
orderService.syncWeidianOrders(0, "system");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.error("定时同步微店订单失败", e);
|
||||||
|
}
|
||||||
dispatchService.triggerAutoDispatch(null, "CRON", "system");
|
dispatchService.triggerAutoDispatch(null, "CRON", "system");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
package com.ruoyi.system.service.af.impl;
|
package com.ruoyi.system.service.af.impl;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.common.utils.http.HttpUtils;
|
||||||
import com.ruoyi.system.domain.af.AfDevice;
|
import com.ruoyi.system.domain.af.AfDevice;
|
||||||
import com.ruoyi.system.domain.af.AfDispatchTask;
|
import com.ruoyi.system.domain.af.AfDispatchTask;
|
||||||
import com.ruoyi.system.domain.af.AfOrder;
|
import com.ruoyi.system.domain.af.AfOrder;
|
||||||
import com.ruoyi.system.domain.af.AfStatusConstants;
|
import com.ruoyi.system.domain.af.AfStatusConstants;
|
||||||
|
import com.ruoyi.system.domain.af.AfWeidianProperties;
|
||||||
import com.ruoyi.system.mapper.af.AfDeviceMapper;
|
import com.ruoyi.system.mapper.af.AfDeviceMapper;
|
||||||
import com.ruoyi.system.mapper.af.AfDispatchTaskMapper;
|
import com.ruoyi.system.mapper.af.AfDispatchTaskMapper;
|
||||||
import com.ruoyi.system.mapper.af.AfOrderMapper;
|
import com.ruoyi.system.mapper.af.AfOrderMapper;
|
||||||
@@ -25,6 +37,10 @@ import com.ruoyi.system.service.af.IAfOrderService;
|
|||||||
@Service
|
@Service
|
||||||
public class AfOrderServiceImpl implements IAfOrderService
|
public class AfOrderServiceImpl implements IAfOrderService
|
||||||
{
|
{
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AfOrderServiceImpl.class);
|
||||||
|
|
||||||
|
private static final String SOURCE_WEIDIAN = "WEIDIAN";
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private AfOrderMapper orderMapper;
|
private AfOrderMapper orderMapper;
|
||||||
|
|
||||||
@@ -38,6 +54,9 @@ public class AfOrderServiceImpl implements IAfOrderService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IAfDispatchService dispatchService;
|
private IAfDispatchService dispatchService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AfWeidianProperties weidianProperties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询订单列表。
|
* 查询订单列表。
|
||||||
*
|
*
|
||||||
@@ -149,4 +168,260 @@ public class AfOrderServiceImpl implements IAfOrderService
|
|||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从微店拉取待充值订单并同步入库。
|
||||||
|
* 调用微店 seller.getOrderListForPC 接口,解析订单列表后逐条 INSERT IGNORE 写入。
|
||||||
|
* 幂等保证:唯一索引 uk_source_order(source, source_order_no) 防止重复插入。
|
||||||
|
*
|
||||||
|
* @param pageNum 页码(从 0 开始)
|
||||||
|
* @param operator 操作人
|
||||||
|
* @return 同步结果摘要,包含 total(接口返回总数)、inserted(本次新增数)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> syncWeidianOrders(int pageNum, String operator)
|
||||||
|
{
|
||||||
|
log.info("syncWeidianOrders 开始,pageNum={}, operator={}", pageNum, operator);
|
||||||
|
|
||||||
|
String responseBody = fetchWeidianOrderList(pageNum);
|
||||||
|
if (StringUtils.isBlank(responseBody))
|
||||||
|
{
|
||||||
|
throw new ServiceException("微店接口请求失败,响应为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject root = JSON.parseObject(responseBody);
|
||||||
|
JSONObject result = root.getJSONObject("result");
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
log.error("syncWeidianOrders 响应格式异常,无 result 字段,body={}", responseBody);
|
||||||
|
throw new ServiceException("微店接口响应格式异常");
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = result.getIntValue("total");
|
||||||
|
JSONArray orderList = result.getJSONArray("orderList");
|
||||||
|
if (orderList == null || orderList.isEmpty())
|
||||||
|
{
|
||||||
|
log.info("syncWeidianOrders 本次无订单数据,total={}", total);
|
||||||
|
Map<String, Object> summary = new LinkedHashMap<>();
|
||||||
|
summary.put("total", total);
|
||||||
|
summary.put("inserted", 0);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
int inserted = 0;
|
||||||
|
List<String> failedOrderNos = new ArrayList<>();
|
||||||
|
for (int i = 0; i < orderList.size(); i++)
|
||||||
|
{
|
||||||
|
JSONObject item = orderList.getJSONObject(i);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AfOrder order = buildOrderFromWeidian(item, operator);
|
||||||
|
int rows = orderMapper.insertAfOrderIgnoreDuplicate(order);
|
||||||
|
if (rows > 0)
|
||||||
|
{
|
||||||
|
inserted++;
|
||||||
|
log.info("syncWeidianOrders 新增订单,sourceOrderNo={}, rechargeAccount={}, amount={}",
|
||||||
|
order.getSourceOrderNo(), order.getRechargeAccount(), order.getAmount());
|
||||||
|
dispatchService.onOrderPending(order.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
String orderId = item.getString("orderId");
|
||||||
|
log.error("syncWeidianOrders 解析或入库失败,orderId={}", orderId, e);
|
||||||
|
failedOrderNos.add(orderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("syncWeidianOrders 完成,total={}, inserted={}, failed={}", total, inserted, failedOrderNos.size());
|
||||||
|
Map<String, Object> summary = new LinkedHashMap<>();
|
||||||
|
summary.put("total", total);
|
||||||
|
summary.put("inserted", inserted);
|
||||||
|
if (!failedOrderNos.isEmpty())
|
||||||
|
{
|
||||||
|
summary.put("failedOrderNos", failedOrderNos);
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用微店订单列表接口,返回原始响应字符串。
|
||||||
|
*
|
||||||
|
* @param pageNum 页码(从 0 开始)
|
||||||
|
* @return 响应字符串
|
||||||
|
*/
|
||||||
|
private String fetchWeidianOrderList(int pageNum)
|
||||||
|
{
|
||||||
|
Map<String, String> headers = new HashMap<>();
|
||||||
|
headers.put("content-type", "application/x-www-form-urlencoded; charset=UTF-8");
|
||||||
|
headers.put("cookie", weidianProperties.getCookie());
|
||||||
|
headers.put("origin", "https://d.weidian.com");
|
||||||
|
headers.put("referer", "https://d.weidian.com/");
|
||||||
|
headers.put("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||||
|
|
||||||
|
JSONObject paramJson = new JSONObject();
|
||||||
|
paramJson.put("listType", 0);
|
||||||
|
paramJson.put("pageNum", pageNum);
|
||||||
|
paramJson.put("pageSize", weidianProperties.getPageSize());
|
||||||
|
paramJson.put("statusList", new String[]{"paid"});
|
||||||
|
paramJson.put("refundStatusList", new String[]{});
|
||||||
|
paramJson.put("channel", "pc");
|
||||||
|
paramJson.put("topOrderType", 0);
|
||||||
|
paramJson.put("shipRole", 0);
|
||||||
|
paramJson.put("orderIdList", "");
|
||||||
|
paramJson.put("itemTitle", "");
|
||||||
|
paramJson.put("buyerName", "");
|
||||||
|
paramJson.put("timeSearch", new JSONObject());
|
||||||
|
paramJson.put("orderBizType", "");
|
||||||
|
paramJson.put("promotionType", "");
|
||||||
|
paramJson.put("shipType", "");
|
||||||
|
paramJson.put("newGhSearchSellerRole", "7");
|
||||||
|
paramJson.put("memberLevel", "");
|
||||||
|
paramJson.put("orderSpecialType", "");
|
||||||
|
paramJson.put("repayStatus", "2");
|
||||||
|
paramJson.put("pushStatus", "1");
|
||||||
|
paramJson.put("bSellerId", "");
|
||||||
|
paramJson.put("itemSource", "");
|
||||||
|
paramJson.put("shipper", "");
|
||||||
|
paramJson.put("nSellerName", "");
|
||||||
|
paramJson.put("partnerName", "");
|
||||||
|
JSONObject noteCondition = new JSONObject();
|
||||||
|
noteCondition.put("buyerNote", "");
|
||||||
|
paramJson.put("noteSearchCondition", noteCondition);
|
||||||
|
JSONObject specialCondition = new JSONObject();
|
||||||
|
specialCondition.put("notShowGroupUnsuccess", 0);
|
||||||
|
specialCondition.put("notShowFxOrder", 0);
|
||||||
|
specialCondition.put("notShowUnRepayOrder", 0);
|
||||||
|
specialCondition.put("notShowBuyerRepayOrder", 0);
|
||||||
|
specialCondition.put("showAllPeriodOrder", 0);
|
||||||
|
specialCondition.put("notShowTencentShopOrder", 0);
|
||||||
|
specialCondition.put("notShowWithoutTimelinessOrder", 0);
|
||||||
|
paramJson.put("specialOrderSearchCondition", specialCondition);
|
||||||
|
paramJson.put("orderType", 4);
|
||||||
|
|
||||||
|
String body;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
body = "param=" + java.net.URLEncoder.encode(paramJson.toJSONString(), "UTF-8")
|
||||||
|
+ "&wdtoken=" + weidianProperties.getWdToken();
|
||||||
|
}
|
||||||
|
catch (java.io.UnsupportedEncodingException ignored)
|
||||||
|
{
|
||||||
|
throw new ServiceException("URL编码失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpUtils.sendPostWithHeaders(weidianProperties.getOrderListUrl(), body, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将微店订单 JSON 对象转换为 AfOrder 实体。
|
||||||
|
* 字段映射:
|
||||||
|
* orderId -> sourceOrderNo
|
||||||
|
* customInfo[name=抖音号].value -> rechargeAccount
|
||||||
|
* totalPrice -> amount
|
||||||
|
*
|
||||||
|
* @param item 微店单条订单 JSON
|
||||||
|
* @param operator 操作人
|
||||||
|
* @return AfOrder 实体
|
||||||
|
*/
|
||||||
|
private AfOrder buildOrderFromWeidian(JSONObject item, String operator)
|
||||||
|
{
|
||||||
|
AfOrder order = new AfOrder();
|
||||||
|
order.setSource(SOURCE_WEIDIAN);
|
||||||
|
|
||||||
|
String orderId = item.getString("orderId");
|
||||||
|
order.setSourceOrderNo(orderId);
|
||||||
|
order.setOrderNo(orderId);
|
||||||
|
|
||||||
|
String rechargeAccount = extractDouyinAccount(item.getJSONArray("customInfo"));
|
||||||
|
order.setRechargeAccount(StringUtils.isNotBlank(rechargeAccount) ? rechargeAccount : "");
|
||||||
|
|
||||||
|
BigDecimal totalPrice = item.getBigDecimal("totalPrice");
|
||||||
|
order.setAmount(totalPrice != null ? totalPrice : BigDecimal.ZERO);
|
||||||
|
|
||||||
|
order.setStatus(AfStatusConstants.ORDER_PENDING);
|
||||||
|
order.setDispatchMode("AUTO");
|
||||||
|
order.setExtJson(item.toJSONString());
|
||||||
|
order.setCreateBy(operator);
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 customInfo 数组中提取 name=抖音号 的 value。
|
||||||
|
*
|
||||||
|
* @param customInfo 微店订单 customInfo 数组
|
||||||
|
* @return 抖音号,未找到时返回空字符串
|
||||||
|
*/
|
||||||
|
private String extractDouyinAccount(JSONArray customInfo)
|
||||||
|
{
|
||||||
|
if (customInfo == null || customInfo.isEmpty())
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
for (int i = 0; i < customInfo.size(); i++)
|
||||||
|
{
|
||||||
|
JSONObject entry = customInfo.getJSONObject(i);
|
||||||
|
if ("抖音号".equals(entry.getString("name")))
|
||||||
|
{
|
||||||
|
return entry.getString("value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deliverWeidianOrder(String sourceOrderNo, String operator)
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(sourceOrderNo))
|
||||||
|
{
|
||||||
|
log.warn("deliverWeidianOrder 订单号为空,跳过");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("deliverWeidianOrder 开始,sourceOrderNo={}, operator={}", sourceOrderNo, operator);
|
||||||
|
|
||||||
|
Map<String, String> headers = new HashMap<>();
|
||||||
|
headers.put("content-type", "application/x-www-form-urlencoded; charset=UTF-8");
|
||||||
|
headers.put("cookie", weidianProperties.getCookie());
|
||||||
|
headers.put("origin", "https://d.weidian.com");
|
||||||
|
headers.put("referer", "https://d.weidian.com/");
|
||||||
|
headers.put("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||||
|
|
||||||
|
JSONObject paramJson = new JSONObject();
|
||||||
|
paramJson.put("from", "pc");
|
||||||
|
paramJson.put("orderId", sourceOrderNo);
|
||||||
|
paramJson.put("expressNo", "");
|
||||||
|
paramJson.put("expressType", 0);
|
||||||
|
paramJson.put("expressCustom", "");
|
||||||
|
paramJson.put("fullDeliver", true);
|
||||||
|
|
||||||
|
String body;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
body = "param=" + java.net.URLEncoder.encode(paramJson.toJSONString(), "UTF-8")
|
||||||
|
+ "&wdtoken=" + weidianProperties.getWdToken();
|
||||||
|
}
|
||||||
|
catch (java.io.UnsupportedEncodingException ignored)
|
||||||
|
{
|
||||||
|
throw new ServiceException("URL编码失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
String responseBody = HttpUtils.sendPostWithHeaders(weidianProperties.getDeliverOrderUrl(), body, headers);
|
||||||
|
if (StringUtils.isBlank(responseBody))
|
||||||
|
{
|
||||||
|
log.error("deliverWeidianOrder 请求失败,响应为空,sourceOrderNo={}", sourceOrderNo);
|
||||||
|
throw new ServiceException("微店发货接口请求失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject root = JSON.parseObject(responseBody);
|
||||||
|
JSONObject status = root.getJSONObject("status");
|
||||||
|
if (status == null || status.getIntValue("code") != 0)
|
||||||
|
{
|
||||||
|
String message = status != null ? status.getString("message") : "未知错误";
|
||||||
|
log.error("deliverWeidianOrder 失败,sourceOrderNo={}, response={}", sourceOrderNo, responseBody);
|
||||||
|
throw new ServiceException("微店发货失败:" + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("deliverWeidianOrder 成功,sourceOrderNo={}", sourceOrderNo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.ruoyi.system.service.af.impl;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
@@ -19,6 +21,7 @@ import com.ruoyi.system.mapper.af.AfDispatchTaskMapper;
|
|||||||
import com.ruoyi.system.mapper.af.AfOrderMapper;
|
import com.ruoyi.system.mapper.af.AfOrderMapper;
|
||||||
import com.ruoyi.system.mapper.af.AfTaskStepLogMapper;
|
import com.ruoyi.system.mapper.af.AfTaskStepLogMapper;
|
||||||
import com.ruoyi.system.service.af.IAfDispatchService;
|
import com.ruoyi.system.service.af.IAfDispatchService;
|
||||||
|
import com.ruoyi.system.service.af.IAfOrderService;
|
||||||
import com.ruoyi.system.service.af.IAfTaskService;
|
import com.ruoyi.system.service.af.IAfTaskService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +32,8 @@ import com.ruoyi.system.service.af.IAfTaskService;
|
|||||||
@Service
|
@Service
|
||||||
public class AfTaskServiceImpl implements IAfTaskService
|
public class AfTaskServiceImpl implements IAfTaskService
|
||||||
{
|
{
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AfTaskServiceImpl.class);
|
||||||
|
|
||||||
private static final int DEVICE_COOLDOWN_SECONDS = 10;
|
private static final int DEVICE_COOLDOWN_SECONDS = 10;
|
||||||
|
|
||||||
@Value("${af.rechargeUrl:}")
|
@Value("${af.rechargeUrl:}")
|
||||||
@@ -50,6 +55,10 @@ public class AfTaskServiceImpl implements IAfTaskService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IAfDispatchService dispatchService;
|
private IAfDispatchService dispatchService;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private IAfOrderService orderService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询任务列表。
|
* 查询任务列表。
|
||||||
*
|
*
|
||||||
@@ -334,6 +343,23 @@ public class AfTaskServiceImpl implements IAfTaskService
|
|||||||
? AfStatusConstants.ORDER_SUCCESS : AfStatusConstants.ORDER_FAILED;
|
? AfStatusConstants.ORDER_SUCCESS : AfStatusConstants.ORDER_FAILED;
|
||||||
orderMapper.updateOrderStatusByExpect(task.getOrderId(), AfStatusConstants.ORDER_PROCESSING, orderTargetStatus, failReason, operator);
|
orderMapper.updateOrderStatusByExpect(task.getOrderId(), AfStatusConstants.ORDER_PROCESSING, orderTargetStatus, failReason, operator);
|
||||||
|
|
||||||
|
// 充值成功后通知微店发货
|
||||||
|
if (StringUtils.equals(resultStatus, AfStatusConstants.TASK_SUCCESS) && task.getOrderId() != null)
|
||||||
|
{
|
||||||
|
com.ruoyi.system.domain.af.AfOrder order = orderMapper.selectAfOrderById(task.getOrderId());
|
||||||
|
if (order != null && "WEIDIAN".equals(order.getSource()))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
orderService.deliverWeidianOrder(order.getSourceOrderNo(), operator);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.error("微店发货通知失败,sourceOrderNo={},不影响订单状态", order.getSourceOrderNo(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 联动设备状态 → IDLE,并使用数据库时间写入固定 10 秒冷却,避免应用时区与数据库时区不一致时放大冷却时长。
|
// 联动设备状态 → IDLE,并使用数据库时间写入固定 10 秒冷却,避免应用时区与数据库时区不一致时放大冷却时长。
|
||||||
AfDevice device = deviceMapper.selectByUserNameAndDeviceCode(task.getUserName(), task.getDeviceCode());
|
AfDevice device = deviceMapper.selectByUserNameAndDeviceCode(task.getUserName(), task.getDeviceCode());
|
||||||
if (StringUtils.isNotNull(device))
|
if (StringUtils.isNotNull(device))
|
||||||
|
|||||||
@@ -138,4 +138,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
where id = #{id}
|
where id = #{id}
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<insert id="insertAfOrderIgnoreDuplicate" parameterType="com.ruoyi.system.domain.af.AfOrder" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
insert ignore into af_order
|
||||||
|
(source, source_order_no, order_no, recharge_account, amount, status, dispatch_mode, ext_json, create_by, create_time, update_by, update_time)
|
||||||
|
values
|
||||||
|
(#{source}, #{sourceOrderNo}, #{orderNo}, #{rechargeAccount}, #{amount}, #{status}, #{dispatchMode}, #{extJson}, #{createBy}, sysdate(), #{createBy}, sysdate())
|
||||||
|
</insert>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user