今日重点

  1. 学生选课订单:
  2. 步骤描述:
  3. 1)当用户点击购买入口(课程购买按钮)时,前端向订单服务请求下单接口
  4. 2)订单服务 调用内容管理服务`获取该课程的发布信息(详细信息)
  5. 3)订单服务 调用使用课程发布信息生成订单信息,保存数据库(订单状态为未支付),将订单信息返回给前端
  6. 4)前端将使用订单信息展示支付页面,并列出支付方式
  7. ES查询是比较快的,因为他的有些数据是缓存在内存中的 能顶得住大并发!
  8. 学生点击完购买后会创建该学生的个人学习记录! 课程也有免费的 是靠表中payid来判断该学员是否付款购买!
  9. 订单微服务会发送俩个请求:
  10. 一是创建订单表(订单信息),一是创建订单支付表(支付信息)
  11. 创建订单业务实现:
  12. 业务分析:
  13. 1.判断关键数据
  14. coursePubId username
  15. 2.判断业务数据
  16. 课程发布数据 :判断是否存在 判断是否收费 免费的课程是无需创建订单的(避免表中数据冗余)
  17. 3.保存订单数据
  18. 判断订单数据是否存在:coursePubId username(一个人对于一门课只有一个订单)
  19. 根据订单支付状态来判断:初始化态
  20. 如果没有
  21. 创建订单:新建的订单状态为--初始态
  22. 如果有
  23. 如果订单数据存在,需要获得最新的课程价格并修改订单的价格数据--活动
  24. 判断订单状态:
  25. 只有是未支付的状态才符合业务操作需要 (status如果已经支付 就不能让其再次购买了)
  26. 4.将结果数据转为DTO并返回
  27. 支付业务中由于设计到事务, 第三方和本地事务进行交互 需要先操作本地开发者可控制事务 如果成功在去调用第三方的功能业务, 如果本地事务失败那么就可以进行回滚操作,因为第三方的业务我们是操作不了的 所以只能先操作自己的事务达到一致性!

学成在线 第06章 讲义-学生选课

1. 学生选课


在学生选课中,需要学员通过学成在线门户网站实现,查看学生的课程购买记录、学生购买课程的下单、学生支付收费课程、分布式任务调度查询支付结果等操作。本次主要是对学成的学生群体来开发对应的功能。

1.1 需求分析


本次主要针对学生学科的业务操作,主要功能包括:
●学生进入首页或课程搜索找到目标课程,进入某个课程的详情页,需要判断是否收费,如果收费需要查询改学员对此课程的购买记录。
●课程收费的情况下,学生对此课程会创建出购买课程的订单,完成课程购买的操作。
●学生需要进行支付,来完成对课程的购买。
●学生可以在学习中心中查看自己所有的所学的课程,包括收费的课程和免费课程。

1.1.1 业务流程


学生选课流程如下:
1.通过首页或课程搜索找到目标课程,进入课程详情页.
●免费课程的显示

Day12-第六章-学生选课-订单支付 - 图1

●收费课程的显示

Day12-第六章-学生选课-订单支付 - 图2

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

Day12-第六章-学生选课-订单支付 - 图3

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

Day12-第六章-学生选课-订单支付 - 图4

Day12-第六章-学生选课-订单支付 - 图5

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

Day12-第六章-学生选课-订单支付 - 图6

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

Day12-第六章-学生选课-订单支付 - 图7

1.1.2 业务分析


业务操作概念图,如下:
学生选课

Day12-第六章-学生选课-订单支付 - 图8

此模块将会实现:
●查询课程发布数据
●查询学生选课记录
●课程下单
●支付订单
●获取支付结果
●已选课程查询
学生选课业务涉及以下服务:
学习中心:定义一个学习中心的微服务来对学成在线中的用户学习相关信息进行集中管理,其中包括用户信息、用户课程记录等。
订单服务:定义一个订单服务的微服务来对学成在线中的订单业务集中管理,其中包括了订单数据、订单支付数据,它还将对接第三方支付(如微信、支付宝)的细节封装于此。
内容服务:在下单前,要查询用户所选的课程数据是否存在,如果存在才可以对课程进行下单操作。
支付系统:用户在支付金额,需要使用第三方的支付平台来实现(微信)。

1.2 引入相关工程


为实现学生选课业务,后端服务中需要将设计到 学习中心微服务订单微服务,学习中心微服务 已经在学员登录时导入,无需再次引入,下面只需要将 订单微服务 导入到 xc-parent 父类工程下。

1.2.1 引入订单服务工程

1.2.1.1 工程导入


导入“资料”下的订单管理服务基础工程:xc-order-service

Day12-第六章-学生选课-订单支付 - 图9

1.2.1.2 数据库初始化


资料里的数据脚本导入到本地 MySQL 数据中,资料位置在 ‘资料/数据库脚本/xc_order.sql’ 。

orders:订单信息表,用于存储课程订单信息,对于收费课程,只有订单更新为已支付,学习中心的course_record表才会生成选课记录。
order_pay: 订单支付信息表用于存储订单的支付交易详情,它是平台订单与第三方支付的纽带。
PS:表的详细内容,在后面设计到了,再去介绍。

1.2.1.3 配置中心


在 nacos 中创建 order-service-dev.properties ,并添加下面的配置

  1. #srpingboot http 配置信息
  2. server.servlet.context-path = /order
  3. server.port=63090
  4. spring.datasource.url = jdbc:mysql://192.168.94.129:3306/xc_order?userUnicode=true&useSSL=false&characterEncoding=utf8

在 xc-order-service 服务中引入nacos配置信息,需要引入对 ribbon 、mp 和 feign 公共配置信息。

  1. #微服务启动参数
  2. spring:
  3. application:
  4. name: learning-service
  5. main:
  6. allow-bean-definition-overriding: true
  7. mvc:
  8. throw-exception-if-no-handler-found: true
  9. cloud:
  10. nacos:
  11. discovery: #配置注册中心
  12. server-addr: 192.168.94.129:8848
  13. namespace: 5c0b093c-4084-46b5-bf33-899321cb7ef5
  14. group: ${group.name}
  15. config: #配置中心
  16. server-addr: 192.168.94.129:8848
  17. namespace: 5c0b093c-4084-46b5-bf33-899321cb7ef5
  18. group: ${group.name}
  19. file-extension: properties
  20. shared-configs:
  21. - dataId: mp-config.properites
  22. group: ${group.name}
  23. - dataId: spring-http-config.properties
  24. group: ${group.name}
  25. - dataId: spring-druid-config.properties
  26. group: ${group.name}
  27. - dataId: feign-config.properties
  28. group: ${group.name}
  29. - dataId: ribbon-config.properties
  30. group: ${group.name}
  31. profiles: # 激活配置环境
  32. active: dev
  33. # 组名称
  34. group:
  35. name: xc-group

1.3 查询课程发布业务实现

在课程下单中,需要在订单服务中查询内容管理的 课程发布信息 ,所以需要在内容管理中定义相应的接口。

1.3.1 查询课程发布接口定义


1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址

Day12-第六章-学生选课-订单支付 - 图10

接口传入传出列表

Day12-第六章-学生选课-订单支付 - 图11

Day12-第六章-学生选课-订单支付 - 图12

返回参数为 CoursePubIndexDTO 数据,之前已经再课程发布已经定义,无需定义。
2.接口定义
在 xc-api 工程的 com.xuecheng.api.search 包下创建接口类接口定义如下:

  1. @Api(value = "课程发布搜索服务API管理")
  2. public interface CoursePubSearchApi {
  3. //其他代码省略
  4. @ApiOperation(value = "根据Id获得课程发布信息")
  5. @ApiImplicitParam(name = "coursePubId",
  6. value = "课程发布ID", required = true,
  7. dataType = "Long", paramType = "path", example = "1")
  8. CoursePubIndexDTO getCoursePubById(Long coursePubId);
  9. }

1.3.2 查询课程发布接口开发


1.dao编写
Mybatis Plus 已经简化了单表操作,它提供的 Api 就可以完成添加数据操作,所有不需要进行编写。
2.service 编写
●接口 在 xc-content 中的 CoursePubService中定义接口。

  1. /**
  2. * 课程搜索服务层
  3. */
  4. public interface CoursePubSearchService {
  5. /**
  6. * 根据课程发布Id查询课程发布索引数据
  7. * @param coursePubId
  8. * @return
  9. */
  10. CoursePubIndexDTO getPubIndexById(Long coursePubId);
  11. }

●实现类

  1. /**
  2. * 课程搜索服务实现层(es原始Api实现)
  3. */
  4. @Slf4j
  5. @Service
  6. public class CoursePubSearchServiceImpl implements CoursePubSearchService {
  7. //其他代码省略
  8. //其他代码省略
  9. public CoursePubIndexDTO getPubIndexById(Long coursePubId) {
  10. // 1.判断关键数据
  11. if (ObjectUtils.isEmpty(coursePubId)) {
  12. ExceptionCast.cast(CommonErrorCode.E_100101);
  13. }
  14. // 2.创建请求对象
  15. GetRequest request = new GetRequest(indexName,coursePubId.toString());
  16. // 3.获得响应对象
  17. GetResponse getResponse = null;
  18. try {
  19. getResponse = client.get(request, RequestOptions.DEFAULT);
  20. } catch (IOException e) {
  21. log.error("查询课程搜索数据失败:{}",e.getMessage());
  22. ExceptionCast.cast(ContentSearchErrorCode.E_150001);
  23. }
  24. // 文档id值
  25. String id = getResponse.getId();
  26. String jsonString = getResponse.getSourceAsString();
  27. CoursePubIndexDTO coursePubIndexDTO = null;
  28. if (StringUtil.isBlank(jsonString)) {
  29. coursePubIndexDTO = new CoursePubIndexDTO();
  30. } else {
  31. coursePubIndexDTO = JsonUtil.jsonToObject(jsonString, CoursePubIndexDTO.class);
  32. coursePubIndexDTO.setIndexId(new Long(id));
  33. }
  34. return coursePubIndexDTO;
  35. }
  36. }

3. controller编写

  1. /**
  2. * 课程搜索服务控制层
  3. */
  4. @RestController
  5. @RequestMapping
  6. public class CoursePubSearchController implements CoursePubSearchApi {
  7. //其他代码省略
  8. @Autowired
  9. private CoursePubSearchService coursePubSearchService;
  10. @GetMapping("course_pub/{coursePubId}")
  11. public CoursePubIndexDTO getCoursePubById(@PathVariable Long coursePubId) {
  12. RestResponse<CoursePubIndexDTO> respo =
  13. coursePubSearchService.getPubIndexById(coursePubId);
  14. return respo;
  15. }
  16. }

1.4 查询选课记录业务实现

1.4.1 系统交互流程


查询购买记录交互流程如下:

Day12-第六章-学生选课-订单支付 - 图13

步骤描述:
1.当用户进入课程详情页后,前端向学习中心请求检索当前登录用户的课程记录
2.学习中心执行数据库检索,将结果返回给前端
3.前端根据返回的数据决定是否展示购买入口(课程购买按钮)

1.4.2 数据模型(表结构)


学习中心服务的数据库 xc_learning 中定义了用户选课记录表 course_record。
1.选课记录表
选课记录表结构

Day12-第六章-学生选课-订单支付 - 图14

在上图中的表结构中,主要字段为:
1.选课人信息描述:user_id, user_name
2.课程信息描述:company_id,course_id, course_pub_id ,course_pub_name 等
3.学习记录信息描述:teachplan_id, teachplan_name
paid 是否已支付:
前提:课程必须为收费
判断依据:
paid=1 课程已经支付
paid=0 课程未支付

1.4.3 构建学习中心服务


学习中心存储用户信息,因此要实现用户注册、登录功能,需要先把它准备好。

1.4.3.1 数据库初始化


资料里的数据脚本导入到本地 MySQL 数据中,资料位置在 ‘资料/数据库脚本/xc_learning.sql’ 。

Day12-第六章-学生选课-订单支付 - 图15

1.4.3.2 工程导入


导入“资料”下的媒资管理服务基础工程:xc-learning-service

Day12-第六章-学生选课-订单支付 - 图16

1.4.3.3 配置中心


在 nacos 中创建 learning-service-dev.properties ,并添加下面的配置

  1. #srpingboot http 配置信息
  2. server.servlet.context-path = /learing
  3. server.port=63070
  4. spring.datasource.url = jdbc:mysql://192.168.94.129:3306/xc_learning?userUnicode=true&useSSL=false&characterEncoding=utf8

在 xc-learning-service 服务中引入nacos配置信息,需要引入对 ribbon 、mp 和 feign 公共配置信息。

  1. #微服务启动参数
  2. spring:
  3. application:
  4. name: learning-service
  5. main:
  6. allow-bean-definition-overriding: true
  7. mvc:
  8. throw-exception-if-no-handler-found: true
  9. cloud:
  10. nacos:
  11. discovery: #配置注册中心
  12. server-addr: 192.168.94.129:8848
  13. namespace: 自己的nacos namespace
  14. group: ${group.name}
  15. config: #配置中心
  16. server-addr: 192.168.94.129:8848
  17. namespace: 自己的nacos namespace
  18. group: ${group.name}
  19. file-extension: properties
  20. shared-configs:
  21. - dataId: mp-config.properites
  22. group: ${group.name}
  23. - dataId: spring-http-config.properties
  24. group: ${group.name}
  25. - dataId: spring-druid-config.properties
  26. group: ${group.name}
  27. - dataId: feign-config.properties
  28. group: ${group.name}
  29. - dataId: ribbon-config.properties
  30. group: ${group.name}
  31. profiles: # 激活配置环境
  32. active: dev
  33. # 组名称
  34. group:
  35. name: xc-group

1.4.4 查询选课记录接口定义


1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址

Day12-第六章-学生选课-订单支付 - 图17

接口传入传出列表

Day12-第六章-学生选课-订单支付 - 图18

2. 传入传出封装类
●传出数据 DTO
在 xc-api 工程中的 com.xuecheng.api.learning.model中定义 DTO 数据

  1. @Data
  2. @ApiModel(value="CourseRecordDTO", description="选课记录")
  3. public class CourseRecordDTO implements Serializable {
  4. @ApiModelProperty(value = "主键")
  5. private Long courseRecordId;
  6. private Long userId;
  7. @ApiModelProperty(value = "选课人")
  8. private String userName;
  9. @ApiModelProperty(value = "课程所属机构标识")
  10. private Long companyId;
  11. @ApiModelProperty(value = "课程标识")
  12. private Long courseId;
  13. @ApiModelProperty(value = "课程发布ID")
  14. private Long coursePubId;
  15. @ApiModelProperty(value = "课程名称")
  16. private String coursePubName;
  17. @ApiModelProperty(value = "教育模式")
  18. private String teachmode;
  19. @ApiModelProperty(value = "课程有效期(不论点播或直播,在该期限内有效)" ,hidden = true)
  20. private LocalDateTime startTime;
  21. @ApiModelProperty(value = "课程有效期(不论点播或直播,在该期限内有效)" ,hidden = true)
  22. private LocalDateTime endTime;
  23. @ApiModelProperty(value = "正在学习的课程计划章节Id")
  24. private Long teachplanId;
  25. @ApiModelProperty(value = "正在学习的课程计划章节名称")
  26. private String teachplanName;
  27. @ApiModelProperty(value = "该课程用户是否已支付")
  28. private Integer paid;
  29. @ApiModelProperty(value = "创建时间")
  30. private LocalDateTime createDate;
  31. private LocalDateTime changeDate;
  32. }

3. 接口编写
在 xc-api 工程的 com.xuecheng.api.learning 包中增加 CourseRecordApi 定义:

  1. package com.xuecheng.api.learning;
  2. import com.xuecheng.api.learning.model.CourseRecordDTO;
  3. import io.swagger.annotations.Api;
  4. import io.swagger.annotations.ApiImplicitParam;
  5. import io.swagger.annotations.ApiOperation;
  6. @Api(value = "用户的学习课程(选课)列表、 一个课程的学习情况 及更新课程的进度")
  7. public interface CourseRecordApi {
  8. @ApiOperation(value = "查询用户某课程记录(获取某课程学习进度)")
  9. @ApiImplicitParam(name = "coursePubId", value = "课程发布Id", required = true, paramType = "path", example = "1")
  10. CourseRecordDTO getCourseRecordByCoursePubId(Long coursePubId);
  11. }

1.4.5 查询选课记录接口开发


1.dao编写
Mybatis Plus 已经简化了单表操作,它提供的 Api 就可以完成添加数据操作,所有不需要进行编写。
2.service 编写
service中需要使用 PO 转 DTO 转换类,在com.xuecheng.learning.convert中新增 CourseRecordConvert 类如下:
●对象属性映射类

  1. package com.xuecheng.learning.convert;
  2. import com.xuecheng.api.learning.model.CourseRecordDTO;
  3. import com.xuecheng.learning.entity.CourseRecord;
  4. import org.mapstruct.Mapper;
  5. import org.mapstruct.Mapping;
  6. import org.mapstruct.Mappings;
  7. import org.mapstruct.factory.Mappers;
  8. import java.util.List;
  9. @Mapper
  10. public interface CourseRecordConvert {
  11. CourseRecordConvert INSTANCE = Mappers.getMapper(CourseRecordConvert.class);
  12. @Mappings({
  13. @Mapping(source = "id", target = "courseRecordId"),
  14. })
  15. CourseRecordDTO entity2dto(CourseRecord entity);
  16. @Mapping(source = "courseRecordId", target = "id")
  17. CourseRecord dto2entity(CourseRecordDTO dto);
  18. List<CourseRecordDTO> entitys2dtos(List<CourseRecord> list);
  19. }

●接口定义
服务层接口定义,在com.xuecheng.learning.service中新增CourseRecordService接口如下:

  1. package com.xuecheng.learning.service;
  2. import com.baomidou.mybatisplus.extension.service.IService;
  3. import com.xuecheng.api.learning.model.CourseRecordDTO;
  4. import com.xuecheng.learning.entity.CourseRecord;
  5. /**
  6. * 选课记录 服务类
  7. */
  8. public interface CourseRecordService extends IService<CourseRecord> {
  9. /**
  10. * 获取用户的课程 学习进度
  11. * @param userName 用户名
  12. * @param coursePubId 课程发布id
  13. * @return
  14. */
  15. CourseRecordDTO getRecordByCoursePubId(String userName, Long coursePubId);
  16. }

●实现类
服务层实现,在 com.xuecheng.learning.service.impl 中新增CourseRecordServiceImpl 如下:

  1. package com.xuecheng.learning.service.impl;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  4. import com.xuecheng.api.learning.model.dto.CourseRecordDTO;
  5. import com.xuecheng.common.domain.code.CommonErrorCode;
  6. import com.xuecheng.common.exception.ExceptionCast;
  7. import com.xuecheng.common.util.StringUtil;
  8. import com.xuecheng.learning.convert.CourseRecordConvert;
  9. import com.xuecheng.learning.entity.CourseRecord;
  10. import com.xuecheng.learning.mapper.CourseRecordMapper;
  11. import com.xuecheng.learning.service.CourseRecordService;
  12. import lombok.extern.slf4j.Slf4j;
  13. import org.springframework.stereotype.Service;
  14. import org.springframework.util.ObjectUtils;
  15. /**
  16. * <p>
  17. * 选课记录 服务实现类
  18. * </p>
  19. *
  20. * @author itcast
  21. */
  22. @Slf4j
  23. @Service
  24. public class CourseRecordServiceImpl extends ServiceImpl<CourseRecordMapper, CourseRecord> implements CourseRecordService {
  25. /*
  26. * 业务分析:
  27. * 1.判断关键数据
  28. * 2.根据条件查询用户的学习记录
  29. * 3.返回学习记录数据
  30. * 如果有数据返回
  31. * 如果没有返回空数据
  32. * */
  33. public CourseRecordDTO getRecordByCoursePubId(Long coursePubId, String username) {
  34. // 1.判断关键数据
  35. if (ObjectUtils.isEmpty(coursePubId)||
  36. StringUtil.isBlank(username)
  37. ) {
  38. ExceptionCast.cast(CommonErrorCode.E_100101);
  39. }
  40. // 2.根据条件查询用户的学习记录(一个人对一门课只有一个学习记录)
  41. LambdaQueryWrapper<CourseRecord> queryWrapper = new LambdaQueryWrapper<>();
  42. queryWrapper.eq(CourseRecord::getCoursePubId, coursePubId);
  43. queryWrapper.eq(CourseRecord::getUserName, username);
  44. CourseRecord courseRecord = this.getOne(queryWrapper);
  45. // 3.返回学习记录数据
  46. CourseRecordDTO courseRecordDTO = null;
  47. if (ObjectUtils.isEmpty(courseRecord)) {
  48. // 如果没有返回空数据
  49. courseRecordDTO = new CourseRecordDTO();
  50. } else {
  51. // 如果有数据返回
  52. courseRecordDTO = CourseRecordConvert.INSTANCE.entity2dto(courseRecord);
  53. }
  54. return courseRecordDTO;
  55. }
  56. }

3. controller编写
●编写Controller

  1. package com.xuecheng.learning.controller;
  2. import com.xuecheng.api.learning.LearnedRecordAPI;
  3. import com.xuecheng.api.learning.model.CourseRecordDTO;
  4. import com.xuecheng.api.uaa.model.LoginUser;
  5. import com.xuecheng.learning.common.SecurityUtil;
  6. import com.xuecheng.learning.service.CourseRecordService;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.web.bind.annotation.GetMapping;
  9. import org.springframework.web.bind.annotation.PathVariable;
  10. /**
  11. * 选课记录 前端控制器
  12. */
  13. public class CourseRecordController implements CourseRecordAPI {
  14. @Autowired
  15. private CourseRecordService courseRecordService;
  16. @GetMapping("learnedRecords/myCourseRec/{coursePubId}")
  17. public CourseRecordDTO myCourseRecord(@PathVariable Long coursePubId) {
  18. LoginUser user = UAASecurityUtil.getUser();
  19. return courseRecordService.getRecordByCoursePubId(user.getUsername(), coursePubId);
  20. }
  21. }

1.4.6 查询选课记录接口测试


启动下面服务
●后端服务
○xc-uaa-gateway-sever(UAA网关中心)
○xc-user-service(用户中心)
○xc-uaa(认证中心)
○xc-teaching-service(教学管理微服务)
○xc-learning-service(学习管理微服务)
●其他服务
○Mysql
○Nacos
使用postman进行接口测试,请求界面如下:
GET http://www.xuecheng.com/api/learning/learnedRecords/myCourseRec/40

Day12-第六章-学生选课-订单支付 - 图19

PS:可以先使用 15022222227(密码:123456) 账号登录,获得令牌后进行测试。

1.5 课程下单

1.5.1 系统交互流程


课程下单时序图

Day12-第六章-学生选课-订单支付 - 图20

步骤描述:
1)当用户点击购买入口(课程购买按钮)时,前端向订单服务请求下单接口
2)订单服务 调用内容管理服务`获取该课程的发布信息(详细信息)
3)订单服务 调用使用课程发布信息生成订单信息,保存数据库(订单状态为未支付),将订单信息返回给前端
4)前端将使用订单信息展示支付页面,并列出支付方式

