学成在线 第06章 讲义-学生选课
1. 学生选课
在学生选课中,需要学员通过学成在线门户网站实现,查看学生的课程购买记录、学生购买课程的下单、学生支付收费课程、分布式任务调度查询支付结果等操作。本次主要是对学成的学生群体来开发对应的功能。
1.1 需求分析
本次主要针对学生学科的业务操作,主要功能包括:
●学生进入首页或课程搜索找到目标课程,进入某个课程的详情页,需要判断是否收费,如果收费需要查询改学员对此课程的购买记录。
●课程收费的情况下,学生对此课程会创建出购买课程的订单,完成课程购买的操作。
●学生需要进行支付,来完成对课程的购买。
●学生可以在学习中心中查看自己所有的所学的课程,包括收费的课程和免费课程。
1.1.1 业务流程
学生选课流程如下:
1.通过首页或课程搜索找到目标课程,进入课程详情页.
●免费课程的显示

●收费课程的显示

2.点击“课程价格”按钮,可进入提交订单页面

3.确认订单无误,并进行课程支付成功后,跳转到课程详情页面,同时按钮变为”马上学习”,若已包含学习进度,该按钮显示为“继续学习”。


4.学生可在个人中心-我的课程中浏览已选课程信息

学生选课流程图如下:
●学生选课流程图

1.2 获取支付结果
在用户进行完订单支付后,由学成在线后端微服务向第三方支付系统平台查询用户支付的结果信息。如果支付完成,我们就要修改订单表中的业务数据。如果没有支付完,则不做操作。
1.2.1 系统交互流程
根据微信流程支付完成的交互流程图,学成可以对其 实现解决方案来实现:

●学成获得支付结果交互流程图

分析操作:
0.搭建分布式事务Seata环境
0.1 搭建Seata的TC服务并注册到Nacos中
0.2 将参与到服务添加Seata依赖并注册到TC服务中
1.微信支付成功后的回调
1.1 微信通知里的参数
1.2 如何解析微信通知数据(Github工具包—对WX的封装)
1.3 使用 RequestMapping 来获得数据(不知道微信的请求方式)
1.4 如何来告诉微信回调Controller地址
在统一下单时已经对WxPayConfig NotifyURL在Nacos已经配置
配置内网穿透的地址
1.5 外网的请求如何来调用局域网的接口地址
内网穿透(cpolar)
2.业务层操作:PayService(订单服务)
2.1.处理通知结果方法
解析通知结果
发送事务消息
2.2 处理本地事务
修改订单和订单支付记录的状态
3.业务层操作:学习记录(学习中心服务)
3.1 处理消息并执行本地方法
3.2 处理本地事务
创建用户的学习记录
判断课程的学习模式来给paid赋值
Seata 的 at 模式

1.搭建TC(事务协调器)环境
搭建TC数据源环境(3张表)
TC服务在Docker环境中启动
TC注册到Nacos
2.搭建服务RM环境
各个服务导入Seata依赖
添加Seata配置
数据库添加快照表
1.2.2 构建Seata服务
1.2.2.1 构建Seata 容器环境
在 Centos7 中docker容器中完成下面操作:
1.下拉容器和创建容器
#下拉镜像docker pull seataio/seata-server:1.4.2#创建容器docker run \-e SEATA_IP=192.168.94.129 \-e SEATA_PORT=8091 \--name seata-server \-p 8091:8091 \-d \seataio/seata-server:1.4.2#创建容器后可以进入到容器中docker exec -it seata-server sh#查看运行的日志docker logs -f seata-server
2.创建Seata的数据库和表
在mysql服务中添加数据库,导入今天 “资料/seata/seata.sql” 文件到数据库中,导入后的结果:

