From f91b8587dab0c420d31033d4a27b58bfda37e043 Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Mon, 9 Feb 2026 18:24:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=8F=E5=A6=82=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/device-list-simple-by-user-api.md | 341 +++++++++ .../admin/channel/ChannelController.java | 7 + .../channel/MergeDuplicateSpuScript.java | 668 ++++++++++++++++++ .../channel/vo/ChannelBatchCreateReqVO.java | 19 + .../channel/vo/ChannelBatchCreateRespVO.java | 27 + .../channel/vo/ChannelImportExcelVO.java | 2 +- .../admin/channel/vo/ChannelPageReqVO.java | 7 +- .../admin/channel/vo/ChannelSaveReqVO.java | 8 +- .../device/DeviceProductSpuController.java | 149 ++++ .../device/vo/DeviceProductSkuRespVO.java | 79 +++ .../device/vo/DeviceProductSkuSaveReqVO.java | 67 ++ .../device/vo/DeviceProductSpuPageReqVO.java | 72 ++ .../device/vo/DeviceProductSpuRespVO.java | 107 +++ .../device/vo/DeviceProductSpuSaveReqVO.java | 93 +++ .../dal/dataobject/channel/ChannelDO.java | 9 +- .../dal/mysql/channel/ChannelMapper.java | 57 +- .../dal/mysql/spu/ProductSpuMapper.java | 42 ++ .../service/channel/ChannelService.java | 34 +- .../service/channel/ChannelServiceImpl.java | 214 +++++- .../service/spu/ProductSpuService.java | 43 ++ .../service/spu/ProductSpuServiceImpl.java | 179 +++++ .../trade/enums/order/TradeOrderTypeEnum.java | 1 + .../handler/TradeDeviceOrderHandler.java | 437 ++++++++---- .../TradePriceCalculatorHelper.java | 15 +- .../src/main/resources/application-local.yaml | 4 +- 25 files changed, 2502 insertions(+), 179 deletions(-) create mode 100644 docs/api/device-list-simple-by-user-api.md create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/MergeDuplicateSpuScript.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelBatchCreateReqVO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelBatchCreateRespVO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/DeviceProductSpuController.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuRespVO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuSaveReqVO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuPageReqVO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuRespVO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuSaveReqVO.java diff --git a/docs/api/device-list-simple-by-user-api.md b/docs/api/device-list-simple-by-user-api.md new file mode 100644 index 0000000..8c82308 --- /dev/null +++ b/docs/api/device-list-simple-by-user-api.md @@ -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 { + code: number; // 响应码,0 表示成功 + data: T; // 响应数据 + msg: string; // 响应消息(成功时通常为空字符串) +} +``` + +### 响应数据结构 + +**成功响应(code = 0)**: + +```typescript +interface DeviceSimpleRespVO { + id: number; // 设备编号(主键) + name: string; // 设备名称 + deviceId: string; // 设备IMEI(设备唯一标识) +} + +// 完整响应结构 +CommonResult +``` + +### 响应示例 + +**成功响应**: + +```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 = 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 { + code: number; + data: T; + msg: string; +} + +async function getDeviceSimpleListByUser(): Promise { + const response = await axios.get>( + '/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([]); + const loading = ref(false); + + const fetchDeviceList = async () => { + loading.value = true; + try { + const response = await axios.get>( + '/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([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const fetchDeviceList = async () => { + setLoading(true); + try { + const response = await axios.get>( + '/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 diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/ChannelController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/ChannelController.java index d5cae03..495e66f 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/ChannelController.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/ChannelController.java @@ -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 batchCreateChannel(@Valid @RequestBody ChannelBatchCreateReqVO batchCreateReqVO) { + return success(channelService.batchCreateChannel(batchCreateReqVO)); + } + @PutMapping("/update") @Operation(summary = "更新货道") @PreAuthorize("@ss.hasPermission('product:channel:update')") diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/MergeDuplicateSpuScript.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/MergeDuplicateSpuScript.java new file mode 100644 index 0000000..86766fb --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/MergeDuplicateSpuScript.java @@ -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 allSpus = getAllSpus(conn); + System.out.println("总共找到 " + allSpus.size() + " 个商品 SPU"); + + // 2. 识别需要合并的 SPU + Map 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 getAllSpus(Connection conn) throws SQLException { + List 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 identifyDuplicateSpus(List spus) { + Map mergeMapping = new HashMap<>(); + Set 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 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 set1 = new HashSet<>(Arrays.asList(keywords1)); + Set set2 = new HashSet<>(Arrays.asList(keywords2)); + + set1.retainAll(set2); + return set1.size(); + } + + /** + * 显示合并计划 + */ + private void displayMergePlan(Connection conn, Map mergeMapping) throws SQLException { + System.out.println("\n=========================================="); + System.out.println("合并计划预览"); + System.out.println("=========================================="); + + Map> groupedMapping = new HashMap<>(); + for (Map.Entry entry : mergeMapping.entrySet()) { + Long keepSpuId = entry.getValue(); + Long mergeSpuId = entry.getKey(); + groupedMapping.computeIfAbsent(keepSpuId, k -> new ArrayList<>()).add(mergeSpuId); + } + + for (Map.Entry> entry : groupedMapping.entrySet()) { + Long keepSpuId = entry.getKey(); + List 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 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 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 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 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 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 mergeMapping) throws SQLException { + Set 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 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 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 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 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; + } +} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelBatchCreateReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelBatchCreateReqVO.java new file mode 100644 index 0000000..5701d14 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelBatchCreateReqVO.java @@ -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 channels; + +} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelBatchCreateRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelBatchCreateRespVO.java new file mode 100644 index 0000000..9fbc655 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelBatchCreateRespVO.java @@ -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 successIds; + + @Schema(description = "创建失败的货道信息,key 为货道编号或名称,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureChannels; + + @Schema(description = "成功数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer successCount; + + @Schema(description = "失败数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer failureCount; + +} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelImportExcelVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelImportExcelVO.java index 0b622c3..0f08062 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelImportExcelVO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelImportExcelVO.java @@ -14,7 +14,7 @@ public class ChannelImportExcelVO { private String name; @ExcelProperty("设备IMEI") - private String deviceId; + private String deviceImei; @ExcelProperty("当前使用的货道或柜门编号") private String cabinetChannel; diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelPageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelPageReqVO.java index c129cb1..ed4adb7 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelPageReqVO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelPageReqVO.java @@ -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; diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelSaveReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelSaveReqVO.java index 837c3db..c9c7173 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelSaveReqVO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelSaveReqVO.java @@ -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; diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/DeviceProductSpuController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/DeviceProductSpuController.java new file mode 100644 index 0000000..ff91c89 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/DeviceProductSpuController.java @@ -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 createDeviceProductSpu(@Valid @RequestBody DeviceProductSpuSaveReqVO createReqVO) { + return success(productSpuService.createDeviceSpu(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新售货机商品 SPU") + @PreAuthorize("@ss.hasPermission('product:spu:update')") + public CommonResult 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 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 getDeviceProductSpuDetail(@RequestParam("id") Long id) { + // 获得商品 SPU + ProductSpuDO spu = productSpuService.getDeviceSpu(id); + if (spu == null) { + return success(null); + } + // 查询商品 SKU + List 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> getDeviceProductSpuPage(@Valid DeviceProductSpuPageReqVO pageVO) { + PageResult pageResult = productSpuService.getDeviceSpuPage(pageVO); + // 查询商品 SKU + List skuList = productSkuService.getSkuListBySpuId( + pageResult.getList().stream().map(ProductSpuCreatorDO::getId).collect(Collectors.toList())); + Map> skuMap = convertMultiMap(skuList, ProductSkuDO::getSpuId); + + // 转换为售货机商品响应 VO(含创建人、创建时间) + List 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 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; + } + +} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuRespVO.java new file mode 100644 index 0000000..f171de2 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuRespVO.java @@ -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 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; + } + +} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuSaveReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuSaveReqVO.java new file mode 100644 index 0000000..708dc2b --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuSaveReqVO.java @@ -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; + +} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuPageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuPageReqVO.java new file mode 100644 index 0000000..468c5aa --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuPageReqVO.java @@ -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; + +} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuRespVO.java new file mode 100644 index 0000000..321638e --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuRespVO.java @@ -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 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 skus; + + // ========== 物流相关字段 ========= + + @Schema(description = "配送方式数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[3]") + private List 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; + +} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuSaveReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuSaveReqVO.java new file mode 100644 index 0000000..53f7d2e --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSpuSaveReqVO.java @@ -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 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 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 skus; + +} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/channel/ChannelDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/channel/ChannelDO.java index 38d9aca..608f0e3 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/channel/ChannelDO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/channel/ChannelDO.java @@ -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; /** * 当前使用的货道或柜门编号 */ diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/channel/ChannelMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/channel/ChannelMapper.java index 01b7494..db1d8ee 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/channel/ChannelMapper.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/channel/ChannelMapper.java @@ -21,6 +21,7 @@ public interface ChannelMapper extends BaseMapperX { return selectPage(reqVO, new LambdaQueryWrapperX() .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 { .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() + .eq(ChannelDO::getDeviceId, deviceId) + .eq(ChannelDO::getCabinetChannel, cabinetChannel)); } - default List 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() + .eq(ChannelDO::getDeviceImei, deviceImei) + .eq(ChannelDO::getCabinetChannel, cabinetChannel)); + } + + /** + * 根据设备ID批量查询货道列表(新方法,推荐使用) + */ + default List getChannelListByDeviceId(Long deviceId){ + return selectList(new LambdaQueryWrapperX() + .eq(ChannelDO::getDeviceId, deviceId) + .orderByAsc(ChannelDO::getChannelLine) + .orderByAsc(ChannelDO::getCabinetChannel)); + } + + /** + * 根据设备ID列表批量查询货道列表 + */ + default List getChannelListByDeviceIds(Collection deviceIds){ + if (deviceIds == null || deviceIds.isEmpty()) { + return Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .in(ChannelDO::getDeviceId, deviceIds) + .orderByAsc(ChannelDO::getDeviceId) + .orderByAsc(ChannelDO::getChannelLine) + .orderByAsc(ChannelDO::getCabinetChannel)); + } + + /** + * 根据设备IMEI查询货道列表(兼容方法) + * @deprecated 建议使用 getChannelListByDeviceId(Long) + */ + @Deprecated + default List getChannelListByDeviceImei(String deviceImei){ + return selectList(new LambdaQueryWrapperX() + .eq(ChannelDO::getDeviceImei, deviceImei) + .orderByAsc(ChannelDO::getChannelLine) + .orderByAsc(ChannelDO::getCabinetChannel)); } } \ No newline at end of file diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java index 9e71c71..9162eff 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java @@ -44,6 +44,27 @@ public interface ProductSpuMapper extends BaseMapperX { return selectPage(reqVO, queryWrapper); } + /** + * 获取商品 SPU 分页列表数据(按创建时间倒序,新的排上面) + * 用于售货机商品分页 + * + * @param reqVO 分页请求参数 + * @return 商品 SPU 分页列表数据 + */ + default PageResult selectPageOrderByCreateTimeDesc(ProductSpuPageReqVO reqVO) { + Integer tabType = reqVO.getTabType(); + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .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 { return selectJoinPage(pageReqVO,ProductSpuCreatorDO.class, query); } + /** + * 获取商品 SPU 分页(带创建人),按创建时间倒序,用于售货机商品分页 + */ + default PageResult selectPageWithCreatorOrderByCreateTimeDesc(ProductSpuPageReqVO pageReqVO) { + Integer tabType = pageReqVO.getTabType(); + MPJLambdaWrapperX query = new MPJLambdaWrapperX() + .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 选项的查询条件 * diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/channel/ChannelService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/channel/ChannelService.java index 4a883f3..b6513a3 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/channel/ChannelService.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/channel/ChannelService.java @@ -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 testChannel(Long id); - List getChannelListByDeviceid(String deviceId); + /** + * 根据设备ID查询货道列表(新方法,推荐使用) + * + * @param deviceId 设备ID(关联 product_device.id) + * @return 货道列表 + */ + List getChannelListByDeviceId(Long deviceId); + + /** + * 根据设备ID列表批量查询货道列表 + * + * @param deviceIds 设备ID列表 + * @return 货道列表 + */ + List getChannelListByDeviceIds(Collection deviceIds); + + /** + * 根据设备IMEI查询货道列表(兼容方法) + * @deprecated 建议使用 getChannelListByDeviceId(Long) + * + * @param deviceImei 设备IMEI + * @return 货道列表 + */ + @Deprecated + List getChannelListByDeviceid(String deviceImei); } \ No newline at end of file diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/channel/ChannelServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/channel/ChannelServiceImpl.java index 290abab..8bacf66 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/channel/ChannelServiceImpl.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/channel/ChannelServiceImpl.java @@ -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 successIds = new ArrayList<>(); + Map failureChannels = new LinkedHashMap<>(); + + // 批量处理设备ID查询,优化性能 + Set deviceIdSet = new HashSet<>(); + Set 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 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 deviceIdToImeiMap = new HashMap<>(); + if (!deviceIdSet.isEmpty()) { + List 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 getChannelListByDeviceid(String deviceId) { - return channelMapper.getChannelListByDeviceid(deviceId); + public List getChannelListByDeviceId(Long deviceId) { + return channelMapper.getChannelListByDeviceId(deviceId); + } + + @Override + public List getChannelListByDeviceIds(Collection deviceIds) { + return channelMapper.getChannelListByDeviceIds(deviceIds); + } + + @Override + @Deprecated + public List 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()); } } \ No newline at end of file diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java index 4525b5f..afb1196 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java @@ -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 getDeviceSpuPage(cn.iocoder.yudao.module.product.controller.admin.device.vo.DeviceProductSpuPageReqVO pageReqVO); + /** * 更新商品 SPU * diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java index 4237c3e..e22d14b 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java @@ -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 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 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 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 convertDeviceSkuToSku(List deviceSkus) { + List 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 的信息 * 主要是计数相关的字段,例如说市场价、最大最小价、库存等等 diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderTypeEnum.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderTypeEnum.java index 4443cdf..df4cfc6 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderTypeEnum.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderTypeEnum.java @@ -22,6 +22,7 @@ public enum TradeOrderTypeEnum implements ArrayValuable { COMBINATION(3, "拼团订单"), POINT(4, "积分商城"), GIFTVOUCHER(5, "提货券订单"), + MACHINE(6, "售货机订单"), ; public static final Integer[] ARRAYS = Arrays.stream(values()).map(TradeOrderTypeEnum::getType).toArray(Integer[]::new); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java index 2380307..156b6c9 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java @@ -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 orderItems) { - //商品SPU ID列表 - List 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 orderItems, + DeviceLogSaveReqVO deviceLogSaveReqVO) { + // 1. 提取 spuId 列表 + List spuIds = orderItems.stream() + .map(TradeOrderItemDO::getSpuId) + .distinct() + .collect(Collectors.toList()); + if (CollUtil.isEmpty(spuIds)) { + log.warn("订单:{} 无有效商品明细,跳过设备出货处理", order.getNo()); + return; + } + + // 2. 查询 spu 信息,并校验是否为“提货柜商品” + “同一设备” List 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 skuIds = orderItems.stream() - .map(TradeOrderItemDO::getSkuId) - .distinct() - .collect(Collectors.toList()); - if(CollUtil.isEmpty(skuIds)){ - return; - } - List 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 requestEntity = new HttpEntity<>(request, headers); - - // 创建RestTemplate - RestTemplate restTemplate = new RestTemplate(); - - try { - // 发送请求并解析响应 - ResponseEntity 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 skuIds = orderItems.stream() + .map(TradeOrderItemDO::getSkuId) + .distinct() + .collect(Collectors.toList()); + if (CollUtil.isEmpty(skuIds)) { + log.warn("订单:{} 无有效 SKU 明细,跳过设备出货处理", order.getNo()); + return; + } + List 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 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 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 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 orderItems, + List 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 requestEntity = new HttpEntity<>(request, headers); + + // 创建 RestTemplate + RestTemplate restTemplate = new RestTemplate(); + + try { + // 发送请求并解析响应 + ResponseEntity 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) { diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java index 2086305..1e0b8ab 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java @@ -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 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())); diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 74419ae..1a30486 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -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 的前缀