1.5.2 数据模型(表结构)


学习中心服务的数据库 xc_order 中定义了用户选课记录表 order。
1.订单表
订单表结构

Day12-第六章-学生选课-订单支付 - 图21

在上图中的表结构中,主要字段为:
1.选课人信息描述:user_id, user_name
2.课程信息描述:company_id,course_id, course_pub_id ,course_pub_name 等
3.订单信息描述:order_no,initial_price,price,vaild,start_time
将order表中的约束修改:start_time end_time 修改为可以为空。

1.5.3 查询课程发布业务实现


在课程下单中,需要在订单服务中查询内容管理的 课程发布信息 ,所以需要在内容管理中定义相应的接口。

1.5.3.1 查询课程发布接口定义


1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址
GET /search/l/course-index/{coursePubId}
接口传入传出列表

Day12-第六章-学生选课-订单支付 - 图22

PS:学成在线微服务的远程调用,要根据开发规范文件来返回 RestResponse 类,所有上面定义的为 RestResponse类中的属性。
2.接口定义
在 xc-api 工程的 com.xuecheng.api.search 包下创建接口类接口定义如下:

  1. @Api(value = "课程发布搜索服务API管理")
  2. public interface CoursePubSearchApi {
  3. //其他代码省略
  4. @ApiOperation(value = "根据Id获得课程发布信息")
  5. @ApiImplicitParam(name = "coursePubId",
  6. value = "课程发布ID", required = true,
  7. dataType = "Long", paramType = "path", example = "1")
  8. RestResponse getById(Long coursePubId);
  9. }


