小如重构

This commit is contained in:
zjf committed 2026-02-09 18:24:51 +08:00
1 parent 2849fdbcb0
commit f91b8587da
25 files changed
+2502 -179

No files matched your search

+341
View File
@@ -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
@@ -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')")
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
@@ -14,7 +14,7 @@ public class ChannelImportExcelVO {
private String name;
@ExcelProperty("设备IMEI")
private String deviceId;
private String deviceImei;
@ExcelProperty("当前使用的货道或柜门编号")
private String cabinetChannel;
@@ -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;
@@ -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;
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
/**
* 当前使用的货道或柜门编号
*/
@@ -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));
}
}
@@ -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 选项的查询条件
*
@@ -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);
}
@@ -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());
}
}
@@ -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
*
@@ -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 的信息
* 主要是计数相关的字段,例如说市场价、最大最小价、库存等等
@@ -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);
@@ -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) {
@@ -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 的前缀