java 篇: 1.基础地基 2.设计原理 3.项目实战
产品原型分析:








执行提供的 sql 语句,新建微服务模块,copy learning 模块的 pom.xml 文件当中的 dependence 到 build 部分,和三个 yml 文件,去掉 pom.xml 当中用不到的依赖,改下 bootstrap.yml 当中的代码,mp 生成代码,这里改为这个

导入枚举相关类,替换实体类当中对应部分,主键自增改为雪花增长。前期准备工作完成。
实现新增优惠券接口:


代码实现:

资源加 s

涉及两张表记得加事务

coupon 实体类,当中字段可能和 mysql 当中关键字撞了,得加上注解,1 左边那个。怎么知道是不是关键字,就把这条 sql 语句放到 Query Console 当中,如果有颜色,就是关键字
接着在前端页面进行测试

记得开 search 服务,然后给 type 字段添加默认值,输入的优惠券名称有长度限制




实现分页查询优惠券接口:




测试:

成功

还可以按条件过滤
实现发放接口:


代码实现:

id 用该是用来拼接路径的吧,而 id 在 DTO 里面有

进行测试:
立即发放:



定时发放:


兑换码算法:

里面的字母和数字不全是因为去掉了容易混淆的字母和数字,比如大写 i 和小写 L 那才叫容易混淆 Il 还有 v v 和 w 也是 VV W。

uuid 生成的是 128 位的二进制数,展示的时候转成了 16 进制。
雪花算法生成的是 64 位二进制数,最终以十进制形式表现出来。
能不能把二进制转为上面的 32 个字母或数字呢,当然可以。32=2 的 5 次方,就是最多能表示 5 个空的二进制数。
那就将二进制数每五个分为一组,计算十进制数,作为脚标,去取对应的字母或数字。这就是所谓的 Base32,Base64 也同理,只不过是字母或数字的总数为 64 罢了。
但是长度不超过 10 字符,比特位最多 50 位,那 uuid 和雪花都不行,那我不能截吗,截了,你能保证是唯一的吗
那现在就剩下自增 id 了可以 redis 自增也可以数据库自增,但是 redis 自增,因为它是单线程的,所以 id 一定不会重复。对于数据量大,要满足 10 亿以上,2 的 32 次方足够,也就是 32 个比特位,那长度就是 6.4 字符,也满足不超过 10 字符的要求,我们只需要把自增 id 转成下面这种字符形式就可以了。



那这么多数字 BitMap 存的下么,巧了

BitMap 刚好能存这么多
这里代码部分,已经准备好了,直接导入就行,一个是自增 id 变对应的字符的,一个就是生成和校验的。


这里进行简单的单元测试


异步生成兑换码:

代码实现:
先进行线程池配置

在 CouponServiceImpl 当中添加生成兑换码的逻辑


package com.tianji.promotion.service.impl;
import com.tianji.common.constants.ErrorInfo;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.mapper.ExchangeCodeMapper;
import com.tianji.promotion.service.IExchangeCodeService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.promotion.utils.CodeUtil;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import static com.tianji.promotion.constants.PromotionConstants._COUPON_CODE_SERIAL_KEY_;
_/**_
_ * <p>_
_ * 兑换码 服务实现类_
_ * </p>_
_ *_
_ * @author 叶小鸡_
_ * @since 2026-05-01_
_ */_
@Service
public class ExchangeCodeServiceImpl extends ServiceImpl<ExchangeCodeMapper, ExchangeCode> implements IExchangeCodeService {
private BoundValueOperations<String, String> serialOps;
public ExchangeCodeServiceImpl(StringRedisTemplate redisTemplate){
this.serialOps = redisTemplate.boundValueOps(_COUPON_CODE_SERIAL_KEY_);
}
@Override
@Async("generateExchangeCodeExecutor")
public void asyncGenerateCode(Coupon coupon) {
//发放数量
Integer totalNum = coupon.getTotalNum();
//1.获取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);
}
}
记得先在启动类开启异步

具体的实现类当中这个和线程池配置当中的方法名一致


接下来具体看实现类代码:
分为三步走
1.获取 Redis 自增序列号
2.生成兑换码
3.保存数据库
涉及 Redis 操作,先定义一个常量 KEY

这里采用这种构造器注入的方法,直接绑定 KEY,避免反复传入 key 参数。


红色:得到的是 Long 类型,但是下面需要 int 类型,所以转换一下
绿色:批量操作
橙色:为啥不直接从 1 开始,因为 maxSerialNum 是 redis 自增的,它不一定都等于 totalNum
别忘了把这改成 INPUT 类型


测试:
先新增指定发放的优惠券:



这里为啥不是从 1 开始的呢,因为之前测试过了,redis 当中还有数据。
作业实现删除优惠券接口:



作业实现根据 id 查询优惠券接口:





作业实现暂停发放接口:



这里事务,感觉像是针对并发场景,目前不确定,过。
如果对你有帮助的话,请点赞,关注,收藏。热爱可抵一切!👍 ❤️ 🔥