1.5.3.2 查询课程发布接口开发


1.dao编写
Mybatis Plus 已经简化了单表操作,它提供的 Api 就可以完成添加数据操作,所有不需要进行编写。
2.service 编写
●接口

  1. 在 xc-content 中的 CoursePubService中定义接口。
  1. /**
  2. * 课程搜索服务层
  3. */
  4. public interface CoursePubSearchService {
  5. /**
  6. * 根据课程发布Id查询课程发布索引数据
  7. * @param coursePubId
  8. * @return
  9. */
  10. RestResponse<CoursePubIndexDTO> getCoursePubIndexById4s(Long coursePubId);
  11. }

●实现类

  1. /**
  2. * 课程搜索服务实现层(es原始Api实现)
  3. */
  4. @Slf4j
  5. @Service
  6. public class CoursePubSearchServiceImpl implements CoursePubSearchService {
  7. //其他代码省略
  8. @Override
  9. public RestResponse<CoursePubIndexDTO> getCoursePubIndexById4s(Long coursePubId) {
  10. if (ObjectUtils.isEmpty(coursePubId)) {
  11. return RestResponse.validfail(CommonErrorCode.E_100101);
  12. }
  13. CoursePubIndexDTO coursePubIndexDTO = null;
  14. try {
  15. GetRequest request = new GetRequest(indexName, coursePubId.toString());
  16. // 2.获得文档响应对象
  17. GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
  18. String jsongString = response.getSourceAsString();
  19. if (StringUtil.isNotBlank(jsongString)) {
  20. String id = response.getId();
  21. coursePubIndexDTO = JsonUtil.jsonToObject(jsongString, CoursePubIndexDTO.class);
  22. coursePubIndexDTO.setIndexId(new Long(id));
  23. return RestResponse.success(coursePubIndexDTO);
  24. } else {
  25. return RestResponse.validfail(ContentSearchErrorCode.E_150001);
  26. }
  27. } catch (IOException e) {
  28. return RestResponse.validfail(ContentSearchErrorCode.E_150001);
  29. }
  30. }
  31. }

