当前位置: 首页 > news >正文

天机学堂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 List<Long> scopes = couponFormDTO.getScopes(); //如果查不到说明前端给的数据是错的 if(CollectionUtils.isEmpty(scopes)){ throw new BadRequestException("前端提交的限定范围信息有误"); } List<CouponScope> couponScopeList = scopes.stream() .map(bizId -> new CouponScope().setBizId(bizId).setCouponId(coupon.getId())) .collect(Collectors.toList()); //2.2保存 couponScopeService.saveBatch(couponScopeList); }
注意点1

这里必须是实体类getId然后赋值,这样就是Mybatisplus存储的正确id

注意点2

name和specific两个字段和数据库内的关键字有冲突,要用@TableField起别的名字

Stream流

把一个 List<String> 或 List<Long> 类型的 scopes 集合,转换成一个 List<CouponScope> 类型的集合。


✅ 逐行拆解(超级通俗)

1.scopes.stream()

  • scopes是一个集合(比如List<Long>List<String>

  • .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对象,打包成List<CouponScope>

-分页查询优惠券

过滤条件

返回参数

id是一定会返回的,因为后续的删除,暂停等操作都需要根据id来

@Override public PageDTO<CouponPageVO> queryCouponByPage(CouponQuery couponQuery) { //1.查询 //1.1过滤条件可能为空,因此先取出来,方便判断是否为空 Integer type = couponQuery.getType(); Integer status = couponQuery.getStatus(); String name = couponQuery.getName(); Page<Coupon> 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.封装结果返回 List<Coupon> records = page.getRecords(); if(CollectionUtils.isEmpty(records)){ return PageDTO.empty(page); } List<CouponPageVO> 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.DRAFT&&coupon.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 个服务员,永远在岗,来客人就分配,没客人就待命,复用员工,不反复招人裁员


二、为什么要用线程池?

  1. 降低资源消耗:避免频繁new Thread()销毁线程,节省 CPU 内存。

  2. 提高响应速度:任务来了直接用空闲线程,不用从头创建。

  3. 可控线程数量:防止无限创建线程导致OOM、服务器崩掉

  4. 统一管理:定时任务、批量任务、异步任务都能统一调度。


三、线程池核心:ThreadPoolExecutor 七大参数(面试必考)

java

运行

public ThreadPoolExecutor( int corePoolSize, // 1.核心线程数 int maximumPoolSize, // 2.最大线程数 long keepAliveTime, // 3.空闲线程存活时间 TimeUnit unit, // 4.时间单位 BlockingQueue<Runnable> workQueue, // 5.任务阻塞队列 ThreadFactory threadFactory, // 6.线程工厂 RejectedExecutionHandler handler // 7.拒绝策略 );

逐个通俗解释

  1. corePoolSize 核心线程数常驻线程,永远不回收,哪怕闲着也留着待命。

  2. maximumPoolSize 最大线程数池子最多能有多少线程,核心线程 + 临时线程 总和上限

  3. keepAliveTime 空闲存活时间临时线程空闲多久没人用,就自动销毁。

  4. unit 时间单位秒、毫秒、分钟等。

  5. workQueue 任务队列核心线程满了,新来任务排队放队列里。常用:ArrayBlockingQueueLinkedBlockingQueue

  6. threadFactory 线程工厂给线程起名字、设置优先级,方便日志排查。

  7. handler 拒绝策略线程满了 + 队列也满了,再来任务怎么处理


四、线程池任务执行流程(背下来就能面试)

  1. 来了任务 →先看核心线程有没有空闲,有就直接用。

  2. 核心线程全忙 →放进阻塞队列排队

  3. 队列也满了 →新建临时线程处理任务。

  4. 线程总数达到最大线程数+ 队列也满 →触发拒绝策略


五、4 种内置拒绝策略

  1. AbortPolicy(默认):直接抛异常

  2. CallerRunsPolicy:谁提交任务谁自己跑(主线程执行)

  3. DiscardPolicy:直接丢掉任务,不报错

  4. 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 ServiceImpl<ExchangeCodeMapper, ExchangeCode> implements IExchangeCodeService { private final BoundValueOperations<String, 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(); List<ExchangeCode> 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缓存,member:couponId,score:兑换码的最大序列号 redisTemplate.opsForZSet().add(COUPON_RANGE_KEY, coupon.getId().toString(), maxSerialNum); }

DAY10领取优惠卷

分析产品原型

-接口统计和分析

用户端查询发放中的优惠卷:查询可以手动领取的优惠卷

兑换码兑换优惠卷:基于来兑换优惠卷

领取优惠卷和兑换优惠卷:其实都是中间表插入数据用户-某个优惠券

-表结构设计

http://www.jsqmd.com/news/763983/

相关文章:

  • 对比自行维护与通过Taotoken调用大模型API的稳定性体验
  • 2026年昆明短视频运营与AI全网推广:本地精准投流与账号代运营完全指南 - 年度推荐企业名录
  • 喜马拉雅音频本地化保存实战手册:Qt5跨平台下载工具深度解析
  • 基于Cloudflare Workers与R2构建无服务器容器镜像仓库实践
  • 无锡苏康虫害防治科技:无锡灭跳蚤公司推荐哪几家 - LYL仔仔
  • 香薰精油补充液代加工:广州欧信的全流程定制化解决方案 - 资讯焦点
  • 2026 GEO服务商深度评测:从技术原理到ROI测算,一篇读懂如何选型 - 速递信息
  • 别再死记硬背了!用Wireshark抓包实战,5分钟搞懂ARP协议和以太网帧
  • YOLO26-seg分割优化:多尺度 | 大内核和倒瓶颈设计CMUNeXt,高效提取全局上下文信息助力医学图像分割
  • NormalMap-Online:3分钟学会用浏览器生成专业级法线贴图
  • MTK BootROM 保护绕过工具深度解析与技术实现指南
  • 支付宝立减金回收渠道哪个好? - 抖抖收
  • PX4-Autopilot架构深度解析:构建高可靠无人机飞控系统的核心技术实践
  • 2026年5月更新:成都本地靠谱口碑佳、高人气装修团队精选推荐 - 成都人评鉴
  • 3步搞定ESP32开发环境:Arduino核心安装终极指南
  • 大模型数据标注:从基础认知到前沿实践的完整技术指南
  • 利用快马ai快速生成vmware虚拟机配置原型,告别手动编写脚本
  • 广东住宅小区消防维保:卓力创的专业守护方案 - 资讯焦点
  • RSSHub Radar:智能订阅发现引擎与浏览器扩展的技术实现
  • 利用Taotoken模型广场为智能客服场景选择性价比最优的大模型
  • FPGA多网卡/交换机实战:手把手教你配置AXI 1G/2.5G Ethernet Subsystem主从级联(以Kintex7四光口为例)
  • 2026年5月最新江诗丹顿官方售后网点核验报告(含迁址新开)| 实测验证报告避坑指南 - 亨得利官方服务中心
  • 权威评测:2026年5月天梭官方售后网点实地探访与深度评测报告(含迁址新开) - 亨得利官方服务中心
  • 如何快速解密RPA文件:5个简单步骤的完整指南
  • 从CTF靶场到真实运维:手把手教你用Python脚本分析Linux/Windows安全日志(附实战代码)
  • Bilibili视频下载实战指南:构建跨平台离线视频库的专业方案
  • 化妆品代加工服务商推荐 - 资讯焦点
  • 基于PySide6与AI的多平台电商智能客服系统实战
  • S32K144低功耗项目实战:如何用GPIO中断和唤醒功能设计电池供电设备
  • 2026年曲靖短视频运营与AI全网推广服务商深度横评指南 - 年度推荐企业名录