怎么解决重复提交订单?
在订单确认页生成一个预订单ID,并返回给前端,真正下订单的时候会把这个id传给后端,把这个id作为数据库主键就可以防止重复提交订单。 idwork.getid()
inner join 和外连接区别
inner join 只返回两个表链结满足条件的,left right 外连接 不满足条件的
内连接,也被称为自然连接,只有两个表相匹配的行才能在结果集中出现。返回的结果集选取了两个表中所有相匹配的数据,舍弃了不匹配的数据。由于内连接是从结果表中删除与其他连接表中没有匹配的所有行,所以内连接可能会造成信息的丢失。
外连接不仅包含符合连接条件的行,还包含左表(左连接时)、右表(右连接时)或两个边接表(全外连接)中的所有数据行。SQL外连接共有三种类型:左外连接(关键字为LEFT OUTER JOIN)、右外连接(关键字为RIGHT OUTER JOIN)和全外连接(关键字为FULL OUTER JOIN)。外连接的用法和内连接一样,只是将INNER JOIN关键字替换为相应的外连接关键字即可。
内连接只显示符合连接条件的记录,外连接除了显示符合条件的记录外,还显示表中的记录,例如,如果使用右外连接,还显示右表中的记录。
maptoint
前端传ids =[1,2,3]s时,后端用@RequestParam接收
优惠券使用
不过,新的问题来了,用户购物的时候自然要选择优惠券来使用。而现在主流的购物网站都会有优惠券的智能推荐功能,那么:
-
优惠券的类型不同,折扣计算规则该如何用代码表示?
-
如何组合优惠券使用才能让用户得到最大优惠?
-
优惠券叠加的计算算法是怎样的?
-
如果下单时使用了优惠券,用户退款时又该如何处理?
优惠券规则定义
所谓的优惠券方案推荐,就是从用户的所有优惠券中筛选出可用的优惠券,并且计算哪种优惠方案用券最少,优惠金额最高。
因此这里包含了对优惠券的下列需求:
-
判断一个优惠券是否可用,也就是检查订单金额是否达到优惠券使用门槛
-
按照优惠规则计算优惠金额,能够计算才能比较并找出最优方案
-
生成优惠券规则描述,目的是在页面直观的展示各种方案,供用户选择
package com.tianji.promotion.strategy.discount;
import com.tianji.promotion.domain.po.Coupon;
/**
* <p>优惠券折扣功能接口</p>
*/
public interface Discount {
/**
* 判断当前价格是否满足优惠券使用限制
* @param totalAmount 订单总价
* @param coupon 优惠券信息
* @return 是否可以使用优惠券
*/
boolean canUse(int totalAmount, Coupon coupon);
/**
* 计算折扣金额
* @param totalAmount 总金额
* @param coupon 优惠券信息
* @return 折扣金额
*/
int calculateDiscount(int totalAmount, Coupon coupon);
/**
* 根据优惠券规则返回规则描述信息
* @return 规则描述信息
*/
String getRule(Coupon coupon);
}
// 工厂模式
public class DiscountStrategy {
private final static EnumMap<DiscountType, Discount> strategies;
static {
strategies = new EnumMap<>(DiscountType.class);
strategies.put(DiscountType.NO_THRESHOLD, new NoThresholdDiscount());
strategies.put(DiscountType.PER_PRICE_DISCOUNT, new PerPriceDiscount());
strategies.put(DiscountType.RATE_DISCOUNT, new RateDiscount());
strategies.put(DiscountType.PRICE_DISCOUNT, new PriceDiscount());
}
public static Discount getDiscount(DiscountType type) {
return strategies.get(type);
}
}
根据优惠卷的类型得到对象的实现对象,然后判断传过来金额数目,判断对于这个数目这个优惠卷是否可用,优惠金额是多少,规则描述是怎样的
就比如说订单金额1000,这个1000的金额是否达到这个优惠卷的门槛了
这个是无门槛优惠卷的实现
@RequiredArgsConstructor
public class NoThresholdDiscount implements Discount{
private static final String RULE_TEMPLATE = "无门槛抵{}元";
@Override
public boolean canUse(int totalAmount, Coupon coupon) {
return totalAmount > coupon.getDiscountValue();
}
@Override
public int calculateDiscount(int totalAmount, Coupon coupon) {
return coupon.getDiscountValue();
}
@Override
public String getRule(Coupon coupon) {
return StringUtils.format(RULE_TEMPLATE, NumberUtils.scaleToStr(coupon.getDiscountValue(), 2));
}
}
优惠券智能推荐
好了,优惠券规则定义好之后,我们就可以正式开发优惠券的相关功能了。
第一个就是优惠券券方案推荐功能。在订单确认页面,前端会向交易微服务发起预下单请求,以获取id和优惠方案列表,页面请求如图:
交易服务首先需要查询课程信息,生成订单id,然后还需要调用优惠促销服务。而促销服务则需要根据订单中的课程信息查询当前用户的优惠券,并给出推荐的优惠组合方案,供用户在页面选择:
思路分析
简单来说,这就是一个查询优惠券、计算折扣、筛选最优解的过程。整体流程如下:
1.查询用户的可用优惠卷
2.初步筛选(先不看使用范围,先直接把没有达到优惠金额门槛的筛掉)
3.细晒(查询出每个优惠卷的可有范围,查看在这个范围中是否可用)
4. 全排列,对每个排列组合查看优惠卷是否可用 ,优惠金额是多少
5.使用多线程计算优惠金额
6.选择最优方案(卷相同的话,选金额最高的(因为排列顺序不同,优惠金额也可能不同),优惠金额相同,选用卷数量最少的)
代码分析
首先弄明白返回什么,前端传递的参数是什么
返回的是多个list,每个list中是这套卷组合的优惠金额
参数是课程id 分类 价格
第一步:查询当前用户的优惠卷(记得判断是否为空)
第二步:初筛(把不能使用的优惠局去掉)
第三步:细筛(根据优惠卷适用范围)
循环遍历优惠卷是否有限定范围,有限定范围的话去查找该优惠卷限定范围,看限定范围里是否有前端传来的课程,没有下一个循环,有的话看是否达到优惠卷使用门槛,最后放到map集合中。
map中放的就是 优惠卷 对应该优惠卷对应前端传来的课程中可用的课程
private Map<Coupon,List<OrderCourseDTO>> findAvailableCoupons(List<Coupon> coupons,List<OrderCourseDTO> orderCourses){
Map<Coupon,List<OrderCourseDTO>> map = new HashMap<>();
//循环遍历初筛后的优惠卷集合
for (Coupon coupon : coupons) {
// 2. 找出每一个优惠卷的可用课程,默认都可用,如果有限定范围则删选出去
List<OrderCourseDTO> availableCourses = orderCourses;
// 2.1 判断优惠卷是否限定了范围,没有限定范围就是默认都可用
if (coupon.getSpecific()){
//2.2 查询限定范围 查询coupon_scope表
List<CouponScope> scopeList = couponScopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();
// 2.3得到限定范围的id集合
List<Long> scopeIds = scopeList.stream().map(CouponScope::getCouponId).collect(Collectors.toList());
// 2.4 从ordercourses 订单中所有的课程集合 筛选该范围内的课程
availableCourses = orderCourses.stream().filter(new Predicate<OrderCourseDTO>() {
@Override
public boolean test(OrderCourseDTO orderCourseDTO) {
return scopeIds.contains(orderCourseDTO.getCateId());
}
}).collect(Collectors.toList());
if (CollUtils.isEmpty(availableCourses)){
continue; // 没有可用课程,直接下一次循环
}
}
// 3.计算该优惠卷是否可用 如果可用 添加到map
int totalSum = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
// 判断优惠卷是否可用 如果可用 则添加到map
Discount discount = getDiscount(coupon.getDiscountType());
if (discount.canUse(totalSum,coupon)){
map.put(coupon,availableCourses);
}
}
return map;
}
第四步 全排列
全排列的工具类
/**
* 基于回溯算法的全排列工具类
*/
public class PermuteUtil {
/**
* 将[0~n)的所有数字重组,生成不重复的所有排列方案
*
* @param n 数字n
* @return 排列组合
*/
public static List<List<Byte>> permute(int n) {
List<List<Byte>> res = new ArrayList<>();
List<Byte> input = new ArrayList<>(n);
for (byte i = 0; i < n; i++) {
input.add(i);
}
backtrack(n, input, res, 0);
return res;
}
/**
* 将指定集合中的元素重组,生成所有的排列组合方案
*
* @param input 输入的集合
* @param <T> 集合类型
* @return 重组后的集合方案
*/
public static <T> List<List<T>> permute(List<T> input) {
List<List<T>> res = new ArrayList<>();
backtrack(input.size(), input, res, 0);
return res;
}
private static <T> void backtrack(int n, List<T> input, List<List<T>> res, int first) {
// 所有数都填完了
if (first == n) {
res.add(new ArrayList<>(input));
}
for (int i = first; i < n; i++) {
// 动态维护数组
Collections.swap(input, first, i);
// 继续递归填下一个数
backtrack(n, input, res, first + 1);
// 撤销操作
Collections.swap(input, first, i);
}
}
}
细筛之后得到map,然后取得map中所有的key,对这些key做全排列,并添加 该组合中对应的单卷
第五步 对每套排列组合做循环,得到每一套组合的优惠明细
这个dto是用来记录,使用了方案后优惠的明细
detailmap是为了记录使用了某个优惠卷之后 每个课程的优惠价格,一开始初始化为优惠金额为0
/**
* 查看每一种优惠卷排序方案对应的优惠卷优惠明细
* @param avaMap 能用的优惠卷 对应订单中可用的课程的map
* @param courses 订单中的课程
* @param solution 优惠卷使用顺序
* @return 这一套优惠卷使用顺序的 优惠价格等
*/
private CouponDiscountDTO calculateSolutionDiscount(Map<Coupon,List<OrderCourseDTO>> avaMap,
List<OrderCourseDTO> courses,
List<Coupon> solution){
//1.创建方案结果dto对象
CouponDiscountDTO dto = new CouponDiscountDTO();
// 2. 初始化商品id 和 商品折扣明细的映射,初始折扣明细全为0,设置map key为 商品的id value初始值都为0
Map<Long, Integer> detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, c -> 0));
// 3. 循环方案,计算优惠信息
for (Coupon coupon : solution) {
// 得出该优惠卷对应的可使用课程
List<OrderCourseDTO> availableCourses = avaMap.get(coupon);
// 计算可用课程的总金额(商品价格-该课程的折扣明细)
int totalAmount = availableCourses.stream().mapToInt(value -> value.getPrice() - detailMap.get(value.getId())).sum();
// 判断优惠卷是否可用
Discount discount = getDiscount(coupon.getDiscountType());
if (!discount.canUse(totalAmount,coupon)){
continue; // 不可用 跳出循环 继续处理下一次循环
}
// 计算该优惠卷使用后的折扣值
int discountAmount = discount.calculateDiscount(totalAmount, coupon);
// 计算商品的折扣明细 更新到 detailMap
calculateDiscountDetails(detailMap, availableCourses, totalAmount, discountAmount);
// 累加每一个优惠卷的优惠金额 赋值给方案结果dto对象
dto.getIds().add(coupon.getId()); // 只要执行这句话一维这这个优惠卷生效了
dto.getRules().add(discount.getRule(coupon));
dto.setDiscountAmount(discountAmount + dto.getDiscountAmount()); // 不能覆盖 应该是所有生效的优惠卷 累加的结果
}
return dto;
}
计算折扣明细,为了防止出现无穷,最后一个课程的折扣金额用 总的折扣金额 - 前面的课程的折扣金额
多线程改造计算优惠明细
CountDownLatch latch = new CountDownLatch(solutions.size());
for (List<Coupon> solution : solutions) {
CompletableFuture.supplyAsync(new Supplier<CouponDiscountDTO>() {
@Override
public CouponDiscountDTO get() {
CouponDiscountDTO dto = calculateSolutionDiscount(availableCouponMap,orderCourses,solution);
return dto;
}
},discountSolutionExecutor).thenAccept(new Consumer<CouponDiscountDTO>() { // 上面return的dto就是下面的参数
@Override
public void accept(CouponDiscountDTO dto) {
dtos.add(dto);
latch.countDown(); // 计数器减一
}
});
try {
latch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("多线程计算优惠明细报错!!!");
}
}
最后一步:计算最优解
现在,我们计算出了成吨的优惠方案及其优惠金额,但是该如何从其中筛选出最优方案呢?最优方案的标准又是什么呢?
首先来看最优标准:
-
用券相同时,优惠金额最高的方案
-
优惠金额相同时,用券最少的方案
其实寻找最优解的流程跟找数组中最小值类似:
-
定义一个变量记录最小值
-
逐个遍历数组,判断当前元素是否比最小值更小
-
如果是,则覆盖最小值;如果否,则放弃
-
循环结束,变量中记录的就是最小值
例子:
比如 最开始 卷1 3 2 优惠金额是50,放入两个map中,然后是 卷 1 2 3 优惠金额20,小于之前的优惠金额,跳过,卷3 2 1 优惠金额是70,则会替换左边map的优惠方案,同时在右边map中新增一个70的键值对,卷1 2优惠金额也是70,则会替换掉原来70的值。这样下来,左边的map 同一个组合的只有一个。取交集正好满足最优方案。
private List<CouponDiscountDTO> findBestSolution(List<CouponDiscountDTO> solutions) {
// 1. 创建两个map 分别记录用卷相同,金额最高 金额相同,用卷最少
Map<String ,CouponDiscountDTO> moreDiscountMap = new HashMap<>();
Map<Integer ,CouponDiscountDTO> lessCouponMap = new HashMap<>();
// 2 循环方案,向map中记录 用卷相同 金额最高 金额相同,用卷最少
for (CouponDiscountDTO solution : solutions) {
// 2.1 对优惠卷id升序,转字符串然后以逗号拼接
String ids = solution.getIds().stream().sorted(Comparator.comparing(Long::longValue)).map(String::valueOf).collect(Collectors.joining(","));
// 2.2 从 moreDiscountMap中取 旧的记录判断旧方案是否大于等于 当前优惠方案
CouponDiscountDTO old = moreDiscountMap.get(ids);
if (old!=null && old.getDiscountAmount()>= solution.getDiscountAmount()){
continue;
}
// 2.从lessCouponMap中取旧的记录 判断旧的方案用卷数量 小于当前方案用卷数量
old = lessCouponMap.get(solution.getDiscountAmount());
int newSize = solution.getIds().size(); //当前方案的用卷数量
if (old!=null && newSize>1 && old.getIds().size() <= newSize){
continue;
}
moreDiscountMap.put(ids,solution);
lessCouponMap.put(solution.getDiscountAmount(),solution);
}
Collection<CouponDiscountDTO> bestSolution = CollUtils.intersection(moreDiscountMap.values(), lessCouponMap.values());
// 排序 优惠金额降序
return bestSolution.stream()
.sorted(Comparator.comparing(CouponDiscountDTO::getDiscountAmount).reversed()).collect(Collectors.toList());
}