小如重构
This commit is contained in:
1 parent
2849fdbcb0
commit
f91b8587da
25 files changed
+2502
-179
No files matched your search
@@ -0,0 +1,341 @@
|
||||
# 获取贩卖机信息精简列表(根据用户绑定过滤)API 文档
|
||||
|
||||
## 接口信息
|
||||
|
||||
- **接口路径**:`GET /product/device/list-simple-by-user`
|
||||
- **接口描述**:获得贩卖机信息精简列表(根据用户绑定过滤,超级管理员返回全部)
|
||||
- **权限要求**:`product:device:query`
|
||||
- **认证方式**:Bearer Token(需要在请求头中携带 `Authorization: Bearer {token}`)
|
||||
|
||||
---
|
||||
|
||||
## 请求说明
|
||||
|
||||
### 请求方法
|
||||
`GET`
|
||||
|
||||
### 请求路径
|
||||
```
|
||||
/product/device/list-simple-by-user
|
||||
```
|
||||
|
||||
### 请求头
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
**无请求参数**(此接口不需要任何查询参数或请求体)
|
||||
|
||||
---
|
||||
|
||||
## 响应说明
|
||||
|
||||
### 响应格式
|
||||
|
||||
所有接口都遵循统一的响应格式:
|
||||
|
||||
```typescript
|
||||
interface CommonResult<T> {
|
||||
code: number; // 响应码,0 表示成功
|
||||
data: T; // 响应数据
|
||||
msg: string; // 响应消息(成功时通常为空字符串)
|
||||
}
|
||||
```
|
||||
|
||||
### 响应数据结构
|
||||
|
||||
**成功响应(code = 0)**:
|
||||
|
||||
```typescript
|
||||
interface DeviceSimpleRespVO {
|
||||
id: number; // 设备编号(主键)
|
||||
name: string; // 设备名称
|
||||
deviceId: string; // 设备IMEI(设备唯一标识)
|
||||
}
|
||||
|
||||
// 完整响应结构
|
||||
CommonResult<DeviceSimpleRespVO[]>
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
**成功响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "贩卖机001",
|
||||
"deviceId": "123456789012345"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "贩卖机002",
|
||||
"deviceId": "123456789012346"
|
||||
}
|
||||
],
|
||||
"msg": ""
|
||||
}
|
||||
```
|
||||
|
||||
**用户未绑定设备时**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": [],
|
||||
"msg": ""
|
||||
}
|
||||
```
|
||||
|
||||
**权限不足时**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 403,
|
||||
"data": null,
|
||||
"msg": "权限不足"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 业务逻辑说明
|
||||
|
||||
### 权限过滤逻辑
|
||||
|
||||
1. **超级管理员(super_admin 角色)**
|
||||
- 返回系统中**所有设备**的列表
|
||||
- 按设备编号(id)降序排列
|
||||
|
||||
2. **普通用户**
|
||||
- 只返回**当前登录用户绑定的设备**列表
|
||||
- 如果用户未绑定任何设备,返回空数组 `[]`
|
||||
- 按设备编号(id)降序排列
|
||||
|
||||
3. **未登录用户**
|
||||
- 返回空数组 `[]`
|
||||
|
||||
### 数据来源
|
||||
|
||||
- 设备数据来自 `product_device` 表
|
||||
- 用户设备绑定关系通过 `admin_user` 表的 `device_id` 字段或关联表维护
|
||||
- 超级管理员判断通过用户角色 `super_admin` 进行判断
|
||||
|
||||
---
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 典型使用场景
|
||||
|
||||
1. **设备下拉选择框**
|
||||
- 前端需要展示用户可操作的设备列表
|
||||
- 超级管理员看到所有设备,普通用户只看到自己绑定的设备
|
||||
|
||||
2. **设备筛选**
|
||||
- 在商品管理、货道管理等页面,需要根据用户权限过滤设备
|
||||
|
||||
3. **权限控制**
|
||||
- 确保用户只能操作自己绑定的设备
|
||||
|
||||
---
|
||||
|
||||
## 前端调用示例
|
||||
|
||||
### JavaScript / TypeScript
|
||||
|
||||
```typescript
|
||||
// 使用 fetch
|
||||
async function getDeviceSimpleListByUser() {
|
||||
const response = await fetch('/product/device/list-simple-by-user', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result: CommonResult<DeviceSimpleRespVO[]> = await response.json();
|
||||
|
||||
if (result.code === 0) {
|
||||
console.log('设备列表:', result.data);
|
||||
return result.data;
|
||||
} else {
|
||||
console.error('获取失败:', result.msg);
|
||||
throw new Error(result.msg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Axios
|
||||
|
||||
```typescript
|
||||
import axios from 'axios';
|
||||
|
||||
interface DeviceSimpleRespVO {
|
||||
id: number;
|
||||
name: string;
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
interface CommonResult<T> {
|
||||
code: number;
|
||||
data: T;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
async function getDeviceSimpleListByUser(): Promise<DeviceSimpleRespVO[]> {
|
||||
const response = await axios.get<CommonResult<DeviceSimpleRespVO[]>>(
|
||||
'/product/device/list-simple-by-user',
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.code === 0) {
|
||||
return response.data.data;
|
||||
} else {
|
||||
throw new Error(response.data.msg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vue 3 Composition API
|
||||
|
||||
```typescript
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
interface DeviceSimpleRespVO {
|
||||
id: number;
|
||||
name: string;
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export function useDeviceList() {
|
||||
const deviceList = ref<DeviceSimpleRespVO[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const fetchDeviceList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await axios.get<CommonResult<DeviceSimpleRespVO[]>>(
|
||||
'/product/device/list-simple-by-user'
|
||||
);
|
||||
|
||||
if (response.data.code === 0) {
|
||||
deviceList.value = response.data.data;
|
||||
} else {
|
||||
console.error('获取设备列表失败:', response.data.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchDeviceList();
|
||||
});
|
||||
|
||||
return {
|
||||
deviceList,
|
||||
loading,
|
||||
fetchDeviceList
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### React Hooks
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
interface DeviceSimpleRespVO {
|
||||
id: number;
|
||||
name: string;
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export function useDeviceList() {
|
||||
const [deviceList, setDeviceList] = useState<DeviceSimpleRespVO[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDeviceList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get<CommonResult<DeviceSimpleRespVO[]>>(
|
||||
'/product/device/list-simple-by-user'
|
||||
);
|
||||
|
||||
if (response.data.code === 0) {
|
||||
setDeviceList(response.data.data);
|
||||
} else {
|
||||
console.error('获取设备列表失败:', response.data.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDeviceList();
|
||||
}, []);
|
||||
|
||||
return { deviceList, loading };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|--------|------|----------|
|
||||
| 0 | 成功 | - |
|
||||
| 401 | 未授权(未登录或 token 过期) | 重新登录获取 token |
|
||||
| 403 | 权限不足 | 检查用户是否有 `product:device:query` 权限 |
|
||||
| 500 | 服务器内部错误 | 联系后端开发人员 |
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限要求**
|
||||
- 调用此接口需要用户已登录
|
||||
- 需要 `product:device:query` 权限
|
||||
|
||||
2. **数据过滤**
|
||||
- 超级管理员会看到所有设备
|
||||
- 普通用户只能看到自己绑定的设备
|
||||
- 如果用户未绑定设备,返回空数组(不会报错)
|
||||
|
||||
3. **排序规则**
|
||||
- 返回的设备列表按 `id` 降序排列(最新的设备在前)
|
||||
|
||||
4. **性能考虑**
|
||||
- 此接口返回的是精简数据(只有 id、name、deviceId),适合下拉选择等场景
|
||||
- 如果需要完整设备信息,请使用其他接口
|
||||
|
||||
---
|
||||
|
||||
## 相关接口
|
||||
|
||||
- **获取设备完整列表**:`GET /product/device/list-simple`(返回所有设备,不过滤用户绑定)
|
||||
- **获取设备详情**:`GET /product/device/get?id={id}`
|
||||
- **分页查询设备**:`GET /product/device/page`
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v1.0
|
||||
**最后更新**:2025-01-27
|
||||
+7
@@ -46,6 +46,13 @@ public class ChannelController {
|
||||
return success(channelService.createChannel(createReqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/batch-create")
|
||||
@Operation(summary = "批量创建货道")
|
||||
@PreAuthorize("@ss.hasPermission('product:channel:create')")
|
||||
public CommonResult<ChannelBatchCreateRespVO> batchCreateChannel(@Valid @RequestBody ChannelBatchCreateReqVO batchCreateReqVO) {
|
||||
return success(channelService.batchCreateChannel(batchCreateReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新货道")
|
||||
@PreAuthorize("@ss.hasPermission('product:channel:update')")
|
||||
|
||||
+668
@@ -0,0 +1,668 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.channel;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 商品 SPU 合并脚本
|
||||
*
|
||||
* 功能:
|
||||
* 1. 识别相同或相似名称的商品 SPU
|
||||
* 2. 合并重复的 SPU(保留 id 最小的)
|
||||
* 3. 更新 SKU 表的 spu_id
|
||||
* 4. 更新订单明细表的 spu_id 和 sku_id
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 配置数据库连接信息
|
||||
* 2. 运行 main 方法
|
||||
* 3. 检查控制台输出的合并计划
|
||||
* 4. 确认后执行合并
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @date 2025-01-27
|
||||
*/
|
||||
public class MergeDuplicateSpuScript {
|
||||
|
||||
// ========== 配置信息 ==========
|
||||
private static final String DB_URL = "jdbc:mysql://localhost:3306/xr_v2?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai";
|
||||
private static final String DB_USER = "root";
|
||||
private static final String DB_PASSWORD = "root";
|
||||
|
||||
// 相似度阈值(0.0 - 1.0),值越大要求越相似
|
||||
private static final double SIMILARITY_THRESHOLD = 0.7;
|
||||
|
||||
// 是否执行合并(false 时只预览,不实际执行)
|
||||
private static final boolean EXECUTE_MERGE = true;
|
||||
|
||||
public static void main(String[] args) {
|
||||
MergeDuplicateSpuScript script = new MergeDuplicateSpuScript();
|
||||
try {
|
||||
script.run();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void run() throws SQLException {
|
||||
System.out.println("==========================================");
|
||||
System.out.println("商品 SPU 合并脚本开始执行");
|
||||
System.out.println("==========================================");
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {
|
||||
// 1. 查询所有未删除的 SPU
|
||||
List<SpuInfo> allSpus = getAllSpus(conn);
|
||||
System.out.println("总共找到 " + allSpus.size() + " 个商品 SPU");
|
||||
|
||||
// 2. 识别需要合并的 SPU
|
||||
Map<Long, Long> mergeMapping = identifyDuplicateSpus(allSpus);
|
||||
System.out.println("识别出 " + mergeMapping.size() + " 个需要合并的 SPU");
|
||||
|
||||
// 3. 显示合并计划
|
||||
displayMergePlan(conn, mergeMapping);
|
||||
|
||||
if (!EXECUTE_MERGE) {
|
||||
System.out.println("\n⚠️ 当前为预览模式,未实际执行合并");
|
||||
System.out.println("如需执行合并,请将 EXECUTE_MERGE 设置为 true");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 确认后执行合并
|
||||
System.out.println("\n开始执行合并...");
|
||||
executeMerge(conn, mergeMapping);
|
||||
|
||||
System.out.println("\n==========================================");
|
||||
System.out.println("合并完成!");
|
||||
System.out.println("==========================================");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有未删除的 SPU
|
||||
*/
|
||||
private List<SpuInfo> getAllSpus(Connection conn) throws SQLException {
|
||||
List<SpuInfo> spus = new ArrayList<>();
|
||||
String sql = "SELECT id, name, device_id, category_id, price, stock, create_time " +
|
||||
"FROM product_spu WHERE deleted = 0 ORDER BY id";
|
||||
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
SpuInfo spu = new SpuInfo();
|
||||
spu.id = rs.getLong("id");
|
||||
spu.name = rs.getString("name");
|
||||
spu.deviceId = rs.getString("device_id");
|
||||
spu.categoryId = rs.getLong("category_id");
|
||||
spu.price = rs.getInt("price");
|
||||
spu.stock = rs.getInt("stock");
|
||||
spu.createTime = rs.getTimestamp("create_time");
|
||||
spus.add(spu);
|
||||
}
|
||||
}
|
||||
return spus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别需要合并的重复 SPU
|
||||
*/
|
||||
private Map<Long, Long> identifyDuplicateSpus(List<SpuInfo> spus) {
|
||||
Map<Long, Long> mergeMapping = new HashMap<>();
|
||||
Set<Long> processed = new HashSet<>();
|
||||
|
||||
for (int i = 0; i < spus.size(); i++) {
|
||||
SpuInfo spu1 = spus.get(i);
|
||||
if (processed.contains(spu1.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 查找与 spu1 相似的其他 SPU
|
||||
for (int j = i + 1; j < spus.size(); j++) {
|
||||
SpuInfo spu2 = spus.get(j);
|
||||
if (processed.contains(spu2.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 判断是否应该合并
|
||||
if (shouldMerge(spu1, spu2)) {
|
||||
// 保留 id 较小的 SPU
|
||||
Long keepSpuId = spu1.id < spu2.id ? spu1.id : spu2.id;
|
||||
Long mergeSpuId = spu1.id < spu2.id ? spu2.id : spu1.id;
|
||||
|
||||
mergeMapping.put(mergeSpuId, keepSpuId);
|
||||
processed.add(mergeSpuId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergeMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个 SPU 是否应该合并
|
||||
*/
|
||||
private boolean shouldMerge(SpuInfo spu1, SpuInfo spu2) {
|
||||
// 0. 设备检查:只有相同 device_id 的 SPU 才能合并
|
||||
// device_id 不同表示不同的售货机,不应该合并
|
||||
if (!isDeviceIdCompatible(spu1.deviceId, spu2.deviceId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. 价格检查:如果两个 SPU 都有价格,价格差异不能超过 20%
|
||||
if (!isPriceCompatible(spu1.price, spu2.price)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 名称完全相同
|
||||
if (spu1.name.equals(spu2.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. 去除空格和特殊字符后相同
|
||||
String name1 = normalizeName(spu1.name);
|
||||
String name2 = normalizeName(spu2.name);
|
||||
if (name1.equals(name2)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. 去除数字后相同(适用于"商品A-1"和"商品A-2")
|
||||
String name1NoNum = name1.replaceAll("\\d", "");
|
||||
String name2NoNum = name2.replaceAll("\\d", "");
|
||||
if (name1NoNum.length() >= 2 && name1NoNum.equals(name2NoNum)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. 计算字符串相似度(使用 Levenshtein 距离)
|
||||
double similarity = calculateSimilarity(name1, name2);
|
||||
if (similarity >= SIMILARITY_THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 6. 提取核心关键词匹配(适用于"可口可乐500ml"和"可口可乐330ml")
|
||||
String[] keywords1 = extractKeywords(name1);
|
||||
String[] keywords2 = extractKeywords(name2);
|
||||
if (keywords1.length > 0 && keywords2.length > 0) {
|
||||
int commonKeywords = countCommonKeywords(keywords1, keywords2);
|
||||
// 如果核心关键词匹配度 >= 70%,则合并
|
||||
if (commonKeywords > 0 &&
|
||||
(double) commonKeywords / Math.max(keywords1.length, keywords2.length) >= 0.7) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个 device_id 是否兼容(只有相同的 device_id 才能合并)
|
||||
*/
|
||||
private boolean isDeviceIdCompatible(String deviceId1, String deviceId2) {
|
||||
// 如果两个 device_id 都为 null 或空字符串,认为兼容(可能是历史数据,允许合并)
|
||||
boolean isEmpty1 = deviceId1 == null || deviceId1.trim().isEmpty();
|
||||
boolean isEmpty2 = deviceId2 == null || deviceId2.trim().isEmpty();
|
||||
|
||||
if (isEmpty1 && isEmpty2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果其中一个为空,另一个不为空,不兼容(可能是不同设备的数据)
|
||||
if (isEmpty1 || isEmpty2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 两个都不为空,必须完全相同才能合并
|
||||
return deviceId1.trim().equals(deviceId2.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个价格是否兼容(差异不超过 20%)
|
||||
*/
|
||||
private boolean isPriceCompatible(Integer price1, Integer price2) {
|
||||
// 如果其中一个价格为空或 <= 0,认为兼容(允许合并)
|
||||
if (price1 == null || price1 <= 0 || price2 == null || price2 <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 计算价格差异百分比
|
||||
int maxPrice = Math.max(price1, price2);
|
||||
int minPrice = Math.min(price1, price2);
|
||||
double diffRatio = (double) (maxPrice - minPrice) / maxPrice;
|
||||
|
||||
// 价格差异不超过 20% 则认为兼容
|
||||
return diffRatio <= 0.2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化商品名称(去除空格、特殊字符、统一大小写)
|
||||
*/
|
||||
private String normalizeName(String name) {
|
||||
if (name == null) {
|
||||
return "";
|
||||
}
|
||||
return name.replaceAll("[\\s ]+", "")
|
||||
.replaceAll("[\\-_/]", "")
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个字符串的相似度(使用 Levenshtein 距离)
|
||||
*/
|
||||
private double calculateSimilarity(String s1, String s2) {
|
||||
if (s1 == null || s2 == null) {
|
||||
return 0.0;
|
||||
}
|
||||
if (s1.equals(s2)) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
int maxLength = Math.max(s1.length(), s2.length());
|
||||
if (maxLength == 0) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
int distance = levenshteinDistance(s1, s2);
|
||||
return 1.0 - (double) distance / maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 Levenshtein 距离
|
||||
*/
|
||||
private int levenshteinDistance(String s1, String s2) {
|
||||
int[][] dp = new int[s1.length() + 1][s2.length() + 1];
|
||||
|
||||
for (int i = 0; i <= s1.length(); i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (int j = 0; j <= s2.length(); j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
|
||||
for (int i = 1; i <= s1.length(); i++) {
|
||||
for (int j = 1; j <= s2.length(); j++) {
|
||||
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dp[s1.length()][s2.length()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取商品名称中的关键词(去除量词、规格等)
|
||||
*/
|
||||
private String[] extractKeywords(String name) {
|
||||
// 常见量词和规格词
|
||||
String[] stopWords = {"ml", "g", "kg", "L", "升", "毫升", "克", "千克",
|
||||
"500", "330", "250", "1000", "瓶", "罐", "包", "盒",
|
||||
"A", "B", "C", "1", "2", "3", "-", "_", " "};
|
||||
|
||||
String normalized = name;
|
||||
for (String stopWord : stopWords) {
|
||||
normalized = normalized.replace(stopWord, " ");
|
||||
}
|
||||
|
||||
// 提取长度 >= 2 的连续中文字符或英文单词
|
||||
List<String> keywords = new ArrayList<>();
|
||||
String[] parts = normalized.split("\\s+");
|
||||
for (String part : parts) {
|
||||
if (part.length() >= 2) {
|
||||
keywords.add(part);
|
||||
}
|
||||
}
|
||||
|
||||
return keywords.toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计共同关键词数量
|
||||
*/
|
||||
private int countCommonKeywords(String[] keywords1, String[] keywords2) {
|
||||
Set<String> set1 = new HashSet<>(Arrays.asList(keywords1));
|
||||
Set<String> set2 = new HashSet<>(Arrays.asList(keywords2));
|
||||
|
||||
set1.retainAll(set2);
|
||||
return set1.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示合并计划
|
||||
*/
|
||||
private void displayMergePlan(Connection conn, Map<Long, Long> mergeMapping) throws SQLException {
|
||||
System.out.println("\n==========================================");
|
||||
System.out.println("合并计划预览");
|
||||
System.out.println("==========================================");
|
||||
|
||||
Map<Long, List<Long>> groupedMapping = new HashMap<>();
|
||||
for (Map.Entry<Long, Long> entry : mergeMapping.entrySet()) {
|
||||
Long keepSpuId = entry.getValue();
|
||||
Long mergeSpuId = entry.getKey();
|
||||
groupedMapping.computeIfAbsent(keepSpuId, k -> new ArrayList<>()).add(mergeSpuId);
|
||||
}
|
||||
|
||||
for (Map.Entry<Long, List<Long>> entry : groupedMapping.entrySet()) {
|
||||
Long keepSpuId = entry.getKey();
|
||||
List<Long> mergeSpuIds = entry.getValue();
|
||||
|
||||
SpuInfo keepSpu = getSpuById(conn, keepSpuId);
|
||||
System.out.println("\n保留 SPU: ID=" + keepSpuId + ", 名称=" + keepSpu.name);
|
||||
System.out.println(" 合并以下 SPU:");
|
||||
|
||||
for (Long mergeSpuId : mergeSpuIds) {
|
||||
SpuInfo mergeSpu = getSpuById(conn, mergeSpuId);
|
||||
int skuCount = getSkuCount(conn, mergeSpuId);
|
||||
System.out.println(" - ID=" + mergeSpuId + ", 名称=" + mergeSpu.name +
|
||||
", SKU数量=" + skuCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行合并操作
|
||||
*/
|
||||
private void executeMerge(Connection conn, Map<Long, Long> mergeMapping) throws SQLException {
|
||||
conn.setAutoCommit(false);
|
||||
|
||||
try {
|
||||
// 1. 创建临时映射表
|
||||
createTempMappingTable(conn, mergeMapping);
|
||||
|
||||
// 2. 更新 SKU 表的 spu_id
|
||||
updateSkuSpuId(conn, mergeMapping);
|
||||
|
||||
// 3. 更新订单明细表的 spu_id 和 sku_id
|
||||
updateOrderItemSpuAndSkuId(conn, mergeMapping);
|
||||
|
||||
// 4. 更新 SPU 统计信息
|
||||
updateSpuStatistics(conn, mergeMapping);
|
||||
|
||||
// 5. 软删除旧的 SPU
|
||||
softDeleteOldSpus(conn, mergeMapping);
|
||||
|
||||
conn.commit();
|
||||
System.out.println("合并操作已提交");
|
||||
|
||||
} catch (Exception e) {
|
||||
conn.rollback();
|
||||
System.err.println("合并操作失败,已回滚: " + e.getMessage());
|
||||
throw e;
|
||||
} finally {
|
||||
conn.setAutoCommit(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建临时映射表
|
||||
*/
|
||||
private void createTempMappingTable(Connection conn, Map<Long, Long> mergeMapping) throws SQLException {
|
||||
String sql = "DROP TABLE IF EXISTS temp_spu_merge_mapping";
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
stmt.execute(sql);
|
||||
}
|
||||
|
||||
sql = "CREATE TABLE temp_spu_merge_mapping (" +
|
||||
"old_spu_id BIGINT PRIMARY KEY, " +
|
||||
"new_spu_id BIGINT NOT NULL)";
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
stmt.execute(sql);
|
||||
}
|
||||
|
||||
sql = "INSERT INTO temp_spu_merge_mapping (old_spu_id, new_spu_id) VALUES (?, ?)";
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
for (Map.Entry<Long, Long> entry : mergeMapping.entrySet()) {
|
||||
stmt.setLong(1, entry.getKey());
|
||||
stmt.setLong(2, entry.getValue());
|
||||
stmt.addBatch();
|
||||
}
|
||||
stmt.executeBatch();
|
||||
}
|
||||
|
||||
System.out.println("已创建临时映射表,共 " + mergeMapping.size() + " 条记录");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 SKU 表的 spu_id
|
||||
*/
|
||||
private void updateSkuSpuId(Connection conn, Map<Long, Long> mergeMapping) throws SQLException {
|
||||
String sql = "UPDATE product_sku sku " +
|
||||
"INNER JOIN temp_spu_merge_mapping m ON sku.spu_id = m.old_spu_id " +
|
||||
"SET sku.spu_id = m.new_spu_id, sku.update_time = NOW() " +
|
||||
"WHERE sku.deleted = 0";
|
||||
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
int count = stmt.executeUpdate(sql);
|
||||
System.out.println("已更新 " + count + " 个 SKU 的 spu_id");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订单明细表的 spu_id 和 sku_id
|
||||
*/
|
||||
private void updateOrderItemSpuAndSkuId(Connection conn, Map<Long, Long> mergeMapping) throws SQLException {
|
||||
// 1. 更新 spu_id
|
||||
String sql = "UPDATE trade_order_item oi " +
|
||||
"INNER JOIN temp_spu_merge_mapping m ON oi.spu_id = m.old_spu_id " +
|
||||
"SET oi.spu_id = m.new_spu_id, oi.update_time = NOW() " +
|
||||
"WHERE oi.deleted = 0";
|
||||
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
int count = stmt.executeUpdate(sql);
|
||||
System.out.println("已更新 " + count + " 个订单明细的 spu_id");
|
||||
}
|
||||
|
||||
// 2. 更新 sku_id(根据新的 spu_id 和 cabinet_channel 匹配)
|
||||
sql = "UPDATE trade_order_item oi " +
|
||||
"INNER JOIN temp_spu_merge_mapping m ON oi.spu_id = m.new_spu_id " +
|
||||
"INNER JOIN product_sku sku ON sku.spu_id = m.new_spu_id " +
|
||||
"AND sku.cabinet_channel = oi.cabinet_channel " +
|
||||
"AND sku.deleted = 0 " +
|
||||
"SET oi.sku_id = sku.id, oi.update_time = NOW() " +
|
||||
"WHERE oi.deleted = 0 " +
|
||||
"AND oi.sku_id IN (SELECT old_spu_id FROM temp_spu_merge_mapping)";
|
||||
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
int count = stmt.executeUpdate(sql);
|
||||
System.out.println("已更新 " + count + " 个订单明细的 sku_id");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 SPU 统计信息
|
||||
*/
|
||||
private void updateSpuStatistics(Connection conn, Map<Long, Long> mergeMapping) throws SQLException {
|
||||
Set<Long> keepSpuIds = new HashSet<>(mergeMapping.values());
|
||||
|
||||
for (Long spuId : keepSpuIds) {
|
||||
// 更新库存
|
||||
String sql = "UPDATE product_spu spu " +
|
||||
"SET spu.stock = (SELECT COALESCE(SUM(sku.stock), 0) " +
|
||||
" FROM product_sku sku " +
|
||||
" WHERE sku.spu_id = spu.id AND sku.deleted = 0), " +
|
||||
" spu.update_time = NOW() " +
|
||||
"WHERE spu.id = ?";
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
stmt.setLong(1, spuId);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
|
||||
// 更新价格(最低价,如果所有 SKU 都没有价格,保留原 SPU 的价格)
|
||||
sql = "UPDATE product_spu spu " +
|
||||
"SET spu.price = COALESCE( " +
|
||||
" (SELECT MIN(sku.price) " +
|
||||
" FROM product_sku sku " +
|
||||
" WHERE sku.spu_id = spu.id AND sku.deleted = 0 AND sku.price > 0), " +
|
||||
" spu.price, " + // 如果所有 SKU 都没有有效价格,保留原 SPU 的价格
|
||||
" 0" + // 如果原 SPU 也没有价格,设置为 0(根据实际业务需求调整)
|
||||
"), " +
|
||||
" spu.update_time = NOW() " +
|
||||
"WHERE spu.id = ?";
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
stmt.setLong(1, spuId);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
|
||||
// 更新销量
|
||||
sql = "UPDATE product_spu spu " +
|
||||
"SET spu.sales_count = (SELECT COALESCE(SUM(sku.sales_count), 0) " +
|
||||
" FROM product_sku sku " +
|
||||
" WHERE sku.spu_id = spu.id AND sku.deleted = 0), " +
|
||||
" spu.update_time = NOW() " +
|
||||
"WHERE spu.id = ?";
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
stmt.setLong(1, spuId);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
|
||||
// 更新 device_id:根据合并后的 SKU 对应的货道反查 device_imei
|
||||
updateSpuDeviceId(conn, spuId);
|
||||
}
|
||||
|
||||
System.out.println("已更新 " + keepSpuIds.size() + " 个 SPU 的统计信息");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 SPU 的 device_id(根据 SKU 对应的货道反查 device_imei)
|
||||
*/
|
||||
private void updateSpuDeviceId(Connection conn, Long spuId) throws SQLException {
|
||||
// 1. 查询该 SPU 下所有 SKU 的 cabinet_channel
|
||||
Set<String> cabinetChannels = new HashSet<>();
|
||||
String sql = "SELECT DISTINCT cabinet_channel FROM product_sku " +
|
||||
"WHERE spu_id = ? AND deleted = 0 AND cabinet_channel IS NOT NULL AND cabinet_channel != ''";
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
stmt.setLong(1, spuId);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
String cabinetChannel = rs.getString("cabinet_channel");
|
||||
if (cabinetChannel != null && !cabinetChannel.isEmpty()) {
|
||||
cabinetChannels.add(cabinetChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cabinetChannels.isEmpty()) {
|
||||
System.out.println(" SPU ID=" + spuId + " 没有找到有效的 cabinet_channel,跳过 device_id 更新");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 根据 cabinet_channel 查询 product_channel 表,获取 device_imei
|
||||
// 注意:同一个 cabinet_channel 可能在不同设备中存在,所以需要统计所有匹配的 device_imei
|
||||
Map<String, Integer> deviceImeiCount = new HashMap<>();
|
||||
sql = "SELECT DISTINCT pc.device_imei " +
|
||||
"FROM product_channel pc " +
|
||||
"WHERE pc.cabinet_channel = ? AND pc.deleted = 0 AND pc.device_imei IS NOT NULL AND pc.device_imei != ''";
|
||||
|
||||
for (String cabinetChannel : cabinetChannels) {
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
stmt.setString(1, cabinetChannel);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
String deviceImei = rs.getString("device_imei");
|
||||
if (deviceImei != null && !deviceImei.isEmpty()) {
|
||||
// 统计每个 device_imei 出现的次数(每个 cabinet_channel 计数一次)
|
||||
deviceImeiCount.put(deviceImei, deviceImeiCount.getOrDefault(deviceImei, 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceImeiCount.isEmpty()) {
|
||||
System.out.println(" SPU ID=" + spuId + " 没有找到对应的 device_imei,跳过 device_id 更新");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 选择出现次数最多的 device_imei(如果多个出现次数相同,选择第一个)
|
||||
String targetDeviceImei = null;
|
||||
int maxCount = 0;
|
||||
for (Map.Entry<String, Integer> entry : deviceImeiCount.entrySet()) {
|
||||
if (entry.getValue() > maxCount) {
|
||||
maxCount = entry.getValue();
|
||||
targetDeviceImei = entry.getKey();
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有货道都属于同一个设备,直接使用;否则使用出现次数最多的
|
||||
if (deviceImeiCount.size() == 1) {
|
||||
targetDeviceImei = deviceImeiCount.keySet().iterator().next();
|
||||
System.out.println(" SPU ID=" + spuId + " 所有货道属于同一设备,更新 device_id=" + targetDeviceImei);
|
||||
} else {
|
||||
System.out.println(" SPU ID=" + spuId + " 货道属于多个设备,选择出现次数最多的 device_id=" + targetDeviceImei + " (出现 " + maxCount + " 次)");
|
||||
}
|
||||
|
||||
// 4. 更新 SPU 的 device_id
|
||||
sql = "UPDATE product_spu SET device_id = ?, update_time = NOW() WHERE id = ?";
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
stmt.setString(1, targetDeviceImei);
|
||||
stmt.setLong(2, spuId);
|
||||
int count = stmt.executeUpdate();
|
||||
if (count > 0) {
|
||||
System.out.println(" ✓ 已更新 SPU ID=" + spuId + " 的 device_id=" + targetDeviceImei);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除旧的 SPU
|
||||
*/
|
||||
private void softDeleteOldSpus(Connection conn, Map<Long, Long> mergeMapping) throws SQLException {
|
||||
String sql = "UPDATE product_spu spu " +
|
||||
"INNER JOIN temp_spu_merge_mapping m ON spu.id = m.old_spu_id " +
|
||||
"SET spu.deleted = 1, spu.update_time = NOW() " +
|
||||
"WHERE spu.deleted = 0";
|
||||
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
int count = stmt.executeUpdate(sql);
|
||||
System.out.println("已软删除 " + count + " 个旧 SPU");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 查询 SPU
|
||||
*/
|
||||
private SpuInfo getSpuById(Connection conn, Long spuId) throws SQLException {
|
||||
String sql = "SELECT id, name FROM product_spu WHERE id = ?";
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
stmt.setLong(1, spuId);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
SpuInfo spu = new SpuInfo();
|
||||
spu.id = rs.getLong("id");
|
||||
spu.name = rs.getString("name");
|
||||
return spu;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 SPU 的 SKU 数量
|
||||
*/
|
||||
private int getSkuCount(Connection conn, Long spuId) throws SQLException {
|
||||
String sql = "SELECT COUNT(*) FROM product_sku WHERE spu_id = ? AND deleted = 0";
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
stmt.setLong(1, spuId);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* SPU 信息类
|
||||
*/
|
||||
static class SpuInfo {
|
||||
Long id;
|
||||
String name;
|
||||
String deviceId;
|
||||
Long categoryId;
|
||||
Integer price;
|
||||
Integer stock;
|
||||
Timestamp createTime;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.channel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 货道批量新增 Request VO")
|
||||
@Data
|
||||
public class ChannelBatchCreateReqVO {
|
||||
|
||||
@Schema(description = "货道列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "货道列表不能为空")
|
||||
@Valid
|
||||
private List<ChannelSaveReqVO> channels;
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.channel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "管理后台 - 货道批量新增 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class ChannelBatchCreateRespVO {
|
||||
|
||||
@Schema(description = "创建成功的货道ID列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<Long> successIds;
|
||||
|
||||
@Schema(description = "创建失败的货道信息,key 为货道编号或名称,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Map<String, String> failureChannels;
|
||||
|
||||
@Schema(description = "成功数量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Integer successCount;
|
||||
|
||||
@Schema(description = "失败数量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Integer failureCount;
|
||||
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@ public class ChannelImportExcelVO {
|
||||
private String name;
|
||||
|
||||
@ExcelProperty("设备IMEI")
|
||||
private String deviceId;
|
||||
private String deviceImei;
|
||||
|
||||
@ExcelProperty("当前使用的货道或柜门编号")
|
||||
private String cabinetChannel;
|
||||
|
||||
+5
-2
@@ -18,8 +18,11 @@ public class ChannelPageReqVO extends PageParam {
|
||||
@Schema(description = "名称", example = "芋艿")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "设备IMEI", example = "12439")
|
||||
private String deviceId;
|
||||
@Schema(description = "设备ID(关联 product_device.id)", example = "1")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "设备IMEI(兼容字段)", example = "12439")
|
||||
private String deviceImei;
|
||||
|
||||
@Schema(description = "当前使用的货道或柜门编号")
|
||||
private String cabinetChannel;
|
||||
|
||||
+5
-3
@@ -15,9 +15,11 @@ public class ChannelSaveReqVO {
|
||||
@Schema(description = "名称", example = "芋艿")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "设备IMEI", requiredMode = Schema.RequiredMode.REQUIRED, example = "12439")
|
||||
@NotEmpty(message = "设备IMEI不能为空")
|
||||
private String deviceId;
|
||||
@Schema(description = "设备ID(关联 product_device.id)", example = "1")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "设备IMEI(兼容字段,如果 deviceId 为空则必填)", example = "12439")
|
||||
private String deviceImei;
|
||||
|
||||
@Schema(description = "当前使用的货道或柜门编号")
|
||||
private String cabinetChannel;
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSkuRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSpuPageReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSpuRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSpuSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuCreatorDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
|
||||
import cn.iocoder.yudao.module.product.service.device.DeviceService;
|
||||
import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
|
||||
import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
|
||||
|
||||
/**
|
||||
* 管理后台 - 售货机商品 SPU Controller
|
||||
*
|
||||
* 注意:售货机商品与商城商品的区别:
|
||||
* 1. deviceId 是设备主键ID(Long类型),需要转换为设备IMEI存储
|
||||
* 2. 不需要品牌(brandId)
|
||||
* 3. 配送方式通常只包含自提柜(deliveryTypes = [3])
|
||||
* 4. SKU 中必须包含 cabinetChannel(货道编号)
|
||||
*
|
||||
* @author 小如之家
|
||||
*/
|
||||
@Tag(name = "管理后台 - 售货机商品 SPU")
|
||||
@RestController
|
||||
@RequestMapping("/product/device-spu")
|
||||
@Validated
|
||||
public class DeviceProductSpuController {
|
||||
|
||||
@Resource
|
||||
private ProductSpuService productSpuService;
|
||||
|
||||
@Resource
|
||||
private ProductSkuService productSkuService;
|
||||
|
||||
@Resource
|
||||
private DeviceService deviceService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建售货机商品 SPU")
|
||||
@PreAuthorize("@ss.hasPermission('product:spu:create')")
|
||||
public CommonResult<Long> createDeviceProductSpu(@Valid @RequestBody DeviceProductSpuSaveReqVO createReqVO) {
|
||||
return success(productSpuService.createDeviceSpu(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新售货机商品 SPU")
|
||||
@PreAuthorize("@ss.hasPermission('product:spu:update')")
|
||||
public CommonResult<Boolean> updateDeviceProductSpu(@Valid @RequestBody DeviceProductSpuSaveReqVO updateReqVO) {
|
||||
productSpuService.updateDeviceSpu(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除售货机商品 SPU")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('product:spu:delete')")
|
||||
public CommonResult<Boolean> deleteDeviceProductSpu(@RequestParam("id") Long id) {
|
||||
productSpuService.deleteDeviceSpu(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get-detail")
|
||||
@Operation(summary = "获得售货机商品 SPU 明细")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('product:spu:query')")
|
||||
public CommonResult<DeviceProductSpuRespVO> getDeviceProductSpuDetail(@RequestParam("id") Long id) {
|
||||
// 获得商品 SPU
|
||||
ProductSpuDO spu = productSpuService.getDeviceSpu(id);
|
||||
if (spu == null) {
|
||||
return success(null);
|
||||
}
|
||||
// 查询商品 SKU
|
||||
List<ProductSkuDO> skus = productSkuService.getSkuListBySpuId(spu.getId());
|
||||
// 转换为售货机商品响应 VO
|
||||
DeviceProductSpuRespVO respVO = convertToDeviceProductSpuRespVO(spu, skus);
|
||||
return success(respVO);
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得售货机商品 SPU 分页")
|
||||
@PreAuthorize("@ss.hasPermission('product:spu:query')")
|
||||
public CommonResult<PageResult<DeviceProductSpuRespVO>> getDeviceProductSpuPage(@Valid DeviceProductSpuPageReqVO pageVO) {
|
||||
PageResult<ProductSpuCreatorDO> pageResult = productSpuService.getDeviceSpuPage(pageVO);
|
||||
// 查询商品 SKU
|
||||
List<ProductSkuDO> skuList = productSkuService.getSkuListBySpuId(
|
||||
pageResult.getList().stream().map(ProductSpuCreatorDO::getId).collect(Collectors.toList()));
|
||||
Map<Long, List<ProductSkuDO>> skuMap = convertMultiMap(skuList, ProductSkuDO::getSpuId);
|
||||
|
||||
// 转换为售货机商品响应 VO(含创建人、创建时间)
|
||||
List<DeviceProductSpuRespVO> respList = pageResult.getList().stream()
|
||||
.map(spu -> convertToDeviceProductSpuRespVO(spu, skuMap.get(spu.getId())))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return success(new PageResult<>(respList, pageResult.getTotal()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 ProductSpuDO/ProductSpuCreatorDO 和 ProductSkuDO 列表转换为 DeviceProductSpuRespVO
|
||||
* 需要将 deviceId(设备IMEI,String)转换为设备主键ID(Long)
|
||||
*/
|
||||
private DeviceProductSpuRespVO convertToDeviceProductSpuRespVO(ProductSpuDO spu, List<ProductSkuDO> skus) {
|
||||
DeviceProductSpuRespVO respVO = BeanUtils.toBean(spu, DeviceProductSpuRespVO.class);
|
||||
|
||||
// 创建人、创建时间(分页数据来自 ProductSpuCreatorDO 时带创建人姓名)
|
||||
respVO.setCreator(spu.getCreator());
|
||||
respVO.setCreateTime(spu.getCreateTime());
|
||||
if (spu instanceof ProductSpuCreatorDO) {
|
||||
respVO.setCreatorName(((ProductSpuCreatorDO) spu).getUsername());
|
||||
}
|
||||
|
||||
// 将 deviceId(设备IMEI,String)转换为设备主键ID(Long)
|
||||
if (spu.getDeviceId() != null && !spu.getDeviceId().isEmpty()) {
|
||||
cn.iocoder.yudao.module.product.api.device.dto.DeviceRespDto device =
|
||||
deviceService.getDeviceByDeviceId(spu.getDeviceId());
|
||||
if (device != null) {
|
||||
respVO.setDeviceId(device.getId()); // 设备主键ID(Long)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换 SKU 列表
|
||||
if (skus != null && !skus.isEmpty()) {
|
||||
respVO.setSkus(BeanUtils.toBean(skus, DeviceProductSkuRespVO.class));
|
||||
}
|
||||
|
||||
return respVO;
|
||||
}
|
||||
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.device.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - 售货机商品 SKU Response VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 售货机商品 SKU Response VO")
|
||||
@Data
|
||||
public class DeviceProductSkuRespVO {
|
||||
|
||||
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "商品 SKU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试商品")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "销售价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
|
||||
private Integer price;
|
||||
|
||||
@Schema(description = "市场价", example = "100")
|
||||
private Integer marketPrice;
|
||||
|
||||
@Schema(description = "成本价", example = "100")
|
||||
private Integer costPrice;
|
||||
|
||||
@Schema(description = "条形码", example = "1")
|
||||
private String barCode;
|
||||
|
||||
@Schema(description = "图片地址", example = "")
|
||||
private String picUrl;
|
||||
|
||||
@Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "15")
|
||||
private Integer stock;
|
||||
|
||||
@Schema(description = "商品重量,单位:kg 千克", example = "0")
|
||||
private Double weight;
|
||||
|
||||
@Schema(description = "商品体积,单位:m^3 平米", example = "0")
|
||||
private Double volume;
|
||||
|
||||
@Schema(description = "限购类型", example = "")
|
||||
private String purchaseBizType;
|
||||
|
||||
@Schema(description = "一级分销的佣金,单位:分", example = "0")
|
||||
private Integer firstBrokeragePrice;
|
||||
|
||||
@Schema(description = "二级分销的佣金,单位:分", example = "0")
|
||||
private Integer secondBrokeragePrice;
|
||||
|
||||
@Schema(description = "货道或柜门编号", example = "1")
|
||||
private String cabinetChannel;
|
||||
|
||||
@Schema(description = "通道类型", example = "")
|
||||
private String cabinetType;
|
||||
|
||||
@Schema(description = "属性数组")
|
||||
private List<Property> properties;
|
||||
|
||||
@Schema(description = "商品属性")
|
||||
@Data
|
||||
public static class Property {
|
||||
@Schema(description = "属性编号", example = "10")
|
||||
private Long propertyId;
|
||||
|
||||
@Schema(description = "属性名字", example = "颜色")
|
||||
private String propertyName;
|
||||
|
||||
@Schema(description = "属性值编号", example = "10")
|
||||
private Long valueId;
|
||||
|
||||
@Schema(description = "属性值名字", example = "红色")
|
||||
private String valueName;
|
||||
}
|
||||
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.device.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 售货机商品 SKU 创建/更新 Request VO
|
||||
*
|
||||
* 注意:售货机商品的 SKU 必须包含 cabinetChannel(货道编号)
|
||||
*/
|
||||
@Schema(description = "管理后台 - 售货机商品 SKU 创建/更新 Request VO")
|
||||
@Data
|
||||
public class DeviceProductSkuSaveReqVO {
|
||||
|
||||
@Schema(description = "商品 SKU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试商品")
|
||||
@NotEmpty(message = "商品 SKU 名字不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "销售价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
|
||||
@NotNull(message = "销售价格不能为空")
|
||||
private Integer price;
|
||||
|
||||
@Schema(description = "市场价,单位:分", example = "100")
|
||||
private Integer marketPrice;
|
||||
|
||||
@Schema(description = "成本价,单位:分", example = "100")
|
||||
private Integer costPrice;
|
||||
|
||||
@Schema(description = "条形码", example = "1")
|
||||
private String barCode;
|
||||
|
||||
@Schema(description = "图片地址", example = "")
|
||||
private String picUrl;
|
||||
|
||||
@Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "15")
|
||||
@NotNull(message = "库存不能为空")
|
||||
private Integer stock;
|
||||
|
||||
@Schema(description = "商品重量,单位:kg 千克", example = "0")
|
||||
private Double weight;
|
||||
|
||||
@Schema(description = "商品体积,单位:m^3 平米", example = "0")
|
||||
private Double volume;
|
||||
|
||||
@Schema(description = "一级分销的佣金,单位:分", example = "0")
|
||||
private Integer firstBrokeragePrice;
|
||||
|
||||
@Schema(description = "二级分销的佣金,单位:分", example = "0")
|
||||
private Integer secondBrokeragePrice;
|
||||
|
||||
@Schema(description = "货道或柜门编号(售货机必填)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotEmpty(message = "货道或柜门编号不能为空")
|
||||
private String cabinetChannel;
|
||||
|
||||
@Schema(description = "通道类型", example = "")
|
||||
private String cabinetType;
|
||||
|
||||
@Schema(description = "限购类型", example = "")
|
||||
private String purchaseBizType;
|
||||
|
||||
@Schema(description = "临时行键(前端使用,用于标识临时数据)", example = "temp_1769625466007_a4oal3mm3")
|
||||
private String rowKey;
|
||||
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.device.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
/**
|
||||
* 管理后台 - 售货机商品 SPU 分页 Request VO
|
||||
*
|
||||
* 注意:deviceId 是设备主键ID(Long类型),不是设备IMEI
|
||||
*/
|
||||
@Schema(description = "管理后台 - 售货机商品 SPU 分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class DeviceProductSpuPageReqVO extends PageParam {
|
||||
|
||||
/**
|
||||
* 出售中商品
|
||||
*/
|
||||
public static final Integer FOR_SALE = 0;
|
||||
|
||||
/**
|
||||
* 仓库中商品
|
||||
*/
|
||||
public static final Integer IN_WAREHOUSE = 1;
|
||||
|
||||
/**
|
||||
* 已售空商品
|
||||
*/
|
||||
public static final Integer SOLD_OUT = 2;
|
||||
|
||||
/**
|
||||
* 警戒库存
|
||||
*/
|
||||
public static final Integer ALERT_STOCK = 3;
|
||||
|
||||
/**
|
||||
* 商品回收站
|
||||
*/
|
||||
public static final Integer RECYCLE_BIN = 4;
|
||||
|
||||
@Schema(description = "商品名称", example = "测试商品")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "前端请求的tab类型", example = "1")
|
||||
private Integer tabType;
|
||||
|
||||
@Schema(description = "商品分类编号", example = "95")
|
||||
private Long categoryId;
|
||||
|
||||
@Schema(description = "商品类型", example = "1")
|
||||
private Integer types;
|
||||
|
||||
@Schema(description = "设备ID(设备主键ID,Long类型)", example = "25245")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "创建人id", example = "1")
|
||||
private String creator;
|
||||
|
||||
@Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.device.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - 售货机商品 SPU Response VO
|
||||
*
|
||||
* 注意:deviceId 是设备主键ID(Long类型),不是设备IMEI
|
||||
*/
|
||||
@Schema(description = "管理后台 - 售货机商品 SPU Response VO")
|
||||
@Data
|
||||
public class DeviceProductSpuRespVO {
|
||||
|
||||
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试商品")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "关键字", example = "")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "商品简介", example = "")
|
||||
private String introduction;
|
||||
|
||||
@Schema(description = "商品详情", example = "")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "商品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "95")
|
||||
private Long categoryId;
|
||||
|
||||
@Schema(description = "设备ID(设备主键ID,Long类型)", requiredMode = Schema.RequiredMode.REQUIRED, example = "25245")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "商品封面图", example = "https://xrhomepccdn.zianxin.com/xxx.jpg")
|
||||
private String picUrl;
|
||||
|
||||
@Schema(description = "商品轮播图", example = "[\"https://xrhomepccdn.zianxin.com/xxx.jpg\"]")
|
||||
private List<String> sliderPicUrls;
|
||||
|
||||
@Schema(description = "排序字段", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "商品类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer types;
|
||||
|
||||
// ========== SKU 相关字段 =========
|
||||
|
||||
@Schema(description = "规格类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
|
||||
private Boolean specType;
|
||||
|
||||
@Schema(description = "商品价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
|
||||
private Integer price;
|
||||
|
||||
@Schema(description = "市场价,单位使用:分", example = "100")
|
||||
private Integer marketPrice;
|
||||
|
||||
@Schema(description = "成本价,单位使用:分", example = "100")
|
||||
private Integer costPrice;
|
||||
|
||||
@Schema(description = "商品库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "15")
|
||||
private Integer stock;
|
||||
|
||||
@Schema(description = "SKU 数组")
|
||||
private List<DeviceProductSkuRespVO> skus;
|
||||
|
||||
// ========== 物流相关字段 =========
|
||||
|
||||
@Schema(description = "配送方式数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[3]")
|
||||
private List<Integer> deliveryTypes;
|
||||
|
||||
// ========== 营销相关字段 =========
|
||||
|
||||
@Schema(description = "赠送积分", example = "0")
|
||||
private Integer giveIntegral;
|
||||
|
||||
@Schema(description = "分销类型", example = "false")
|
||||
private Boolean subCommissionType;
|
||||
|
||||
// ========== 统计相关字段 =========
|
||||
|
||||
@Schema(description = "商品销量", example = "0")
|
||||
private Integer salesCount;
|
||||
|
||||
@Schema(description = "虚拟销量", example = "0")
|
||||
private Integer virtualSalesCount;
|
||||
|
||||
@Schema(description = "浏览量", example = "0")
|
||||
private Integer browseCount;
|
||||
|
||||
@Schema(description = "创建人ID", example = "1")
|
||||
private String creator;
|
||||
|
||||
@Schema(description = "创建人姓名", example = "张三")
|
||||
private String creatorName;
|
||||
|
||||
@Schema(description = "商品创建时间", example = "2023-05-24 00:00:00")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.device.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 售货机商品 SPU 新增/更新 Request VO
|
||||
*
|
||||
* 注意:与商城商品不同,售货机商品有以下特点:
|
||||
* 1. deviceId 是设备主键ID(Long类型),需要转换为设备IMEI存储到 product_spu.device_id
|
||||
* 2. deliveryTypes 通常只包含 [3](自提柜)
|
||||
* 3. 不需要品牌(brandId)
|
||||
* 4. SKU 中必须包含 cabinetChannel(货道编号)
|
||||
*/
|
||||
@Schema(description = "管理后台 - 售货机商品 SPU 新增/更新 Request VO")
|
||||
@Data
|
||||
public class DeviceProductSpuSaveReqVO {
|
||||
|
||||
@Schema(description = "商品编号", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试商品")
|
||||
@NotEmpty(message = "商品名称不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "关键字", example = "")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "商品简介", example = "")
|
||||
private String introduction;
|
||||
|
||||
@Schema(description = "商品详情", example = "")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "商品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "95")
|
||||
@NotNull(message = "商品分类不能为空")
|
||||
private Long categoryId;
|
||||
|
||||
@Schema(description = "设备ID(设备主键ID,Long类型)", requiredMode = Schema.RequiredMode.REQUIRED, example = "25245")
|
||||
@NotNull(message = "设备ID不能为空")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "商品封面图", example = "https://xrhomepccdn.zianxin.com/xxx.jpg")
|
||||
private String picUrl;
|
||||
|
||||
@Schema(description = "商品轮播图", example = "[\"https://xrhomepccdn.zianxin.com/xxx.jpg\"]")
|
||||
private List<String> sliderPicUrls;
|
||||
|
||||
@Schema(description = "排序字段", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "商品类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "商品类型不能为空")
|
||||
private Integer types;
|
||||
|
||||
// ========== SKU 相关字段 =========
|
||||
|
||||
@Schema(description = "规格类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
|
||||
@NotNull(message = "商品规格类型不能为空")
|
||||
private Boolean specType;
|
||||
|
||||
// ========== 物流相关字段 =========
|
||||
|
||||
@Schema(description = "配送方式数组(售货机通常为 [3] 自提柜)", requiredMode = Schema.RequiredMode.REQUIRED, example = "[3]")
|
||||
@NotEmpty(message = "配送方式不能为空")
|
||||
private List<Integer> deliveryTypes;
|
||||
|
||||
// ========== 营销相关字段 =========
|
||||
|
||||
@Schema(description = "赠送积分", example = "0")
|
||||
private Integer giveIntegral;
|
||||
|
||||
@Schema(description = "分销类型", example = "false")
|
||||
private Boolean subCommissionType;
|
||||
|
||||
// ========== 统计相关字段 =========
|
||||
|
||||
@Schema(description = "虚拟销量", example = "0")
|
||||
private Integer virtualSalesCount;
|
||||
|
||||
// ========== SKU 数组 =========
|
||||
|
||||
@Schema(description = "SKU 数组", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@Valid
|
||||
@NotEmpty(message = "SKU 数组不能为空")
|
||||
private List<DeviceProductSkuSaveReqVO> skus;
|
||||
|
||||
}
|
||||
+7
-2
@@ -32,9 +32,14 @@ public class ChannelDO extends BaseDO {
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 设备IMEI
|
||||
* 设备ID(关联 product_device.id)
|
||||
*/
|
||||
private String deviceId;
|
||||
private Long deviceId;
|
||||
/**
|
||||
* 设备IMEI(兼容字段,保留原有数据)
|
||||
* 注意:新代码应优先使用 deviceId 字段
|
||||
*/
|
||||
private String deviceImei;
|
||||
/**
|
||||
* 当前使用的货道或柜门编号
|
||||
*/
|
||||
|
||||
+53
-4
@@ -21,6 +21,7 @@ public interface ChannelMapper extends BaseMapperX<ChannelDO> {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<ChannelDO>()
|
||||
.likeIfPresent(ChannelDO::getName, reqVO.getName())
|
||||
.eqIfPresent(ChannelDO::getDeviceId, reqVO.getDeviceId())
|
||||
.eqIfPresent(ChannelDO::getDeviceImei, reqVO.getDeviceImei())
|
||||
.eqIfPresent(ChannelDO::getCabinetChannel, reqVO.getCabinetChannel())
|
||||
.eqIfPresent(ChannelDO::getMaxNum, reqVO.getMaxNum())
|
||||
.eqIfPresent(ChannelDO::getChannelLine, reqVO.getChannelLine())
|
||||
@@ -28,11 +29,59 @@ public interface ChannelMapper extends BaseMapperX<ChannelDO> {
|
||||
.orderByDesc(ChannelDO::getId));
|
||||
}
|
||||
|
||||
default ChannelDO selectByDeviceIdAndCabinetChannel(String deviceId, String cabinetChannel){
|
||||
return selectOne(ChannelDO::getDeviceId, deviceId, ChannelDO::getCabinetChannel, cabinetChannel);
|
||||
/**
|
||||
* 根据设备ID和货道编号查询(新方法,使用 deviceId)
|
||||
*/
|
||||
default ChannelDO selectByDeviceIdAndCabinetChannel(Long deviceId, String cabinetChannel){
|
||||
return selectOne(new LambdaQueryWrapperX<ChannelDO>()
|
||||
.eq(ChannelDO::getDeviceId, deviceId)
|
||||
.eq(ChannelDO::getCabinetChannel, cabinetChannel));
|
||||
}
|
||||
|
||||
default List<ChannelDO> getChannelListByDeviceid(String deviceId){
|
||||
return selectList(ChannelDO::getDeviceId, deviceId);
|
||||
/**
|
||||
* 根据设备IMEI和货道编号查询(兼容方法,使用 deviceImei)
|
||||
* @deprecated 建议使用 selectByDeviceIdAndCabinetChannel(Long, String)
|
||||
*/
|
||||
@Deprecated
|
||||
default ChannelDO selectByDeviceImeiAndCabinetChannel(String deviceImei, String cabinetChannel){
|
||||
return selectOne(new LambdaQueryWrapperX<ChannelDO>()
|
||||
.eq(ChannelDO::getDeviceImei, deviceImei)
|
||||
.eq(ChannelDO::getCabinetChannel, cabinetChannel));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备ID批量查询货道列表(新方法,推荐使用)
|
||||
*/
|
||||
default List<ChannelDO> getChannelListByDeviceId(Long deviceId){
|
||||
return selectList(new LambdaQueryWrapperX<ChannelDO>()
|
||||
.eq(ChannelDO::getDeviceId, deviceId)
|
||||
.orderByAsc(ChannelDO::getChannelLine)
|
||||
.orderByAsc(ChannelDO::getCabinetChannel));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备ID列表批量查询货道列表
|
||||
*/
|
||||
default List<ChannelDO> getChannelListByDeviceIds(Collection<Long> deviceIds){
|
||||
if (deviceIds == null || deviceIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return selectList(new LambdaQueryWrapperX<ChannelDO>()
|
||||
.in(ChannelDO::getDeviceId, deviceIds)
|
||||
.orderByAsc(ChannelDO::getDeviceId)
|
||||
.orderByAsc(ChannelDO::getChannelLine)
|
||||
.orderByAsc(ChannelDO::getCabinetChannel));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备IMEI查询货道列表(兼容方法)
|
||||
* @deprecated 建议使用 getChannelListByDeviceId(Long)
|
||||
*/
|
||||
@Deprecated
|
||||
default List<ChannelDO> getChannelListByDeviceImei(String deviceImei){
|
||||
return selectList(new LambdaQueryWrapperX<ChannelDO>()
|
||||
.eq(ChannelDO::getDeviceImei, deviceImei)
|
||||
.orderByAsc(ChannelDO::getChannelLine)
|
||||
.orderByAsc(ChannelDO::getCabinetChannel));
|
||||
}
|
||||
}
|
||||
+42
@@ -44,6 +44,27 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
|
||||
return selectPage(reqVO, queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品 SPU 分页列表数据(按创建时间倒序,新的排上面)
|
||||
* 用于售货机商品分页
|
||||
*
|
||||
* @param reqVO 分页请求参数
|
||||
* @return 商品 SPU 分页列表数据
|
||||
*/
|
||||
default PageResult<ProductSpuDO> selectPageOrderByCreateTimeDesc(ProductSpuPageReqVO reqVO) {
|
||||
Integer tabType = reqVO.getTabType();
|
||||
LambdaQueryWrapperX<ProductSpuDO> queryWrapper = new LambdaQueryWrapperX<ProductSpuDO>()
|
||||
.likeIfPresent(ProductSpuDO::getName, reqVO.getName())
|
||||
.eqIfPresent(ProductSpuDO::getDeviceId, reqVO.getDeviceId())
|
||||
.eqIfPresent(ProductSpuDO::getCategoryId, reqVO.getCategoryId())
|
||||
.eqIfPresent(ProductSpuDO::getTypes, reqVO.getTypes())
|
||||
.betweenIfPresent(ProductSpuDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(ProductSpuDO::getCreateTime)
|
||||
.orderByDesc(ProductSpuDO::getId);
|
||||
appendTabQuery(tabType, queryWrapper);
|
||||
return selectPage(reqVO, queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询触发警戒库存的 SPU 数量
|
||||
*
|
||||
@@ -178,6 +199,27 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
|
||||
return selectJoinPage(pageReqVO,ProductSpuCreatorDO.class, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品 SPU 分页(带创建人),按创建时间倒序,用于售货机商品分页
|
||||
*/
|
||||
default PageResult<ProductSpuCreatorDO> selectPageWithCreatorOrderByCreateTimeDesc(ProductSpuPageReqVO pageReqVO) {
|
||||
Integer tabType = pageReqVO.getTabType();
|
||||
MPJLambdaWrapperX<ProductSpuDO> query = new MPJLambdaWrapperX<ProductSpuDO>()
|
||||
.selectAll(ProductSpuDO.class)
|
||||
.selectAs(AdminUserDO::getUsername, ProductSpuCreatorDO::getUsername)
|
||||
.likeIfPresent(ProductSpuDO::getName, pageReqVO.getName())
|
||||
.eqIfPresent(ProductSpuDO::getDeviceId, pageReqVO.getDeviceId())
|
||||
.eqIfPresent(ProductSpuDO::getCategoryId, pageReqVO.getCategoryId())
|
||||
.eqIfPresent(ProductSpuDO::getTypes, pageReqVO.getTypes())
|
||||
.eqIfPresent(ProductSpuDO::getCreator, pageReqVO.getCreator())
|
||||
.betweenIfPresent(ProductSpuDO::getCreateTime, pageReqVO.getCreateTime())
|
||||
.orderByDesc(ProductSpuDO::getCreateTime)
|
||||
.orderByDesc(ProductSpuDO::getId);
|
||||
query.leftJoin(AdminUserDO.class, AdminUserDO::getId, ProductSpuDO::getCreator);
|
||||
appendTabQuery2(tabType, query);
|
||||
return selectJoinPage(pageReqVO, ProductSpuCreatorDO.class, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加后台 Tab 选项的查询条件
|
||||
*
|
||||
|
||||
+33
-1
@@ -24,6 +24,14 @@ public interface ChannelService {
|
||||
*/
|
||||
Long createChannel(@Valid ChannelSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 批量创建货道
|
||||
*
|
||||
* @param batchCreateReqVO 批量创建信息
|
||||
* @return 批量创建结果
|
||||
*/
|
||||
ChannelBatchCreateRespVO batchCreateChannel(@Valid ChannelBatchCreateReqVO batchCreateReqVO);
|
||||
|
||||
/**
|
||||
* 更新货道
|
||||
*
|
||||
@@ -58,5 +66,29 @@ public interface ChannelService {
|
||||
|
||||
CommonResult<String> testChannel(Long id);
|
||||
|
||||
List<ChannelDO> getChannelListByDeviceid(String deviceId);
|
||||
/**
|
||||
* 根据设备ID查询货道列表(新方法,推荐使用)
|
||||
*
|
||||
* @param deviceId 设备ID(关联 product_device.id)
|
||||
* @return 货道列表
|
||||
*/
|
||||
List<ChannelDO> getChannelListByDeviceId(Long deviceId);
|
||||
|
||||
/**
|
||||
* 根据设备ID列表批量查询货道列表
|
||||
*
|
||||
* @param deviceIds 设备ID列表
|
||||
* @return 货道列表
|
||||
*/
|
||||
List<ChannelDO> getChannelListByDeviceIds(Collection<Long> deviceIds);
|
||||
|
||||
/**
|
||||
* 根据设备IMEI查询货道列表(兼容方法)
|
||||
* @deprecated 建议使用 getChannelListByDeviceId(Long)
|
||||
*
|
||||
* @param deviceImei 设备IMEI
|
||||
* @return 货道列表
|
||||
*/
|
||||
@Deprecated
|
||||
List<ChannelDO> getChannelListByDeviceid(String deviceImei);
|
||||
}
|
||||
+203
-11
@@ -4,6 +4,8 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.channel.vo.ChannelBatchCreateReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.channel.vo.ChannelBatchCreateRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.channel.vo.ChannelImportExcelVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.channel.vo.ChannelImportRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.channel.vo.ChannelPageReqVO;
|
||||
@@ -21,10 +23,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
|
||||
@@ -45,6 +44,9 @@ public class ChannelServiceImpl implements ChannelService {
|
||||
@Resource
|
||||
private ProductDeviceLogService productDeviceLogService;
|
||||
|
||||
@Resource
|
||||
private cn.iocoder.yudao.module.product.service.device.DeviceService deviceService;
|
||||
|
||||
// TOKEN
|
||||
@Value("${xr.cabinet.token}")
|
||||
private String token;
|
||||
@@ -55,17 +57,150 @@ public class ChannelServiceImpl implements ChannelService {
|
||||
|
||||
@Override
|
||||
public Long createChannel(ChannelSaveReqVO createReqVO) {
|
||||
ChannelDO dbChannel = channelMapper.selectByDeviceIdAndCabinetChannel(createReqVO.getDeviceId(), createReqVO.getCabinetChannel());
|
||||
// 处理设备ID:如果传入的是 deviceImei,需要转换为 deviceId
|
||||
Long deviceId = createReqVO.getDeviceId();
|
||||
String deviceImei = createReqVO.getDeviceImei();
|
||||
|
||||
if (deviceId == null && deviceImei != null && !deviceImei.isEmpty()) {
|
||||
// 根据 deviceImei 查询设备ID
|
||||
cn.iocoder.yudao.module.product.api.device.dto.DeviceRespDto device = deviceService.getDeviceByDeviceId(deviceImei);
|
||||
if (device == null) {
|
||||
throw exception(cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS);
|
||||
}
|
||||
deviceId = device.getId();
|
||||
}
|
||||
|
||||
if (deviceId == null) {
|
||||
throw exception(cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 校验货道是否已存在
|
||||
ChannelDO dbChannel = channelMapper.selectByDeviceIdAndCabinetChannel(deviceId, createReqVO.getCabinetChannel());
|
||||
if (dbChannel != null) {
|
||||
throw exception(CHANNEL_EXISTS);
|
||||
}
|
||||
|
||||
// 插入
|
||||
ChannelDO channel = BeanUtils.toBean(createReqVO, ChannelDO.class);
|
||||
channel.setDeviceId(deviceId);
|
||||
// 如果 deviceImei 为空,从设备表获取
|
||||
if (deviceImei == null || deviceImei.isEmpty()) {
|
||||
cn.iocoder.yudao.module.product.api.device.dto.DeviceRespDto device = deviceService.getDeviceListByIds(Collections.singletonList(deviceId))
|
||||
.stream().findFirst().orElse(null);
|
||||
if (device != null) {
|
||||
channel.setDeviceImei(device.getDeviceId());
|
||||
}
|
||||
} else {
|
||||
channel.setDeviceImei(deviceImei);
|
||||
}
|
||||
channelMapper.insert(channel);
|
||||
// 返回
|
||||
return channel.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelBatchCreateRespVO batchCreateChannel(ChannelBatchCreateReqVO batchCreateReqVO) {
|
||||
// 参数校验
|
||||
if (CollUtil.isEmpty(batchCreateReqVO.getChannels())) {
|
||||
throw exception(CHANNEL_IMPORT_LIST_IS_EMPTY);
|
||||
}
|
||||
|
||||
List<Long> successIds = new ArrayList<>();
|
||||
Map<String, String> failureChannels = new LinkedHashMap<>();
|
||||
|
||||
// 批量处理设备ID查询,优化性能
|
||||
Set<Long> deviceIdSet = new HashSet<>();
|
||||
Set<String> deviceImeiSet = new HashSet<>();
|
||||
for (ChannelSaveReqVO channel : batchCreateReqVO.getChannels()) {
|
||||
if (channel.getDeviceId() != null) {
|
||||
deviceIdSet.add(channel.getDeviceId());
|
||||
}
|
||||
if (channel.getDeviceImei() != null && !channel.getDeviceImei().isEmpty()) {
|
||||
deviceImeiSet.add(channel.getDeviceImei());
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询设备信息
|
||||
Map<String, Long> imeiToDeviceIdMap = new HashMap<>();
|
||||
if (!deviceImeiSet.isEmpty()) {
|
||||
for (String deviceImei : deviceImeiSet) {
|
||||
cn.iocoder.yudao.module.product.api.device.dto.DeviceRespDto device = deviceService.getDeviceByDeviceId(deviceImei);
|
||||
if (device != null) {
|
||||
imeiToDeviceIdMap.put(deviceImei, device.getId());
|
||||
deviceIdSet.add(device.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询设备IMEI信息
|
||||
Map<Long, String> deviceIdToImeiMap = new HashMap<>();
|
||||
if (!deviceIdSet.isEmpty()) {
|
||||
List<cn.iocoder.yudao.module.product.api.device.dto.DeviceRespDto> devices = deviceService.getDeviceListByIds(deviceIdSet);
|
||||
for (cn.iocoder.yudao.module.product.api.device.dto.DeviceRespDto device : devices) {
|
||||
deviceIdToImeiMap.put(device.getId(), device.getDeviceId());
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历处理每个货道
|
||||
for (ChannelSaveReqVO channelReqVO : batchCreateReqVO.getChannels()) {
|
||||
try {
|
||||
// 处理设备ID
|
||||
Long deviceId = channelReqVO.getDeviceId();
|
||||
String deviceImei = channelReqVO.getDeviceImei();
|
||||
String channelKey = channelReqVO.getCabinetChannel() != null
|
||||
? channelReqVO.getCabinetChannel()
|
||||
: (channelReqVO.getName() != null ? channelReqVO.getName() : "未知");
|
||||
|
||||
if (deviceId == null && deviceImei != null && !deviceImei.isEmpty()) {
|
||||
deviceId = imeiToDeviceIdMap.get(deviceImei);
|
||||
if (deviceId == null) {
|
||||
failureChannels.put(channelKey, "设备IMEI不存在:" + deviceImei);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceId == null) {
|
||||
failureChannels.put(channelKey, "设备ID或设备IMEI不能为空");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 校验货道是否已存在
|
||||
ChannelDO dbChannel = channelMapper.selectByDeviceIdAndCabinetChannel(deviceId, channelReqVO.getCabinetChannel());
|
||||
if (dbChannel != null) {
|
||||
failureChannels.put(channelKey, CHANNEL_EXISTS.getMsg());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 插入货道
|
||||
ChannelDO channel = BeanUtils.toBean(channelReqVO, ChannelDO.class);
|
||||
channel.setDeviceId(deviceId);
|
||||
// 如果 deviceImei 为空,从设备表获取
|
||||
if (deviceImei == null || deviceImei.isEmpty()) {
|
||||
deviceImei = deviceIdToImeiMap.get(deviceId);
|
||||
if (deviceImei != null) {
|
||||
channel.setDeviceImei(deviceImei);
|
||||
}
|
||||
} else {
|
||||
channel.setDeviceImei(deviceImei);
|
||||
}
|
||||
channelMapper.insert(channel);
|
||||
successIds.add(channel.getId());
|
||||
} catch (Exception e) {
|
||||
String channelKey = channelReqVO.getCabinetChannel() != null
|
||||
? channelReqVO.getCabinetChannel()
|
||||
: (channelReqVO.getName() != null ? channelReqVO.getName() : "未知");
|
||||
failureChannels.put(channelKey, "创建失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return ChannelBatchCreateRespVO.builder()
|
||||
.successIds(successIds)
|
||||
.failureChannels(failureChannels)
|
||||
.successCount(successIds.size())
|
||||
.failureCount(failureChannels.size())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateChannel(ChannelSaveReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
@@ -111,9 +246,29 @@ public class ChannelServiceImpl implements ChannelService {
|
||||
.failureChannelnames(new LinkedHashMap<>())
|
||||
.build();
|
||||
importList.forEach(excelVO -> {
|
||||
ChannelDO dbChannel = channelMapper.selectByDeviceIdAndCabinetChannel(excelVO.getDeviceId(), excelVO.getCabinetChannel());
|
||||
// 根据 deviceImei 查询设备ID
|
||||
Long deviceId = null;
|
||||
if (excelVO.getDeviceImei() != null && !excelVO.getDeviceImei().isEmpty()) {
|
||||
cn.iocoder.yudao.module.product.api.device.dto.DeviceRespDto device = deviceService.getDeviceByDeviceId(excelVO.getDeviceImei());
|
||||
if (device == null) {
|
||||
respVO.getFailureChannelnames().put(excelVO.getName(), "设备IMEI不存在:" + excelVO.getDeviceImei());
|
||||
return;
|
||||
}
|
||||
deviceId = device.getId();
|
||||
}
|
||||
|
||||
if (deviceId == null) {
|
||||
respVO.getFailureChannelnames().put(excelVO.getName(), "设备IMEI不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验货道是否已存在
|
||||
ChannelDO dbChannel = channelMapper.selectByDeviceIdAndCabinetChannel(deviceId, excelVO.getCabinetChannel());
|
||||
if (dbChannel == null) {
|
||||
channelMapper.insert(BeanUtils.toBean(excelVO, ChannelDO.class));
|
||||
ChannelDO channel = BeanUtils.toBean(excelVO, ChannelDO.class);
|
||||
channel.setDeviceId(deviceId);
|
||||
channel.setDeviceImei(excelVO.getDeviceImei());
|
||||
channelMapper.insert(channel);
|
||||
respVO.getCreateChannelnames().add(excelVO.getName());
|
||||
return;
|
||||
}
|
||||
@@ -123,6 +278,8 @@ public class ChannelServiceImpl implements ChannelService {
|
||||
}
|
||||
ChannelDO channel = BeanUtils.toBean(excelVO, ChannelDO.class);
|
||||
channel.setId(dbChannel.getId());
|
||||
channel.setDeviceId(deviceId);
|
||||
channel.setDeviceImei(excelVO.getDeviceImei());
|
||||
channelMapper.updateById(channel);
|
||||
respVO.getUpdateChannelnames().add(excelVO.getName());
|
||||
});
|
||||
@@ -136,8 +293,24 @@ public class ChannelServiceImpl implements ChannelService {
|
||||
if (channelDO == null) {
|
||||
throw exception(CHANNEL_NOT_EXISTS);
|
||||
}
|
||||
// 获取设备IMEI(用于API调用)
|
||||
String deviceImei = channelDO.getDeviceImei();
|
||||
if (deviceImei == null || deviceImei.isEmpty()) {
|
||||
// 如果 deviceImei 为空,从设备表获取
|
||||
if (channelDO.getDeviceId() != null) {
|
||||
cn.iocoder.yudao.module.product.api.device.dto.DeviceRespDto device = deviceService.getDeviceListByIds(Collections.singletonList(channelDO.getDeviceId()))
|
||||
.stream().findFirst().orElse(null);
|
||||
if (device != null) {
|
||||
deviceImei = device.getDeviceId();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deviceImei == null || deviceImei.isEmpty()) {
|
||||
throw exception(cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS);
|
||||
}
|
||||
|
||||
DispenseRequestBo request = new DispenseRequestBo();
|
||||
request.setDeviceId(channelDO.getDeviceId());
|
||||
request.setDeviceId(deviceImei);
|
||||
String message = "1," + channelDO.getCabinetChannel() + "&1";
|
||||
request.setMessage(message);
|
||||
// 设置请求头
|
||||
@@ -152,7 +325,7 @@ public class ChannelServiceImpl implements ChannelService {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
//测试日志
|
||||
ProductDeviceLogSaveReqVO deviceLog = new ProductDeviceLogSaveReqVO();
|
||||
deviceLog.setDeviceId(channelDO.getDeviceId());
|
||||
deviceLog.setDeviceId(deviceImei);
|
||||
deviceLog.setCabinetChannel(channelDO.getCabinetChannel());
|
||||
// try {
|
||||
// 发送请求并解析响应
|
||||
@@ -195,8 +368,27 @@ public class ChannelServiceImpl implements ChannelService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChannelDO> getChannelListByDeviceid(String deviceId) {
|
||||
return channelMapper.getChannelListByDeviceid(deviceId);
|
||||
public List<ChannelDO> getChannelListByDeviceId(Long deviceId) {
|
||||
return channelMapper.getChannelListByDeviceId(deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChannelDO> getChannelListByDeviceIds(Collection<Long> deviceIds) {
|
||||
return channelMapper.getChannelListByDeviceIds(deviceIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public List<ChannelDO> getChannelListByDeviceid(String deviceImei) {
|
||||
// 兼容方法:根据 deviceImei 查询设备ID,然后查询货道
|
||||
if (deviceImei == null || deviceImei.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
cn.iocoder.yudao.module.product.api.device.dto.DeviceRespDto device = deviceService.getDeviceByDeviceId(deviceImei);
|
||||
if (device == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return channelMapper.getChannelListByDeviceId(device.getId());
|
||||
}
|
||||
|
||||
}
|
||||
+43
@@ -31,6 +31,49 @@ public interface ProductSpuService {
|
||||
*/
|
||||
Long createSpu(@Valid ProductSpuSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 创建售货机商品 SPU
|
||||
*
|
||||
* 注意:售货机商品与商城商品的区别:
|
||||
* 1. deviceId 是设备主键ID(Long类型),需要转换为设备IMEI存储
|
||||
* 2. 不需要品牌(brandId)
|
||||
* 3. 配送方式通常只包含自提柜(deliveryTypes = [3])
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createDeviceSpu(@Valid cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSpuSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新售货机商品 SPU
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateDeviceSpu(@Valid cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSpuSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除售货机商品 SPU
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteDeviceSpu(Long id);
|
||||
|
||||
/**
|
||||
* 获得售货机商品 SPU
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 商品 SPU
|
||||
*/
|
||||
ProductSpuDO getDeviceSpu(Long id);
|
||||
|
||||
/**
|
||||
* 获得售货机商品 SPU 分页(含创建人、创建时间,按创建时间倒序)
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 商品 SPU 分页(含创建人姓名)
|
||||
*/
|
||||
PageResult<ProductSpuCreatorDO> getDeviceSpuPage(cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSpuPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 更新商品 SPU
|
||||
*
|
||||
|
||||
+179
@@ -7,11 +7,14 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.category.vo.ProductCategoryListReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSkuSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSpuSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.*;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.stocklog.vo.StockLogSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.category.ProductCategoryDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.channel.ChannelDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.device.DeviceDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuCreatorDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
|
||||
@@ -20,6 +23,7 @@ import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
|
||||
import cn.iocoder.yudao.module.product.service.brand.ProductBrandService;
|
||||
import cn.iocoder.yudao.module.product.service.category.ProductCategoryService;
|
||||
import cn.iocoder.yudao.module.product.service.channel.ChannelService;
|
||||
import cn.iocoder.yudao.module.product.service.device.DeviceService;
|
||||
import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
|
||||
import cn.iocoder.yudao.module.product.service.stocklog.StockLogService;
|
||||
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||
@@ -68,6 +72,9 @@ public class ProductSpuServiceImpl implements ProductSpuService {
|
||||
@Resource
|
||||
private ChannelService channelService;
|
||||
|
||||
@Resource
|
||||
private DeviceService deviceService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createSpu(ProductSpuSaveReqVO createReqVO) {
|
||||
@@ -111,6 +118,178 @@ public class ProductSpuServiceImpl implements ProductSpuService {
|
||||
productSkuService.updateSkuList(updateObj.getId(), updateReqVO.getSkus());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createDeviceSpu(DeviceProductSpuSaveReqVO createReqVO) {
|
||||
// 1. 校验设备是否存在,并获取设备IMEI
|
||||
DeviceDO device = deviceService.getDevice(createReqVO.getDeviceId());
|
||||
if (device == null) {
|
||||
throw exception(DEVICE_NOT_EXISTS);
|
||||
}
|
||||
String deviceImei = device.getDeviceId(); // device.getDeviceId() 返回的是设备IMEI(字符串)
|
||||
|
||||
// 2. 校验分类(售货机商品不需要品牌)
|
||||
validateCategory(createReqVO.getCategoryId());
|
||||
|
||||
// 3. 将 DeviceProductSkuSaveReqVO 转换为 ProductSkuSaveReqVO
|
||||
List<ProductSkuSaveReqVO> skuSaveReqList = convertDeviceSkuToSku(createReqVO.getSkus());
|
||||
|
||||
// 4. 校验 SKU
|
||||
productSkuService.validateSkuList(skuSaveReqList, createReqVO.getSpecType());
|
||||
|
||||
// 5. 将 DeviceProductSpuSaveReqVO 转换为 ProductSpuSaveReqVO
|
||||
ProductSpuSaveReqVO spuSaveReqVO = convertDeviceSpuToSpu(createReqVO, deviceImei);
|
||||
|
||||
// 6. 创建 SPU(复用现有逻辑)
|
||||
ProductSpuDO spu = BeanUtils.toBean(spuSaveReqVO, ProductSpuDO.class);
|
||||
// 初始化 SPU 中 SKU 相关属性
|
||||
initSpuFromSkus(spu, skuSaveReqList);
|
||||
// 传入部门
|
||||
spu.setDeptId(getLoginUserDeptId());
|
||||
// 插入 SPU
|
||||
productSpuMapper.insert(spu);
|
||||
// 插入 SKU
|
||||
productSkuService.createSkuList(spu.getId(), skuSaveReqList);
|
||||
// 返回
|
||||
return spu.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 DeviceProductSpuSaveReqVO 转换为 ProductSpuSaveReqVO
|
||||
*/
|
||||
private ProductSpuSaveReqVO convertDeviceSpuToSpu(DeviceProductSpuSaveReqVO deviceSpu, String deviceImei) {
|
||||
ProductSpuSaveReqVO spu = new ProductSpuSaveReqVO();
|
||||
spu.setId(deviceSpu.getId());
|
||||
spu.setName(deviceSpu.getName());
|
||||
spu.setKeyword(deviceSpu.getKeyword() != null ? deviceSpu.getKeyword() : "");
|
||||
spu.setIntroduction(deviceSpu.getIntroduction() != null ? deviceSpu.getIntroduction() : "");
|
||||
spu.setDescription(deviceSpu.getDescription() != null ? deviceSpu.getDescription() : "");
|
||||
spu.setCategoryId(deviceSpu.getCategoryId());
|
||||
spu.setDeviceId(deviceImei); // 转换为设备IMEI(字符串)
|
||||
spu.setBrandId(null); // 售货机商品不需要品牌
|
||||
spu.setPicUrl(deviceSpu.getPicUrl());
|
||||
spu.setSliderPicUrls(deviceSpu.getSliderPicUrls());
|
||||
spu.setSort(deviceSpu.getSort() != null ? deviceSpu.getSort() : 0);
|
||||
spu.setTypes(deviceSpu.getTypes());
|
||||
spu.setSpecType(deviceSpu.getSpecType());
|
||||
spu.setDeliveryTypes(deviceSpu.getDeliveryTypes());
|
||||
spu.setDeliveryTemplateId(null); // 售货机商品通常不需要物流模板
|
||||
spu.setGiveIntegral(deviceSpu.getGiveIntegral() != null ? deviceSpu.getGiveIntegral() : 0);
|
||||
spu.setSubCommissionType(deviceSpu.getSubCommissionType() != null ? deviceSpu.getSubCommissionType() : false);
|
||||
spu.setVirtualSalesCount(deviceSpu.getVirtualSalesCount() != null ? deviceSpu.getVirtualSalesCount() : 0);
|
||||
// SKU 列表会在 convertDeviceSkuToSku 中处理
|
||||
return spu;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateDeviceSpu(DeviceProductSpuSaveReqVO updateReqVO) {
|
||||
// 1. 校验 SPU 是否存在
|
||||
ProductSpuDO spu = validateSpuExists(updateReqVO.getId());
|
||||
|
||||
// 2. 校验设备是否存在,并获取设备IMEI
|
||||
DeviceDO device = deviceService.getDevice(updateReqVO.getDeviceId());
|
||||
if (device == null) {
|
||||
throw exception(DEVICE_NOT_EXISTS);
|
||||
}
|
||||
String deviceImei = device.getDeviceId(); // device.getDeviceId() 返回的是设备IMEI(字符串)
|
||||
|
||||
// 3. 校验分类(售货机商品不需要品牌)
|
||||
validateCategory(updateReqVO.getCategoryId());
|
||||
|
||||
// 4. 将 DeviceProductSkuSaveReqVO 转换为 ProductSkuSaveReqVO
|
||||
List<ProductSkuSaveReqVO> skuSaveReqList = convertDeviceSkuToSku(updateReqVO.getSkus());
|
||||
|
||||
// 5. 校验 SKU
|
||||
productSkuService.validateSkuList(skuSaveReqList, updateReqVO.getSpecType());
|
||||
|
||||
// 6. 将 DeviceProductSpuSaveReqVO 转换为 ProductSpuSaveReqVO
|
||||
ProductSpuSaveReqVO spuSaveReqVO = convertDeviceSpuToSpu(updateReqVO, deviceImei);
|
||||
|
||||
// 7. 更新 SPU(复用现有逻辑)
|
||||
ProductSpuDO updateObj = BeanUtils.toBean(spuSaveReqVO, ProductSpuDO.class).setStatus(spu.getStatus());
|
||||
initSpuFromSkus(updateObj, skuSaveReqList);
|
||||
productSpuMapper.updateById(updateObj);
|
||||
// 批量更新 SKU
|
||||
productSkuService.updateSkuList(updateObj.getId(), skuSaveReqList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDeviceSpu(Long id) {
|
||||
// 复用现有的删除逻辑(售货机商品和商城商品的删除逻辑相同)
|
||||
deleteSpu(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProductSpuDO getDeviceSpu(Long id) {
|
||||
// 复用现有的查询逻辑
|
||||
return getSpu(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<ProductSpuCreatorDO> getDeviceSpuPage(cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSpuPageReqVO pageReqVO) {
|
||||
// 1. 将 DeviceProductSpuPageReqVO 转换为 ProductSpuPageReqVO
|
||||
// 需要将 deviceId(Long)转换为 deviceImei(String)
|
||||
ProductSpuPageReqVO spuPageReqVO = convertDevicePageToSpuPage(pageReqVO);
|
||||
|
||||
// 2. 售货机商品分页:带创建人、创建时间,按创建时间倒序(新的排上面)
|
||||
return productSpuMapper.selectPageWithCreatorOrderByCreateTimeDesc(spuPageReqVO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 DeviceProductSpuPageReqVO 转换为 ProductSpuPageReqVO
|
||||
* 需要将 deviceId(Long)转换为 deviceImei(String)
|
||||
*/
|
||||
private ProductSpuPageReqVO convertDevicePageToSpuPage(
|
||||
cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSpuPageReqVO devicePageReqVO) {
|
||||
ProductSpuPageReqVO spuPageReqVO = new ProductSpuPageReqVO();
|
||||
spuPageReqVO.setPageNo(devicePageReqVO.getPageNo());
|
||||
spuPageReqVO.setPageSize(devicePageReqVO.getPageSize());
|
||||
spuPageReqVO.setName(devicePageReqVO.getName());
|
||||
spuPageReqVO.setTabType(devicePageReqVO.getTabType());
|
||||
spuPageReqVO.setCategoryId(devicePageReqVO.getCategoryId());
|
||||
spuPageReqVO.setTypes(devicePageReqVO.getTypes());
|
||||
spuPageReqVO.setCreator(devicePageReqVO.getCreator());
|
||||
spuPageReqVO.setCreateTime(devicePageReqVO.getCreateTime());
|
||||
|
||||
// 将 deviceId(Long)转换为 deviceImei(String)
|
||||
if (devicePageReqVO.getDeviceId() != null) {
|
||||
DeviceDO device = deviceService.getDevice(devicePageReqVO.getDeviceId());
|
||||
if (device != null) {
|
||||
spuPageReqVO.setDeviceId(device.getDeviceId()); // device.getDeviceId() 返回的是设备IMEI
|
||||
}
|
||||
}
|
||||
|
||||
return spuPageReqVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 DeviceProductSkuSaveReqVO 列表转换为 ProductSkuSaveReqVO 列表
|
||||
*/
|
||||
private List<ProductSkuSaveReqVO> convertDeviceSkuToSku(List<DeviceProductSkuSaveReqVO> deviceSkus) {
|
||||
List<ProductSkuSaveReqVO> skus = new ArrayList<>();
|
||||
for (DeviceProductSkuSaveReqVO deviceSku : deviceSkus) {
|
||||
ProductSkuSaveReqVO sku = new ProductSkuSaveReqVO();
|
||||
sku.setName(deviceSku.getName());
|
||||
sku.setPrice(deviceSku.getPrice());
|
||||
sku.setMarketPrice(deviceSku.getMarketPrice());
|
||||
sku.setCostPrice(deviceSku.getCostPrice());
|
||||
sku.setBarCode(deviceSku.getBarCode());
|
||||
sku.setPicUrl(deviceSku.getPicUrl() != null ? deviceSku.getPicUrl() : "");
|
||||
sku.setStock(deviceSku.getStock());
|
||||
sku.setWeight(deviceSku.getWeight() != null ? deviceSku.getWeight() : 0.0);
|
||||
sku.setVolume(deviceSku.getVolume() != null ? deviceSku.getVolume() : 0.0);
|
||||
sku.setFirstBrokeragePrice(deviceSku.getFirstBrokeragePrice() != null ? deviceSku.getFirstBrokeragePrice() : 0);
|
||||
sku.setSecondBrokeragePrice(deviceSku.getSecondBrokeragePrice() != null ? deviceSku.getSecondBrokeragePrice() : 0);
|
||||
sku.setCabinetChannel(deviceSku.getCabinetChannel());
|
||||
sku.setCabinetType(deviceSku.getCabinetType());
|
||||
sku.setPurchaseBizType(deviceSku.getPurchaseBizType());
|
||||
// properties 会在 validateSkuList 中自动处理(单规格会添加默认属性)
|
||||
skus.add(sku);
|
||||
}
|
||||
return skus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 SKU 的信息,初始化 SPU 的信息
|
||||
* 主要是计数相关的字段,例如说市场价、最大最小价、库存等等
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ public enum TradeOrderTypeEnum implements ArrayValuable<Integer> {
|
||||
COMBINATION(3, "拼团订单"),
|
||||
POINT(4, "积分商城"),
|
||||
GIFTVOUCHER(5, "提货券订单"),
|
||||
MACHINE(6, "售货机订单"),
|
||||
;
|
||||
|
||||
public static final Integer[] ARRAYS = Arrays.stream(values()).map(TradeOrderTypeEnum::getType).toArray(Integer[]::new);
|
||||
|
||||
+285
-152
@@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
|
||||
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
|
||||
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuTypesEnum;
|
||||
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.controller.admin.devicelog.vo.DeviceLogSaveReqVO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
|
||||
@@ -29,13 +28,26 @@ import org.springframework.web.client.RestTemplate;
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_RECEIVE_FAIL_STATUS_NOT_OVERPAY;
|
||||
|
||||
/**
|
||||
* 贩卖机出货的 {@link TradeOrderHandler} 实现类
|
||||
* 贩卖机 / 自提柜出货的 {@link TradeOrderHandler} 实现类
|
||||
*
|
||||
* 触发时机:
|
||||
* - 支付模块回调成功后,TradeOrderUpdateServiceImpl.updateOrderPaid(...) 中会依次执行所有 TradeOrderHandler.afterPayOrder(...)
|
||||
* - 本类就在 afterPayOrder 中,根据订单行信息判断是否为“提货柜订单”,如果是就调用设备出货接口
|
||||
*
|
||||
* 功能概览:
|
||||
* 1. 校验订单中的商品是否全部为“提货柜商品类型”
|
||||
* 2. 校验订单是否只涉及一台设备(同一个 deviceId)
|
||||
* 3. 校验订单是否只涉及一种通道类型(同一个 cabinetType)
|
||||
* 4. 按设备协议格式组装出货指令 message,并调用 xr.cabinet.api_url 出货
|
||||
* 5. 将出货结果写入“贩卖机出货日志”
|
||||
* 6. 最后尝试将订单状态从当前状态更新为 COMPLETED(已完成)
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@@ -55,9 +67,6 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler {
|
||||
@Resource
|
||||
private ProductSkuApi productSkuApi;
|
||||
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@Resource
|
||||
private TradeOrderMapper tradeOrderMapper;
|
||||
|
||||
@@ -75,166 +84,290 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler {
|
||||
@Value("${xr.cabinet.api_url}")
|
||||
private String api_url;
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
|
||||
//商品SPU ID列表
|
||||
List<Long> spuIds = orderItems.stream()
|
||||
.map(TradeOrderItemDO::getSpuId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
// if(null==spuIds && spuIds.size()<=0){
|
||||
// System.out.println("商品SPU ID列表 为空");
|
||||
// return;
|
||||
// }
|
||||
// 这里不要抛异常,否则会影响“支付成功后”的主流程
|
||||
// 所有错误通过日志 + 设备出货日志记录
|
||||
|
||||
// 构建基础的设备出货日志 VO(后续各种分支都会复用)
|
||||
DeviceLogSaveReqVO deviceLogSaveReqVO = new DeviceLogSaveReqVO();
|
||||
deviceLogSaveReqVO.setUserId(order.getUserId());
|
||||
deviceLogSaveReqVO.setOrderId(order.getId());
|
||||
deviceLogSaveReqVO.setOrderNo(order.getNo());
|
||||
//查询spu表
|
||||
try {
|
||||
handleDeviceOrderAfterPaid(order, orderItems, deviceLogSaveReqVO);
|
||||
} catch (Throwable ex) {
|
||||
// 兜底保护:这里不再向上抛异常,仅记录日志,避免影响支付回调流程
|
||||
log.error("订单--{}:[设备出货处理异常]:原因: {}", order.getNo(), ex.getMessage(), ex);
|
||||
deviceLogSaveReqVO.setDeviceStatus(500);
|
||||
deviceLogSaveReqVO.setContent("[出货异常]原因:" + ex.getMessage());
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付成功后,针对“提货柜订单”执行自动出货逻辑
|
||||
*
|
||||
* @param order 订单主表
|
||||
* @param orderItems 订单明细
|
||||
* @param deviceLogSaveReqVO 出货日志基础对象
|
||||
*/
|
||||
private void handleDeviceOrderAfterPaid(TradeOrderDO order,
|
||||
List<TradeOrderItemDO> orderItems,
|
||||
DeviceLogSaveReqVO deviceLogSaveReqVO) {
|
||||
// 1. 提取 spuId 列表
|
||||
List<Long> spuIds = orderItems.stream()
|
||||
.map(TradeOrderItemDO::getSpuId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (CollUtil.isEmpty(spuIds)) {
|
||||
log.warn("订单:{} 无有效商品明细,跳过设备出货处理", order.getNo());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 查询 spu 信息,并校验是否为“提货柜商品” + “同一设备”
|
||||
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(spuIds);
|
||||
//判断商品类型是否有不是提货柜商品的
|
||||
if (CollUtil.isNotEmpty(spuList)) {
|
||||
boolean hasNonDevice = spuList.stream()
|
||||
.anyMatch(spu -> !ProductSpuTypesEnum.ENABLE.getTypes().equals(spu.getTypes()));
|
||||
if (hasNonDevice) {
|
||||
//订单中包含非提货柜商品
|
||||
log.error("订单中包含非提货柜商品");
|
||||
}else {
|
||||
//判断是不是有不同机器的订单
|
||||
boolean isSingleCreator = spuList.stream()
|
||||
.map(ProductSpuRespDTO::getDeviceId) //获取每个商品的DeviceId
|
||||
.distinct() //去重
|
||||
.count() == 1; // 判断是否只有一个设备
|
||||
if (isSingleCreator) {
|
||||
// 只有一个Creator的逻辑处理
|
||||
System.out.println("订单明细中只有一个设备");
|
||||
//查询商品创建人的设备IMEI
|
||||
// AdminUserRespDTO user = adminUserApi.getUser(Long.valueOf(spuList.get(0).getCreator()));
|
||||
// if (user == null|| user.getDeviceId() == null){
|
||||
// return;
|
||||
// }
|
||||
String deviceId = spuList.get(0).getDeviceId();
|
||||
if (deviceId == null){
|
||||
//imei为空
|
||||
log.error("订单:{},商品的设备编号为空",order.getNo());
|
||||
deviceLogSaveReqVO.setDeviceStatus(500);
|
||||
deviceLogSaveReqVO.setContent("[出货异常]原因:商品的设备编号为空");
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
return;
|
||||
}
|
||||
//获取商品通道
|
||||
List<Long> skuIds = orderItems.stream()
|
||||
.map(TradeOrderItemDO::getSkuId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if(CollUtil.isEmpty(skuIds)){
|
||||
return;
|
||||
}
|
||||
List<ProductSkuRespDTO> skuList = productSkuApi.getSkuList(skuIds);
|
||||
//判断是不是有不同通道类型的订单
|
||||
boolean isSingleCabinetType = skuList.stream()
|
||||
.map(ProductSkuRespDTO::getCabinetType) //获取每个商品的通道类型
|
||||
.distinct() //去重
|
||||
.count() == 1; // 判断是否只有一个唯一的通道类型
|
||||
if (isSingleCabinetType) {
|
||||
DispenseRequestBo request = new DispenseRequestBo();
|
||||
request.setDeviceId(deviceId);
|
||||
StringBuilder message= new StringBuilder(skuList.get(0).getCabinetType() + ",");
|
||||
//遍历订单明细
|
||||
for (int i = 0; i < orderItems.size(); i++) {
|
||||
TradeOrderItemDO orderItem = orderItems.get(i);
|
||||
ProductSkuRespDTO sku = productSkuApi.getSku(orderItem.getSkuId());
|
||||
message.append(sku.getCabinetChannel()).append("&").append(orderItem.getCount());
|
||||
if (i != orderItems.size() - 1) {
|
||||
message.append(",");
|
||||
}
|
||||
//记录最后一次出货的货道
|
||||
if (i == orderItems.size() - 1) {
|
||||
int d = deviceApi.updateDeviceLastChannel(deviceId, sku.getCabinetChannel());
|
||||
}
|
||||
}
|
||||
request.setMessage(message.toString());
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", token);
|
||||
|
||||
// 创建请求实体
|
||||
HttpEntity<DispenseRequestBo> requestEntity = new HttpEntity<>(request, headers);
|
||||
|
||||
// 创建RestTemplate
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
try {
|
||||
// 发送请求并解析响应
|
||||
ResponseEntity<DispenseResponseBo> response = restTemplate.exchange(
|
||||
api_url,
|
||||
HttpMethod.POST,
|
||||
requestEntity,
|
||||
DispenseResponseBo.class
|
||||
);
|
||||
|
||||
DispenseResponseBo body = response.getBody();
|
||||
if (body != null) {
|
||||
System.out.println("响应结果: " + body);
|
||||
if (body.getCode() == 0) {
|
||||
System.out.println("出货成功!详情: " + body.getData());
|
||||
|
||||
deviceLogSaveReqVO.setDeviceId(deviceId);
|
||||
deviceLogSaveReqVO.setDeviceStatus(body.getCode());
|
||||
deviceLogSaveReqVO.setContent("出货成功!详情: "+body.getData());
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
} else {
|
||||
System.out.println("出货失败!原因: " + body.getMsg());
|
||||
log.error("订单--{}:[出货失败]:原因: {}",order.getNo(), body.getMsg());
|
||||
deviceLogSaveReqVO.setDeviceId(deviceId);
|
||||
deviceLogSaveReqVO.setDeviceStatus(body.getCode());
|
||||
deviceLogSaveReqVO.setContent("[出货失败]原因: "+body.getMsg());
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
}
|
||||
}
|
||||
// 3. 更新 TradeOrderDO 状态为已完成
|
||||
TradeOrderDO tradeOrderDO = tradeOrderMapper.selectById(order.getId());
|
||||
int updateCount = tradeOrderMapper.updateByIdAndStatus(tradeOrderDO.getId(), tradeOrderDO.getStatus(),
|
||||
new TradeOrderDO().setStatus(TradeOrderStatusEnum.COMPLETED.getStatus()).setReceiveTime(LocalDateTime.now()));
|
||||
if (updateCount == 0) {
|
||||
throw exception(ORDER_RECEIVE_FAIL_STATUS_NOT_OVERPAY);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("请求异常: " + e.getMessage());
|
||||
log.error("订单--{}:[出货异常]:原因: {}",order.getNo(), e.getMessage());
|
||||
deviceLogSaveReqVO.setDeviceId(deviceId);
|
||||
deviceLogSaveReqVO.setDeviceStatus(500);
|
||||
deviceLogSaveReqVO.setContent("[出货异常]原因:"+e.getMessage());
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
}
|
||||
}else {
|
||||
System.out.println("订单明细中包含多个通道类型");
|
||||
log.error("订单:{},明细中包含多个通道类型",order.getNo());
|
||||
deviceLogSaveReqVO.setDeviceId(deviceId);
|
||||
deviceLogSaveReqVO.setDeviceStatus(500);
|
||||
deviceLogSaveReqVO.setContent("[出货异常]原因:订单明细中包含多个通道类型");
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
}
|
||||
} else {
|
||||
System.out.println("订单明细中包含多个商品创建人");
|
||||
log.error("订单:{},明细中包含多个商品创建人",order.getNo());
|
||||
deviceLogSaveReqVO.setDeviceStatus(500);
|
||||
deviceLogSaveReqVO.setContent("[出货异常]原因:订单明细中包含多台贩卖机商品");
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
}
|
||||
}
|
||||
}else {
|
||||
System.out.println("提货柜订单spu列表为空");
|
||||
log.error("订单:{},提货柜订单spu列表为空",order.getNo());
|
||||
if (CollUtil.isEmpty(spuList)) {
|
||||
log.error("订单:{}, 提货柜订单spu列表为空", order.getNo());
|
||||
deviceLogSaveReqVO.setDeviceStatus(500);
|
||||
deviceLogSaveReqVO.setContent("[出货异常]原因:提货柜订单spu列表为空");
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasNonDeviceSpu(spuList)) {
|
||||
// 订单中包含非提货柜商品,只打日志,不抛异常
|
||||
log.error("订单:{}, 中包含非提货柜商品,跳过设备出货处理", order.getNo());
|
||||
return;
|
||||
}
|
||||
|
||||
String deviceId = findUniqueDeviceId(order, spuList, deviceLogSaveReqVO);
|
||||
if (deviceId == null) {
|
||||
// 已在方法内部记录日志
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 查询 SKU 信息,并校验是否为“单一通道类型”
|
||||
List<Long> skuIds = orderItems.stream()
|
||||
.map(TradeOrderItemDO::getSkuId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (CollUtil.isEmpty(skuIds)) {
|
||||
log.warn("订单:{} 无有效 SKU 明细,跳过设备出货处理", order.getNo());
|
||||
return;
|
||||
}
|
||||
List<ProductSkuRespDTO> skuList = productSkuApi.getSkuList(skuIds);
|
||||
if (CollUtil.isEmpty(skuList)) {
|
||||
log.warn("订单:{} 根据 SKU 编号未查询到有效 SKU,跳过设备出货处理", order.getNo());
|
||||
return;
|
||||
}
|
||||
|
||||
String cabinetType = findUniqueCabinetType(order, skuList, deviceId, deviceLogSaveReqVO);
|
||||
if (cabinetType == null) {
|
||||
// 已在方法内部记录日志
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 组装出货请求
|
||||
DispenseRequestBo request = buildDispenseRequest(orderItems, skuList, deviceId, cabinetType);
|
||||
|
||||
// 5. 调用设备出货接口,并记录出货日志
|
||||
sendDispenseRequest(order, request, deviceLogSaveReqVO);
|
||||
|
||||
// 6. 无论出货成功/失败,尝试将订单状态更新为已完成(保持原有语义)
|
||||
completeOrder(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否存在“非提货柜商品类型”的 spu
|
||||
*/
|
||||
private boolean hasNonDeviceSpu(List<ProductSpuRespDTO> spuList) {
|
||||
return spuList.stream()
|
||||
.anyMatch(spu -> !Objects.equals(ProductSpuTypesEnum.ENABLE.getTypes(), spu.getTypes()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并返回订单涉及的唯一设备编号 deviceId
|
||||
*
|
||||
* 要求:
|
||||
* 1. 所有 spu 都必须有 deviceId
|
||||
* 2. 所有 spu 的 deviceId 必须相同
|
||||
*/
|
||||
private String findUniqueDeviceId(TradeOrderDO order,
|
||||
List<ProductSpuRespDTO> spuList,
|
||||
DeviceLogSaveReqVO deviceLogSaveReqVO) {
|
||||
boolean isSingleDevice = spuList.stream()
|
||||
.map(ProductSpuRespDTO::getDeviceId)
|
||||
.distinct()
|
||||
.count() == 1;
|
||||
if (!isSingleDevice) {
|
||||
log.error("订单:{}, 明细中包含多个设备的商品", order.getNo());
|
||||
deviceLogSaveReqVO.setDeviceStatus(500);
|
||||
deviceLogSaveReqVO.setContent("[出货异常]原因:订单明细中包含多台贩卖机商品");
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
return null;
|
||||
}
|
||||
|
||||
String deviceId = spuList.get(0).getDeviceId();
|
||||
if (deviceId == null) {
|
||||
// 设备编号为空,无法出货
|
||||
log.error("订单:{}, 商品的设备编号为空", order.getNo());
|
||||
deviceLogSaveReqVO.setDeviceStatus(500);
|
||||
deviceLogSaveReqVO.setContent("[出货异常]原因:商品的设备编号为空");
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
return null;
|
||||
}
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并返回订单涉及的唯一通道类型 cabinetType(字符串形式)
|
||||
*/
|
||||
private String findUniqueCabinetType(TradeOrderDO order,
|
||||
List<ProductSkuRespDTO> skuList,
|
||||
String deviceId,
|
||||
DeviceLogSaveReqVO deviceLogSaveReqVO) {
|
||||
boolean isSingleCabinetType = skuList.stream()
|
||||
.map(ProductSkuRespDTO::getCabinetType)
|
||||
.distinct()
|
||||
.count() == 1;
|
||||
if (!isSingleCabinetType) {
|
||||
log.error("订单:{}, 明细中包含多个通道类型", order.getNo());
|
||||
deviceLogSaveReqVO.setDeviceId(deviceId);
|
||||
deviceLogSaveReqVO.setDeviceStatus(500);
|
||||
deviceLogSaveReqVO.setContent("[出货异常]原因:订单明细中包含多个通道类型");
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
return null;
|
||||
}
|
||||
|
||||
return skuList.get(0).getCabinetType();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建设备出货请求体
|
||||
*
|
||||
* message 协议格式(示例):
|
||||
* T,C1&N1,C2&N2
|
||||
* 其中:
|
||||
* - T:通道类型(cabinetType)
|
||||
* - C:货道或柜门编号(cabinetChannel)
|
||||
* - N:出货数量(订单行的 count)
|
||||
*/
|
||||
private DispenseRequestBo buildDispenseRequest(List<TradeOrderItemDO> orderItems,
|
||||
List<ProductSkuRespDTO> skuList,
|
||||
String deviceId,
|
||||
String cabinetType) {
|
||||
DispenseRequestBo request = new DispenseRequestBo();
|
||||
request.setDeviceId(deviceId);
|
||||
|
||||
StringBuilder message = new StringBuilder(cabinetType).append(",");
|
||||
for (int i = 0; i < orderItems.size(); i++) {
|
||||
TradeOrderItemDO orderItem = orderItems.get(i);
|
||||
ProductSkuRespDTO sku = productSkuApi.getSku(orderItem.getSkuId());
|
||||
if (sku == null) {
|
||||
// 理论上不应该为空,这里简单跳过该行,后续可以根据需要增强处理
|
||||
continue;
|
||||
}
|
||||
message.append(sku.getCabinetChannel())
|
||||
.append("&")
|
||||
.append(orderItem.getCount());
|
||||
if (i != orderItems.size() - 1) {
|
||||
message.append(",");
|
||||
}
|
||||
|
||||
// 记录最后一次出货的货道(用于设备记录“上一次出货货道”)
|
||||
if (i == orderItems.size() - 1) {
|
||||
deviceApi.updateDeviceLastChannel(deviceId, sku.getCabinetChannel());
|
||||
}
|
||||
}
|
||||
request.setMessage(message.toString());
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用自提柜出货接口,并记录设备出货日志
|
||||
*/
|
||||
private void sendDispenseRequest(TradeOrderDO order,
|
||||
DispenseRequestBo request,
|
||||
DeviceLogSaveReqVO deviceLogSaveReqVO) {
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", token);
|
||||
|
||||
// 创建请求实体
|
||||
HttpEntity<DispenseRequestBo> requestEntity = new HttpEntity<>(request, headers);
|
||||
|
||||
// 创建 RestTemplate
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
try {
|
||||
// 发送请求并解析响应
|
||||
ResponseEntity<DispenseResponseBo> response = restTemplate.exchange(
|
||||
api_url,
|
||||
HttpMethod.POST,
|
||||
requestEntity,
|
||||
DispenseResponseBo.class
|
||||
);
|
||||
|
||||
DispenseResponseBo body = response.getBody();
|
||||
if (body == null) {
|
||||
log.error("订单--{}:[出货失败]:响应体为空", order.getNo());
|
||||
deviceLogSaveReqVO.setDeviceId(request.getDeviceId());
|
||||
deviceLogSaveReqVO.setDeviceStatus(500);
|
||||
deviceLogSaveReqVO.setContent("[出货失败]原因:响应为空");
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("订单--{}:[设备出货响应]: code={}, msg={}, data={}", order.getNo(),
|
||||
body.getCode(), body.getMsg(), body.getData());
|
||||
|
||||
deviceLogSaveReqVO.setDeviceId(request.getDeviceId());
|
||||
deviceLogSaveReqVO.setDeviceStatus(body.getCode());
|
||||
if (body.getCode() == 0) {
|
||||
// 出货成功
|
||||
deviceLogSaveReqVO.setContent("出货成功!详情: " + body.getData());
|
||||
} else {
|
||||
// 出货失败
|
||||
log.error("订单--{}:[出货失败]:原因: {}", order.getNo(), body.getMsg());
|
||||
deviceLogSaveReqVO.setContent("[出货失败]原因: " + body.getMsg());
|
||||
}
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
} catch (Exception e) {
|
||||
// HTTP 请求异常
|
||||
log.error("订单--{}:[出货异常]:原因: {}", order.getNo(), e.getMessage(), e);
|
||||
deviceLogSaveReqVO.setDeviceId(request.getDeviceId());
|
||||
deviceLogSaveReqVO.setDeviceStatus(500);
|
||||
deviceLogSaveReqVO.setContent("[出货异常]原因:" + e.getMessage());
|
||||
deviceLogService.createDeviceLog(deviceLogSaveReqVO);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将订单状态更新为“已完成”
|
||||
*
|
||||
* 说明:
|
||||
* - 为了保持与原逻辑一致,这里无论设备出货成功或失败,都会尝试将订单状态从当前状态更新为 COMPLETED
|
||||
* - 如果状态不符合预期(例如已经不是“待发货/待收货”),则抛出业务异常
|
||||
*/
|
||||
private void completeOrder(TradeOrderDO order) {
|
||||
TradeOrderDO tradeOrderDO = tradeOrderMapper.selectById(order.getId());
|
||||
if (tradeOrderDO == null) {
|
||||
log.error("订单--{}:[完成订单失败]:订单不存在", order.getId());
|
||||
return;
|
||||
}
|
||||
int updateCount = tradeOrderMapper.updateByIdAndStatus(
|
||||
tradeOrderDO.getId(),
|
||||
tradeOrderDO.getStatus(),
|
||||
new TradeOrderDO()
|
||||
.setStatus(TradeOrderStatusEnum.COMPLETED.getStatus())
|
||||
.setReceiveTime(LocalDateTime.now())
|
||||
);
|
||||
if (updateCount == 0) {
|
||||
// 与原逻辑保持一致:这里抛出业务异常
|
||||
throw exception(ORDER_RECEIVE_FAIL_STATUS_NOT_OVERPAY);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
+14
-1
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.product.api.device.DeviceApi;
|
||||
import cn.iocoder.yudao.module.product.api.device.dto.DeviceRespDto;
|
||||
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
|
||||
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
|
||||
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuTypesEnum;
|
||||
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
|
||||
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
|
||||
@@ -47,7 +48,19 @@ public class TradePriceCalculatorHelper {
|
||||
List<ProductSkuRespDTO> skuList) {
|
||||
// 创建 PriceCalculateRespDTO 对象
|
||||
TradePriceCalculateRespBO result = new TradePriceCalculateRespBO();
|
||||
result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>());
|
||||
// 1. 先根据活动参数计算基础订单类型
|
||||
Integer orderType = getOrderType(param);
|
||||
// 2. 如果还是普通订单,则根据商品 SPU 类型判断是否为售货机订单
|
||||
if (TradeOrderTypeEnum.isNormal(orderType) && spuList != null) {
|
||||
boolean hasMachineSpu = spuList.stream()
|
||||
.anyMatch(spu -> ProductSpuTypesEnum.ENABLE.getTypes().equals(spu.getTypes()));
|
||||
if (hasMachineSpu) {
|
||||
orderType = TradeOrderTypeEnum.MACHINE.getType();
|
||||
}
|
||||
}
|
||||
result.setType(orderType)
|
||||
.setPromotions(new ArrayList<>())
|
||||
.setGiveCouponTemplateCounts(new LinkedHashMap<>());
|
||||
|
||||
// 创建它的 OrderItem 属性
|
||||
result.setItems(new ArrayList<>(param.getItems().size()));
|
||||
|
||||
@@ -203,8 +203,8 @@ wx:
|
||||
# appid: wx66186af0759f47c9
|
||||
# secret: 3218bcbd112cbc614c7264ceb20144ac
|
||||
# 正式
|
||||
appid: wx741ef9b42243284a
|
||||
secret: 3b211e4bf0eeaaf27c8a369bcd10c9fb
|
||||
appid: wx0f238c72847db7c6
|
||||
secret: 8997ac41f3c6a430f1aa4970d14c7bca
|
||||
config-storage:
|
||||
type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
|
||||
key-prefix: wa # Redis Key 的前缀
|
||||
|
||||
Reference in new issue
Block a user