3. controller编写

  1. /**
  2. * 课程搜索服务控制层
  3. */
  4. @RestController
  5. @RequestMapping
  6. public class CoursePubSearchController implements CoursePubSearchApi {
  7. //其他代码省略
  8. @Autowired
  9. private CoursePubSearchService coursePubSearchService;
  10. @GetMapping("l/course_pub/{coursePubId}")
  11. public RestResponse getCoursePubById(@PathVariable Long coursePubId) {
  12. RestResponse<CoursePubIndexDTO> respo =
  13. coursePubSearchService.getCoursePubIndexById4s(coursePubId);
  14. return respo;
  15. }
  16. }

1.5.3.3 信息接口测试


1.测试环境需要启动的微服务有:
1.注册中心 xc-discover-service (端口:63000)
2.UAA服务网关 xc-uaa-gateway-server (端口:63010)
3.用户中心服务 xc-user-service (端口:63130)
4.UAA服务 xc-uaa(端口:63020)
5.搜索服务 xc-content-search-service (端口:63040)
2.测试接口路径
GET http://127.0.0.1:63010/search/l/course_pub/24

Day12-第六章-学生选课-订单支付 - 图23

1.5.4 课程下单的业务实现


课程下单的操作是在 订单服务 中创建出用户对课程购买的订单,具体代码实现如下:
1.在 订单微服务 中定义下单的接口
2.在订单微服务中定义远程内容管理的 Feign
3.完成业务的代码实现

1.5.4.1 课程下单接口定义


1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址

Day12-第六章-学生选课-订单支付 - 图24

PS:路径中第一个 order 为服务的根路径,后面的 orders 为业务模块的名称。
接口传入传出列表

Day12-第六章-学生选课-订单支付 - 图25

2. 传入传出封装类
●传出数据 DTO(代码生成)
在 xc-api 工程中的 com.xuecheng.api.learning.model 中定义 DTO 数据

  1. package com.xuecheng.api.order.model;
  2. import io.swagger.annotations.ApiModel;
  3. import io.swagger.annotations.ApiModelProperty;
  4. import lombok.Data;
  5. import javax.validation.constraints.NotNull;
  6. import java.io.Serializable;
  7. import java.math.BigDecimal;
  8. import java.time.LocalDateTime;
  9. @Data
  10. @ApiModel(value="OrdersDTO", description="订单信息")
  11. public class OrdersDTO implements Serializable {
  12. @ApiModelProperty(value = "订单标识")
  13. private Long orderId;
  14. @ApiModelProperty(value = "订单号")
  15. private String orderNo;
  16. @ApiModelProperty(value = "课程Id")
  17. @NotNull(message = "课程Id不能为空")
  18. private Long coursePubId;
  19. @ApiModelProperty(value = "课程名称")
  20. private String coursePubName;
  21. @ApiModelProperty(value = "机构ID")
  22. private Long companyId;
  23. @ApiModelProperty(value = "机构名称")
  24. private String companyName;
  25. @ApiModelProperty(value = "定价")
  26. private BigDecimal initialPrice;
  27. @ApiModelProperty(value = "交易价")
  28. private BigDecimal price;
  29. @ApiModelProperty(value = "课程有效性")
  30. private String valid;
  31. @ApiModelProperty(value = "起始时间")
  32. private LocalDateTime startTime;
  33. @ApiModelProperty(value = "结束时间")
  34. private LocalDateTime endTime;
  35. @ApiModelProperty(value = "交易状态(0初始、 1已支付 -1已取消 -2已关闭 -3已退款)")
  36. private Integer status;
  37. @ApiModelProperty(value = "用户ID")
  38. private Long userId;
  39. @ApiModelProperty(value = "用户名")
  40. private String userName;
  41. @ApiModelProperty(value = "创建日期")
  42. private LocalDateTime createDate;
  43. @ApiModelProperty(value = "修改日期")
  44. private LocalDateTime changeDate;
  45. }

3. 接口编写
在 xc-api 工程的 com.xuecheng.api.order 包中增加 CourseRecordApi 定义:

  1. /**
  2. * <P>
  3. * 订单服务API
  4. * </p>
  5. */
  6. @Api(value = "订单服务API, 订单生成、查看、取消、删除", tags = "11..订单管理")
  7. public interface OrderApi {
  8. @ApiOperation("根据课程发布ID生成课程支付订单")
  9. @ApiImplicitParam(name = "coursePubId", value = "课程发布ID", required = true, dataType = "Long", paramType = "path", example = "1")
  10. OrdersDTO createOrder(Long coursePubId);
  11. }

1.5.4.2 远程获得课程发布


学成在订单服务中对某个课程进行下单操作,业务中需要查询对要下单的课程发布信息是否存在,所以需要远程调用内容管理服务获得 CoursePub 信息。
下面需要在 订单微服务 中创建一下内容:
1.Feign远程调用接口
在 xc-order-service 中创建远程调用的接口,如下:

  1. package com.xuecheng.order.agent;
  2. import com.xuecheng.api.search.model.dto.CoursePubIndexDTO;
  3. import com.xuecheng.common.constant.XcFeignServiceNameList;
  4. import com.xuecheng.common.domain.response.RestResponse;
  5. import org.springframework.cloud.openfeign.FeignClient;
  6. import org.springframework.web.bind.annotation.GetMapping;
  7. import org.springframework.web.bind.annotation.PathVariable;
  8. /**
  9. * <p></p>
  10. *
  11. * @Description:
  12. */
  13. @FeignClient(XcFeignServiceNameList.XC_SEARCH_SERVICE)
  14. public interface CoursePubIndexApiAgent {
  15. @GetMapping("/search/l/course-index/{coursePubId}")
  16. RestResponse<CoursePubIndexDTO> getCoursePubIndexById4s(@PathVariable
  17. Long coursePubId);
  18. }

