学成-选课下单

1
2
- 免费课:选课后直接可学
- 收费课:选课 → 生成订单 → 支付成功 → 加入课程表 → 才能学习

一、选课

实现

1
2
3
4
5
6
7
8
9
10
11
12
@ApiOperation("添加选课")
@PostMapping("/choosecourse/{courseId}")
public XcChooseCourseDto addChooseCourse(@PathVariable("courseId") Long courseId) {

//获取当前登录用户
SecurityUtil.XcUser user = SecurityUtil.getUser();
String userId = user.getId();

XcChooseCourseDto xcChooseCourseDto = myCourseTablesService.addChooseCourse(userId, courseId);

return xcChooseCourseDto;
}

image-20260408193610354

并且将选课id存入数据库

二、支付

支付流程

image-20260408182238123

1.环境配置

image-20260408194110866

image-20260408194224391

image-20260408194325695

配置于nacos配置文件中即可

支付宝网关更新

1
https://openapi-sandbox.dl.alipaydev.com/gateway.do

需要到

1
src/main/java/com/xuecheng/orders/config/AlipayConfig.java

这个类下做出修改

image-20260408194456130

然后手机扫码之后返回代码,需要点击右上角浏览器打开的方式唤起客户端

2.实现

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 支付宝SDK -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>3.7.73.ALL</version>
</dependency>

<!-- 支付宝SDK依赖的日志 -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>

配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.xuecheng.orders.config;
/**
* @description 支付宝配置参数
* @author Mr.M
* @date 2022/10/20 22:45
* @version 1.0
*/
public class AlipayConfig {
// 商户appid
// public static String APPID = "";
// 私钥 pkcs8格式的
// public static String RSA_PRIVATE_KEY = "";
// 服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
public static String notify_url = "http://商户网关地址/alipay.trade.wap.pay-JAVA-UTF-8/notify_url.jsp";
// 页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址
public static String return_url = "http://商户网关地址/alipay.trade.wap.pay-JAVA-UTF-8/return_url.jsp";
// 请求网关地址
public static String URL = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
// 编码
public static String CHARSET = "UTF-8";
// 返回格式
public static String FORMAT = "json";
// 支付宝公钥
// public static String ALIPAY_PUBLIC_KEY = "";
// 日志记录目录
public static String log_path = "/log";
// RSA2
public static String SIGNTYPE = "RSA2";
}

请求接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Controller
public class PayTestController {

@Value("${pay.alipay.APP_ID}")
String APP_ID;
@Value("${pay.alipay.APP_PRIVATE_KEY}")
String APP_PRIVATE_KEY;

@Value("${pay.alipay.ALIPAY_PUBLIC_KEY}")
String ALIPAY_PUBLIC_KEY;



@RequestMapping("/alipaytest")
public void doPost(HttpServletRequest httpRequest,
HttpServletResponse httpResponse) throws ServletException, IOException, AlipayApiException {
AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, AlipayConfig.FORMAT, AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY,AlipayConfig.SIGNTYPE);
//获得初始化的AlipayClient
AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
// alipayRequest.setReturnUrl("http://domain.com/CallBack/return_url.jsp");
// alipayRequest.setNotifyUrl("http://domain.com/CallBack/notify_url.jsp");//在公共参数中设置回跳和通知地址
alipayRequest.setBizContent("{" +
" \"out_trade_no\":\"202210100010101002\"," +
" \"total_amount\":0.1," +
" \"subject\":\"Iphone6 16G\"," +
" \"product_code\":\"QUICK_WAP_WAY\"" +
" }");//填充业务参数
String form = alipayClient.pageExecute(alipayRequest).getBody(); //调用SDK生成表单
httpResponse.setContentType("text/html;charset=" + AlipayConfig.CHARSET);
httpResponse.getWriter().write(form);//直接将完整的表单html输出到页面
httpResponse.getWriter().flush();
}

}

3.支付结果回调

支付流程如下

0e324c4a-ed1d-4a58-ad23-251e81293e27

结果接收一共分为俩种

3.1、被动接收

