小如重构
This commit is contained in:
1 parent
984b2007a8
commit
4a6515c803
5 files changed
+680
No files matched your search
+47
@@ -0,0 +1,47 @@
|
||||
package cn.iocoder.yudao.module.product.enums.spu;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 商品限购类型枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PurchaseLimitTypeEnum implements ArrayValuable<Integer> {
|
||||
|
||||
NONE(0, "不限购"),
|
||||
DAILY(1, "每天限购"),
|
||||
WEEKLY(2, "每周限购"),
|
||||
LIFETIME(3, "终身限购(任意时间只能买一次)");
|
||||
|
||||
public static final Integer[] ARRAYS = Arrays.stream(values()).map(PurchaseLimitTypeEnum::getType).toArray(Integer[]::new);
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private final Integer type;
|
||||
/**
|
||||
* 类型名
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
@Override
|
||||
public Integer[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
public static PurchaseLimitTypeEnum valueOf(Integer type) {
|
||||
for (PurchaseLimitTypeEnum value : values()) {
|
||||
if (value.getType().equals(type)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.spu;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 修复 product_spu 表的 device_id 字段脚本
|
||||
*
|
||||
* 功能:
|
||||
* 1. 检测 device_id 字段是否错误地存储了设备主键ID(数字)
|
||||
* 2. 通过关联 product_device 表,将主键ID转换为设备IMEI
|
||||
* 3. 批量更新所有需要修复的记录
|
||||
*
|
||||
* 背景:
|
||||
* - product_spu.device_id 字段应该存储设备IMEI(字符串,如"869667070283722")
|
||||
* - 但如果历史数据中错误地存储了设备表主键ID(数字,如"123"),需要转换为IMEI
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 配置数据库连接信息
|
||||
* 2. 设置 EXECUTE_UPDATE = false 预览要修复的数据
|
||||
* 3. 确认无误后设置 EXECUTE_UPDATE = true 执行修复
|
||||
* 4. 运行 main 方法
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @date 2025-01-27
|
||||
*/
|
||||
public class FixProductSpuDeviceIdScript {
|
||||
|
||||
// ========== 配置信息 ==========
|
||||
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";
|
||||
|
||||
// 是否执行更新(false 时只预览,不实际执行)
|
||||
private static final boolean EXECUTE_UPDATE = false;
|
||||
|
||||
public static void main(String[] args) {
|
||||
FixProductSpuDeviceIdScript script = new FixProductSpuDeviceIdScript();
|
||||
try {
|
||||
script.run();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void run() throws SQLException {
|
||||
System.out.println("==========================================");
|
||||
System.out.println("product_spu 表 device_id 字段修复脚本");
|
||||
System.out.println("==========================================");
|
||||
System.out.println("说明:将错误存储的设备主键ID转换为设备IMEI");
|
||||
System.out.println("==========================================\n");
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {
|
||||
// 1. 检测需要修复的记录
|
||||
List<SpuDeviceInfo> needFixList = detectNeedFixRecords(conn);
|
||||
|
||||
if (needFixList.isEmpty()) {
|
||||
System.out.println("✅ 未发现需要修复的记录,所有 device_id 都已正确存储为IMEI格式");
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("发现 " + needFixList.size() + " 条需要修复的记录:\n");
|
||||
|
||||
// 2. 显示修复计划
|
||||
displayFixPlan(needFixList);
|
||||
|
||||
if (!EXECUTE_UPDATE) {
|
||||
System.out.println("\n⚠️ 当前为预览模式,未实际执行修复");
|
||||
System.out.println("如需执行修复,请将 EXECUTE_UPDATE 设置为 true");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 执行修复
|
||||
System.out.println("\n开始执行修复...");
|
||||
executeUpdate(conn, needFixList);
|
||||
|
||||
System.out.println("\n==========================================");
|
||||
System.out.println("修复完成!");
|
||||
System.out.println("==========================================");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测需要修复的记录
|
||||
*/
|
||||
private List<SpuDeviceInfo> detectNeedFixRecords(Connection conn) throws SQLException {
|
||||
List<SpuDeviceInfo> needFixList = new ArrayList<>();
|
||||
|
||||
// 查询可能需要修复的记录:
|
||||
// 1. device_id 不为空
|
||||
// 2. device_id 是纯数字(可能是主键ID)
|
||||
// 3. device_id 长度较短(主键ID通常比IMEI短,IMEI一般15位)
|
||||
String sql = "SELECT spu.id, spu.name, spu.device_id, spu.creator, spu.create_time, " +
|
||||
" device.device_id AS device_imei " +
|
||||
"FROM product_spu spu " +
|
||||
"LEFT JOIN product_device device ON CAST(spu.device_id AS UNSIGNED) = device.id " +
|
||||
"WHERE spu.device_id IS NOT NULL " +
|
||||
" AND spu.device_id != '' " +
|
||||
" AND spu.device_id REGEXP '^[0-9]+$' " + // 纯数字
|
||||
" AND LENGTH(spu.device_id) < 10 " + // 长度小于10(IMEI通常15位)
|
||||
" AND spu.deleted = 0 " +
|
||||
"ORDER BY spu.id";
|
||||
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
SpuDeviceInfo info = new SpuDeviceInfo();
|
||||
info.spuId = rs.getLong("id");
|
||||
info.spuName = rs.getString("name");
|
||||
info.currentDeviceId = rs.getString("device_id");
|
||||
info.creator = rs.getString("creator");
|
||||
info.createTime = rs.getTimestamp("create_time");
|
||||
info.correctDeviceImei = rs.getString("device_imei");
|
||||
|
||||
// 只添加能找到对应设备的记录
|
||||
if (info.correctDeviceImei != null && !info.correctDeviceImei.isEmpty()) {
|
||||
needFixList.add(info);
|
||||
} else {
|
||||
System.out.println("⚠️ 警告:SPU ID=" + info.spuId + " 的 device_id=" + info.currentDeviceId +
|
||||
" 无法找到对应的设备,请手动检查");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return needFixList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示修复计划
|
||||
*/
|
||||
private void displayFixPlan(List<SpuDeviceInfo> needFixList) {
|
||||
System.out.println("修复计划预览:");
|
||||
System.out.println("------------------------------------------------------------------------------");
|
||||
System.out.printf("%-8s %-30s %-15s %-20s %-10s%n",
|
||||
"SPU ID", "商品名称", "当前device_id", "正确IMEI", "创建人");
|
||||
System.out.println("------------------------------------------------------------------------------");
|
||||
|
||||
for (SpuDeviceInfo info : needFixList) {
|
||||
System.out.printf("%-8d %-30s %-15s %-20s %-10s%n",
|
||||
info.spuId,
|
||||
truncate(info.spuName, 30),
|
||||
info.currentDeviceId,
|
||||
info.correctDeviceImei,
|
||||
info.creator != null ? info.creator : "无");
|
||||
}
|
||||
|
||||
System.out.println("------------------------------------------------------------------------------");
|
||||
System.out.println("说明:");
|
||||
System.out.println("- '当前device_id' 列显示的是错误的设备主键ID(数字)");
|
||||
System.out.println("- '正确IMEI' 列显示的是对应设备的IMEI(将被更新到 device_id 字段)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行更新
|
||||
*/
|
||||
private void executeUpdate(Connection conn, List<SpuDeviceInfo> needFixList) throws SQLException {
|
||||
String sql = "UPDATE product_spu SET device_id = ?, update_time = NOW() WHERE id = ?";
|
||||
|
||||
conn.setAutoCommit(false);
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (SpuDeviceInfo info : needFixList) {
|
||||
try {
|
||||
stmt.setString(1, info.correctDeviceImei);
|
||||
stmt.setLong(2, info.spuId);
|
||||
int count = stmt.executeUpdate();
|
||||
|
||||
if (count > 0) {
|
||||
successCount++;
|
||||
System.out.println("✓ SPU ID=" + info.spuId + " 已更新:" +
|
||||
info.currentDeviceId + " → " + info.correctDeviceImei);
|
||||
} else {
|
||||
failCount++;
|
||||
System.out.println("✗ SPU ID=" + info.spuId + " 更新失败");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
failCount++;
|
||||
System.out.println("✗ SPU ID=" + info.spuId + " 更新异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
conn.commit();
|
||||
System.out.println("\n更新统计:");
|
||||
System.out.println("- 成功:" + successCount + " 条");
|
||||
System.out.println("- 失败:" + failCount + " 条");
|
||||
|
||||
} catch (Exception e) {
|
||||
conn.rollback();
|
||||
System.err.println("更新操作失败,已回滚: " + e.getMessage());
|
||||
throw e;
|
||||
} finally {
|
||||
conn.setAutoCommit(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断字符串
|
||||
*/
|
||||
private String truncate(String str, int maxLength) {
|
||||
if (str == null) {
|
||||
return "";
|
||||
}
|
||||
if (str.length() <= maxLength) {
|
||||
return str;
|
||||
}
|
||||
return str.substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* SPU 设备信息类
|
||||
*/
|
||||
static class SpuDeviceInfo {
|
||||
Long spuId;
|
||||
String spuName;
|
||||
String currentDeviceId; // 当前存储的device_id(可能是错误的主键ID)
|
||||
String correctDeviceImei; // 正确的设备IMEI
|
||||
String creator;
|
||||
Timestamp createTime;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.yudao.module.product.controller.app.device.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 用户 APP - 售货机列表 Response VO
|
||||
*
|
||||
* 用于移动端展示售货机选择列表(只返回启用的设备)
|
||||
*/
|
||||
@Schema(description = "用户 APP - 售货机列表 Response VO")
|
||||
@Data
|
||||
public class AppDeviceListRespVO {
|
||||
|
||||
@Schema(description = "设备 IMEI", requiredMode = Schema.RequiredMode.REQUIRED, example = "869667070287061")
|
||||
private String deviceId;
|
||||
|
||||
@Schema(description = "售货机名称(显示在顶部地址栏)", requiredMode = Schema.RequiredMode.REQUIRED, example = "一号楼售货机")
|
||||
private String deviceName;
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
package cn.iocoder.yudao.module.product.framework.datapermission;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.device.DeviceDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
|
||||
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuTypesEnum;
|
||||
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
|
||||
import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
|
||||
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.google.common.collect.Sets;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.StringValue;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
||||
import net.sf.jsqlparser.expression.operators.relational.InExpression;
|
||||
import net.sf.jsqlparser.expression.Parenthesis;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 自定义商品 SPU 数据权限规则:
|
||||
* - types = 0(商城商品):按部门数据权限(dept_id + creator)过滤
|
||||
* - types = 1(售货机商品):按当前用户绑定的设备过滤(product_spu.device_id 在用户绑定设备的 IMEI 列表中)
|
||||
* - 超级管理员(super_admin 角色):不过滤,返回全部
|
||||
*
|
||||
* 注意:ProductSpuDO 已在 ProductWebConfiguration 中移除通用的 DeptDataPermissionRule 绑定,
|
||||
* 避免与本规则叠加冲突。
|
||||
*/
|
||||
@Component
|
||||
public class ProductSpuDataPermissionRule implements DataPermissionRule {
|
||||
|
||||
private static final String PRODUCT_SPU_TABLE =
|
||||
TableInfoHelper.getTableInfo(ProductSpuDO.class).getTableName();
|
||||
|
||||
@Resource
|
||||
private PermissionApi permissionApi;
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
@Resource
|
||||
private cn.iocoder.yudao.module.product.dal.mysql.device.DeviceMapper deviceMapper;
|
||||
|
||||
@Override
|
||||
public Set<String> getTableNames() {
|
||||
return Sets.newHashSet(PRODUCT_SPU_TABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getExpression(String tableName, Alias tableAlias) {
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
if (loginUser == null) {
|
||||
return null;
|
||||
}
|
||||
// 只对管理端用户生效
|
||||
if (!ObjectUtil.equal(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Long userId = loginUser.getId();
|
||||
|
||||
// 超级管理员:不过滤任何数据
|
||||
if (permissionApi.hasAnyRoles(userId, "super_admin")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. 构造「商城商品」(types = 0) 的部门数据权限表达式
|
||||
Expression mallExpr = buildMallExpression(tableName, tableAlias, loginUser);
|
||||
|
||||
// 2. 构造「售货机商品」(types = 1) 的设备绑定数据权限表达式
|
||||
Expression deviceExpr = buildDeviceExpression(tableName, tableAlias, userId);
|
||||
|
||||
// 3. 最终条件: (types = 0 AND 部门权限) OR (types = 1 AND 设备绑定权限)
|
||||
if (mallExpr == null && deviceExpr == null) {
|
||||
// 没有任何权限,返回一个恒为假的条件,保证查不到数据
|
||||
return new EqualsTo(null, null);
|
||||
}
|
||||
if (mallExpr == null) {
|
||||
return deviceExpr;
|
||||
}
|
||||
if (deviceExpr == null) {
|
||||
return mallExpr;
|
||||
}
|
||||
return new Parenthesis(new OrExpression(mallExpr, deviceExpr));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建商城商品(types = 0)的部门数据权限表达式
|
||||
*/
|
||||
private Expression buildMallExpression(String tableName, Alias tableAlias, LoginUser loginUser) {
|
||||
DeptDataPermissionRespDTO deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());
|
||||
if (deptDataPermission == null) {
|
||||
// 没有任何数据权限配置,直接返回空
|
||||
return null;
|
||||
}
|
||||
// 情况一,如果是 ALL 可查看全部,则无需 Dept/User 条件,但仍然需要 types = 0(商城商品)
|
||||
if (Boolean.TRUE.equals(deptDataPermission.getAll())) {
|
||||
return buildTypesEqualsExpression(tableName, tableAlias, ProductSpuTypesEnum.DISABLE.getTypes());
|
||||
}
|
||||
|
||||
// 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
|
||||
if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
|
||||
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) {
|
||||
// 仍然返回恒假,保证查不到商城商品
|
||||
Expression alwaysFalse = new EqualsTo(null, null);
|
||||
return new AndExpression(
|
||||
buildTypesEqualsExpression(tableName, tableAlias, ProductSpuTypesEnum.DISABLE.getTypes()),
|
||||
alwaysFalse
|
||||
);
|
||||
}
|
||||
|
||||
// 情况三,拼接 Dept 和 User 的条件,最后组合
|
||||
Expression deptExpression = buildDeptExpression(tableName, tableAlias, deptDataPermission.getDeptIds());
|
||||
Expression userExpression = buildUserExpression(tableName, tableAlias,
|
||||
deptDataPermission.getSelf(), loginUser.getId());
|
||||
|
||||
Expression deptUserExpr;
|
||||
if (deptExpression == null && userExpression == null) {
|
||||
// 理论上不会出现,如果出现则不做部门过滤
|
||||
deptUserExpr = null;
|
||||
} else if (deptExpression == null) {
|
||||
deptUserExpr = userExpression;
|
||||
} else if (userExpression == null) {
|
||||
deptUserExpr = deptExpression;
|
||||
} else {
|
||||
// WHERE (dept_id IN ? OR creator = ?)
|
||||
deptUserExpr = new Parenthesis(new OrExpression(deptExpression, userExpression));
|
||||
}
|
||||
|
||||
if (deptUserExpr == null) {
|
||||
// 仅按照 types = 0 过滤(商城商品)
|
||||
return buildTypesEqualsExpression(tableName, tableAlias, ProductSpuTypesEnum.DISABLE.getTypes());
|
||||
}
|
||||
|
||||
// 最终:types = 0 AND (dept/user 条件)
|
||||
return new AndExpression(
|
||||
buildTypesEqualsExpression(tableName, tableAlias, ProductSpuTypesEnum.DISABLE.getTypes()),
|
||||
deptUserExpr
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建售货机商品(types = 1)的设备绑定数据权限表达式
|
||||
*/
|
||||
private Expression buildDeviceExpression(String tableName, Alias tableAlias, Long userId) {
|
||||
// 当前用户绑定的设备主键 ID 列表
|
||||
List<Long> userDeviceIds = adminUserApi.getUserDeviceIds(userId);
|
||||
if (CollUtil.isEmpty(userDeviceIds)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查询对应的设备,拿到 IMEI 列表
|
||||
List<DeviceDO> devices = deviceMapper.selectBatchIds(userDeviceIds);
|
||||
if (CollUtil.isEmpty(devices)) {
|
||||
return null;
|
||||
}
|
||||
List<String> deviceImeis = devices.stream()
|
||||
.map(DeviceDO::getDeviceId)
|
||||
.filter(StrUtil::isNotEmpty)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (CollUtil.isEmpty(deviceImeis)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// device_id IN (imei1, imei2, ...)
|
||||
ExpressionList exprList = new ExpressionList(
|
||||
deviceImeis.stream().map(StringValue::new).collect(Collectors.toList()));
|
||||
InExpression inDeviceExpr = new InExpression(
|
||||
MyBatisUtils.buildColumn(tableName, tableAlias, "device_id"),
|
||||
new Parenthesis(exprList)
|
||||
);
|
||||
|
||||
// 最终:types = 1 AND device_id IN (绑定设备 IMEI)
|
||||
return new AndExpression(
|
||||
buildTypesEqualsExpression(tableName, tableAlias, ProductSpuTypesEnum.ENABLE.getTypes()),
|
||||
inDeviceExpr
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 dept_id IN (...) 条件
|
||||
*/
|
||||
private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) {
|
||||
if (CollUtil.isEmpty(deptIds)) {
|
||||
return null;
|
||||
}
|
||||
ExpressionList exprList = new ExpressionList(
|
||||
deptIds.stream().map(LongValue::new).collect(Collectors.toList()));
|
||||
return new InExpression(
|
||||
MyBatisUtils.buildColumn(tableName, tableAlias, "dept_id"),
|
||||
new Parenthesis(exprList)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 creator = 当前用户 条件
|
||||
*/
|
||||
private Expression buildUserExpression(String tableName, Alias tableAlias,
|
||||
Boolean self, Long userId) {
|
||||
if (Boolean.FALSE.equals(self)) {
|
||||
return null;
|
||||
}
|
||||
return new EqualsTo(
|
||||
MyBatisUtils.buildColumn(tableName, tableAlias, "creator"),
|
||||
new LongValue(userId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 types = ? 条件
|
||||
*/
|
||||
private Expression buildTypesEqualsExpression(String tableName, Alias tableAlias, Integer types) {
|
||||
return new EqualsTo(
|
||||
MyBatisUtils.buildColumn(tableName, tableAlias, "types"),
|
||||
new LongValue(types.longValue())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package cn.iocoder.yudao.module.trade.service.order.handler;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
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.PurchaseLimitTypeEnum;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
|
||||
import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderItemMapper;
|
||||
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.*;
|
||||
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.*;
|
||||
|
||||
/**
|
||||
* 限购校验订单处理器
|
||||
* <p>
|
||||
* 在订单创建前(beforeOrderCreate),校验商品是否设置了限购规则,如果设置了则检查用户历史购买记录是否超限
|
||||
* <p>
|
||||
* 限购规则仅对售货机订单(type = MACHINE)生效
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TradePurchaseLimitOrderHandler implements TradeOrderHandler {
|
||||
|
||||
@Resource
|
||||
private ProductSpuApi productSpuApi;
|
||||
|
||||
@Resource
|
||||
private TradeOrderItemMapper tradeOrderItemMapper;
|
||||
|
||||
@Override
|
||||
public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
|
||||
// 限购校验仅对售货机订单生效
|
||||
if (!Objects.equals(order.getType(), TradeOrderTypeEnum.MACHINE.getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取订单中所有的 SPU ID
|
||||
Set<Long> spuIds = orderItems.stream()
|
||||
.map(TradeOrderItemDO::getSpuId)
|
||||
.collect(Collectors.toSet());
|
||||
if (CollUtil.isEmpty(spuIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量查询 SPU 信息
|
||||
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(new ArrayList<>(spuIds));
|
||||
if (CollUtil.isEmpty(spuList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 过滤出有限购规则的商品
|
||||
List<ProductSpuRespDTO> limitedSpus = spuList.stream()
|
||||
.filter(spu -> spu.getPurchaseLimitType() != null
|
||||
&& !Objects.equals(spu.getPurchaseLimitType(), PurchaseLimitTypeEnum.NONE.getType())
|
||||
&& spu.getPurchaseLimitCount() != null
|
||||
&& spu.getPurchaseLimitCount() > 0)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (CollUtil.isEmpty(limitedSpus)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 逐个校验限购规则
|
||||
for (ProductSpuRespDTO spu : limitedSpus) {
|
||||
// 查找本次订单中该 SPU 的购买数量
|
||||
int currentPurchaseCount = orderItems.stream()
|
||||
.filter(item -> Objects.equals(item.getSpuId(), spu.getId()))
|
||||
.mapToInt(TradeOrderItemDO::getCount)
|
||||
.sum();
|
||||
|
||||
// 查询用户历史购买记录(根据限购类型决定查询时间范围)
|
||||
int historyPurchaseCount = getHistoryPurchaseCount(order.getUserId(), spu.getId(), spu.getPurchaseLimitType());
|
||||
|
||||
// 判断是否超限
|
||||
int totalCount = currentPurchaseCount + historyPurchaseCount;
|
||||
if (totalCount > spu.getPurchaseLimitCount()) {
|
||||
String limitDesc = getLimitDescription(spu.getPurchaseLimitType());
|
||||
throw exception(ORDER_CREATE_PURCHASE_LIMIT_EXCEED, spu.getName(), limitDesc, spu.getPurchaseLimitCount());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户历史购买数量(只统计已支付的订单)
|
||||
*
|
||||
* @param userId 用户 ID
|
||||
* @param spuId 商品 SPU ID
|
||||
* @param limitType 限购类型
|
||||
* @return 历史购买数量
|
||||
*/
|
||||
private int getHistoryPurchaseCount(Long userId, Long spuId, Integer limitType) {
|
||||
PurchaseLimitTypeEnum limitTypeEnum = PurchaseLimitTypeEnum.valueOf(limitType);
|
||||
if (limitTypeEnum == null || limitTypeEnum == PurchaseLimitTypeEnum.NONE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LocalDateTime startTime = getStartTimeByLimitType(limitTypeEnum);
|
||||
if (startTime == null) {
|
||||
// 终身限购:不限制开始时间
|
||||
startTime = LocalDateTime.of(2000, 1, 1, 0, 0);
|
||||
}
|
||||
|
||||
// 查询该时间范围内,用户已支付的订单项(订单项表 trade_order_item)
|
||||
// 注意:需要关联订单表(trade_order)判断订单是否已支付(pay_status = true)
|
||||
return tradeOrderItemMapper.selectPurchaseCountByUserAndSpu(userId, spuId, startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据限购类型获取查询起始时间
|
||||
*
|
||||
* @param limitType 限购类型
|
||||
* @return 起始时间
|
||||
*/
|
||||
private LocalDateTime getStartTimeByLimitType(PurchaseLimitTypeEnum limitType) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
switch (limitType) {
|
||||
case DAILY:
|
||||
// 今天 00:00:00
|
||||
return now.toLocalDate().atStartOfDay();
|
||||
case WEEKLY:
|
||||
// 本周一 00:00:00
|
||||
return now.with(TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY))
|
||||
.toLocalDate().atStartOfDay();
|
||||
case LIFETIME:
|
||||
// 终身限购:不限制开始时间(从很早的时间开始统计)
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取限购类型的可读描述
|
||||
*/
|
||||
private String getLimitDescription(Integer limitType) {
|
||||
PurchaseLimitTypeEnum limitTypeEnum = PurchaseLimitTypeEnum.valueOf(limitType);
|
||||
if (limitTypeEnum == null) {
|
||||
return "限购";
|
||||
}
|
||||
return limitTypeEnum.getName();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user