@FeignClient 需要赋值属性:
value:指定系统管理在注册中心的名称,这里使用 XcFeignServiceNameList 类来获得
fallbackFactory:指定远程调用系统管理接口熔断器工程类
@GetMapping 赋值属性:
由于是微服务的远程调用,需要在系统管理服务根路径下加上 /l 标识。
2.Feign远程接口测试
对于 Feign 接口编写完后,需要在 xc-order-service 测试目录下进行测试,测试代码如下:

  1. @SpringBootTest
  2. @RunWith(SpringRunner.class)
  3. public class CoursePubSearchTest {
  4. @Autowired
  5. private CoursePubSearchApiAgent searchApiAgent;
  6. @Test
  7. public void test01() {
  8. RestResponse resp = searchApiAgent.getCoursePubIndexById4s(24L);
  9. if (resp.isSuccessful()) {
  10. Object result = resp.getResult();
  11. System.out.println(result);
  12. } else {
  13. System.out.println("获得失败");
  14. }
  15. }
  16. }

测试环境需要启动的微服务有:
1.注册中心 xc-discover-service (端口:63000)
2.UAA服务网关 xc-uaa-gateway-server (端口:63010)
3.用户中心服务 xc-user-service (端口:63130)
4.UAA服务 xc-uaa(端口:63020)
5.搜索服务 xc-content-search-service (端口:63040)

1.5.4.3 课程下单接开发


1.dao编写
Mybatis Plus 已经简化了单表操作,它提供的 Api 就可以完成添加数据操作,所有不需要进行编写。
2.service 编写
service中需要使用 PO 转 DTO 转换类,在com.xuecheng.order.convert中新增 OrderConvert 类如下:
●对象属性映射类

  1. package com.xuecheng.order.convert;
  2. import com.xuecheng.api.order.model.OrdersDTO;
  3. import com.xuecheng.order.entity.Orders;
  4. import org.mapstruct.Mapper;
  5. import org.mapstruct.Mapping;
  6. import org.mapstruct.Mappings;
  7. import org.mapstruct.factory.Mappers;
  8. import java.util.List;
  9. @Mapper
  10. public interface OrderConvert {
  11. OrderConvert INSTANCE = Mappers.getMapper(OrderConvert.class);
  12. @Mappings({
  13. @Mapping(source = "id", target = "orderId"),
  14. })
  15. OrdersDTO entity2dto(Orders entity);
  16. @Mapping(source = "orderId", target = "id")
  17. Orders dto2entity(OrdersDTO dto);
  18. List<OrdersDTO> entitys2dtos(List<Orders> list);
  19. }

●接口
服务层接口定义,在com.xuecheng.order.service中新增OrdersService接口如下:

  1. package com.xuecheng.order.service;
  2. import com.baomidou.mybatisplus.extension.service.IService;
  3. import com.xuecheng.api.order.OrdersDTO;
  4. import com.xuecheng.api.order.model.OrdersModel;
  5. import com.xuecheng.common.domain.page.PageRequestParams;
  6. import com.xuecheng.common.domain.page.PageVO;
  7. import com.xuecheng.order.entity.Orders;
  8. /**
  9. * 订单 服务类
  10. */
  11. public interface OrdersService extends IService<Orders> {
  12. /**
  13. * 创建订单
  14. * @param username 课程发布Id
  15. * @param coursePubId 用户
  16. * @return
  17. */
  18. public OrdersDTO createOrder(String username, Long coursePubId)
  19. }

●实现类
服务层实现,在com.xuecheng.order.service.impl 中新增OrdersServiceImpl接口如下:

  1. package com.xuecheng.order.service.impl;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  4. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  5. import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
  6. import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderResult;
  7. import com.github.binarywang.wxpay.service.WxPayService;
  8. import com.xuecheng.api.order.model.dto.OrdersDTO;
  9. import com.xuecheng.api.order.model.pay.PayCodeUrlResult;
  10. import com.xuecheng.api.search.model.dto.CoursePubIndexDTO;
  11. import com.xuecheng.common.domain.code.CommonErrorCode;
  12. import com.xuecheng.common.domain.response.RestResponse;
  13. import com.xuecheng.common.enums.content.CourseChargeEnum;
  14. import com.xuecheng.common.enums.order.OrderDealStatusEnum;
  15. import com.xuecheng.common.exception.ExceptionCast;
  16. import com.xuecheng.common.util.PaymentUtil;
  17. import com.xuecheng.common.util.StringUtil;
  18. import com.xuecheng.order.agent.CoursePubIndexApiAgent;
  19. import com.xuecheng.order.common.constant.OrderErrorCode;
  20. import com.xuecheng.order.convert.OrderConvert;
  21. import com.xuecheng.order.entity.Orders;
  22. import com.xuecheng.order.entity.Pay;
  23. import com.xuecheng.order.mapper.OrdersMapper;
  24. import com.xuecheng.order.service.OrdersService;
  25. import com.xuecheng.order.service.PayService;
  26. import lombok.extern.slf4j.Slf4j;
  27. import org.springframework.beans.factory.annotation.Autowired;
  28. import org.springframework.stereotype.Service;
  29. import org.springframework.transaction.annotation.Transactional;
  30. import org.springframework.util.ObjectUtils;
  31. import java.net.InetAddress;
  32. /**
  33. * <p>
  34. * 订单 服务实现类
  35. * </p>
  36. *
  37. * @author itcast
  38. */
  39. @Slf4j
  40. @Service
  41. public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {
  42. @Autowired
  43. private CoursePubIndexApiAgent searchIndexApiAgent;
  44. @Autowired
  45. private WxPayService wxPayService;
  46. @Autowired
  47. private PayService payService;
  48. /*
  49. * 业务分析:
  50. * 1.判断关键数据
  51. * coursePubId username
  52. * 2.判断业务数据
  53. * 课程发布数据
  54. * 3.保存订单数据
  55. * 判断订单数据是否存在:coursePubId username(一个人对于一门课只有一个订单)
  56. * 根据订单支付状态来判断:初始化态
  57. * 如果没有
  58. * 创建订单:新建的订单状态为--初始态
  59. * 如果有
  60. * 如果订单数据存在,需要获得最新的课程价格并修改订单的价格数据--活动
  61. *
  62. * 4.将结果数据转为DTO并返回
  63. * */
  64. @Transactional
  65. public OrdersDTO createOrder(Long coursePubId, String username) {
  66. // 1.判断关键数据
  67. // coursePubId username
  68. if (ObjectUtils.isEmpty(coursePubId)||
  69. StringUtil.isBlank(username)
  70. ) {
  71. ExceptionCast.cast(CommonErrorCode.E_100101);
  72. }
  73. // 2.判断业务数据
  74. // 课程发布数据
  75. RestResponse<CoursePubIndexDTO> restResponse = searchIndexApiAgent.getPubIndexById4s(coursePubId);
  76. if (!(restResponse.isSuccessful())) {
  77. ExceptionCast.castWithCodeAndDesc(restResponse.getCode(),restResponse.getMsg());
  78. }
  79. CoursePubIndexDTO coursePub = restResponse.getResult();
  80. // 3.保存订单数据
  81. // 判断订单数据是否存在:coursePubId username(一个人对于一门课只有一个订单)
  82. // 根据订单支付状态来判断:初始化态
  83. LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
  84. queryWrapper.eq(Orders::getUserName, username);
  85. queryWrapper.eq(Orders::getCoursePubId, coursePubId);
  86. queryWrapper.eq(Orders::getStatus,new Integer(OrderDealStatusEnum.ORDER_DEAL_INIT_STATUS.getCode()) );
  87. Orders orders = this.getOne(queryWrapper);
  88. boolean result = false;
  89. if (ObjectUtils.isEmpty(orders)) {
  90. // 如果没有
  91. // 创建订单
  92. orders = new Orders();
  93. // 订单编号:有学成的标识+时间+唯一标识
  94. orders.setOrderNo(PaymentUtil.genUniquePayOrderNo());
  95. orders.setCoursePubId(coursePub.getIndexId());
  96. orders.setCoursePubName(coursePub.getName());
  97. orders.setCompanyId(coursePub.getCompanyId());
  98. orders.setUserName(username);
  99. orders.setInitialPrice(coursePub.getPrice());
  100. orders.setPrice(coursePub.getPrice());
  101. orders.setStatus(new Integer(OrderDealStatusEnum.ORDER_DEAL_INIT_STATUS.getCode()));
  102. result = this.save(orders);
  103. } else {
  104. // 修改订单的价格
  105. // 如果订单数据存在,需要获得最新的课程价格并修改订单的价格数据--活动
  106. LambdaUpdateWrapper<Orders> updateWrapper = new LambdaUpdateWrapper<>();
  107. updateWrapper.set(Orders::getPrice, coursePub.getPrice());
  108. updateWrapper.set(Orders::getChangeDate, LocalDateTime.now());
  109. updateWrapper.eq(Orders::getId, orders.getId());
  110. result = this.update(updateWrapper);
  111. }
  112. if (!result) {
  113. ExceptionCast.cast(OrderErrorCode.E_160015);
  114. }
  115. // 4.将结果数据转为DTO并返回
  116. Orders po = this.getById(orders.getId());
  117. OrdersDTO resultDTO = OrderConvert.INSTANCE.entity2dto(po);
  118. return resultDTO;
  119. }
  120. }

