当用户点击支付宝支付时,在添加选课记录后,请求订单微服务下单,生成支付二维码。

接口信息

路径地址 http://localhost:63020/orders/generatepaycode
请求方式 POST
请求参数 AddOrderDTO
返回结果 PayRecordVO

参数DTO

AddOrderDTO
/**
* 创建商品订单
*/
@Data
@ToString
public class AddOrderDTO {

/**
* 总价
*/
@NotNull(message = "价格不能为空")
private Float totalPrice;

/**
* 订单类型
*/
@NotEmpty(message = "未知订单类型")
private String orderType;

/**
* 订单名称
*/
private String orderName;
/**
* 订单描述
*/
@NotEmpty(message = "请添加订单描述")
private String orderDescrip;

/**
* 订单明细json,不可为空
* [{"goodsId":"","goodsType":"","goodsName":"","goodsPrice":"","goodsDetail":""},{...}]
*/
@NotEmpty(message = "订单明细不可为空")
private String orderDetail;

/**
* 外部系统业务id
*/
@NotEmpty(message = "请先选课")
private String outBusinessId;

}

视图VO

PayRecordVO
/**
* 支付记录dto
*/
@Data
@ToString
@EqualsAndHashCode(callSuper = true)
public class PayRecordVO extends XcPayRecord {

//二维码
private String qrcode;

}

流程分析

用户点击支付宝支付,请求二维码接口,生成订单记录,商品信息,和支付记录;携带支付记录中的支付编号返回给前端;用户使用支付宝APP扫码时,会请求二维码中的地址,其中就有支付编号。

二维码信息如下,该地址为扫码下单接口地址,即本项目的地址。由支付宝发起请求

http://mb25gx.natappfree.cc/orders/requestpay?payNo=%s

二维码工具

learning-online-base工程的pom文件中添加二维码生成的依赖:

pom.xml
<!-- 二维码生成 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>

二维码生成工具

QRCodeUtil
/**
* 二维码生成工具
*/
public class QRCodeUtil {
private final Logger log = LoggerFactory.getLogger(QRCodeUtil.class);

/**
* 生成二维码
*
* @param content 二维码对应的URL
* @param width 二维码图片宽度
* @param height 二维码图片高度
* @return base64图片
*/
public String createQRCode(String content, int width, int height) throws IOException {
String resultImage = "";
//除了尺寸,传入内容不能为空
if (!StringUtils.isEmpty(content)) {
ServletOutputStream stream = null;
ByteArrayOutputStream os = new ByteArrayOutputStream();
//二维码参数
@SuppressWarnings("rawtypes")
HashMap<EncodeHintType, Comparable> hints = new HashMap<>();
//指定字符编码为“utf-8”
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
//L M Q H四个纠错等级从低到高,指定二维码的纠错等级为M
//纠错级别越高,可以修正的错误就越多,需要的纠错码的数量也变多,相应的二维吗可储存的数据就会减少
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
//设置图片的边距
hints.put(EncodeHintType.MARGIN, 1);

try {
//zxing生成二维码核心类
QRCodeWriter writer = new QRCodeWriter();
//把输入文本按照指定规则转成二维吗
BitMatrix bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints);
//生成二维码图片流
BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
//输出流
ImageIO.write(bufferedImage, "png", os);
// 原生转码前面没有 data:image/png;base64 这些字段,返回给前端是无法被解析,所以加上前缀
resultImage = "data:image/png;base64," + EncryptUtil.encodeBase64(os.toByteArray());
return resultImage;
} catch (Exception e) {
log.error("生成二维码出错", e);
throw new RuntimeException("生成二维码出错");
} finally {
if (stream != null) {
stream.flush();
stream.close();
}
}
}
return null;
}
}

编码工具

EncryptUtil
public class EncryptUtil {
private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);

public static String encodeBase64(byte[] bytes){
String encoded = Base64.getEncoder().encodeToString(bytes);
return encoded;
}

public static byte[] decodeBase64(String str){
byte[] bytes = null;
bytes = Base64.getDecoder().decode(str);
return bytes;
}

public static String encodeUTF8StringBase64(String str){
String encoded = null;
try {
encoded = Base64.getEncoder().encodeToString(str.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
logger.warn("不支持的编码格式",e);
}
return encoded;

}

public static String decodeUTF8StringBase64(String str){
String decoded = null;
byte[] bytes = Base64.getDecoder().decode(str);
try {
decoded = new String(bytes,"utf-8");
}catch(UnsupportedEncodingException e){
logger.warn("不支持的编码格式",e);
}
return decoded;
}

public static String encodeURL(String url) {
String encoded = null;
try {
encoded = URLEncoder.encode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLEncode失败", e);
}
return encoded;
}


public static String decodeURL(String url) {
String decoded = null;
try {
decoded = URLDecoder.decode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLDecode失败", e);
}
return decoded;
}

public static void main(String [] args){
String str = "abcd{'a':'b'}";
String encoded = EncryptUtil.encodeUTF8StringBase64(str);
String decoded = EncryptUtil.decodeUTF8StringBase64(encoded);
System.out.println(str);
System.out.println(encoded);
System.out.println(decoded);

String url = "== wo";
String urlEncoded = EncryptUtil.encodeURL(url);
String urlDecoded = EncryptUtil.decodeURL(urlEncoded);

System.out.println(url);
System.out.println(urlEncoded);
System.out.println(urlDecoded);
}
}

创建订单Service

com.swx.orders.service包下的 OrderService 中定义创建订单方法:

XcOrdersService
/**
* 订单相关Service
*/
public interface OrderService {
/**
* 创建订单
* 新增订单信息,新增支付记录,生成支付二维码
*
* @param userId 用户id
* @param dto 订单信息
* @return 二维码
*/
public PayRecordVO createOrder(String userId, AddOrderDTO dto);
}

在实现类中实现该方法

OrderServiceImpl
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

private final XcOrdersService xcOrdersService;
private final XcPayRecordService xcPayRecordService;

@Value("${pay.qrcodeUrl}")
private String qrcodeUrl;

public OrderServiceImpl(XcOrdersService xcOrdersService, XcPayRecordService xcPayRecordService) {
this.xcOrdersService = xcOrdersService;
this.xcPayRecordService = xcPayRecordService;
}

/**
* 创建订单
* 新增订单信息,新增支付记录,生成支付二维码
*
* @param userId 用户id
* @param dto 订单信息
* @return 二维码
*/
@Override
@Transactional(rollbackFor = Exception.class)
public PayRecordVO createOrder(String userId, AddOrderDTO dto) {
// 新增订单信息
XcOrders orders = xcOrdersService.saveXcOrders(userId, dto);

if (orders.getStatus().equals("601002")) {
// 已支付
throw new BizException("订单已支付");
}
// 新增支付记录
XcPayRecord payRecord = xcPayRecordService.createPayRecord(orders);

String url = String.format(qrcodeUrl, payRecord.getPayNo());
QRCodeUtil qrCodeUtil = new QRCodeUtil();
// 二维码图片
String qrCode = null;
try {
qrCode = qrCodeUtil.createQRCode(url, 200, 200);
} catch (IOException e) {
throw new BizException("生成二维码出错");
}

PayRecordVO payRecordVO = new PayRecordVO();
BeanUtils.copyProperties(payRecord, payRecordVO);
payRecordVO.setQrcode(qrCode);
return payRecordVO;
}
}

新增订单Service

首先根据用户ID和选课ID查询订单信息,确保一个用户只能对一个选课创建一个订单,然后根据参数组装 XcOrders 实体类,使用雪花算法生成订单号,并将数据保存到数据库中。

com.swx.orders.service包下的 XcOrdersService 中定义新增订单方法:

XcOrdersService
public interface XcOrdersService extends IService<XcOrders> {

/**
* 保存订单信息
*
* @param userId 用户ID
* @param dto 订单信息
*/
XcOrders saveXcOrders(String userId, AddOrderDTO dto);
}

在实现类中实现该方法