1
2
3
@ApiOperation("接收支付结果通知")
@PostMapping("/receivenotify")
public void receivenotify(HttpServletRequest request) throws UnsupportedEncodingException, AlipayApiException {
image-20260408195813548

如上,我们自定义支付宝请求接口

image-20260408195652275

3.2、主动查询

1
2
3
4
5
@Override
public PayRecordDto queryPayResult(String payNo){
XcPayRecord payRecord = getPayRecordByPayno(payNo);
if (payRecord == null) {
XueChengPl

image-20260408200949725

主动查询如上

三、支付成功通知

订单支付成功后会发消息,消息内容大概是:

1
2
3
4
5
{
"outBusinessId": "选课记录ID",
"orderType": "60201", // 表示购买课程
"status": "支付成功"
}

其中最关键的是 outBusinessId,就是之前创建订单时传进去的选课记录ID

learning 收到消息后,靠这个 ID 找到对应的选课记录,然后开通学习资格

这个不同服务之间进行通知,通常需要保证保证一系列信息

1.MQ

项目用的是 RabbitMQ,代码里用的是 Spring AMQP(@RabbitListener

RabbitMQ 的核心模型

image-20260408201640460

消息不是直接发到队列的,必须先发给交换机,交换机再根据规则路由到队列

2.具体实现

2.1、MQ 配置

项目用的是 Nacos 配置中心,不是硬编码在代码里。

1
2
3
4
5
6
orders 服务     → bootstrap.yml → 引用 Nacos
learning 服务 → bootstrap.yml → 引用 Nacos

两边都引用了 shared-configs 里的:
rabbitmq-${spring.profiles.active}.yml
(比如 rabbitmq-dev.yaml)
  • RabbitMQ 的 host、port、用户名、密码、ack模式等参数,全部在 Nacos 的 rabbitmq-dev.yaml 里统一管理
  • 代码里只负责声明交换机/队列/消息逻辑,不写死连接参数

2.2、支付通知这条 MQ 拓扑结构

两服务各自有一个 PayNotifyConfig.java,但声明的是同一套东西

1
2
3
交换机:paynotify_exchange_fanout   类型:Fanout
队列: paynotify_queue
消息体:payresult_notify

Fanout 是广播模式,消息发到交换机后,会投递给所有绑定的队列,不需要 routingKey。

好处是:如果以后还有别的服务也关心”支付成功”这个事件,直接绑一个新队列就行,订单服务的发送代码完全不用改

1
2
3
4
5
orders 发消息

paynotify_exchange_fanout
├──→ paynotify_queue(learning 消费)
└──→ 未来可能的其他队列(比如积分服务)

两边都声明的原因是:谁先启动,谁负责把 Exchange 和 Queue 创建好,防止对方还没起来时消息找不到地方放

2.3、生产者发消息

OrderServiceImpl.javasaveAliPayStatus 方法里:

第一步:消息先落本地库

1
mqMessageService.addMessage("payresult_notify", orders.getOutBusinessId(), orders.getOrderType(), null)

消息先写入本地 mq_message,状态为待处理,还没发到 RabbitMQ

第二步:notifyPayResult 发消息

1
2
3
4
5
6
7
8
9
10
// 消息体序列化成 JSON
// 设置 MessageDeliveryMode.PERSISTENT(持久化)
// 设置 CorrelationData(messageId) 用于 Confirm 关联

rabbitTemplate.convertAndSend(
PAYNOTIFY_EXCHANGE_FANOUT, // 交换机
"", // routingKey 为空(fanout不需要)
msgObj,
correlationData
)

第三步:Confirm 回调怎么处理

1
2
3
4
5
发送成功(ack)  → mqMessageService.completed(messageId)
消息从待处理表移入历史表,标记完成

发送失败(nack) → 打日志,等待后续补偿
定时任务会扫描未完成的消息重新投递

2.4、消费者收消息

ReceivePayNotifyService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
@RabbitListener(queues = PayNotifyConfig.PAYNOTIFY_QUEUE)
public void receive(MqMessage message) {
// 只处理符合条件的消息
if (message.getMessageType().equals("payresult_notify")
&& message.getBusinessKey2().equals("60201")) { // 60201 = 购买课程

String choosecourseId = message.getBusinessKey1();

// 开通学习资格
myCourseTablesService.saveChooseCourseSuccess(choosecourseId);
}
// 业务失败会抛异常 → 消息不被标记成功消费 → 重新投递
}

注意最后一点:业务失败就抛异常,不手动 ACK,让 RabbitMQ 重新投递这条消息

3.MQ优势

3.1、生产者可靠性

问题:订单服务发消息,但RabbitMQ挂了,或者网络抖动,消息丢了怎么办?

image-20260408202016293

3.2、Broker可靠性

问题:消息发到RabbitMQ了,但RabbitMQ重启,消息没了怎么办?

image-20260408203908517

3.3、消息处理得了 消费者可靠性

image-20260408203949879

四、总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
用户点选课

learning 调 content,查课程是否收费

写入选课记录(状态:待支付 701002)

前端拿选课记录ID,调 orders 创建订单
(outBusinessId = 选课记录ID,这个很关键)

orders 生成:订单 + 支付记录 + 支付二维码

用户扫码支付(调支付宝SDK)

支付宝回调 /notify

orders 做两件事:
1. 验签(防伪造)
2. 验金额(防篡改)

更新支付记录(601002)+ 更新订单(600002)

发 MQ 消息通知 learning

learning 消费消息:
1. 把选课记录改为成功(701001)
2. 写入我的课程表

用户正式具备学习资格