3.Controller编写
服务层接口定义,在com.xuecheng.order.controller 中新增 OrdersController 接口如下:

  1. /**
  2. * <p>
  3. * 订单 前端控制器
  4. * </p>
  5. *
  6. * @author itcast
  7. */
  8. @RestController
  9. public class OrdersController implements OrderApi {
  10. @Autowired
  11. private OrdersService ordersService;
  12. @GetMapping("orders/create/{coursePubId}")
  13. public OrdersDTO createOrder(@PathVariable Long coursePubId) {
  14. LoginUser user = UAASecurityUtil.getUser();
  15. ExceptionCast.cast(ObjectUtils.isEmpty(user), CommonErrorCode.E_100108);
  16. return ordersService.createOrder(coursePubId,user.getUsername());
  17. }
  18. }

1.5.4.4 接口测试


1.启动下面服务
●后端服务
○xc-discover-sever(注册中心)
○xc-uaa-gateway-sever(UAA网关中心)
○xc-user-service(用户中心)
○xc-uaa(认证中心)
○xc-teaching-service(教学管理微服务)
○xc-content-service(内容管理微服务)
○xc-order-service(订单管理微服务)
●其他服务
○Mysql
○Apollo
2.通过 postman 获得令牌信息

Day12-第六章-学生选课-订单支付 - 图26

3.测试接口:
GET http://127.0.0.1:63010/order/orders/create/24
使用postman进行接口测试,请求界面如下:

Day12-第六章-学生选课-订单支付 - 图27

1.6 订单支付


在用户确认要购买课程的时候,学成在线会在后端微服务生成购买课程的订单。这个时候在页面中会生成课程购买的二维码。

1.6.1 系统交互流程

Day12-第六章-学生选课-订单支付 - 图28

  1. 1)当用户在支付页面中,选择某支付方式并确认时,前端向订单服务请求创建支付接口<br /> 2)订单服务 通过订单id获取要支付的订单信息,并将订单信息组装为第三方支付的下单请求数据,调用第三方支付服务下单。<br /> 3)订单服务 接收第三方支付返回的支付二维码url,并将此url返回给前端<br /> 4)前端将url生成为二维码,展示给用户

1.6.2 微信支付

1.6.2.1 微信支付集成流程


(1)场景介绍
用户扫描商户展示在各种场景的二维码进行支付。
步骤1:商户根据微信支付的规则,为不同商品生成不同的二维码(如图6.1),展示在各种场景,用于用户扫描购买。
步骤2:用户使用微信“扫一扫”(如图6.2)扫描二维码后,获取商品支付信息,引导用户完成支付(如图6.3)。

Day12-第六章-学生选课-订单支付 - 图29

步骤3:用户确认支付,输入支付密码(如图6.4)。
步骤4:支付完成后会提示用户支付成功(如图6.5),商户后台得到支付成功的通知,然后进行发货处理。

Day12-第六章-学生选课-订单支付 - 图30

(2)接口交互图

5_0.png

