支付模块 - 需求分析、添加选课
文章目录
- 支付模块 - 需求分析、添加选课
- 一、需求分析
- 1.1 选课业务流程
- 1.2 支付业务流程
- 1.3 在线学习业务流程
- 1.4 课程续期业务流程
- 二、添加选课
- 2.1 执行流程
- 2.2 数据模型
- 2.2.1 选课记录表 choose_course
- 2.2.2 用户课程表 course_tables
- 2.3 查询课程基本信息
- 2.3.1 content服务 - 查询课程发布信息
- 2.3.2 开启Feign - 内容管理远程接口
- 2.3.3 课程发布表Dto - 特别处理
- 2.4 选课
- 2.4.0 扩展类
- 2.4.1 MyCourseTablesController 接口
- 2.4.2 MyCourseTablesServiceImpl 实现类
- 2.4.3 测试
- 2.5 查询学习资格
一、需求分析
实现了学生选课、下单支付、学习的整体流程
网站的课程有免费和收费两种,对于免费课程学生选课后可直接学习,对于收费课程学生需要下单且支付成功方可选课、学习
选课:是将课程加入我的课程表的过程
我的课程表:记录我在网站学习的课程,我的课程表中有免费课程和收费课程两种,对于免费课程可直接添加到我的课程表,对于收费课程需要下单、支付成功后自动加入我的课程表
模块整体流程:
1.1 选课业务流程
用户通过搜索课程、课程推荐等信息进入课程详情页面,点击“马上学习” 引导进入学习界面去学习
具体流程如下图所示
- 进入课程详情点击马上学习
- 课程免费时引导加入我的课程表、或进入学习界面
- 课程收费时引导去支付、或试学
选课是将课程加入我的课程表的过程。
对免费课程选课后可直接加入我的课程表,对收费课程选课后需要下单支付成功系统自动加入我的课程表
1.2 支付业务流程
通过下面的图,我们就能发现在支付前的操作就是选课
1.3 在线学习业务流程
选课成功用户可以在线学习,对于免费课程无需选课即可在线学习
1.4 课程续期业务流程
免费课程加入我的课程表默认为1年有效期,到期用户可申请续期
二、添加选课
新建learning工程完成选课操作
2.1 执行流程
选课是将课程加入我的课程表的过程,根据选课的业务流程进行详细分析,业务流程
选课信息存入选课记录表
选课记录表:记录了什么人在什么时候选择了哪一门课程
如果选择的课程是免费的,那么在选课记录表中,选课状态就是成功,并且此课程已经加入到课表中了
如果选择的课程是收费的,那么在选课记录表中,选课状态就是待支付,等待支付成功后,此课程会加入到课表中
免费课程被选课除了进入选课记录表同时进入我的课程表
收费课程进入选课记录表后需要经过下单、支付成功才可以进入我的课程表
收费课程和免费课程的区别就是,收费课程多了一步付款而已
在学习引导处,可以直接将免费课程加入我的课程表,如下图
对于收费课程先创建选课记录表,支付成功后,收到支付结果由系统自动加入我的课程表
执行流程如下
2.2 数据模型
2.2.1 选课记录表 choose_course
order_type 选课类型:是免费还是收费
status 选课状态:此课程是选课成功还是待支付、选课删除
此表的作用简单的说:什么人在什么时间选择了哪门课,并且选择课程的状态是选课成功还是待支付
对于免费课程:课程价格为0,有效期默认365,开始服务时间为选课时间,结束服务时间为选课时间加1年后的时间,选课状态为选课成功
对于收费课程:按课程的现价、有效期确定开始服务时间、结束服务时间,选课状态为待支付
收费课程的选课记录需要支付成功后选课状态为成功
@Data
@TableName("xc_choose_course")
public class XcChooseCourse implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 课程id*/private Long courseId;/*** 课程名称*/private String courseName;/*** 用户id*/private String userId;/*** 机构id*/private Long companyId;/*** 选课类型*/private String orderType;/*** 添加时间*/@TableField(fill = FieldFill.INSERT)private LocalDateTime createDate;/*** 课程有效期(天)*/private Integer validDays;private Float coursePrice;/*** 选课状态*/private String status;/*** 开始服务时间*/private LocalDateTime validtimeStart;/*** 结束服务时间*/private LocalDateTime validtimeEnd;/*** 备注*/private String remarks;}
2.2.2 用户课程表 course_tables
课程表的数据来源于选课记录表
对于免费课程创建选课记录后同时向我的课程表添加记录
对于收费课程创建选课记录后需要下单支付成功后自动向我的课程表添加记录
choose_course_id字段其实就是某个选课记录choose_course表中的主键
@Data
@TableName("xc_course_tables")
public class XcCourseTables implements Serializable {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 选课记录id*/private Long chooseCourseId;/*** 用户id*/private String userId;/*** 课程id*/private Long courseId;/*** 机构id*/private Long companyId;/*** 课程名称*/private String courseName;/*** 课程名称*/private String courseType;/*** 添加时间*/@TableField(fill = FieldFill.INSERT)private LocalDateTime createDate;/*** 开始服务时间*/private LocalDateTime validtimeStart;/*** 到期时间*/private LocalDateTime validtimeEnd;/*** 更新时间*/private LocalDateTime updateDate;/*** 备注*/private String remarks;}
2.3 查询课程基本信息
根据之前的流程,首先我们要查询一下课程信息,主要是想知道某个课程是收费的还是免费的
知道课程基本信息后,我们就能确定收费怎么做、免费怎么做
查询课程基本信息的操作我们要在学习中心learning服务远程调用内容管理content服务
之后content服务会查询数据库中课程发布表,看看此课程是否已经发布以及课程的收费规则
2.3.1 content服务 - 查询课程发布信息
content服务中新增接口 - 查询发布表中某个课程基本信息
内容管理服务提供查询课程信息接口,此接口从课程发布表查询
此接口主要提供其它微服务远程调用,所以此接口不用授权,本项目标记此类接口统一以 /r开头
将来会在白名单中配置
@ApiOperation("查询课程发布信息")
@ResponseBody
@GetMapping("/r/coursepublish/{courseId}")
public CoursePublish getCoursepublish(@PathVariable("courseId") Long courseId) {CoursePublish coursePublish = coursePublishService.getCoursePublish(courseId);return coursePublish;
}
/*** 查询课程发布信息** @param courseId 课程id* @return*/
@Override
public CoursePublish getCoursePublish(Long courseId) {return coursePublishMapper.selectById(courseId);
}
2.3.2 开启Feign - 内容管理远程接口
在learning-service模块添加Feign
/*** @description 内容管理远程接口*/
@FeignClient(value = "content-api",fallbackFactory = ContentServiceClientFallbackFactory.class)
public interface ContentServiceClient {@ResponseBody@GetMapping("/content/r/coursepublish/{courseId}")public CoursePublish getCoursepublish(@PathVariable("courseId") Long courseId);}
做好熔断降级处理
@Slf4j
@Component
public class ContentServiceClientFallbackFactory implements FallbackFactory<ContentServiceClient> {@Overridepublic ContentServiceClient create(Throwable throwable) {return new ContentServiceClient() {@Overridepublic CoursePublish getCoursepublish(Long courseId) {log.error("调用内容管理服务发生熔断:{}", throwable.toString(),throwable);return null;}};}
}
可以使用下面的代码远程调用一下content服务中的查询课程发布信息接口
@SpringBootTest
public class FeignClientTest {@AutowiredContentServiceClient contentServiceClient;@Testpublic void testContentServiceClient() {CoursePublish coursepublish = contentServiceClient.getCoursepublish(18L);Assertions.assertNotNull(coursepublish);}
}
2.3.3 课程发布表Dto - 特别处理
在进行feign远程调用时会将字符串转成LocalDateTime,
在CoursePublish 类中LocalDateTime的属性上边添加如下代码:
@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd HH:mm:ss")
import com.fasterxml.jackson.annotation.JsonFormat;@Data
@TableName("course_publish")
public class CoursePublish implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/private Long id;/*** 机构ID*/private Long companyId;/*** 公司名称*/private String companyName;/*** 课程名称*/private String name;/*** 适用人群*/private String users;/*** 标签*/private String tags;/*** 创建人*/private String username;/*** 大分类*/private String mt;/*** 大分类名称*/private String mtName;/*** 小分类*/private String st;/*** 小分类名称*/private String stName;/*** 课程等级*/private String grade;/*** 教育模式*/private String teachmode;/*** 课程图片*/private String pic;/*** 课程介绍*/private String description;/*** 课程营销信息,json格式*/private String market;/*** 所有课程计划,json格式*/private String teachplan;/*** 教师信息,json格式*/private String teachers;/*** 发布时间*/@TableField(fill = FieldFill.INSERT)@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime createDate;/*** 上架时间*/@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime onlineDate;/*** 下架时间*/@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime offlineDate;/*** 发布状态*/private String status;/*** 备注*/private String remark;/*** 收费规则,对应数据字典--203*/private String charge;/*** 现价*/private Float price;/*** 原价*/private Float originalPrice;/*** 课程有效期天数*/private Integer validDays;}
之前我们没使用上面的注解是因为使用Http请求的接口,但是我们已经把序列化和反序列化的相关配置都配置好了,如下图所示
使用的都是Jackson的方式
不管是序列化还是反序列化,我们的时间类型都是yyyy-MM-dd HH:mm:ss格式
但是Feign远程调用的时候,下面的配置就不会生效了
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;@Configuration
public class LocalDateTimeConfig {/** 序列化内容* LocalDateTime -> String* 服务端返回给客户端内容* */@Beanpublic LocalDateTimeSerializer localDateTimeSerializer() {return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));}/** 反序列化内容* String -> LocalDateTime* 客户端传入服务端数据* */@Beanpublic LocalDateTimeDeserializer localDateTimeDeserializer() {return new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));}// 配置@Beanpublic Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {return builder -> {builder.serializerByType(LocalDateTime.class, localDateTimeSerializer());builder.deserializerByType(LocalDateTime.class, localDateTimeDeserializer());};}}
2.4 选课
也就是下图中圈出来的这一步
以及下面这一步
2.4.0 扩展类
@Data
@ToString
public class XcChooseCourseDto extends XcChooseCourse {//学习资格,[{"code":"702001","desc":"正常学习"},{"code":"702002","desc":"没有选课或选课后没有支付"},{"code":"702003","desc":"已过期需要申请续期或重新支付"}]public String learnStatus;}
@Data
@ToString
public class XcCourseTablesDto extends XcCourseTables {//学习资格,[{"code":"702001","desc":"正常学习"},{"code":"702002","desc":"没有选课或选课后没有支付"},{"code":"702003","desc":"已过期需要申请续期或重新支付"}]public String learnStatus;
}
2.4.1 MyCourseTablesController 接口
@ApiOperation("添加选课")@PostMapping("/choosecourse/{courseId}")public XcChooseCourseDto addChooseCourse(@PathVariable("courseId") Long courseId) {// 调用工具类,拿到当前操作的用户SecurityUtil.XcUser user = SecurityUtil.getUser();if (user == null){XueChengPlusException.cast("用户未登录");}String userId = user.getId();// 添加选课XcChooseCourseDto xcChooseCourseDto = myCourseTablesService.addChooseCourse(userId, courseId);return xcChooseCourseDto;}
2.4.2 MyCourseTablesServiceImpl 实现类
/*** 选课相关操作*/
@Slf4j
@Service
public class MyCourseTablesServiceImpl implements MyCourseTablesService {@AutowiredXcChooseCourseMapper xcChooseCourseMapper;@AutowiredXcCourseTablesMapper xcCourseTablesMapper;@AutowiredContentServiceClient contentServiceClient;@Transactional@Overridepublic XcChooseCourseDto addChooseCourse(String userId, Long courseId) {// 1.Feign远程调用内容管理服务查询课程收费规则(从发布表中查询对应的课程是收费还是免费)CoursePublish coursepublish = contentServiceClient.getCoursepublish(courseId);if (coursepublish == null) {XueChengPlusException.cast("课程不存在");}// 课程收费规则(是否收费)String charge = coursepublish.getCharge();// 选课记录XcChooseCourse chooseCourse = null;// 2.免费课程:向选课记录表、我的课程表中写入数据(课程表的数据来源于选课记录表)if ("201000".equals(charge)) {// 向选课记录表中写入chooseCourse = addFreeCourse(userId, coursepublish);// 向课程表中写入XcCourseTables xcCourseTables = addCourseTables(chooseCourse);} else {// 3.收费课程:向炫酷记录表写入数据,等待用户支付完成后再向课程表中写入数据// 此模块不会向课程表中添加记录了chooseCourse = addChargeCourse(userId, coursepublish);}// 4.判断学生目前对此课程是否具有学习资格,并且要将此学习资格返回XcCourseTablesDto courseTablesDto = getLearningStatus(userId, courseId);// 构造返回值XcChooseCourseDto xcChooseCourseDto = new XcChooseCourseDto();BeanUtils.copyProperties(chooseCourse, xcChooseCourseDto);xcChooseCourseDto.setLearnStatus(courseTablesDto.getLearnStatus());return xcChooseCourseDto;}//添加免费课程,免费课程加入选课记录表public XcChooseCourse addFreeCourse(String userId, CoursePublish coursepublish) {// 不一定是添加,因为可能会有人多次点击“添加课程/学习课程”之类的按钮// 如果此课程已经被此用户选择了且选课的状态为成功,那就不允许用户再选择,直接返回结果即可LambdaQueryWrapper<XcChooseCourse> lqw = new LambdaQueryWrapper<>();// 哪一位用户lqw.eq(XcChooseCourse::getUserId, userId)// 课程id.eq(XcChooseCourse::getCourseId, coursepublish.getId())// 课程类型为免费课程.eq(XcChooseCourse::getOrderType, "700001")// 选课成功.eq(XcChooseCourse::getStatus, "701001");List<XcChooseCourse> xcChooseCourses = xcChooseCourseMapper.selectList(lqw);if (xcChooseCourses.size() > 0) {return xcChooseCourses.get(0);}// 运行到这里说明数据库中没有对应的选课记录,添加一份即可XcChooseCourse chooseCourse = new XcChooseCourse();chooseCourse.setCourseId(coursepublish.getId()); //课程idchooseCourse.setCourseName(coursepublish.getName()); //课程名称chooseCourse.setCoursePrice(coursepublish.getPrice());//免费课程价格为0chooseCourse.setUserId(userId); //用户名chooseCourse.setCompanyId(coursepublish.getCompanyId());//机构idchooseCourse.setOrderType("700001");//免费课程代码标识chooseCourse.setCreateDate(LocalDateTime.now()); //创建时间chooseCourse.setStatus("701001");//选课成功,选课状态标识chooseCourse.setValidDays(365);//免费课程默认365chooseCourse.setValidtimeStart(LocalDateTime.now());// 课程开始时间chooseCourse.setValidtimeEnd(LocalDateTime.now().plusDays(365)); //课程结束时间int insert = xcChooseCourseMapper.insert(chooseCourse);if (insert <= 0) {XueChengPlusException.cast("添加课程失败");}return chooseCourse;}//添加收费课程public XcChooseCourse addChargeCourse(String userId, CoursePublish coursepublish) {// 不一定是添加,因为可能会有人多次点击“添加课程/学习课程”之类的按钮// 查询选课表中,是否有此收费课程在选课记录表中的选课状态为待支付LambdaQueryWrapper<XcChooseCourse> lqw = new LambdaQueryWrapper<>();// 哪一位用户lqw.eq(XcChooseCourse::getUserId, userId)// 课程id.eq(XcChooseCourse::getCourseId, coursepublish.getId())// 课程类型为收费课程.eq(XcChooseCourse::getOrderType, "700002")// 状态不是选课成功,而是待支付.eq(XcChooseCourse::getStatus, "701002");List<XcChooseCourse> xcChooseCourses = xcChooseCourseMapper.selectList(lqw);if (xcChooseCourses.size() > 0) {return xcChooseCourses.get(0);}// 运行到这里说明数据库中没有对应的选课记录,添加一份即可XcChooseCourse chooseCourse = new XcChooseCourse();chooseCourse.setCourseId(coursepublish.getId()); //课程idchooseCourse.setCourseName(coursepublish.getName()); //课程名称chooseCourse.setCoursePrice(coursepublish.getPrice());//免费课程价格为0chooseCourse.setUserId(userId); //用户名chooseCourse.setCompanyId(coursepublish.getCompanyId());//机构idchooseCourse.setOrderType("700002");//收费课程代码标识chooseCourse.setCreateDate(LocalDateTime.now()); //创建时间chooseCourse.setStatus("701002");//选课成功,选课状态标识chooseCourse.setValidDays(365);//免费课程默认365chooseCourse.setValidtimeStart(LocalDateTime.now());// 课程开始时间chooseCourse.setValidtimeEnd(LocalDateTime.now().plusDays(365)); //课程结束时间int insert = xcChooseCourseMapper.insert(chooseCourse);if (insert <= 0) {XueChengPlusException.cast("添加课程失败");}return chooseCourse;}//添加到我的课程表(同一个人同一门课只会有同一条记录,因为这里我们已经在数据库添加约束了)public XcCourseTables addCourseTables(XcChooseCourse xcChooseCourse) {//选课记录完成且未过期可以添加课程到课程表String status = xcChooseCourse.getStatus();if (!"701001".equals(status)) {// 701001代表选课完成,其他状态都代表未完成XueChengPlusException.cast("选课未成功,无法添加到课程表");}XcCourseTables xcCourseTables = getXcCourseTables(xcChooseCourse.getUserId(), xcChooseCourse.getCourseId());if (xcCourseTables != null) {// 说明课程已经在课程表中了return xcCourseTables;}xcCourseTables = new XcCourseTables();xcCourseTables.setChooseCourseId(xcChooseCourse.getId()); // 选课表中的主键xcCourseTables.setUserId(xcChooseCourse.getUserId());xcCourseTables.setCourseId(xcChooseCourse.getCourseId());xcCourseTables.setCompanyId(xcChooseCourse.getCompanyId());xcCourseTables.setCourseName(xcChooseCourse.getCourseName());xcCourseTables.setCreateDate(LocalDateTime.now());xcCourseTables.setValidtimeStart(xcChooseCourse.getValidtimeStart());xcCourseTables.setValidtimeEnd(xcChooseCourse.getValidtimeEnd());xcCourseTables.setCourseType(xcChooseCourse.getOrderType());int insert = xcCourseTablesMapper.insert(xcCourseTables);if (insert <= 0) {XueChengPlusException.cast("课程添加到课程表失败");}return xcCourseTables;}/*** @param userId* @param courseId* @return com.xuecheng.learning.model.po.XcCourseTables* @description 根据课程和用户查询我的课程表中某一门课程*/public XcCourseTables getXcCourseTables(String userId, Long courseId) {LambdaQueryWrapper<XcCourseTables> lqw = new LambdaQueryWrapper<>();lqw.eq(XcCourseTables::getUserId, userId).eq(XcCourseTables::getCourseId, courseId);return xcCourseTablesMapper.selectOne(lqw);}/*** 查询课程表** @param userId* @param courseId* @return XcCourseTablesDto 学习资格状态 [{"code":"702001","desc":"正常学习"},{"code":"702002","desc":"没有选课或选课后没有支付"},{"code":"702003","desc":"已过期需要申请续期或重新支付"}]* @description 判断学习资格*/@Overridepublic XcCourseTablesDto getLearningStatus(String userId, Long courseId) {// 查询我的课程表XcCourseTables xcCourseTables = getXcCourseTables(userId, courseId);if (xcCourseTables == null) {// 如果查不到,说明没有选课或者选课后未支付XcCourseTablesDto xcCourseTablesDto = new XcCourseTablesDto();xcCourseTablesDto.setLearnStatus("702002");return xcCourseTablesDto;}//如果有记录,判断是否过期,如果过期了就不能学习,如果没过期可以正常学习XcCourseTablesDto xcCourseTablesDto = new XcCourseTablesDto();BeanUtils.copyProperties(xcCourseTables, xcCourseTablesDto);//是否过期,true过期,false未过期boolean isExpires = xcCourseTables.getValidtimeEnd().isBefore(LocalDateTime.now());if (!isExpires) {//正常学习xcCourseTablesDto.setLearnStatus("702001");return xcCourseTablesDto;} else {//已过期xcCourseTablesDto.setLearnStatus("702003");return xcCourseTablesDto;}}
}
2.4.3 测试
2.5 查询学习资格
我们只需要写一个Controller接口就行了,具体的实现其实在2.4.2中实现了
@ApiOperation("查询学习资格")
@PostMapping("/choosecourse/learnstatus/{courseId}")
public XcCourseTablesDto getLearnstatus(@PathVariable("courseId") Long courseId) {SecurityUtil.XcUser user = SecurityUtil.getUser();if (user == null) {XueChengPlusException.cast("用户未登录");}String userId = user.getId();return myCourseTablesService.getLearningStatus(userId, courseId);}boolean isExpires = xcCourseTables.getValidtimeEnd().isBefore(LocalDateTime.now());if (!isExpires) {//正常学习xcCourseTablesDto.setLearnStatus("702001");return xcCourseTablesDto;} else {//已过期xcCourseTablesDto.setLearnStatus("702003");return xcCourseTablesDto;}}
}