XcOrdersServiceImpl
@Service
public class XcOrdersServiceImpl extends ServiceImpl<XcOrdersMapper, XcOrders> implements XcOrdersService {

private final XcOrdersGoodsService xcOrdersGoodsService;

public XcOrdersServiceImpl(XcOrdersGoodsService xcOrdersGoodsService) {
this.xcOrdersGoodsService = xcOrdersGoodsService;
}

/**
* 保存订单信息
*
* @param userId 用户ID
* @param dto 订单信息
*/
@Override
public XcOrders saveXcOrders(String userId, AddOrderDTO dto) {
// 进行幂等性判断,同一个选课记录只能有一个订单
XcOrders order = getOneByBizId(userId, dto.getOutBusinessId());
if (order != null) {
return order;
}

// 插入订单表
order = new XcOrders();
// 使用雪花算法生成订单号
BeanUtils.copyProperties(dto, order);
order.setId(IdWorkerUtil.getInstance().nextId());
order.setCreateDate(LocalDateTime.now());
order.setStatus("600001"); // 未支付
order.setUserId(userId);
order.setOrderType("60201"); // 业务订单类型,购买课程
boolean save = save(order);
if (!save) {
throw new BizException("添加订单失败");
}

Long orderId = order.getId();
String orderDetailJson = dto.getOrderDetail();
List<XcOrdersGoods> xcOrdersGoods = JSON.parseArray(orderDetailJson, XcOrdersGoods.class);
for (XcOrdersGoods xcOrdersGood : xcOrdersGoods) {
xcOrdersGood.setOrderId(orderId);
}
boolean saveGoods = xcOrdersGoodsService.saveBatch(xcOrdersGoods);
return order;
}

/**
* 根据业务ID查询一条记录
*
* @param userId 用户ID
* @param bizId 业务ID
* @return com.swx.orders.model.po.XcOrders 订单
*/
public XcOrders getOneByBizId(String userId, String bizId) {
return getOne(Wrappers.<XcOrders>lambdaQuery().eq(XcOrders::getUserId, userId).eq(XcOrders::getOutBusinessId, bizId));
}
}

新增支付记录Service

创建支付记录,此时的支付状态为:未支付。

com.swx.orders.service包下的 XcPayRecordService 中定义新增订单方法:

XcPayRecordService
public interface XcPayRecordService extends IService<XcPayRecord> {

/**
* 创建支付记录
*
* @param orders 订单
*/
XcPayRecord createPayRecord(XcOrders orders);
}

在实现类中实现该方法

XcPayRecordServiceImpl
/**
* <p>
* 服务实现类
* </p>
*
* @author sw-code
* @since 2023-09-04
*/
@Service
public class XcPayRecordServiceImpl extends ServiceImpl<XcPayRecordMapper, XcPayRecord> implements XcPayRecordService {

/**
* 创建支付记录
*
* @param orders 订单
*/
@Override
public XcPayRecord createPayRecord(XcOrders orders) {
XcPayRecord xcPayRecord = new XcPayRecord();
xcPayRecord.setPayNo(IdWorkerUtil.getInstance().nextId());
xcPayRecord.setOrderId(orders.getId());
xcPayRecord.setOrderName(orders.getOrderName());
xcPayRecord.setTotalPrice(orders.getTotalPrice());
xcPayRecord.setCurrency("CNY");
xcPayRecord.setCreateDate(LocalDateTime.now());
xcPayRecord.setStatus("601001"); // 未支付
xcPayRecord.setUserId(orders.getUserId());
boolean save = save(xcPayRecord);
if (!save) {
throw new BizException("添加支付记录失败");
}
return xcPayRecord;
}
}

创建订单Controller

@Api(value = "订单支付接口", tags = "订单支付接口")
@RestController
public class OrderController {
private final OrderService orderService;

public OrderController(OrderService orderService) {
this.orderService = orderService;
}

@ApiOperation("生成支付二维码")
@PostMapping("/generatepaycode")
public PayRecordVO generatePayCode(@RequestBody AddOrderDTO dto) {
SecurityUtil.XcUser user = SecurityUtil.getUser();
String userId = user.getId();
return orderService.createOrder(userId, dto);
}
}