业务流程说明:
(1)商户后台系统根据用户选购的商品生成订单。
(2)用户确认支付后调用微信支付【统一下单API】生成预支付交易;
(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
(4)商户后台系统根据返回的code_url生成二维码。
(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
(8)微信支付系统根据用户授权完成支付交易。
(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
(10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
(11)未收到支付通知的情况,商户后台系统调用【查询订单API】。
(12)商户确认订单已支付后给用户发货。
发货对应我们的需求就是生成 课程记录
官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1

1.6.2.2 微信支付账号和开发文档


微信支付的使用需要先在官网中进行账号的开通,获得商户的微信支付账号信息才可以使用。由于现学习阶段,并不具备开通的条件:企业营业执照、公章、认证费用300元等,所以将使用传智平台的微信商户信息,如下:
测试使用的微信账号:
appid:微信公众账号或开放平台APP的唯一标识 wx8397f8696b538317
mch_id:商户号 1473426802
key:商户密钥 T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
在具体微信支付商户信息后,需要进入官网查新微信支付平台的开发文档。
在线微信支付开发文档:
https://pay.weixin.qq.com/wiki/doc/api/index.html
请求参数如下,主要关注必填项目:
红色:支付渠道参数配置的内容
蓝色:微信sdk(开发工具包)自动配置
绿色:程序设置

Day12-第六章-学生选课-订单支付 - 图32

Day12-第六章-学生选课-订单支付 - 图33

Day12-第六章-学生选课-订单支付 - 图34

Day12-第六章-学生选课-订单支付 - 图35

Day12-第六章-学生选课-订单支付 - 图36

Day12-第六章-学生选课-订单支付 - 图37

Day12-第六章-学生选课-订单支付 - 图38

1.6.2.3 集成微信支付


1.增加微信支付sdk的maven依赖到 xc-order-service 中

  1. <dependency>
  2. <groupId>com.github.binarywang</groupId>
  3. <artifactId>weixin-java-pay</artifactId>
  4. <version>3.4.0</version>
  5. </dependency>

对于微信的支付依赖包,我们并没有采用官方的SDK来实现。项目中将采用更加方便的基于 Spring Boot 项目对微信的 SDK 进行二次封装依赖包,此包是国人制作并在 GitHub上开源,下面是项目的 GitHub 地址:
https://github.com/Wechat-Group/WxJava/wiki/微信支付
2.增加微信支付的nacos配置参数
在 xc-order-service 配置应用中添加下面的信息。

  1. #商户微信公共号或开放平台唯一标识
  2. weixinpay.app-id = wx8397f8696b538317
  3. #商户号
  4. weixinpay.mch-id = 1473426802
  5. #商户密钥
  6. weixinpay.mch-key = T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
  7. #微信回调商户的地址
  8. weixinpay.notify-url = http://xxx
  9. #商户的支付类型(NATIVE 为扫码支付)
  10. weixinpay.trade-type = NATIVE

3.在订单微服务中添加配置类信息
在xc-order-service 中的 com.xuecheng.order.config 包下添加如下配置信息:

  1. package com.xuecheng.order.config;
  2. import com.github.binarywang.wxpay.config.WxPayConfig;
  3. import com.github.binarywang.wxpay.service.WxPayService;
  4. import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
  5. import org.springframework.beans.factory.annotation.Qualifier;
  6. import org.springframework.boot.context.properties.ConfigurationProperties;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  10. /**
  11. * <P>
  12. * WebMvc Config
  13. * </p>
  14. */
  15. @Configuration
  16. public class WXPayConfig{
  17. /**
  18. * 第三方jar包已存在微信支付配置类,这里直接和配置属性关联
  19. * @return
  20. */
  21. @Bean("wxPayConfig")
  22. @ConfigurationProperties(prefix = "weixinpay")
  23. public WxPayConfig wxPayConfig(){
  24. return new WxPayConfig();
  25. }
  26. /**
  27. * 创建 WxPayService 实现类,并将其 WxPayConfig 配置类配置到支付服务类中
  28. * @return
  29. */
  30. @Bean
  31. public WxPayService wxPayService(@Qualifier("wxPayConfig") WxPayConfig wxConfig) {
  32. WxPayService wxPayService = new WxPayServiceImpl();
  33. wxPayService.setConfig(wxConfig);
  34. return wxPayService;
  35. }
  36. }

4.编写测试类测试生成支付二维码

  1. @SpringBootTest
  2. @RunWith(SpringRunner.class)
  3. public class WXPayTest {
  4. @Autowired
  5. private WxPayService wxPayService;
  6. @Test
  7. public void test() throws UnknownHostException, WxPayException {
  8. // 1.创建统一下单的请求对象
  9. WxPayUnifiedOrderRequest unifiedOrderRequest = new WxPayUnifiedOrderRequest();
  10. // 2.设置统一下单数据
  11. // 商品描述
  12. unifiedOrderRequest.setBody("测试数据");
  13. // 商品订单号
  14. unifiedOrderRequest.setOutTradeNo(PaymentUtil.genUniquePayOrderNo());
  15. // 商品金额(单位为:分)
  16. unifiedOrderRequest.setTotalFee(100);
  17. // 生成二维码的终端IP地址
  18. unifiedOrderRequest.setSpbillCreateIp(InetAddress.getLocalHost().getHostAddress());
  19. // 订单的详情描述
  20. unifiedOrderRequest.setDetail("测试一下");
  21. // 订单的商品Id号
  22. unifiedOrderRequest.setProductId("123123123");
  23. // 3.调用service获得生成二维码的结果数据
  24. WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(unifiedOrderRequest);
  25. System.out.println("-----------------获得结果信息--------------------");
  26. // 获得返回码
  27. System.out.println(result.getReturnCode());
  28. // 获得结果码
  29. System.out.println(result.getResultCode());
  30. // 获得二维码路径
  31. String codeURL = result.getCodeURL();
  32. System.out.println(codeURL);
  33. }
  34. }

测试程序输出结果:

  1. -----------------获得结果信息--------------------
  2. weixin://wxpay/bizpayurl?pr=2Rz37tJ
  3. SUCCESS
  4. SUCCESS

可访问https://cli.im/ ,将codeURL转换为二维码,然后用微信手机客户端扫码支付。

1.6.3 数据模型(表结构)


1.订单支付表
订单支付表结构

Day12-第六章-学生选课-订单支付 - 图39

在上图中的表结构中,主要字段为:
1.选课人信息描述: user_name
2.订单信息描述:order_id , company_id
3.订单支付信息描述:pay_number,status,pay_method,pay_date,total_amount 等

1.6.4 接口定义


前端调用此接口获取第三方支付的支付凭证(目前使用的是微信扫码支付,因此返回支付二维码链接),而后生成支付二维码。
1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址

Day12-第六章-学生选课-订单支付 - 图40

接口传入传出列表

Day12-第六章-学生选课-订单支付 - 图41

2. 接口编写
在 xc-api 工程的 com.xuecheng.api.order.model.pay 包中增加PayCodeUrlResult定义:

  1. package com.xuecheng.api.order.model.pay;
  2. import com.xuecheng.common.enums.common.CommonEnum;
  3. import io.swagger.annotations.ApiModel;
  4. import io.swagger.annotations.ApiModelProperty;
  5. import lombok.Data;
  6. /**
  7. * <p></p>
  8. *
  9. * @Description:
  10. */
  11. @Data
  12. @ApiModel("获得支付链接地址封装类")
  13. public class PayCodeUrlResult {
  14. public static final String WX_PAY_SUCCESS_FLAG = "SUCCESS";
  15. public static final String WX_PAY_FLAG = "WX";
  16. public static final String AlI_PAY_FLAG = "ALI";
  17. public static final String PAIED = "1";
  18. public static final String NOT_PAY = "0";
  19. @ApiModelProperty("生成二维码的状态:1-成功,0-失败")
  20. private String status;
  21. @ApiModelProperty("二维码链接地址")
  22. private String codeURL;
  23. @ApiModelProperty("异常信息:获得链接地址失败时,message才会有值")
  24. private String message;
  25. /*
  26. * 成功获得支付url数据
  27. * */
  28. public static PayCodeUrlResult success(String codeURL) {
  29. PayCodeUrlResult result = new PayCodeUrlResult();
  30. result.setStatus(CommonEnum.USING_FLAG.getCode());
  31. result.setCodeURL(codeURL);
  32. return result;
  33. }
  34. /*
  35. * 失败获得支付url数据
  36. * */
  37. public static PayCodeUrlResult failed(String message) {
  38. PayCodeUrlResult result = new PayCodeUrlResult();
  39. result.setStatus(CommonEnum.DELETE_FLAG.getCode());
  40. result.setMessage(message);
  41. return result;
  42. }
  43. }

1.6.5 接口实现


(1)服务层实现
服务层接口定义,在com.xuecheng.order.service中新增PayService接口如下:

  1. package com.xuecheng.order.service;
  2. import com.baomidou.mybatisplus.extension.service.IService;
  3. import com.xuecheng.order.entity.OrderPay;
  4. import java.util.Map;
  5. public interface OrdersService extends IService<Orders> {
  6. /**
  7. * 生成支付的地址
  8. * @param orderNo
  9. * @return
  10. */
  11. PayCodeUrlResult createPayCodeResult(String orderNo,String username);
  12. }

服务层实现,在com.xuecheng.order.service中新增PayServiceImpl接口如下:

  1. package com.xuecheng.order.service.impl;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  4. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  5. import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
  6. import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderResult;
  7. import com.github.binarywang.wxpay.service.WxPayService;
  8. import com.xuecheng.agent.order.ContentSearchApiAgent;
  9. import com.xuecheng.api.order.model.dto.OrdersDTO;
  10. import com.xuecheng.api.order.model.pay.PayCodeUrlResult;
  11. import com.xuecheng.api.search.model.dto.CoursePubIndexDTO;
  12. import com.xuecheng.common.domain.code.CommonErrorCode;
  13. import com.xuecheng.common.domain.response.RestResponse;
  14. import com.xuecheng.common.enums.common.CommonEnum;
  15. import com.xuecheng.common.enums.order.OrderDealStatusEnum;
  16. import com.xuecheng.common.exception.ExceptionCast;
  17. import com.xuecheng.common.util.PaymentUtil;
  18. import com.xuecheng.common.util.StringUtil;
  19. import com.xuecheng.order.common.constant.OrderErrorCode;
  20. import com.xuecheng.order.convert.OrderConvert;
  21. import com.xuecheng.order.entity.Orders;
  22. import com.xuecheng.order.entity.Pay;
  23. import com.xuecheng.order.mapper.OrdersMapper;
  24. import com.xuecheng.order.service.OrdersService;
  25. import com.xuecheng.order.service.PayService;
  26. import lombok.extern.slf4j.Slf4j;
  27. import org.springframework.beans.factory.annotation.Autowired;
  28. import org.springframework.stereotype.Service;
  29. import org.springframework.transaction.annotation.Transactional;
  30. import org.springframework.util.ObjectUtils;
  31. import java.net.InetAddress;
  32. import java.time.LocalDateTime;
  33. /**
  34. * <p>
  35. * 订单 服务实现类
  36. * </p>
  37. *
  38. * @author itcast
  39. */
  40. @Slf4j
  41. @Service
  42. public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {
  43. @Autowired
  44. private ContentSearchApiAgent searchApiAgent;
  45. @Autowired
  46. private WxPayService wxPayService;
  47. @Autowired
  48. private PayService payService;
  49. /*
  50. PS:该方法不管也什么异常,都需要使用PayCodeUrlResult数据进行封装
  51. * 业务分析:
  52. * 1.判断关键数据
  53. * orderNo username
  54. * 2.判断业务数据
  55. * 订单数据
  56. * 判断是否存在:orderNo username status:初始态
  57. 特别说明:本次的操作由于无法向外抛出异常,是的事务无法回顾:
  58. 1.先操作第三方支付系统
  59. 2.操作本地数据
  60. 3.和wx平台进行交互获得支付链接地址
  61. 4.保存订单支付数据
  62. *
  63. *
  64. * */
  65. @Transactional
  66. public PayCodeUrlResult createPayCodeResult(String orderNo, String username) {
  67. // 1.判断关键数据
  68. // orderNo username
  69. if (StringUtil.isBlank(orderNo)||
  70. StringUtil.isBlank(username)
  71. ) {
  72. return PayCodeUrlResult.failed(CommonErrorCode.E_100101.getDesc());
  73. }
  74. // 2.判断业务数据
  75. // 订单数据
  76. // 判断是否存在:orderNo username status:初始态
  77. LambdaQueryWrapper<Orders> ordersQueryWrapper = new LambdaQueryWrapper<>();
  78. ordersQueryWrapper.eq(Orders::getUserName, username);
  79. ordersQueryWrapper.eq(Orders::getOrderNo, orderNo);
  80. String statusCodeInt = OrderDealStatusEnum.ORDER_DEAL_INIT_STATUS.getCode();
  81. ordersQueryWrapper.eq(Orders::getStatus, statusCodeInt);
  82. int count = this.count(ordersQueryWrapper);
  83. if (count < 1) {
  84. return PayCodeUrlResult.failed(OrderErrorCode.E_160008.getDesc());
  85. }
  86. // 3.和wx平台进行交互获得支付链接地址
  87. // 3.1 创建wx统一下单请求对象
  88. // 根据orders数据进行统一下单
  89. ordersQueryWrapper = new LambdaQueryWrapper<>();
  90. ordersQueryWrapper.eq(Orders::getOrderNo, orderNo);
  91. Orders orders = this.getOne(ordersQueryWrapper);
  92. PayCodeUrlResult urlResult = null;
  93. try {
  94. WxPayUnifiedOrderRequest unifiedOrderRequest = new WxPayUnifiedOrderRequest();
  95. unifiedOrderRequest.setBody("课程名称:"+orders.getCoursePubName());
  96. unifiedOrderRequest.setOutTradeNo(orders.getOrderNo());
  97. Integer yuan = WxPayUnifiedOrderRequest.yuanToFen(orders.getPrice().toString());
  98. unifiedOrderRequest.setTotalFee(yuan);
  99. // 获得程序服务所在服务的ip地址
  100. unifiedOrderRequest.setSpbillCreateIp(InetAddress.getLocalHost().getHostAddress());
  101. unifiedOrderRequest.setProductId("课程id:"+orders.getCoursePubId());
  102. // 3.2 获得响应数据
  103. WxPayUnifiedOrderResult unifiedOrderResult = wxPayService.unifiedOrder(unifiedOrderRequest);
  104. // 3.3 解析响应数据--支付二维码的url
  105. urlResult = null;
  106. String returnCode = unifiedOrderResult.getReturnCode();
  107. String resultCode = unifiedOrderResult.getResultCode();
  108. if (PayCodeUrlResult.WX_PAY_SUCCESS_FLAG.equalsIgnoreCase(returnCode) &&
  109. PayCodeUrlResult.WX_PAY_SUCCESS_FLAG.equalsIgnoreCase(resultCode)
  110. ) {
  111. // 获得支付二维码的url成功
  112. urlResult = PayCodeUrlResult.success(unifiedOrderResult.getCodeURL());
  113. } else {
  114. // 获得支付二维码的url失败
  115. log.error(OrderErrorCode.E_160016.getDesc()+" errCode : {} , errMsg : {}",
  116. unifiedOrderResult.getErrCode(),unifiedOrderResult.getErrCodeDes());
  117. return PayCodeUrlResult.failed(OrderErrorCode.E_160016.getDesc());
  118. }
  119. } catch (Exception e) {
  120. log.error(OrderErrorCode.E_160016.getDesc()+" errMsg : {}",e.getMessage());
  121. return PayCodeUrlResult.failed(OrderErrorCode.E_160016.getDesc());
  122. }
  123. // 4.保存订单支付数据
  124. // 判断订单支付数据是否存在
  125. // orderid username companyid status
  126. LambdaQueryWrapper<Pay> payQueryWrapper = new LambdaQueryWrapper<>();
  127. payQueryWrapper.eq(Pay::getOrderId, orders.getId());
  128. payQueryWrapper.eq(Pay::getUserName, username);
  129. payQueryWrapper.eq(Pay::getCompanyId, orders.getCompanyId());
  130. payQueryWrapper.eq(Pay::getStatus, PayCodeUrlResult.NOT_PAY);
  131. // 如果不存在
  132. // 给订单创建出一个支付数据
  133. // 如果存在
  134. // 不作操作
  135. int payCount = payService.count(payQueryWrapper);
  136. if (payCount < 1) {
  137. Pay pay = new Pay();
  138. pay.setUserName(username);
  139. pay.setCompanyId(orders.getCompanyId());
  140. pay.setOrderId(orders.getId());
  141. pay.setStatus(PayCodeUrlResult.NOT_PAY);
  142. pay.setPayMethod(PayCodeUrlResult.WX_PAY_FLAG);
  143. // 实收金额和付款金额在支付后的通知中再去赋值,本次秩序赋值订单金额即可
  144. pay.setTotalAmount(new BigDecimal(orders.getPrice().toString()));
  145. boolean payResult = payService.save(pay);
  146. if (!payResult) {
  147. return PayCodeUrlResult.failed(OrderErrorCode.E_160017.getDesc());
  148. }
  149. }
  150. return urlResult;
  151. }
  152. }

(2)Controller实现

  1. /**
  2. * <p>
  3. * 订单 前端控制器
  4. * </p>
  5. *
  6. * @author itcast
  7. */
  8. @RestController
  9. public class OrdersController implements OrderApi {
  10. @Autowired
  11. private OrdersService ordersService;
  12. @Autowired
  13. private PayService payService;
  14. @GetMapping("orderPay/wxPay/createPay")
  15. public PayCodeUrlResult createPayCodeResult(String orderNo) {
  16. LoginUser user = UAASecurityUtil.getUser();
  17. ExceptionCast.cast(ObjectUtils.isEmpty(user), CommonErrorCode.E_100108);
  18. return ordersService.createPayCodeResult(orderNo,user.getUsername());
  19. }
  20. }

1.6.6 接口测试


1.启动下面服务
●后端服务
○xc-discover-sever(注册中心)
○xc-uaa-gateway-sever(UAA网关中心)
○xc-user-service(用户中心)
○xc-uaa(认证中心)
○xc-teaching-service(教学管理微服务)
○xc-content-search-service(内容搜索微服务)
○xc-order-service(订单管理微服务)
●其他服务
○Mysql
○Nacos
2.通过 postman 获得令牌信息

Day12-第六章-学生选课-订单支付 - 图42

3.测试接口:
使用postman进行接口测试,请求界面如下:
GET http://127.0.0.1:63010/order/orderPay/wxPay/createPay?orderNo=XCxxxxxx

Day12-第六章-学生选课-订单支付 - 图43

可访问https://cli.im/ ,将codeURL转换为二维码,然后用微信手机客户端扫码支付。

Day12-第六章-学生选课-订单支付 - 图44

使用微信扫描获得的支付界面

Day12-第六章-学生选课-订单支付 - 图45

  1. #spring http 配置信息
  2. server.servlet.context-path = /order
  3. server.port=63090
  4. #spring druid 配置信息
  5. spring.datasource.url = jdbc:mysql://192.168.94.129:3306/xc_order?userUnicode=true&useSSL=false&characterEncoding=utf8
  6. #商户微信公共号或开放平台唯一标识
  7. weixinpay.app-id = wx8397f8696b538317
  8. #商户号
  9. weixinpay.mch-id = 1473426802
  10. #商户密钥
  11. weixinpay.mch-key = T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
  12. #微信回调商户的地址
  13. weixinpay.notify-url = http://www.xuecheng.com/api
  14. #商户的支付类型(NATIVE 为扫码支付)
  15. weixinpay.trade-type = NATIVE