3.配置seata的tc配置信息
在nacos中namespace下创建配置信息:seataServer.properties
# 数据存储方式,db代表数据库store.mode=dbstore.db.datasource=druidstore.db.dbType=mysqlstore.db.driverClassName=com.mysql.jdbc.Driverstore.db.url=jdbc:mysql://192.168.94.129:3306/seata?useUnicode=true&rewriteBatchedStatements=truestore.db.user=rootstore.db.password=rootstore.db.minConn=5store.db.maxConn=30store.db.globalTable=global_tablestore.db.branchTable=branch_tablestore.db.queryLimit=100store.db.lockTable=lock_tablestore.db.maxWait=5000# 事务、日志等配置server.recovery.committingRetryPeriod=1000server.recovery.asynCommittingRetryPeriod=1000server.recovery.rollbackingRetryPeriod=1000server.recovery.timeoutRetryPeriod=1000server.maxCommitRetryTimeout=-1server.maxRollbackRetryTimeout=-1server.rollbackRetryTimeoutUnlockEnable=falseserver.undo.logSaveDays=7server.undo.logDeletePeriod=86400000# 客户端与服务端传输方式transport.serialization=seatatransport.compressor=none# 关闭metrics功能,提高性能metrics.enabled=falsemetrics.registryType=compactmetrics.exporterList=prometheusmetrics.exporterPrometheusPort=9898
将导入今天 “资料/seata/registry.conf” 导入到Centos环境中,并执行下面的操作:
#将配置文件拷贝到seata容器中docker cp registry.conf seata-server:/seata-server/resources#退出容器exit#重新启动seata服务docker restart seata-server
PS:一定要修改registry.conf 配置信息
nacos中的 id地址、组名、namespace
重新启动后,seata-server 的tc服务已经注册到 nacos,并检查信息。

4.通过Docker日志命令查看是否运行成功

1.2.2.4 服务注册到TC服务中
1.微服务注册到 TC 服务中
在 order-service 和 learning-service 服务中的pom文件中添加seata的依赖,并添加配置。
<!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><!--版本较低,1.3.0,因此排除--><exclusion><artifactId>seata-spring-boot-starter</artifactId><groupId>io.seata</groupId></exclusion></exclusions></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><!--seata starter 采用1.4.2版本--><version>1.4.2</version></dependency>
Seata中模式较为多,如下:
XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
TCC模式:最终一致的分阶段事务模式,有业务侵入
SAGA模式:长事务模式,有业务侵入
处于本项目的业务操作,AT模式完全满足本次操作,AT模式如下:

在 order-service 和 learning-service 服务中的bootstrap.yml文件中添加seata配置,如下。
seata:registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址type: nacos # 注册中心类型 nacosnacos:server-addr: 192.168.94.129:8848 # nacos地址namespace: 2393d63b-3ef1-4764-ad32-767f8fea31e3 # namespace,默认为空group: ${group.name} # 分组,默认是DEFAULT_GROUPapplication: seata-tc-server # seata服务名称username: nacospassword: nacostx-service-group: seata-xc # 事务组名称service:vgroup-mapping: # 事务组与cluster的映射关系seata-xc: SHdata-source-proxy-mode: AT
2.业务服务添加undo-log表
在 xc_order 和 xc_learning 数据添加 undo 表,sql文件如下:
DROP TABLE IF EXISTS `undo_log`;CREATE TABLE `undo_log` (`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',`rollback_info` longblob NOT NULL COMMENT 'rollback info',`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',`log_created` datetime(6) NOT NULL COMMENT 'create datetime',`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
1.2.3 添加支付后课程学习记录
在订单支付成功后,需要在学习中心服务添加用户的学习记录,在 xc-learing-service 中定义服务接口,如下:
在xc-api工程下 com.xuecheng.api.learning 添加接口信息:
1.2.3.1 接口定义
根据支付平台通知信息来定义接口
Http接口地址
post /learning/l/course-record/paid/{username}/{coursePubId}
1.接口参数列表
接口传入传出列表
传入参数:
Path方式:
username String 购课人的名称
coursePubId Long 课程发布id
传出参数:
无数据
在xc-api工程下的com.xuecheng.api.learning.CourseRecordApi 下定义方法
/*** <p></p>** @Description:*/@Api(value = "学习记录api文档",tags = "学习记录Api接口文档信息")public interface CourseRecordApi {@ApiOperation("支付后创建用户学习记录")@ApiImplicitParams({@ApiImplicitParam(name = "username",value = "购课人账号",required = true,dataType = "String",paramType = "path"),@ApiImplicitParam(name = "coursePubId",value = "课程发布Id",required = true,dataType = "Long",paramType = "path")})void createCourseRecord4s(String username, Long coursePubId);}
1.2.3.2 接口开发
在 xc-learning-service 下的 CourseRecordService 业务类中实现
/*** <p>* 选课记录 服务类* </p>*/public interface CourseRecordService extends IService<CourseRecord> {/*** 订单支付后创建用户学习记录* @param courseRecordDTO* @return*/void createCourseRecord4S(String username, Long coursePubId);}
其实现类 com.xuecheng.learning.service.impl.CourseRecordServiceImpl 实现代码如下:
/** 业务分析:* 1.判断关键数据* username coursePubId* 2.判断业务数据* 课程发布信息* 判断是否存在* 课程计划数据获得* 从课程发布信息中** 3.保存用户的学习记录* 由于是先执行订单服务修改订单状态,在没有问题的情况下再创建学习记录* 创建学习记录* 默认章节为:第一章第一小节* 要给paid赋值为已经支付:1** 4.将新增的数据查询并转化为dto并返回** PS:* 如果出现业务上出操作数据问题,本业务层直接抛出异常,无需返回RestResponse规范接口数据原因:本次的操作事务都是由Seata环境来控制,如果抛出异常,会是的整个的事务进行回滚操作** */@Transactionalpublic RestResponse<CourseRecordDTO> createCourseRecord4S(String username, Long coursePubId) {//1.判断关键数据// username coursePubId////if (StringUtil.isBlank(username)||ObjectUtils.isEmpty(coursePubId)) {ExceptionCast.cast(CommonErrorCode.E_100101);}// 2.判断业务数据// 课程发布信息// 判断是否存在RestResponse<CoursePubIndexDTO> pubIndexResponse = coursePubSearchApi.getCoursePubIndexById4s(coursePubId);if (!(pubIndexResponse.isSuccessful())) {ExceptionCast.castWithCodeAndDesc(pubIndexResponse.getCode(),pubIndexResponse.getMsg());}CoursePubIndexDTO coursePub = pubIndexResponse.getResult();// 课程计划数据获得// 从课程发布信息中String teachplanJsonString = coursePub.getTeachplan();TeachplanDTO teachplanDTO = JsonUtil.jsonToObject(teachplanJsonString, TeachplanDTO.class);// 获得第一章节List<TeachplanDTO> secPlanTreeNodes = teachplanDTO.getTeachPlanTreeNodes();if (CollectionUtils.isEmpty(secPlanTreeNodes)) {ExceptionCast.cast(LearningErrorCode.E_202205);}TeachplanDTO secTeachplan = secPlanTreeNodes.get(0);if (ObjectUtils.isEmpty(secTeachplan)) {ExceptionCast.cast(LearningErrorCode.E_202205);}// 获得三级课程计划数据List<TeachplanDTO> thridTeachplans = secTeachplan.getTeachPlanTreeNodes();if (CollectionUtils.isEmpty(thridTeachplans)) {ExceptionCast.cast(LearningErrorCode.E_202206);}TeachplanDTO thridTeachplan = thridTeachplans.get(0);if (ObjectUtils.isEmpty(thridTeachplan)) {ExceptionCast.cast(LearningErrorCode.E_202206);}// 3.保存用户的学习记录// 如果学员已经有学习记录--收费课程支付后,要重置数据(用户体验不好,但是可以解决大部分的问题)LambdaQueryWrapper<CourseRecord> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(CourseRecord::getUserName, username);queryWrapper.eq(CourseRecord::getCoursePubId, coursePubId);CourseRecord courseRecord = this.getOne(queryWrapper);boolean result = false;if (ObjectUtils.isEmpty(courseRecord)) {// 如果已经支付--判断用户是否已经支付// 无需操作-订单支付后的操作,由于是先执行订单服务修改订单状态,在没有问题的情况下再创建学习记录// 创建学习记录// 默认章节为:第一章第一小节// 要给paid赋值为已经支付:1courseRecord = new CourseRecord();courseRecord.setUserName(username);courseRecord.setCompanyId(coursePub.getCompanyId());courseRecord.setCourseId(coursePub.getCourseId());courseRecord.setCoursePubId(coursePub.getIndexId());courseRecord.setCoursePubName(coursePub.getName());courseRecord.setTeachmode(coursePub.getTeachmode());courseRecord.setTeachplanId(thridTeachplan.getTeachPlanId());courseRecord.setTeachplanName(thridTeachplan.getPname());courseRecord.setPaid(new Integer(PayCodeUrlResult.PAIED));result = this.save(courseRecord);} else {//要重置数据(用户体验不好,但是可以解决大部分的问题)LambdaUpdateWrapper<CourseRecord> updateWrapper = new LambdaUpdateWrapper<>();updateWrapper.set(CourseRecord::getTeachplanId, thridTeachplan.getTeachPlanId());updateWrapper.set(CourseRecord::getTeachplanName, thridTeachplan.getPname());updateWrapper.set(CourseRecord::getPaid,new Integer(PayCodeUrlResult.PAIED) );updateWrapper.set(CourseRecord::getChangeDate, LocalDateTime.now());updateWrapper.eq(CourseRecord::getId,courseRecord.getId() );result = this.update(updateWrapper);}if (!result) {ExceptionCast.cast(LearningErrorCode.E_202204);}// 4.将新增的数据查询并转化为dto并返回CourseRecord po = this.getById(courseRecord.getId());CourseRecordDTO resultDTO = CourseRecordConvert.INSTANCE.entity2dto(po);return RestResponse.success(resultDTO);}
Controller层实现
package com.xuecheng.learning.controller;import com.xuecheng.api.learning.CourseRecordApi;import com.xuecheng.api.learning.model.dto.CourseRecordDTO;import com.xuecheng.learning.common.utils.UAASecurityUtil;import com.xuecheng.learning.service.CourseRecordService;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;/*** <p>* 选课记录 前端控制器* </p>** @author itcast*/@Slf4j@RestControllerpublic class CourseRecordController implements CourseRecordApi {@Autowiredprivate CourseRecordService courseRecordService;@PostMapping("l/course-record/paid/{username}/{coursePubId}")public RestResponse<CourseRecordDTO> createCourseRecord4S(@PathVariable String username,@PathVariable Long coursePubId) {return courseRecordService.createCourseRecord4S(username, coursePubId);}}
1.2.4 支付通知功能
本功能将实现第三方支付平台通知学成在线订单服务接口支付成功消息。
1.2.4.1 支付通知功能实现接口定义
根据支付平台通知信息来定义接口
Http接口地址
无请求方式 /order/order-pay/wx-pay/notify-result
1.接口参数列表
接口传入传出列表
根据微信平台 支付通知接口 ,接口接收和响应的参数都为xml格式,再次可以使用相关依赖包将其进行封装处理,如下:
●传入传出参数依赖包封装Api: 代码中Result为自定义返回对象,WxPayUnifiedOrderRequest 中还有一些参数会根据配置文件的配置自动填充,不需要单独设置,例如:appId、partnerId、notifyURL、tradeType等参数。 订单创建成功之后会自动调用校验方法,校验微信返回的结果。例如校验签名,校验返回的业务代码是否正常。所以代码中不需要手动校验订单创建结果,只需要处理下异常情况即可!
@ResponseBody@RequestMapping("接口地址(需要开发者自行定义)")public String payNotify(HttpServletRequest request, HttpServletResponse response) {try {String xmlResult = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());WxPayOrderNotifyResult result = wxPayService.parseOrderNotifyResult(xmlResult);// 加入自己处理订单的业务逻辑,需要判断订单是否已经支付过,否则可能会重复调用String orderId = result.getOutTradeNo();String tradeNo = result.getTransactionId();String totalFee = BaseWxPayResult.fenToYuan(result.getTotalFee());return WxPayNotifyResponse.success("处理成功!");} catch (Exception e) {log.error("微信回调结果异常,异常原因{}", e.getMessage());return WxPayNotifyResponse.fail(e.getMessage());}}
参考文档连接:https://github.com/Wechat-Group/WxJava/wiki/微信支付
2. 接口编写
在 xc-order-service 工程的 OrderController 定义接口:
/*** <p>* 订单 前端控制器* </p>** @author itcast*/@RestControllerpublic class OrdersController implements OrderApi {@Autowiredprivate OrdersService ordersService;@Autowiredprivate PayService payService;// 其他代码省略/* 给wx支付平台来调用的方法,没有在Api接口中定义方法 */@RequestMapping("order-pay/wx-pay/notify-result")public String notifyPayment(HttpServletRequest request) {System.out.println("订单支付通知方法开始执行");try {String xmlResult = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());// 加入自己处理订单的业务逻辑,需要判断订单是否已经支付过,否则可能会重复调用// 调用service层处理业务payService.notifyPayment(xmlResult);return WxPayNotifyResponse.success("处理成功!");} catch (Exception e) {log.error("微信回调结果异常,异常原因{}", e.getMessage());return WxPayNotifyResponse.fail(e.getMessage());}}}
PS:由于此接口是给第三方支付系统来使用,并不是在给前端来使用,所有无需在 OrderApi 接口中定义方法。
1.2.4.2 支付通知功能实现接口实现
在支付通知实现中,需要根据流程图完成下面的件事:
1将第三方支付结果数据进行解析
2修改订单服务中订单状态和订单支付状态
3调用学习中心添加用户收费课程的学习记录
4返回消息给支付平台
下面就来通过 PayService 来实现此功能。
0.编写关联Feign接口调用查询
在 xc-order-service 服务添加 Feign 接口
package com.xuecheng.order.agent;import com.xuecheng.api.search.model.dto.CoursePubIndexDTO;import com.xuecheng.common.constant.XcFeignServiceNameList;import com.xuecheng.common.domain.response.RestResponse;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;/*** <p></p>** @Description:*/@FeignClient(XcFeignServiceNameList.XC_SEARCH_SERVICE)public interface CoursePubIndexApiAgent {@GetMapping("/search/l/course-index/{coursePubId}")RestResponse<CoursePubIndexDTO> getCoursePubIndexById4s(@PathVariableLong coursePubId);}
package com.xuecheng.order.agent;import com.xuecheng.api.learning.model.dto.CourseRecordDTO;import com.xuecheng.common.constant.XcFeignServiceNameList;import com.xuecheng.common.domain.response.RestResponse;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;/*** <p></p>** @Description:*/@FeignClient(value = XcFeignServiceNameList.XC_LEARNING_SERVICE)public interface LearningApiAgent {String PREFIX_FLAG = "/learning/l/";@PostMapping(PREFIX_FLAG+"course-record/paid/{username}/{coursePubId}")RestResponse<CourseRecordDTO> createCourseRecord4S(@PathVariable String username, @PathVariable Long coursePubId);}
1.编写业务处理实现
在 xc-order-service 工程的 PayService 声明下面接口。
package com.xuecheng.order.service;import com.baomidou.mybatisplus.extension.service.IService;import com.xuecheng.api.order.model.PayResultModel;import com.xuecheng.order.entity.Pay;import java.util.Map;/*** <p>* 订单支付信息 服务类* </p>*/public interface PayService extends IService<Pay> {/*** 处理 WX 支付平台支付结果消息* @param xmlResult*/String notifyPayResult(String xmlResult) throws Exception;}
实现类:
package com.xuecheng.order.service.impl;/*** <p>* 订单支付信息 服务实现类* </p>** @author itcast*/@Slf4j@Servicepublic class PayServiceImpl extends ServiceImpl<PayMapper, Pay> implements PayService {@Autowiredprivate WxPayService wxPayService;@Autowiredprivate OrdersService ordersService;@Autowiredprivate LearningApiAgent learningApiAgent;/** 业务操作* 1.判断并解析wx通知内容* 保证wx支付是成功:returncode和resultcode为SUCCESS* 如果支付失败,无需完成订单服务的业务逻辑操作* 2.修改订单状态** 完成消息幂等(wx支付平台会重复发消息)* 判断订单是否已经支付* 通过订单编号:out_trade_no** PS:如果业务数据操作失败,需要让wx支付平台继续通知,继续通知的方式是业务层对外抛出异常** 订单数据:status* 判断订单是否存在* 判断订单的状态:支付前状态必须初始态* 修改内容:* status* 订单支付数据:支付后的结果* 判断订单支付是否存在* 判断订单支付的状态:status** 修改内容:* status* pay_number* pay_date* receipt_amount* buyer_pay_amount* pay_response** 3.给用户支付后的内容创建一个默认的学习记录* 在学习中心完成:Feign远程调用*** 4.返回通知结果-wx平台* 如果正常,执行代码就可以* 如果数据不正常,业务需要抛出异常* controller会针对service的执行结果来返回数据给wx支付平台* */@Transactionalpublic void notifyPayment(String xmlResultString) {// 1.判断并解析wx通知内容// 保证wx支付是成功:returncode和resultcode为SUCCESS// 如果支付失败,无需完成订单服务的业务逻辑操作if (StringUtil.isBlank(xmlResultString)) {ExceptionCast.cast(CommonErrorCode.E_100101);}WxPayOrderNotifyResult notifyResult = null;try {notifyResult = wxPayService.parseOrderNotifyResult(xmlResultString);} catch (WxPayException e) {log.error(OrderErrorCode.E_160018.getDesc()+" : errMsg {}",e.getMessage());ExceptionCast.cast(OrderErrorCode.E_160018);}String returnCode = notifyResult.getReturnCode();String resultCode = notifyResult.getResultCode();String outTradeNo = notifyResult.getOutTradeNo();// 支付通知成功if (PayCodeUrlResult.WX_PAY_SUCCESS_FLAG.equalsIgnoreCase(returnCode)&&PayCodeUrlResult.WX_PAY_SUCCESS_FLAG.equalsIgnoreCase(resultCode)) {// 2.修改订单状态// 完成消息幂等(wx支付平台会重复发消息)// 判断订单是否已经支付// 通过订单编号:out_trade_noLambdaQueryWrapper<Orders> ordersQueryWrapper = new LambdaQueryWrapper<>();// 创建订单没有支付的查询条件ordersQueryWrapper.eq(Orders::getOrderNo, outTradeNo);ordersQueryWrapper.eq(Orders::getStatus,new Integer(OrderDealStatusEnum.ORDER_DEAL_INIT_STATUS.getCode()));int count = ordersService.count(ordersQueryWrapper);if (count < 1) {log.error("订单信息不是初始态,订单编号:{}", outTradeNo);return;}// 订单数据:status// 判断订单是否存在// 判断订单的状态:支付前状态必须初始态// 修改内容:// statusLambdaUpdateWrapper<Orders> ordersUpdateWrapper = new LambdaUpdateWrapper<>();ordersUpdateWrapper.set(Orders::getStatus, new Integer(OrderDealStatusEnum.ORDER_DEAL_PAID_STATUS.getCode()));ordersUpdateWrapper.set(Orders::getChangeDate, LocalDateTime.now());ordersUpdateWrapper.eq(Orders::getOrderNo,outTradeNo);boolean orderResult = ordersService.update(ordersUpdateWrapper);// 如果业务数据操作失败,需要让wx支付平台继续通知,继续通知的方式是业务层对外抛出异常if (!orderResult) {ExceptionCast.cast(OrderErrorCode.E_160015);}// 订单支付数据:支付后的结果// 判断订单支付是否存在// 判断订单支付的状态:status 0// 修改内容:// status// pay_number// pay_date// receipt_amount// buyer_pay_amount// pay_responseordersQueryWrapper = new LambdaQueryWrapper<>();ordersQueryWrapper.eq(Orders::getOrderNo, outTradeNo);Orders orders = ordersService.getOne(ordersQueryWrapper);LambdaQueryWrapper<Pay> payQueryWrapper = new LambdaQueryWrapper<>();payQueryWrapper.eq(Pay::getOrderId,orders.getId());payQueryWrapper.eq(Pay::getStatus,PayCodeUrlResult.NOT_PAY);Pay pay = this.getOne(payQueryWrapper);if (ObjectUtils.isEmpty(pay)) {log.error("订单支付信息不是未支付,订单编号:{}", outTradeNo);return;}pay.setStatus(PayCodeUrlResult.PAIED);// 记录wx的支付编号pay.setPayNumber(notifyResult.getTransactionId());// 记录wx支付平台的用户支付时间String timeEnd = notifyResult.getTimeEnd();DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");LocalDateTime payTime = LocalDateTime.parse(timeEnd, dateTimeFormatter);pay.setPayDate(payTime);Integer settlementTotalFee = notifyResult.getSettlementTotalFee();if (!(ObjectUtils.isEmpty(settlementTotalFee))) {String settlementTotalFeeString = BaseWxPayResult.fenToYuan(settlementTotalFee);pay.setReceiptAmount(new BigDecimal(settlementTotalFeeString));}Integer cashFee = notifyResult.getCashFee();if (!(ObjectUtils.isEmpty(cashFee))) {String cashFeeString = BaseWxPayResult.fenToYuan(cashFee);pay.setBuyerPayAmount(new BigDecimal(cashFeeString));}pay.setPayResponse(xmlResultString);boolean payResult = this.updateById(pay);if (!payResult) {ExceptionCast.cast(OrderErrorCode.E_160017);}// 3.给用户支付后的内容创建一个默认的学习记录// 在学习中心完成:Feign远程调用learningApiAgent.createCourseRecord4S(orders.getUserName(),orders.getCoursePubId());// 支付通知失败} else {log.error("支付通过内容失败,订单编号:{}", outTradeNo);}}}
1.2.4.3 支付通知接口内网穿透
1.内网和外网的介绍
现如今的网络环境是分为内网和外网,下面我们来简单来说明下它们的区别:
内网:通俗的说就是局域网, 内网的计算机以NAT(Network Address Translation )网络地址转换协议,通过一个公共的网关访问Internet。内网的计算机可向Internet上的其他计算机发送连接请求,但Internet上其他的计算机无法向内网的计算机发送连接请求。
外网:通俗的说就是与因特网相通的WAN(Wide Area Network )广域网,外网的计算机和Internet上的其他计算机可随意互相访问。
内网和外网示意图:

根据网络环境的特点得出下面的结果:
1.内网可以访问外网的服务吗?
可以通过 modem 调制解调器来访问互联网服务上的资源信息。
2.外网可以访问内网的终端吗?
互联网中通过 WAN 来相互访问,WAN 网无法通过直接访问 NAT 协议下的内网。
3.学成测试开发是在内网环境下和还是外网环境下开发?
学成是在内网环境下进行开发。
4.第三方支付平台可以调用学成测试环境下的支付通知接口吗?
不能,第三方支付平台是在外网环境下,无法直接访问内网环境下的学成支付通知接口路径地址。
2.外网调用支付通知接口解决方案
大体有两种解决方案:
1.购买第三方云服务
第三方的云服务是具有内网和外网两个地址,支付平台可以通过云服务的外网地址来访问云服务上的接口。
2.使用内网穿透工具
在项目开发阶段,可以使用内网穿透工具使得外网服务间接调用内网环境的接口。
针对上面两个方案的对比:
1.云服务:云服务需要进行购买,学成环境要求云服务的配置加高,开发阶段成本较大,所以开发阶段不会考虑。项目正式上线会购买云服务,并将项目部署到云服务环境中。
2.内网穿透工具:几乎为零成本,只需要本机可以访问互联网,并能按照软件即可。
3.内网穿透介绍
内网穿透,也即 NAT 穿透 。它可以让外网主机在网络中与 内网 NAT 设备进行相互访问穿透方法。

4.内网穿透实现
内容穿透需要在本机中相关的软件来实现,本次我们将采用 cpolar 来实现,参考下发资料中的 cpolar 的安装和配置。
配置好后,需要对本地的网关进行内容穿透

内网穿透订单的端口号

在 nacos 中的 order-service-dev.properties 中修改支付通知消息:
#微信回调商户的地址weixinpay.notify-url = http://自己的域名/order/order-pay/wx-pay/notify-result
1.2.8 业务功能集成测试
启动下面服务
●后端服务
○xc-uaa-gateway-sever(UAA网关中心:63010)
○xc-user-service(用户中心:63130)
○xc-uaa(认证中心:63020)
○xc-teaching-service(教学管理微服务:63060)
○xc-content-search-service(内容搜索微服务:63080)
○xc-order-service(订单管理微服务:63090)
○xc-system-service(系统管理微服务:63110)
○xc-learning-service (学习中心:63070)
●其他服务
○Mysql
○Nacos
○rocketMQ
○Nginx
○ElasticSearch
●前端工程
○project-xczx2-portal-vue-ts 门户前端工程
1.3 查询已选课程
本功能需要在学习中心微服务中进行开发,来查询学生已经购买的课程数据。
3.7.1 系统交互流程

1)用户进入 **用户中心-我的课程** 页面,前端向 学习中心 发起**课程记录**检索<br /> 2)**学习中心** 检索当前登录用户的课程记录返回给前端<br /> 3)前端展示当前登录用户已选课程的列表
3.7.2 已选课程接口定义
根据前后端传入参数列表来定义接口
Http接口地址

1.接口参数列表
接口传入传出列表

2.接口参数定义
传入参数为分页数据,会使用 PageRequestParams 来封装参数。
传出参数无需定义,传出参数 CourseRecordDTO,之前已经通过代码生成器生成,无需再次定义。
3. 接口编写
在 xc-api 工程的 com.xuecheng.api.learning 包中增加 CourseRecordApi 定义:
package com.xuecheng.api.learning;import com.xuecheng.api.learning.model.CourseRecordDTO;import io.swagger.annotations.Api;import io.swagger.annotations.ApiImplicitParam;import io.swagger.annotations.ApiOperation;@Api(value = "用户的学习课程(选课)列表、 一个课程的学习情况 及更新课程的进度")public interface CourseRecordApi {@ApiOperation(value = "查询用户课程记录列表(我的选课信息列表)")PageVO<CourseRecordDTO> queryCourseRecordList(PageRequestParams pageParams);}
3.7.3 已选课程接口开发
1.DAO编写
Mybatis Plus 已经简化了单表操作,它提供的 Api 就可以完成添加数据操作,所有不需要进行编写。
2.service 编写
●接口
服务层接口定义,在com.xuecheng.learning.service.CourseRecordService中新增接口如下:
package com.xuecheng.learning.service;import com.baomidou.mybatisplus.extension.service.IService;import com.xuecheng.api.learning.model.CourseRecordDTO;import com.xuecheng.learning.entity.CourseRecord;/*** 选课记录 服务类*/public interface CourseRecordService extends IService<CourseRecord> {//其他代码省略/*** 根据用户名查询课程学习记录* @param userName 用户名* @param params 查询参数* @return*/PageVO<CourseRecordDTO> queryCourseRecordList(String userName, PageRequestParams params);}
●实现类
服务层实现,在com.xuecheng.learning.service.CourseRecordServiceImpl中新增接口实现如下:
/*** 选课记录 服务实现类*/@Slf4j@Servicepublic class CourseRecordServiceImpl extends ServiceImpl<CourseRecordMapper, CourseRecord> implements CourseRecordService {@Overridepublic PageVO<CourseRecordDTO> queryCourseRecordList(String username,PageRequestParams pageParams) {// 1.判断传入参数if (pageParams.getPageNo() < 1) {pageParams.setPageNo(1L);}if (pageParams.getPageSize() < 1) {pageParams.setPageSize(5);}//2.创建查询条件LambdaQueryWrapper<CourseRecord> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(CourseRecord::getUserName, username);queryWrapper.orderByDesc(CourseRecord::getChangeDate);//3.创建分页数据Page page = new Page<>(pageParams.getPageNo(),pageParams.getPageSize());//4.调用方法进行条件分页查询IPage<CourseRecord> pageResult = this.page(page, queryWrapper);//5.将 PO 数据转为 DTO 数据List<CourseRecordDTO> dtos = new ArrayList<>();if (!(ObjectUtils.isEmpty(pageResult.getRecords())) || pageResult.getSize() > 0) {dtos = CourseRecordConvert.INSTANCE.entitys2dtos(pageResult.getRecords());}//6. 构建 PageVO数据PageVO<CourseRecordDTO> resultPage =new PageVO<>(dtos, pageResult.getTotal(),pageParams.getPageNo(), pageParams.getPageSize());return resultPage;}}
(2)Controller实现
package com.xuecheng.learning.controller;import com.xuecheng.api.learning.LearnedRecordAPI;import com.xuecheng.api.learning.model.CourseRecordDTO;import com.xuecheng.api.uaa.model.LoginUser;import com.xuecheng.learning.common.SecurityUtil;import com.xuecheng.learning.service.CourseRecordService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;/*** 选课记录 前端控制器*/public class CourseRecordController implements CourseRecordAPI {@Autowiredprivate CourseRecordService courseRecordService;//其他代码省略@PostMapping("learnedRecords/list")public PageVO<CourseRecordDTO> queryCourseRecordList(PageRequestParams pageParams) {LoginUser user = UAASecurityUtil.getUser();return courseRecordService.queryCourseRecordList(user.getUsername(), pageParams);}}
3.7.4 已选课程接口测试
1.按照接口定义,请求Method为POST
2.参数是通过 Restful 方法进行传递
3.接口地址如下:http://127.0.0.1:63070/learning/learnedRecored/list
使用postman进行接口测试,请求界面如下:

响应内容如下:

