01选课模块之添加免费/收费选课

添加选课

界面原型

第一步:用户通过搜索课程、课程推荐等信息进入课程详情页面,点击马上学习进行学习

在这里插入图片描述

第二步:课程免费时可以直接加入我的课程表并且免费课程可以直接在线学习,免费课程默认一年有效期,到期需要申请续期

在这里插入图片描述

第三步:课程收费时需要下单支付,支付成功后会自动加入我的课程表

在这里插入图片描述

数据模型

选课记录表(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.yamllearning-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(待支付)

在这里插入图片描述

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值