添加选课
界面原型
第一步:用户通过搜索课程、课程推荐等信息进入课程详情页面,点击马上学习
进行学习
第二步:课程免费时可以直接加入我的课程表并且免费课程可以直接在线学习,免费课程默认一年有效期,到期需要申请续期
第三步:课程收费时需要下单支付,支付成功后会自动加入我的课程表
数据模型
选课记录表(xc_choose_couse)
:将课程添加到课程表时会先创建对应的选课记录,数据来源于课程发布表
-
免费课程
:选课状态默认为选课成功, 课程价格为0,有效期默认365天,开始服务时间为选课时间,结束服务时间为选课时间加1年后的时间 -
收费课程
:选课状态默认为待支付,课程价格为课程的现价,开始服务时间为支付成功时间,有效期由用户决定,结束服务时间为选课时间加有效期
我的课程表(xc_couse_table)
: 记录了用户选课成功的课程(免费课程和已经支付的收费课程),我的课程表的数据来源于选课记录表
-
选择免费课程: 创建完选课记录后同时向我的课程表添加选课信息
-
选择收费课程: 创建完选课记录后需要下单且支付成功后会自动向我的课程表添加选课信息
请求响应模型类
请求参数
:课程id、当前用户id
响应模型类
: 除了包含用户的选课记录信息
还有用户的学习资格
@Data
@ToString
public class XcChooseCourseDto extends XcChooseCourse {
//学习资格
/* [{"code":"702001","desc":"正常学习"},
{"code":"702002","desc":"没有选课或选课后没有支付"},
{"code":"702003","desc":"已过期需要申请续期或重新支付"}]
*/
public String learnStatus;
}
环境搭建
第一步: 在项目工程根目录下创建工程xuecheng_plus_learning
及其对应的数据库xc_learning
第二步: 在本地创建bootstrap.yml
文件
spring:
application:
name: learning-api
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: dev
group: xuecheng-plus-project
config:
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
extension-configs:
- data-id: learning-service-${spring.profiles.active}.yaml
group: xuecheng-plus-project
refresh: true
shared-configs:
- data-id: swagger-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: logging-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: feign-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: rabbitmq-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
profiles:
active: dev
第三步: 在Nacos的dev环境下创建远程配置文件learning-api-dev.yaml
和learning-service-dev.yaml
server:
servlet:
context-path: /learning
port: 63020
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/xc_learning?serverTimezone=UTC&userUnicode=true&useSSL=false&
username: root
password: 123456
第四步:在nacos上的gateway-dev.yaml
添加路由到学习中心服务的配置
- id: learning-api
uri: lb://learning-api
predicates:
- Path=/learning/**
选课流程
网站的课程有免费和收费两种,学生选课是将课程加入我的课程表的过程
免费课程
: 学生选课后可直接加入我的课程表直接学习收费课程
: 学生需要下单且支付成功后会自动加入我的课程表
课程发布信息查询接口
第一步:在内容管理服务
提供查询课程信息接口
从课程发布表
查询课程最终的发布信息,此接口主要其它微服务远程调用所以不用授权(本项目标记此类接口统一以/r
开头)
@Slf4j
@RestController
public class CoursePublishController {
@Autowired
private CoursePublishService coursePublishService;
@ApiOperation("查询课程发布信息")
@ResponseBody
@GetMapping("/r/coursepublish/{courseId}")
public CoursePublish getCoursepublish(@PathVariable("courseId") Long courseId) {
CoursePublish coursePublish = coursePublishService.getCoursePublish(courseId);
return coursePublish;
}
}
第二步:定义CoursePublishService
接口及其实现类查询课程发布信息
CoursePublish getCoursePublish(Long courseId);
@Slf4j
@Service
public class CoursePublishServiceImpl implements CoursePublishService {
@Autowired
private CoursePublishMapper coursePublishMapper;
public CoursePublish getCoursePublish(Long courseId){
CoursePublish coursePublish = coursePublishMapper.selectById(courseId);
return coursePublish;
}
}
第三步:由于是在网关处校验令牌的合法性,在微服务处不再校验令牌的合法性,修改内容管理服务content-api
工程的ResouceServerConfig
类屏蔽authenticated()
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 禁用CSRF保护
.authorizeRequests()// 配置对请求的授权策略
// authenticated表示指定"/r/"和"/course/"这两个路径需要进行身份认证才能访问
//.antMatchers("/r/**","/course/**").authenticated()
.anyRequest().permitAll();// 允许除了上面指定的路径之外的其他请求都可以被访问,不需要进行身份认证
}
第四步:启动内容管理服务,使用httpclient测试查询课程发布信息的接口
### 查询课程发布信息
GET {{content_host}}/content/r/coursepublish/2
远程调用查询课程信息接口
第一步:在学习中心模块的启动类上添加@EnableFeignClients(basePackages={“com.xuecheng.*.feignclient”})
@EnableFeignClients(basePackages = {"com.xuecheng.*.feignclient"})
@SpringBootApplication
public class LearningApiApplication {
public static void main(String[] args) {
SpringApplication.run(LearningApiApplication.class, args);
}
}
第二步:在学习中心模块(learning)
的service工程
中添加Feign接口feignclient/ContentServiceClient
和降级方法feignclient/ContentServiceClientFallbackFactory
,远程调用内容管理模块
提供的查询课程发布信息的接口
@FeignClient(value = "content-api", fallbackFactory = ContentServiceClientFallbackFactory.class)
@RequestMapping("/content")
public interface ContentServiceClient {
@GetMapping("/r/coursepublish/{courseId}")
CoursePublish getCoursePublish(@PathVariable("courseId") Long courseId);
}
@Slf4j
@Component
public class ContentServiceClientFallbackFactory implements FallbackFactory<ContentServiceClient> {
@Override
public ContentServiceClient create(Throwable throwable) {
return new ContentServiceClient() {
@Override
public CoursePublish getCoursePublish(Long courseId) {
log.error("远程调用内容管理服务熔断异常:{}",throwable.getMessage());
return new CoursePublish();
}
};
}
}
第三步:在进行Feign远程调用时得到的是json字符串,还需要转化成CoursePublish
类型的对象,将字符串的日期格式转成LocalDateTime
类型的属性时需要指定时间格式
@Data
@TableName("course_publish")
public class CoursePublish implements Serializable {
private static final long serialVersionUID = 1L;
// ......
/**
* 发布时间
*/
@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;
}
第四步:编写测试类FeignClientTest
进行远程调用
/**
* @description Feign接口测试类
* @author Mr.M
*/
@SpringBootTest
public class FeignClientTest {
@Autowired
ContentServiceClient contentServiceClient;
@Test
public void testContentServiceClient(){
CoursePublish coursepublish = contentServiceClient.getCoursepublish(18L);
Assertions.assertNotNull(coursepublish);
}
}
添加免费/收费课程
第一步:在学习中心服务的接口工程中的MyCourseTablesController
中提供根据课程Id添加选课(免费和收费)
的接口
@Api(value = "我的课程表接口", tags = "我的课程表接口")
@Slf4j
@RestController
public class MyCourseTablesController {
@Autowired
MyCourseTablesService myCourseTablesService;
@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();
return myCourseTablesService.addChooseCourse(userId, courseId);
}
}
第二步:编写MyCourseTablesService
接口及其实现类
选择免费课程
:添加选课记录同时添加到我的课程表选择收费课程
:只添加选课记录,下单且支付成功后再添加到我的课程表
public interface MyCourseTablesService {
/**
* 添加选课
* @param userId 用户id
* @param courseId 课程id
*/
XcChooseCourseDto addChooseCourse(String userId, Long courseId);
}
@Slf4j
@Service
@Transactional
public class MyCourseTablesServiceImpl implements MyCourseTablesService {
@Autowired
ContentServiceClient contentServiceClient;
@Autowired
XcChooseCourseMapper chooseCourseMapper;
@Autowired
XcCourseTablesMapper courseTablesMapper;
@Override
@Transactional
public XcChooseCourseDto addChooseCourse(String userId, Long courseId) {
// 1. 调用内容管理服务提供的查询课程发布信息接口,查询课程收费规则
CoursePublish coursePublish = contentServiceClient.getCoursePublish(courseId);
if (coursePublish == null) {
XueChengPlusException.cast("课程不存在");
}
// 获取收费规则
String charge = coursePublish.getCharge();
XcChooseCourse chooseCourse = null;
if ("201000".equals(charge)) {
// 2. 如果是免费课程,向选课记录表、我的课程表添加数据
log.info("添加免费课程..");
// 将免费课程添加到选课记录表,数据来源于课程发布表
chooseCourse = addFreeCourse(userId, coursePublish);
// 将免费课程添加到我的课程表,数据来源于选课记录表
addCourseTables(chooseCourse);
} else {
// 3. 如果是收费课程,只添加选课记录,下单且支付成功后再添加到我的课程表
log.info("添加收费课程");
chooseCourse = addChargeCourse(userId, coursePublish);
}
// 4. 获取学生的学习资格
XcCourseTablesDto courseTablesDto = getLearningStatus(userId, courseId);
// 5. 封装返回值包含选课信息和用户的学习资格
XcChooseCourseDto chooseCourseDto = new XcChooseCourseDto();
BeanUtils.copyProperties(chooseCourse, chooseCourseDto);
// 设置学习资格的状态
chooseCourseDto.setLearnStatus(courseTablesDto.learnStatus);
return chooseCourseDto;
}
}
第三步:将免费
课程加入到选课记录表,课程信息数据来源于课程发布表
,避免用户重复添加课程的情况减少脏数据
/**
* 将免费课程加入到选课表
*
* @param userId 用户id
* @param coursePublish 课程发布信息
* @return 选课记录
*/
@Transactional
public XcChooseCourse addFreeCourse(String userId, CoursePublish coursePublish) {
// 1. 先判断是否已经存在对应的选课记录(免费且选课成功),因为数据库中没有约束,所以可能存在同一个人对同一门免费课程重复添加多条相同的选课记录
LambdaQueryWrapper<XcChooseCourse> lambdaQueryWrapper = new LambdaQueryWrapper<XcChooseCourse>()
.eq(XcChooseCourse::getUserId, userId)
.eq(XcChooseCourse::getCourseId, coursePublish.getId())
.eq(XcChooseCourse::getOrderType, "700001") // 免费课程
.eq(XcChooseCourse::getStatus, "701001");// 选课成功
// 1.1 由于可能存在多条,所以这里用selectList
List<XcChooseCourse> chooseCourses = chooseCourseMapper.selectList(lambdaQueryWrapper);
// 1.2 如果已经存在对应的选课数据,返回一条即可
if (!chooseCourses.isEmpty()) {
return chooseCourses.get(0);
}
// 2. 数据库中不存在数据,添加选课信息,对照着数据库中的属性挨个set即可
XcChooseCourse chooseCourse = new XcChooseCourse();
chooseCourse.setCourseId(coursePublish.getId());
chooseCourse.setCourseName(coursePublish.getName());
chooseCourse.setUserId(userId);
chooseCourse.setCompanyId(coursePublish.getCompanyId());
chooseCourse.setOrderType("700001");
chooseCourse.setStatus("701001");
xcChooseCourse.setCoursePrice(0f);//免费课程价格为0
chooseCourse.setValidDays(365);
chooseCourse.setValidtimeStart(LocalDateTime.now());
chooseCourse.setValidtimeEnd(LocalDateTime.now().plusDays(365));
chooseCourse.setCreateDate(LocalDateTime.now());
int insert = chooseCourseMapper.insert(chooseCourse);
if (insert <= 0) {
XueChengPlusException.cast("添加选课记录失败");
}
return chooseCourse;
}
第四步:将免费
课程添加到我的课程表(用户Id和课程Id有唯一索引约束),课程信息数据来源于选课记录表
- 如果我的课程表已经存在课程, 课程可能已经过期
- 如果有新的选课记录,则需要更新我的课程表中的现有信息
/**
* 添加到我的课程表
*
* @param chooseCourse 选课记录
*/
@Transactional
public XcCourseTables addCourseTables(XcChooseCourse chooseCourse) {
// 选课成功的课程才可以添加到我的课程表
String status = chooseCourse.getStatus();
if (!"701001".equals(status)) {
XueChengPlusException.cast("选课未成功,无法添加到课程表");
}
// 根据用户id和课程id查询我的课程表中的某一门课程
XcCourseTables courseTables = getXcCourseTables(chooseCourse.getUserId(), chooseCourse.getCourseId());
if (courseTables != null) {
return courseTables;
}
courseTables = new XcCourseTables();
BeanUtils.copyProperties(chooseCourse, courseTables);
// 记录选课Id
courseTables.setChooseCourseId(chooseCourse.getId());
// 课程类型即选课类型
courseTables.setCourseType(chooseCourse.getOrderType());
// 课程过期后需要记录更新课程的时间
courseTables.setUpdateDate(LocalDateTime.now());
int insert = courseTablesMapper.insert(courseTables);
if (insert <= 0) {
XueChengPlusException.cast("添加我的课程表失败");
}
return courseTables;
}
/**
* 根据用户id和课程id查询我的课程表中的某一门课程
*
* @param userId 用户id
* @param courseId 课程id
* @return 我的课程表中的课程
*/
public XcCourseTables getXcCourseTables(String userId, Long courseId) {
// 用户Id和课程Id有唯一索引约束,对于同一门课程同一个用户只能添加一次,最终只能查到一条数据
return courseTablesMapper.selectOne(new LambdaQueryWrapper<XcCourseTables>()
.eq(XcCourseTables::getUserId, userId)
.eq(XcCourseTables::getCourseId, courseId));
}
添加收费课程与添加免费课程几乎没有区别,只是选课状态和选课类型
不同
- 添加收费课程到选课记录同样需要避免用户重复添加课程的情况减少脏数据
- 只添加选课记录,下单且支付成功后再添加到我的课程表
/**
* 将付费课程加入到选课记录表
*
* @param userId 用户id
* @param coursePublish 课程发布信息
* @return 选课记录
*/
@Transactional
public XcChooseCourse addChargeCourse(String userId, CoursePublish coursePublish) {
// 1. 先判断是否已经存在对应的选课(收费且待支付的课程),因为数据库中没有约束,所以可能存在相同数据的选课
LambdaQueryWrapper<XcChooseCourse> lambdaQueryWrapper = new LambdaQueryWrapper<XcChooseCourse>()
.eq(XcChooseCourse::getUserId, userId)
.eq(XcChooseCourse::getCourseId, coursePublish.getId())
.eq(XcChooseCourse::getOrderType, "700002") // 收费课程
.eq(XcChooseCourse::getStatus, "701002");// 待支付
// 1.1 由于可能存在多条,所以这里用selectList
List<XcChooseCourse> chooseCourses = chooseCourseMapper.selectList(lambdaQueryWrapper);
// 1.2 如果已经存在对应的选课数据,返回一条即可
if (!chooseCourses.isEmpty()) {
return chooseCourses.get(0);
}
// 2. 数据库中不存在数据,添加选课信息
XcChooseCourse chooseCourse = new XcChooseCourse();
// 课程发布Id
chooseCourse.setCourseId(coursePublish.getId());
chooseCourse.setCourseName(coursePublish.getName());
chooseCourse.setUserId(userId);
chooseCourse.setCompanyId(coursePublish.getCompanyId());
chooseCourse.setOrderType("700002");
chooseCourse.setStatus("701002");
chooseCourse.setCoursePrice(coursePublish.getPrice());
chooseCourse.setValidDays(365);
chooseCourse.setValidtimeStart(LocalDateTime.now());
chooseCourse.setValidtimeEnd(LocalDateTime.now().plusDays(365));
chooseCourse.setCreateDate(LocalDateTime.now());
int insert = chooseCourseMapper.insert(chooseCourse);
if (insert<=0){
XueChengPlusException.cast("添加选课记录失败");
}
return chooseCourse;
}
获取学习资格
我们点击马上学习
时会通过查询用户的我的课程表
判断用户对该课程的学习资格
@Api(value = "我的课程表接口", tags = "我的课程表接口")
@Slf4j
@RestController
public class MyCourseTablesController {
@Autowired
MyCourseTablesService myCourseTablesService;
@ApiOperation("查询学习资格")
@PostMapping("/choosecourse/learnstatus/{courseId}")
public XcCourseTablesDto getLearnstatus(@PathVariable Long courseId) {
SecurityUtil.XcUser user = SecurityUtil.getUser();
if (user == null) {
XueChengPlusException.cast("请登录后继续选课");
}
String userId = user.getId();
return myCourseTablesService.getLearningStatus(userId, courseId);
}
}
用户的学习资格是从我的课程表
中某个课程中获取的,所以需要先判断课程表中对应的课程是否存在
- 如果查不到说明用户没有选课或选课后没有支付导致选课不成功,返回状态码为
702002
的对象 - 如果查到了选课成功的记录还要判断课程是否过期,如果过期返回状态码为
702003
的对象**
public interface MyCourseTablesService {
/**
* 获取学习资格
* @param userId 用户id
* @param courseId 课程id
* @return 学习资格状态
*/
XcCourseTablesDto getLearningStatus(String userId, Long courseId);
}
@Slf4j
@Service
@Transactional
public class MyCourseTablesServiceImpl implements MyCourseTablesService {
/**
* 判断学习资格
* @param userId 用户idrrr
* @param courseId 课程id
* @return 学习资格状态:查询数据字典 [{"code":"702001","desc":"正常学习"},{"code":"702002","desc":"没有选课或选课后没有支付"},{"code":"702003","desc":"已过期需要申请续期或重新支付"}]
*/
@Override
public XcCourseTablesDto getLearningStatus(String userId, Long courseId) {
// 1. 查询我的课程表
XcCourseTablesDto courseTablesDto = null;
XcCourseTables courseTables = getXcCourseTables(userId, courseId);
// 2. 未查到,返回状态码"702002"表示没有选课或选课后没有支付
if (courseTables == null) {
courseTablesDto = new XcCourseTablesDto();
courseTablesDto.setLearnStatus("702002");
return courseTablesDto;
}
// 3. 查到了,判断是否过期
boolean isExpires = LocalDateTime.now().isAfter(courseTables.getValidtimeEnd());
// 3.1 已过期(当前时间在过期时间之后),返回状态码"702003"表示已过期需要申请续期或重新支付
if (isExpires) {
BeanUtils.copyProperties(courseTables, courseTablesDto);
courseTablesDto.setLearnStatus("702003");
return courseTablesDto;
}else { // 3.2 未过期,返回状态码"702001"表示正常学习
BeanUtils.copyProperties(courseTables, courseTablesDto);
courseTablesDto.setLearnStatus("702001");
return courseTablesDto;
}
}
}
测试
对于免费课程点击加入我的课程表
会自动跳转至学习页面,同时数据库中有我的课程表记录和选课记录
对于付费课程会显示支付页面,点击微信支付/支付宝支付
仅会将数据加入到选课记录表中且选课状态为701002(待支付)