生成下单接口二维码
界面原型
打开课程支付引导界面,点击支付宝支付
按钮商户系统生成下单的二维码接口,用户扫描二维码后商户系统开始请求支付宝下单
用户扫码开始请求支付宝下单,但是在生成下单接口的二维码前前端需要做一些操作
- 前端调用学习中心服务的
添加课程的接口(收费课程只添加选课记录)
–>添加选课记录成功后前端继续调用订单服务生成二维码的接口
用户扫码支付流程
数据模型
订单表
:记录订单信息
out_business_id
: 与订单相关的业务主键,如选课记录Id
订单明细表
: 记录订单的详细信息
支付交易记录表记录每次请求微信或支付宝下单接口时支付的交易明细,关联商品订单,对于支付成功的商品订单会修改订单的状态,且下次不会再为其创建支付记录
传入商品订单号
: 当用户支付失败或因为其他原因导致该订单没有支付成功,此时再次调用第三方支付平台的下单接口就会提示订单号已存在
,对于没有支付成功的订单重新创建一个新订单是不合理的传入支付交易记录的流水号
: 用户每次发起支付请求都会创建一个新的支付交易记录,此交易记录与商品订单关联,当给订单生成支付记录前需要判断订单的支付状态,如果订单支付成功则不再生成, 避免出现用户对同一个订单重复支付的情况
订单号生成方案
生成的订单要具有: 唯一性、安全性、尽量短
等特点
-
一般场景
: 时间戳+随机数: 年月日时分秒毫秒+随机数 -
高并发场景
: 年月日时分秒毫秒+随机数+redis自增序列 -
特殊业务场景
:订单号中加上业务标识,如淘宝订单号加上业务标识方便客服处理业务需求,比如第10位是业务类型,第11位是用户类型等 -
雪花算法
: 推特内部使用的分布式环境下的唯一ID生成算法,它基于时间戳生成保证有序递增,加以入计算机硬件等元素,可以满足高并发环境下ID不重复
在base工程的utils包下创建IdWorkerUtils
基于雪花算法生成订单号
public final class IdWorkerUtils {
private static final Random RANDOM = new Random();
private static final long WORKER_ID_BITS = 5L;
private static final long DATACENTERIDBITS = 5L;
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTERIDBITS);
private static final long SEQUENCE_BITS = 12L;
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTERIDBITS;
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
private static final IdWorkerUtils ID_WORKER_UTILS = new IdWorkerUtils();
private long workerId;
private long datacenterId;
private long idepoch;
private long sequence = '0';
private long lastTimestamp = -1L;
private IdWorkerUtils() {
this(RANDOM.nextInt((int) MAX_WORKER_ID), RANDOM.nextInt((int) MAX_DATACENTER_ID), 1288834974657L);
}
private IdWorkerUtils(final long workerId, final long datacenterId, final long idepoch) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID));
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATACENTER_ID));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
this.idepoch = idepoch;
}
/**
* Gets instance.
*
* @return the instance
*/
public static IdWorkerUtils getInstance() {
return ID_WORKER_UTILS;
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - idepoch) << TIMESTAMP_LEFT_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT) | sequence;
}
private long tilNextMillis(final long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
/**
* Build part number string.
*
* @return the string
*/
public String buildPartNumber() {
return String.valueOf(ID_WORKER_UTILS.nextId());
}
/**
* Create uuid string.
*
* @return the string
*/
public String createUUID() {
return String.valueOf(ID_WORKER_UTILS.nextId());
}
public static void main(String[] args) {
System.out.println(IdWorkerUtils.getInstance().nextId());
}
}
生成订单对应二维码接口
在订单服务中定义生成订单对应的支付二维码接口,同时完成创建商品订单记录和支付交易记录
的操作
@Api(value = "订单支付接口", tags = "订单支付接口")
@RestController
@Slf4j
public class OrderController {
@Autowired
OrderService orderService;
@ApiOperation("生成支付二维码")
@PostMapping("/generatepaycode")
public PayRecordDto generatePayCode(@RequestBody AddOrderDto addOrderDto) {
SecurityUtil.XcUser user = SecurityUtil.getUser();
if (user == null) {
XueChengPlusException.cast("请登录后继续选课");
}
return orderService.createOrder(user.getId(), addOrderDto);
}
}
请求响应模型类
请求模型类
: 包含商品订单的相关信息
@Data
@ToString
public class AddOrderDto {
/**
* 总价
*/
private Float totalPrice;
/**
* 订单类型
*/
private String orderType;
/**
* 订单名称
*/
private String orderName;
/**
* 订单描述
*/
private String orderDescrip;
/**
* 订单明细json,不可为空
* [{"goodsId":"","goodsType":"","goodsName":"","goodsPrice":"","goodsDetail":""},{...}]
*/
private String orderDetail;
/**
* 外部系统业务id
*/
private String outBusinessId;
}
响应模型类
:包含商品订单对应的支付交易记录信息
和二维码信息
@Data
@ToString
public class PayRecordDto extends XcPayRecord {
//二维码
private String qrcode;
}
创建商品订单记录和交易支付记录
定义OrderService
接口及其实现类, 生成商品订单二维码前需要完成创建商品订单(同一个选课只能创建一个订单) --->创建订单的交易支付记录(支付成功的订单不再创建)-->生成订单的二维码
的操作
public interface OrderService {
/**
* 创建商品订单二维码,同时完成创建商品订单记录和支付交易记录的操作
* @param userId 用户id
* @param addOrderDto 订单信息
* @return 支付交易记录
*/
PayRecordDto createOrder(String userId, AddOrderDto addOrderDto);
}
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
XcOrdersMapper xcOrdersMapper;
@Autowired
XcPayRecordMapper xcPayRecordMapper;
@Autowired
XcOrdersGoodsMapper xcOrdersGoodsMapper;
@Value("${pay.qrcodeurl}")
String qrcodeurl;
@Override
public PayRecordDto createOrder(String userId, AddOrderDto addOrderDto) {
// 1. 创建商品订单记录
XcOrders orders = saveOrders(userId, addOrderDto);
// 2. 创建订单对应的支付交易记录
XcPayRecord payRecord = createPayRecord(orders);
// 3. 生成二维码
String qrCode = null;
try {
// 3.1 用本系统支付交易号填充占位符
qrcodeurl = String.format(qrcodeurl, payRecord.getPayNo());
// 3.2 生成二维码
qrCode = new QRCodeUtil().createQRCode(qrcodeurl, 200, 200);
} catch (IOException e) {
XueChengPlusException.cast("生成二维码出错");
}
PayRecordDto payRecordDto = new PayRecordDto();
BeanUtils.copyProperties(payRecord, payRecordDto);
payRecordDto.setQrcode(qrCode);
return payRecordDto;
}
}
创建商品订单记录和订单中包含的具体产品记录
: 订单记录的订单信息来源于对应的选课记录
,所以在订单表需要存入选课记录的ID
幂等性判断
: 同一个选课记录只能创建一个订单
@Autowired
XcOrdersMapper xcOrdersMapper;
@Autowired
XcOrdersGoodsMapper xcOrdersGoodsMapper;
/**
* 保存订单信息到订单表和订单明细表
* @param userId 用户id
* @param addOrderDto 选课信息
* @return
*/
@Transactional
public XcOrders saveOrders(String userId, AddOrderDto addOrderDto) {
// 1. 幂等性判断,同一个选课记录只能创建一个订单
XcOrders order = getOrderByBusinessId(addOrderDto.getOutBusinessId());
if (order != null) {
return order;
}
// 2. 插入订单表
order = new XcOrders();
BeanUtils.copyProperties(addOrderDto, order);
order.setId(IdWorkerUtils.getInstance().nextId());
order.setCreateDate(LocalDateTime.now());
order.setUserId(userId);
order.setStatus("600001");//未支付
order.setUserId(userId);
order.setOrderType(addOrderDto.getOrderType());// 订单类型,如60201表示购买课程
order.setOrderName(addOrderDto.getOrderName());
order.setOrderDetail(addOrderDto.getOrderDetail());
order.setOrderDescrip(addOrderDto.getOrderDescrip());
order.setOutBusinessId(addOrderDto.getOutBusinessId());// 如购买课程业务对应选课记录id
int insert = xcOrdersMapper.insert(order);
if (insert <= 0) {
XueChengPlusException.cast("插入订单记录失败");
}
// 3. 插入订单明细表,包含所属商品订单记录的Id
Long orderId = order.getId();
// 将前端传入的订单明细的json字符串转成List集合,集合元素类型是XcOrdersGoods订单产品类型
String orderDetail = addOrderDto.getOrderDetail();
List<XcOrdersGoods> xcOrdersGoodsList = JSON.parseArray(orderDetail, XcOrdersGoods.class);
xcOrdersGoodsList.forEach(goods -> {
goods.setOrderId(orderId);
int insert1 = xcOrdersGoodsMapper.insert(goods);
if (insert1 <= 0) {
XueChengPlusException.cast("插入订单明细失败");
}
});
return order;
}
//根据业务id(选课记录)查询订单
public XcOrders getOrderByBusinessId(String businessId) {
// 数据库有约束,同一个选课记录只能有一个订单
XcOrders orders = ordersMapper.selectOne(new LambdaQueryWrapper<XcOrders>().eq(XcOrders::getOutBusinessId, businessId));
return orders;
}
创建订单对应的支付交易记录
: 对于支付成功的订单不再创建交易记录
- 支付交易记录表中的
第三方支付交易流水号,第三方支付渠道编号,支付状态(默认未支付),支付成功时间
字段值在接收支付宝通知时确定
public XcPayRecord createPayRecord(XcOrders orders) {
// 订单不存在不能添加支付记录
if (orders == null) {
XueChengPlusException.cast("订单不存在");
}
// 对于支付成功的订单不再创建交易记录避免重复支付
if ("600002".equals(orders.getStatus()) {
XueChengPlusException.cast("订单已支付");
}
// 添加支付记录
XcPayRecord payRecord = new XcPayRecord();
payRecord.setPayNo(IdWorkerUtils.getInstance().nextId());// 本系统生成的支付交易记录号将来传给支付宝
payRecord.setOrderId(orders.getId());
payRecord.setOrderName(orders.getOrderName());
payRecord.setTotalPrice(orders.getTotalPrice());
payRecord.setCurrency("CNY");
payRecord.setCreateDate(LocalDateTime.now());
payRecord.setStatus("601001"); // 未支付
payRecord.setUserId(orders.getUserId());
int insert = xcPayRecordMapper.insert(payRecord);
if (insert <= 0) {
XueChengPlusException.cast("插入支付交易记录失败");
}
return payRecord;
}
生成支付二维码
: 在Nacos中dev环境下的orders-service-dev.yaml
中配置二维码的URL要求支付的时候也需要带上订单号
pay:
qrcodeurl: http://127.0.0.1:63030/orders/requestpay?payNo=%s
@Value("${pay.qrcodeurl}")
String qrcodeurl;
// 生成二维码
String qrCode = null;
try {
// 用本系统支付交易号填充占位符
qrcodeurl = String.format(qrcodeurl, payRecord.getPayNo());
// 生成二维码
qrCode = new QRCodeUtil().createQRCode(qrcodeurl, 200, 200);
} catch (IOException e) {
XueChengPlusException.cast("生成二维码出错");
}
重启所有服务,测试二维码是否可以正常生成,同时查看数据库中的选课记录表、订单表(包含选课记录Id)、订单明细表(包含商品订单Id)、支付交易记录表(包含商品订单Id)
对应记录