天机学堂DAY09-12
DAY09优惠卷系统产品原型分析-优惠卷管理-分析业务流程发放优惠卷定时任务每隔一段时间看一下优惠卷有没有过期过期了就把状态由发放改为其他状态暂停优惠卷优惠卷除了发放未发放还有暂停状态用于在发现问题后用于解决问题-接口统计和分析分页查询优惠卷的列表新增优惠卷手动领取是指需要我们在主页自己点击之后领取指定发放是指优惠卷直接发放到用户账号上会生成对应的兑换码兑换码的数量是有限的指定发放给某一些学员之后就会用完根据id查询优惠卷点击编辑按钮之后会跳转到编辑页面但是会根据id回显数据这就要求我们在新增优惠卷之后传递优惠卷的id给前端更新优惠卷在对已经发放的优惠卷编辑完成之后更新优惠卷根据id删除优惠卷只有待发放的优惠卷可以删除发放优惠卷暂停优惠卷生成兑换码对于指定发放的优惠卷在发放的时候生成兑换码定时完结优惠卷接口统计-表结构设计优惠卷表分类和优惠卷的多对多关联表兑换码表结构优惠券管理-新增优惠券Override Transactional public void saveCoupon(CouponFormDTO couponFormDTO) { //1.保存优惠卷信息 //1.1转po Coupon coupon BeanUtils.copyProperties(couponFormDTO, Coupon.class); //1.2保存 save(coupon); //2.保存分类信息 if(!couponFormDTO.getSpecific()){ return; } //2.1转po ListLong scopes couponFormDTO.getScopes(); //如果查不到说明前端给的数据是错的 if(CollectionUtils.isEmpty(scopes)){ throw new BadRequestException(前端提交的限定范围信息有误); } ListCouponScope couponScopeList scopes.stream() .map(bizId - new CouponScope().setBizId(bizId).setCouponId(coupon.getId())) .collect(Collectors.toList()); //2.2保存 couponScopeService.saveBatch(couponScopeList); }注意点1这里必须是实体类getId然后赋值这样就是Mybatisplus存储的正确id注意点2name和specific两个字段和数据库内的关键字有冲突要用TableField起别的名字Stream流把一个 ListString 或 ListLong 类型的 scopes 集合转换成一个 ListCouponScope 类型的集合。✅ 逐行拆解超级通俗1.scopes.stream()scopes是一个集合比如ListLong或ListString.stream()就是把集合变成流水线作用让集合可以使用流式操作map、filter、collect…2..map( bizId - ... )map 转换、映射意思把流里的每一个元素 bizId转换成另一个东西。这里java运行bizId - new CouponScope().setBizId(bizId).setCouponId(coupon.getId())就是每一个 bizId → 变成一个 CouponScope 对象3.new CouponScope().setBizId(bizId).setCouponId(coupon.getId())这是链式创建对象并赋值等价于java运行CouponScope scope new CouponScope(); scope.setBizId(bizId); // 把当前循环的 bizId 设进去 scope.setCouponId(coupon.getId()); // 把优惠券ID设进去4..collect(Collectors.toList())把流转回 List 集合作用把上面转换好的一堆CouponScope对象打包成ListCouponScope-分页查询优惠券过滤条件返回参数id是一定会返回的因为后续的删除暂停等操作都需要根据id来Override public PageDTOCouponPageVO queryCouponByPage(CouponQuery couponQuery) { //1.查询 //1.1过滤条件可能为空因此先取出来方便判断是否为空 Integer type couponQuery.getType(); Integer status couponQuery.getStatus(); String name couponQuery.getName(); PageCoupon page lambdaQuery() .eq(type ! null, Coupon::getType, couponQuery.getType()) .eq(status ! null, Coupon::getStatus, couponQuery.getStatus()) .like(name ! null, Coupon::getName, couponQuery.getName()) .page(couponQuery.toMpPageDefaultSortByCreateTimeDesc()); //2.封装结果返回 ListCoupon records page.getRecords(); if(CollectionUtils.isEmpty(records)){ return PageDTO.empty(page); } ListCouponPageVO ts BeanUtils.copyList(records, CouponPageVO.class); return PageDTO.of(page, ts); }-实现发放接口Override public void beginIssue(CouponIssueFormDTO couponIssueFormDTO) { //判断当前状态是否为暂停或者未发放 Long CouponId couponIssueFormDTO.getId(); Coupon coupon getById(CouponId); if(coupon null){ throw new BadRequestException(前端传递的参数有误); } if(coupon.getStatus()! CouponStatus.DRAFTcoupon.getStatus()!CouponStatus.PAUSE){ return; } // 3.判断是否是立刻发放 LocalDateTime issueBeginTime couponIssueFormDTO.getIssueBeginTime(); LocalDateTime now LocalDateTime.now(); boolean isBegin issueBeginTime null || !issueBeginTime.isAfter(now); // 4.更新优惠券 //转po Coupon c BeanUtils.copyProperties(couponIssueFormDTO, Coupon.class); // 4.2.更新状态 if (isBegin) { c.setStatus(ISSUING); c.setIssueBeginTime(now); }else{ c.setStatus(UN_ISSUE); } // 4.3.写入数据库 updateById(c); }-兑换码算法UUID指的是128位的二进制数字Showflake指的是64位的二进制数字1.可以把这些二进制数字每五位记为一个数字对应一个编号该编号对应24和字母和8个数字2.兑换码长度不超过10个字符--二进制数字不超过五十个字符但是UUID和Showflake长度不合适被排除了只剩下自增id自增id的大小取决于我们自己3.自增id可以满足10亿以上的兑换码需求不会重复4.用BitMap标记是否兑换是第几个自增id就去哪一位看有没有被兑换5.防止爆刷添加密钥用自增长序列号生成一段32位的自增长序列号根据密钥加权求和得到的结果为签名如果篡改了某一位签名就会变化拼接一段新鲜值用于分辨是哪一个密钥把自增长的序列号乘上密钥算出来的数字用八个比特位来表示不足的部分补成零得到的五十个比特位用bitMap转换成24个字母和8个数字-异步生成兑换码线程池一、什么是线程池线程池就是一个存放线程的 “池子”提前创建好一批线程来了任务直接复用不用频繁新建 / 销毁线程。生活比喻不用线程池来一个客人临时招一个服务员干完就开除浪费时间、成本极高。用线程池提前固定招好5 个服务员永远在岗来客人就分配没客人就待命复用员工不反复招人裁员。二、为什么要用线程池降低资源消耗避免频繁new Thread()销毁线程节省 CPU 内存。提高响应速度任务来了直接用空闲线程不用从头创建。可控线程数量防止无限创建线程导致OOM、服务器崩掉。统一管理定时任务、批量任务、异步任务都能统一调度。三、线程池核心ThreadPoolExecutor 七大参数面试必考java运行public ThreadPoolExecutor( int corePoolSize, // 1.核心线程数 int maximumPoolSize, // 2.最大线程数 long keepAliveTime, // 3.空闲线程存活时间 TimeUnit unit, // 4.时间单位 BlockingQueueRunnable workQueue, // 5.任务阻塞队列 ThreadFactory threadFactory, // 6.线程工厂 RejectedExecutionHandler handler // 7.拒绝策略 );逐个通俗解释corePoolSize 核心线程数常驻线程永远不回收哪怕闲着也留着待命。maximumPoolSize 最大线程数池子最多能有多少线程核心线程 临时线程 总和上限。keepAliveTime 空闲存活时间临时线程空闲多久没人用就自动销毁。unit 时间单位秒、毫秒、分钟等。workQueue 任务队列核心线程满了新来任务排队放队列里。常用ArrayBlockingQueue、LinkedBlockingQueuethreadFactory 线程工厂给线程起名字、设置优先级方便日志排查。handler 拒绝策略线程满了 队列也满了再来任务怎么处理。四、线程池任务执行流程背下来就能面试来了任务 →先看核心线程有没有空闲有就直接用。核心线程全忙 →放进阻塞队列排队。队列也满了 →新建临时线程处理任务。线程总数达到最大线程数 队列也满 →触发拒绝策略。五、4 种内置拒绝策略AbortPolicy默认直接抛异常CallerRunsPolicy谁提交任务谁自己跑主线程执行DiscardPolicy直接丢掉任务不报错DiscardOldestPolicy丢掉队列最老的任务加新任务编写代码为什么只需要判断旧对象是草稿就可以保证现在的对象是从草稿到发布的而不是从暂停到发布1. 代码里的逻辑是这样的// 从数据库查出来的旧对象 Coupon coupon getById(dto.getId()); // 状态判断必须是 草稿 或 暂停否则抛异常 if(coupon.getStatus() ! CouponStatus.DRAFT coupon.getStatus() ! CouponStatus.PAUSE){ throw new BizIllegalException(优惠券状态错误); } // 用新对象更新状态为 发放中 / 待发放 updateById(c);2. 所以结论是只要代码能执行到if(coupon.getObtainWay() ObtainType.ISSUE coupon.getStatus() CouponStatus.DRAFT)这一行就意味着旧对象coupon一定是草稿状态而且已经通过了前面的状态校验只能是草稿或暂停这里又是草稿后面一定会执行updateById(c)把状态改成发放中 / 待发放所以只要执行到这一步就一定是从草稿更新到发布的过程不会有重复触发的风险。补充说明如果优惠券之前是暂停状态PAUSE前面的校验会通过但这里的coupon.getStatus() DRAFT条件就不成立不会生成兑换码避免了重复生成。/** * p * 兑换码 服务实现类 * /p * * author 虎哥 * since 2026-05-05 */ Service public class ExchangeCodeServiceImpl extends ServiceImplExchangeCodeMapper, ExchangeCode implements IExchangeCodeService { private final BoundValueOperationsString, String serialOps; private final StringRedisTemplate redisTemplate; //要保证所有的自增长序列不重复 private ExchangeCodeServiceImpl(StringRedisTemplate redisTemplate) { this.redisTemplate redisTemplate; this.serialOps redisTemplate.boundValueOps(COUPON_CODE_SERIAL_KEY); } Async(generateExchangeCodeExecutor) Override public void asyncGenerateCode(Coupon coupon) { //获取要生成的兑换码的数量 Integer totalNum coupon.getIssueNum(); //获取redis自增长序列 Long result serialOps.increment(totalNum); if (result null) { return; } int maxSerialNum result.intValue(); ListExchangeCode list new ArrayList(totalNum); for (int serialNum maxSerialNum - totalNum 1; serialNum maxSerialNum; serialNum) { // 2.生成兑换码 String code CodeUtil.generateCode(serialNum, coupon.getId()); ExchangeCode e new ExchangeCode(); e.setCode(code); e.setId(serialNum); e.setExchangeTargetId(coupon.getId()); e.setExpiredTime(coupon.getIssueEndTime()); list.add(e); } // 3.保存数据库 saveBatch(list); // 4.写入Redis缓存membercouponIdscore兑换码的最大序列号 redisTemplate.opsForZSet().add(COUPON_RANGE_KEY, coupon.getId().toString(), maxSerialNum); }DAY10领取优惠卷分析产品原型-接口统计和分析用户端查询发放中的优惠卷查询可以手动领取的优惠卷兑换码兑换优惠卷基于来兑换优惠卷领取优惠卷和兑换优惠卷其实都是中间表插入数据用户-某个优惠券-表结构设计