学成在线--选课学习模块

模块需求分析

模块介绍

  • 本模块实现了学生选课、下单支付、学习的整体流程
  • 网站的课程有收费和免费两种
    • 对于免费课程,学生选课后可以直接学习
    • 对于收费课程,学生需要下单且支付成功后,方可选课、学习
  • 选课:是将课程加入到我的课程表的过程
  • 我的课程表:记录我在网站学习的课程,我的课程表中有免费课程和收费课程两种
    • 对于免费课程可以直接添加到我的课程表
    • 对于收费课程需要下单、支付成功后自动添加到我的课程表
  • 模块流程如下

业务流程

学习引导

  • 用户通过搜索课程、课程推荐等信息进入课程详情页面,点击马上学习引导进入课程学习界面去学习,流程如下
    1. 进入课程详情,点击马上学习
    2. 课程免费时,引导加入我的课程表,或者进入学习界面
    3. 课程收费时,引导去支付、试学

选课流程

  • 选课是将课程加入我的课程表的过程
  • 对于免费课程,选课后可以直接加入到我的课程表
  • 对于收费课程,需要下单支付成功后,系统自动加入我的课程表

支付流程

  • 本项目与第三方支付平台对接完成支付操作

在线学习

  • 选课成功,用户可以在线学习,对于免费课程,无需选课即可在线学习

免费课程续期

  • 免费课程加入我的课程表后,默认为一年有效期,到期用户可申请续期

添加选课

需求分析

数据模型

  • 首先创建xc_learning数据库,导入黑马提供的sql脚本,里面包含三张表:xc_choose_course(选课记录表)、xc_course_tables(我的课程表)、xc_learn_record(学习记录表)
  • 选课是将课程加入我的课程表的过程,根据选课业务流程进行详细分析,业务流程如下
  • 选课信息存入选课记录表
    • 如果选的是免费课程,除了要将信息存入选课记录表,同时也要存入我的课程表
    • 如果选的是收费课程,将信息存入选课信息表后,要经过下单、支付成功后,才可以存入我的课程表
  • 选课记录表结构
    • 选课类型:免费课程、收费课程
    • 选课状态:选课成功、待支付、选课删除
    • 对于免费课程:课程价格为0,默认有效期为365天,开始服务时间为选课时间,结束服务时间为选课时间加一年后的时间,选课状态为选课成功
    • 对于收费课程:按课程的现价、有效期确定开始服务时间、结束服务时间、选课状态为待支付
    • 收费课程的选课记录需要支付成功后,选课状态为选课成功
  • 我的课程表
    • 对于免费课程:创建选课记录时,同时向我的课程表添加记录
    • 对于收费课程:创建选课记录后,需要下单支付成功后,自动向我的课程表添加记录
  • 选课流程如下

接口开发

创建学习中心工程

  • 拷贝黑马提供的xuecheng-plus-learning工程到项目根目录
  • 在nacos中添加配置文件learning-api-dev.yaml
server:
  servlet:
    context-path: /learning
  port: 53020
  • 在nacos中添加配置文件learning-service-dev.yaml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/xc_learning?serverTimezone=UTC&userUnicode=true&useSSL=false&
    username: root
    password: root

添加查询课程接口

  • 内容管理服务提供查询课程信息接口,此接口从课程发布表查询内容
  • 此接口主要供其他微服务远程调用,所以此接口不用授权,本项目标记此类接口统一以/r开头
  • 在内容管理服务中添加查询课程接口
    @ApiOperation("查询课程发布信息")
    @GetMapping("/r/coursepublish/{courseId}")
    public CoursePublish getCoursePublish(@PathVariable("courseId") Long courseId) {
        return coursePublishService.getCoursePunlish(courseId);
    }
  • 对应的Service实现方法如下
    @Override
    public CoursePublish getCoursePunlish(Long courseId) {
        return coursePublishMapper.selectById(courseId);
    }
  • 使用HttpClient测试接口
### 查询课程发布信息
GET {{content_host}}/content/r/coursepublish/160
Content-Type: application/json

{% note warning no-icon %}

  • 如果这里报未授权,请检查内容管理模块下的ResourceServerConfig中是否配置了/r/**的授权策略,如果配了,注释掉
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()           // 禁用 CSRF 保护
                .authorizeRequests()    //配置对请求的授权策略
              //.antMatchers("/r/**", "/course/**").authenticated() // 指定 "/r/" 和 "/course/" 这两个路径需要进行身份认证才能访问。
                .anyRequest().permitAll();  // 允许所有其他请求(除了上面指定的路径之外)都可以被访问,不需要进行身份认证。
    }

{% endnote %}

远程调用查询课程信息接口

  • 学习中心服务远程调用内容管理服务的查询课程发布信息接口
  1. 在学习中心service工程添加Feign接口
@FeignClient(value = "content-api", fallbackFactory = ContentServiceClientFallbackFactory.class)
@RequestMapping("/content")
public interface ContentServiceClient {

    @GetMapping("/r/coursepublish/{courseId}")
    CoursePublish getCoursePublish(@PathVariable("courseId") Long courseId);
}
  1. 编写降级方法
@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();
            }
        };
    }
}
  1. 在启动类添加@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);
    }
}
  1. 编写测试接口
@SpringBootTest
public class FeignClientTest {

    @Autowired
    ContentServiceClient contentServiceClient;

    @Test
    public void testContentServiceClient() {
        CoursePublish coursepublish = contentServiceClient.getCoursePublish(160L);
        System.out.println(coursepublish);
    }
}
  • 在进行feign远程调用时,会将字符串转成LocalDateTime,在CoursePublish类中的LocalDateTime类型的属性上添加如下代码
@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd HH:mm:ss")
  • 启动测试,控制台可以输出对应的课程发布信息

添加选课接口

接口分析
  • 本接口支持免费课程选课、收费课程选课
    • 免费课程选课:添加选课记录、添加我的课程表
    • 收费课程选课:添加选课记录(由于支付功能我们还没做,所以暂时不添加到我的课程表)
接口定义
  1. 请求参数:课程id、当前用户id
  2. 响应结果:选课记录信息、学习资格
  • 定义的接口如下
@Api(value = "我的课程表接口", tags = "我的课程表接口")
@Slf4j
@RestController
public class MyCourseTablesController {
    @ApiOperation("添加选课")
    @PostMapping("/choosecourse/{courseId}")
    public XcChooseCourseDto addChooseCourse(@PathVariable("courseId") Long courseId) {

        return null;
    }
}
  • Service接口定义
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;

    @Override
    public XcChooseCourseDto addChooseCourse(String userId, Long courseId) {
        // 1. 选课调用内容管理服务提供的查询课程接口,查询课程收费规则
        // 1.1 查询课程
        CoursePublish coursePublish = contentServiceClient.getCoursePublish(courseId);
        // 1.2 获取收费规则
        String charge = coursePublish.getCharge();
        XcChooseCourse chooseCourse = null;
        if ("201000".equals(charge)) {
            // 2. 如果是免费课程,向选课记录表、我的课程表添加数据
            chooseCourse = addFreeCourse(userId, coursePublish);
            XcCourseTables courseTables = addCourseTables(chooseCourse);
        } else {
            // 3. 如果是收费课程,向选课记录表添加数据
            chooseCourse = addChargeCourse(userId, coursePublish);
        }

        // 4. 判断学生的学习资格
        
        return null;
    }

    /**
     * 添加到我的课程表
     * @param chooseCourse  选课记录
     */
    private XcChooseCourse addCourseTables(XcChooseCourse chooseCourse) {
        
        return null;
    }

    /**
     * 将付费课程加入到选课记录表
     *
     * @param userId   用户id
     * @param courseId 课程id
     * @return 选课记录
     */
    private XcChooseCourse addChargeCourse(String userId, CoursePublish coursePublish) {

        return null;
    }

    /**
     * 将免费课程加入到选课表
     *
     * @param userId   用户id
     * @param courseId 课程id
     * @return 选课记录
     */
    private XcChooseCourse addFreeCourse(String userId, CoursePublish coursePublish) {

        return null;
    }
}
添加免费课程
  • 由于数据库中没有约束,所以可能存在重复添加的情况,我们需要事先做一下判断
    • 如果已经存在了数据,则直接返回
    • 如果不存在数据,则构造封装返回结果
    /**
     * 将免费课程加入到选课表
     *
     * @param userId   用户id
     * @param coursePublish 课程发布信息
     * @return 选课记录
     */
    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, "701007");// 选课成功
        // 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.setCreateDate(LocalDateTime.now());
        chooseCourse.setCoursePrice(coursePublish.getPrice());
        chooseCourse.setValidDays(365);
        chooseCourse.setStatus("701001");
        chooseCourse.setValidtimeStart(LocalDateTime.now());
        chooseCourse.setValidtimeEnd(LocalDateTime.now().plusDays(365));
        chooseCourseMapper.insert(chooseCourse);
        return chooseCourse;
    }
添加我的选课表
  • 我的选课表的记录来源于选课记录,选课记录成功,将课程信息添加到我的课程表
  • 如果我的课程表已经存在课程,课程可能已经过期,如果有新的选课记录,则需要更新我的课程表中的现有信息
    /**
     * 添加到我的课程表
     *
     * @param chooseCourse 选课记录
     */
    public XcCourseTables addCourseTables(XcChooseCourse chooseCourse) {
        String status = chooseCourse.getStatus();
        if (!"701001".equals(status)) {
            XueChengPlusException.cast("选课未成功,无法添加到课程表");
        }
        XcCourseTables courseTables = getXcCourseTables(chooseCourse.getUserId(), chooseCourse.getCourseId());
        if (courseTables != null) {
            return courseTables;
        }
        courseTables = new XcCourseTables();
        BeanUtils.copyProperties(chooseCourse, courseTables);
        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) {
        return courseTablesMapper.selectOne(new LambdaQueryWrapper<XcCourseTables>()
                .eq(XcCourseTables::getUserId, userId)
                .eq(XcCourseTables::getCourseId, courseId));
    }
添加收费课程
  • 此方法与添加免费课程几乎没有区别,只是选课状态和选课类型不同
    /**
     * 将付费课程加入到选课记录表
     *
     * @param userId        用户id
     * @param coursePublish 课程发布信息
     * @return 选课记录
     */
    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. 数据库中不存在数据,添加选课信息,对照着数据库中的属性挨个set即可
        XcChooseCourse chooseCourse = new XcChooseCourse();
        chooseCourse.setCourseId(coursePublish.getId());
        chooseCourse.setCourseName(coursePublish.getName());
        chooseCourse.setUserId(userId);
        chooseCourse.setCompanyId(coursePublish.getCompanyId());
        chooseCourse.setOrderType("700002");
        chooseCourse.setCreateDate(LocalDateTime.now());
        chooseCourse.setCoursePrice(coursePublish.getPrice());
        chooseCourse.setValidDays(365);
        chooseCourse.setStatus("701002");
        chooseCourse.setValidtimeStart(LocalDateTime.now());
        chooseCourse.setValidtimeEnd(LocalDateTime.now().plusDays(365));
        int insert = chooseCourseMapper.insert(chooseCourse);
        if (insert<=0){
            XueChengPlusException.cast("添加选课记录失败");
        }
        return chooseCourse;
    }
获取学习资格
  • 定义获取学习资格接口
    /**
     * 获取学习资格
     * @param userId        用户id
     * @param courseId      课程id
     * @return  学习资格状态
     */
    XcCourseTablesDto getLearningStatus(String userId, Long courseId);
  • 对应的接口实现思路
    1. 查询我的课程表,如果查不到,则说明没有选课,返回状态码为"702002"的对象
    2. 如果查到了选课,判断是否过期
      • 如果过期则不能学习,返回状态码为"702003"的对象
      • 未过期可以学习
    /**
     * 判断学习资格
     * @param userId        用户id
     * @param courseId      课程id
     * @return  学习资格状态:查询数据字典 [{"code":"702001","desc":"正常学习"},{"code":"702002","desc":"没有选课或选课后没有支付"},{"code":"702003","desc":"已过期需要申请续期或重新支付"}]
     */
    @Override
    public XcCourseTablesDto getLearningStatus(String userId, Long courseId) {
        XcCourseTablesDto courseTablesDto = new XcCourseTablesDto();
        // 1. 查询我的课程表
        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;
        }
        // 3.2 未过期,返回状态码为"702001"的对象
        else {
            BeanUtils.copyProperties(courseTables, courseTablesDto);
            courseTablesDto.setLearnStatus("702001");
            return courseTablesDto;
        }
    }
接口完善
  1. 完善Service接口
    @Override
    @Transactional
    public XcChooseCourseDto addChooseCourse(String userId, Long courseId) {
        // 1. 选课调用内容管理服务提供的查询课程接口,查询课程收费规则
        // 1.1 查询课程
        CoursePublish coursePublish = contentServiceClient.getCoursePublish(courseId);
        if (coursePublish == null) {
            XueChengPlusException.cast("课程不存在");
        }
        // 1.2 获取收费规则
        String charge = coursePublish.getCharge();
        XcChooseCourse chooseCourse = null;
        if ("201000".equals(charge)) {
            // 2. 如果是免费课程,向选课记录表、我的课程表添加数据
            log.info("添加免费课程..");
            chooseCourse = myCourseTablesService.addFreeCourse(userId, coursePublish);
            addCourseTables(chooseCourse);
        } else {
            // 3. 如果是收费课程,向选课记录表添加数据
            log.info("添加收费课程");
            chooseCourse = myCourseTablesService.addChargeCourse(userId, coursePublish);
        }

        // 4. 获取学生的学习资格
        XcCourseTablesDto courseTablesDto = getLearningStatus(userId, courseId);
        // 5. 封装返回值
        XcChooseCourseDto chooseCourseDto = new XcChooseCourseDto();
        BeanUtils.copyProperties(chooseCourse, chooseCourseDto);
        chooseCourseDto.setLearnStatus(courseTablesDto.learnStatus);
        return chooseCourseDto;
    }
  1. 完善Controller接口
@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);
    }
}

查询学习资格接口

  • 当我们点击马上学习时,会查询用户的学习资格,我们需要添加一个查询学习资格的接口
    @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);
    }

测试

  1. 将所有服务启动,并在nacos上的gateway-dev.yaml添加如下配置
- id: learning-api
    uri: lb://learning-api
    predicates:
    - Path=/learning/**
  1. 发布一门免费课程和一门付费课程
  2. 进入课程详情界面,点击马上学习
  3. 免费课程点击加入我的课程表,会自动跳转至学习页面,同时数据库中有我的课程表记录和选课记录
  4. 付费课程会显示支付页面,点击微信支付/支付宝支付,仅会将数据加入到选课记录表中,且选课状态为701002(待支付)

支付

需求分析

执行流程

  • 用户去学习收费课程时,会被引导去支付
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-47Zeoeml-1679736284743)(])
  • 当用户点击微信支付支付宝支付时,执行流程如下
    1. 请求学习中心服务创建选课记录
    2. 请求订单服务创建商品订单、生成支付二维码
    3. 用户扫码请求订单支付服务,订单支付服务请求第三方支付平台生成支付订单
    4. 前端唤起支付客户端,用户输入密码完成支付
    5. 第三方支付平台支付完成后,发起支付通知
    6. 订单支付服务接收支付通知结果
    7. 用户在前端查询支付结果,请求订单支付服务查询支付结果,如果订单服务还没有收到支付结果,则请求学习中心查询支付结果
    8. 订单支付服务向学习中心通知支付结果
    9. 学习中心服务收到支付结果,如果支付成功则更新选课记录,并添加到我的课程表

通用订单服务设计

  • 在本项目中不仅选课需要下单,购买学习资料、老师一对一答疑等所有收费项目都需要支付下单
    • 所以本项目设计通用的订单服务,通用的订单服务承接各业务模块的收费支付需求,当用户需要交费时,统一生成商品订单进行支付
  • 所有收费业务最终转换为订单记录,在订单服务的商品订单表中存储
  • 以选课为例,选课记录表的ID在商品订单表的out_business_id字段

准备开发环境

支付宝开发环境

  1. 配置沙箱环境
    • 沙箱环境是支付宝开放平台为开发者提供的与生产环境完全隔离的联调测试环境
    • 开发者在沙箱环境中完成的接口调用不会对生产环境中的数据造成任何影响
    • 沙箱环境配置文档:https://opendocs.alipay.com/common/02kkv7
  2. 模拟器
    • 下载并安装模拟器:http://mumu.163.com/
    • 注意安装目录尽量为全英文
  3. 在模拟器中安装沙箱版本的支付宝
    • 使用沙箱环境的买家账号登录沙箱版本的支付宝

创建订单服务

  • 拷贝黑马提供的xuecheng-plus-orders到自己的项目根目录,然后修改bootstrap.yml文件中的nacos配置信息
  • 然后在nacos中添加配置文件
    1. orders-api-dev.yaml
    server:
      servlet:
        context-path: /orders
      port: 53030
    
    spring:
     cloud:
      config:
        override-none: true
    
    1. orders-service-dev.yaml
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/xc_orders?serverTimezone=UTC&userUnicode=true&useSSL=false&
        username: root
        password: root
    
    xxl:
      job:
        admin: 
          addresses: http://192.168.101.128:18088/xxl-job-admin/
        executor:
          appname: payresultnotify-job
          address: 
          ip: 
          port: 8989
          logpath: /data/applogs/xxl-job/jobhandler
          logretentiondays: 30
        accessToken: default_token
    
  • 创建xc_orders数据库,导入黑马提供的SQL脚本

支付接口测试

阅读接口定义

  • 手机网站支付接入流程详细参见:https://docs.open.alipay.com/203/105285/
    1. 接口交互流程如下
      1. 用户在商户的H5网站下单支付后,商户系统按照手机网站支付接口API的参数规范生成订单数据
      2. 前端页面通过Form表单的形式请求到支付宝,此时支付宝会自动将页面跳转至支付宝H5收银台页面,如果用户手机上安装了支付宝,则会自动唤起支付宝App
      3. 输入支付密码完成支付
      4. 用户在支付宝App或H5收银台完成支付后,会根据商户在手机网站支付API中传入的前台回调地址return_url自动跳转回商户页面,同时在URL请求中以QueryString的形式附带上支付结果参数,详细回调函数参见手机网站支付接口的前台回调函数
      5. 支付宝还会根据原始支付Api中传入的异步通知地址notify_url,通过POST请求的形式将支付结果作为参数通知到商户系统,详情见支付结果异步通知
    2. 接口定义
      • 文档:https://opendocs.alipay.com/open/203/107090
      • 接口定义:外部商户请求支付宝创建订单并支付
      • 请求地址使用沙箱地址:https://openapi.alipaydev.com/gateway.do
      • 请求参数查阅官方文档:https://opendocs.alipay.com/open/203/107090
        • 一部分由SDK设置,一部分需要编写程序时指定
    3. 示例代码
    public void doPost(HttpServletRequest httpRequest,
                    HttpServletResponse httpResponse) throws ServletException, IOException {
        AlipayClient alipayClient = ... //获得初始化的AlipayClient
        AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
        alipayRequest.setReturnUrl("http://domain.com/CallBack/return_url.jsp");
        alipayRequest.setNotifyUrl("http://domain.com/CallBack/notify_url.jsp");//在公共参数中设置回跳和通知地址
        alipayRequest.setBizContent("{" +
                "    \"out_trade_no\":\"20150320010101002\"," +
                "    \"total_amount\":88.88," +
                "    \"subject\":\"Iphone6 16G\"," +
                "    \"product_code\":\"QUICK_WAP_WAY\"" +
                "  }");//填充业务参数
        String form = alipayClient.pageExecute(alipayRequest).getBody(); //调用SDK生成表单
        httpResponse.setContentType("text/html;charset=" + AlipayServiceEnvConstants.CHARSET);
        httpResponse.getWriter().write(form);//直接将完整的表单html输出到页面
        httpResponse.getWriter().flush();
    }
    

支付接口测试

编写下单代码
  • 根据接口流程,首先在订单服务编写测试类请求支付宝下单的接口
    1. 在订单服务api工程中添加依赖
    <!-- 支付宝SDK -->
    <dependency>
        <groupId>com.alipay.sdk</groupId>
        <artifactId>alipay-sdk-java</artifactId>
        <version>3.7.73.ALL</version>
    </dependency>
    
    <!-- 支付宝SDK依赖的日志 -->
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.2</version>
    </dependency>
    
    1. 拷贝黑马提供的AlipayConfig.java到订单服务的service工程的config包下
    public class AlipayConfig {
        // 商户appid
    //	public static String APPID = "";
        // 私钥 pkcs8格式的
    //	public static String RSA_PRIVATE_KEY = "";
        // 服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
        public static String notify_url = "http://商户网关地址/alipay.trade.wap.pay-JAVA-UTF-8/notify_url.jsp";
        // 页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址
        public static String return_url = "http://商户网关地址/alipay.trade.wap.pay-JAVA-UTF-8/return_url.jsp";
        // 请求网关地址
        public static String URL = "https://openapi.alipaydev.com/gateway.do";
        // 编码
        public static String CHARSET = "UTF-8";
        // 返回格式
        public static String FORMAT = "json";
        // 支付宝公钥
    //	public static String ALIPAY_PUBLIC_KEY = "";
        // 日志记录目录
        public static String log_path = "/log";
        // RSA2
        public static String SIGNTYPE = "RSA2";
    }
    
    1. 在api工程下编写Controller类请求支付宝下单的接口
    import com.alipay.api.AlipayApiException;
    import com.alipay.api.AlipayClient;
    import com.alipay.api.DefaultAlipayClient;
    import com.alipay.api.request.AlipayTradeWapPayRequest;
    import com.xuecheng.orders.config.AlipayConfig;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Controller
    public class PayTestController {
    
        @Value("${pay.alipay.APP_ID}")
        String APP_ID;
    
        @Value("${pay.alipay.APP_PRIVATE_KEY}")
        String APP_PRIVATE_KEY;
    
        @Value("${pay.alipay.ALIPAY_PUBLIC_KEY}")
        String ALIPAY_PUBLIC_KEY;
    
        @RequestMapping("/alipaytest")
        public void doPost(HttpServletRequest httpRequest,
                        HttpServletResponse httpResponse) throws ServletException, IOException, AlipayApiException {
            AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, AlipayConfig.FORMAT, AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY,AlipayConfig.SIGNTYPE);
            //获得初始化的AlipayClient
            AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
    //        alipayRequest.setReturnUrl("http://domain.com/CallBack/return_url.jsp");
    //        alipayRequest.setNotifyUrl("http://domain.com/CallBack/notify_url.jsp");//在公共参数中设置回跳和通知地址
            alipayRequest.setBizContent("{" +
                    "    \"out_trade_no\":\"202210100010101002\"," +
                    "    \"total_amount\":100000," +
                    "    \"subject\":\"Iphone6 16G\"," +
                    "    \"product_code\":\"QUICK_WAP_WAY\"" +
                    "  }");//填充业务参数
            String form = alipayClient.pageExecute(alipayRequest).getBody(); //调用SDK生成表单
            httpResponse.setContentType("text/html;charset=" + AlipayConfig.CHARSET);
            httpResponse.getWriter().write(form);//直接将完整的表单html输出到页面
            httpResponse.getWriter().flush();
        }
    
    }
    
    1. 在nacos中的orders-service-dev.yaml中配置公钥和私钥
    pay:
      alipay:
        APP_ID: 写你自己的AppID
        APP_PRIVATE_KEY: 写你自己的应用私钥
        ALIPAY_PUBLIC_KEY: 写你自己的支付宝公钥
    


    {% note warning no-icon %}
    • 注意:应用公钥和支付宝公钥不一样,nacos配置里填支付宝公钥
      {% endnote %}
生成二维码
  • 用户在前端使用支付宝沙箱环境,通过扫码请求下单接口,我们需要生成订单服务的下单接口二维码
  • ZXing是一个开源的类库,是用Java编写的多格式的1D/2D条码图像处理库,使用ZXing可以生成、识别QR Code(二维码)
    1. 在base工程中引入ZXing的依赖
    <!-- 二维码生成&识别组件 -->
    <dependency>
        <groupId>com.google.zxing</groupId>
        <artifactId>core</artifactId>
        <version>3.3.3</version>
    </dependency>
    
    <dependency>
        <groupId>com.google.zxing</groupId>
        <artifactId>javase</artifactId>
        <version>3.3.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    
    1. 生成二维码
      • 拷贝黑马提供的工具类QRCodeUtil.java到base工程的utils包下
      public class QRCodeUtil {
          /**
          * 生成二维码
          *
          * @param content 二维码对应的URL
          * @param width   二维码图片宽度
          * @param height  二维码图片高度
          * @return
          */
          public String createQRCode(String content, int width, int height) throws IOException {
              String resultImage = "";
              //除了尺寸,传入内容不能为空
              if (!StringUtils.isEmpty(content)) {
                  ServletOutputStream stream = null;
                  ByteArrayOutputStream os = new ByteArrayOutputStream();
                  //二维码参数
                  @SuppressWarnings("rawtypes")
                  HashMap<EncodeHintType, Comparable> hints = new HashMap<>();
                  //指定字符编码为“utf-8”
                  hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
                  //L M Q H四个纠错等级从低到高,指定二维码的纠错等级为M
                  //纠错级别越高,可以修正的错误就越多,需要的纠错码的数量也变多,相应的二维吗可储存的数据就会减少
                  hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
                  //设置图片的边距
                  hints.put(EncodeHintType.MARGIN, 1);
      
                  try {
                      //zxing生成二维码核心类
                      QRCodeWriter writer = new QRCodeWriter();
                      //把输入文本按照指定规则转成二维吗
                      BitMatrix bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints);
                      //生成二维码图片流
                      BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
                      //输出流
                      ImageIO.write(bufferedImage, "png", os);
                      /**
                      * 原生转码前面没有 data:image/png;base64 这些字段,返回给前端是无法被解析,所以加上前缀
                      */
                      resultImage = new String("data:image/png;base64," + EncryptUtil.encodeBase64(os.toByteArray()));
                      return resultImage;
                  } catch (Exception e) {
                      e.printStackTrace();
                      throw new RuntimeException("生成二维码出错");
                  } finally {
                      if (stream != null) {
                          stream.flush();
                          stream.close();
                      }
                  }
              }
              return null;
          }
      }
      
      • 速览一遍代码,我们只需要传入跳转url、图片的长宽,就会生成一个base64编码的图片,我们可以在浏览器中打开并扫码,现在编写一个main方法进行测试
      public static void main(String[] args) throws IOException {
          QRCodeUtil qrCodeUtil = new QRCodeUtil();
          System.out.println(qrCodeUtil.createQRCode("https://cyborg2077.github.io/", 200, 200));
      }
      
      • 在浏览器输入生成的base64,扫描二维码,可以跳转到我的博客首页
      data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIAQAAAACFI5MzAAABbUlEQVR42u2YQY7DIAxFzSrH4KZAbppjsArz/zdJ00qzGY2EF0WocvpYuDb+dmPjt2Vf8kfSDSt1S2OcNHPTZwyS4eLeM3bbjgoDzz0KaYanwzZsOI4j+BGRSLeywVmj19HIQCyt0vcRijDb40jKs0693YOlxKsk79f+rJ+FxBfcZBHT9w/dWUlUuCiRcmkMolumuqwmFDy67OXbtD3b64k89Qy3bUaXRgiS71QbBSaft9fLiXSFjkv5Et0+kt/R9QRXbya8wtiGa0wQAn9h6juWS5JOxyBHUflWGlLlzlMhSPfylaIolpihShBCJWbz55XULhpVQhBpye51zBHPijfbCESDMLItVeZnC0R83oSiKJZaKQp5TQE84p3tc0JZRK7pCeGsz+k4BJkX0FuZRgArz39gS4lP62porGaT+AUid1zVb6+IBiFKMuuDfePN66VkDgJzdDI2t8dNXErm2wjvFVVBPYOQ7/uq/yU/1d3RTjWcm10AAAAASUVORK5CYII=
      
接口测试
  • 生成订单服务下单接口的二维码
    • 修改我们之前的main方法,将url换成下单接口,注意这里不要用localhost,得用本机局域网ip
    public static void main(String[] args) throws IOException {
        QRCodeUtil qrCodeUtil = new QRCodeUtil();
        System.out.println(qrCodeUtil.createQRCode("http://192.168.43.204:53030/orders/alipaytest", 200, 200));
    }
    
    • 运行main方法,复制生成的base64串,在浏览器中打开,使用模拟器扫码支付

支付结果查询接口

  • 支付完成后,可以调用第三方支付平台的支付结果查询接口查询支付结果
    • 文档:https://opendocs.alipay.com/open/02ivbt
AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipaydev.com/gateway.do", "app_id", "your private_key", "json", "GBK", "alipay_public_key", "RSA2");
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", "20150320010101001");
//bizContent.put("trade_no", "2014112611001004680073956707");
request.setBizContent(bizContent.toString());
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
    System.out.println("调用成功");
} else {
    System.out.println("调用失败");
}
  • 刚刚的订单我们已经支付成功,现在可以使用out_trade_on商品订单号或支付宝的交易流水号trade_no去查询支付结果
    • out_trade_no:商品订单号,是在下单请求时指定的商品订单号
    • trade_no:支付宝交易流水号,是支付完成后,支付宝通知支付结果时发送的trade_no
    • 响应结果是Json格式
    {
        "alipay_trade_query_response": {
            "code": "10000",
            "msg": "Success",
            "trade_no": "2013112011001004330000121536",
            "out_trade_no": "6823789339978248",
            "open_id": "2088102122524333",
            "buyer_logon_id": "159****5620",
            "trade_status": "TRADE_CLOSED",
            "total_amount": 88.88,
            "trans_currency": "TWD",
            "settle_currency": "USD",
            "settle_amount": 2.96,
            "pay_currency": 1,
            "pay_amount": "8.88",
            "settle_trans_rate": "30.025",
            "trans_pay_rate": "0.264",
            "alipay_store_id": "2015040900077001000100001232",
            "buyer_pay_amount": 8.88,
            "point_amount": 10,
            "invoice_amount": 12.11,
            "send_pay_date": "2014-11-27 15:45:57",
            "receipt_amount": "15.25",
            "store_id": "NJ_S_001",
            "terminal_id": "NJ_T_001",
            "fund_bill_list": [
                {
                    "fund_channel": "ALIPAYACCOUNT",
                    "bank_code": "CEB",
                    "amount": 10,
                    "real_amount": 11.21,
                    "fund_type": "DEBIT_CARD"
                }
            ],
            "store_name": "证大五道口店",
            "buyer_user_id": "2088101117955611",
            "discount_goods_detail": "[{\"goods_id\":\"STANDARD1026181538\",\"goods_name\":\"雪碧\",\"discount_amount\":\"100.00\",\"voucher_id\":\"2015102600073002039000002D5O\"}]",
            "industry_sepc_detail": "{\"registration_order_pay\":{\"brlx\":\"1\",\"cblx\":\"1\"}}",
            "industry_sepc_detail_gov": "{\"registration_order_pay\":{\"brlx\":\"1\",\"cblx\":\"1\"}}",
            "industry_sepc_detail_acc": "{\"registration_order_pay\":{\"brlx\":\"1\",\"cblx\":\"1\"}}",
            "voucher_detail_list": [
                {
                    "id": "2015102600073002039000002D5O",
                    "name": "XX超市5折优惠",
                    "type": "ALIPAY_FIX_VOUCHER",
                    "amount": 10,
                    "merchant_contribute": 9,
                    "other_contribute": 1,
                    "memo": "学生专用优惠",
                    "template_id": "20171030000730015359000EMZP0",
                    "other_contribute_detail": [
                        {
                            "contribute_type": "BRAND",
                            "contribute_amount": 8
                        }
                    ],
                    "purchase_buyer_contribute": 2.01,
                    "purchase_merchant_contribute": 1.03,
                    "purchase_ant_contribute": 0.82
                }
            ],
            "charge_amount": "8.88",
            "charge_flags": "bluesea_1",
            "settlement_id": "2018101610032004620239146945",
            "trade_settle_info": {
                "trade_settle_detail_list": [
                    {
                        "operation_type": "replenish",
                        "operation_serial_no": "2321232323232",
                        "operation_dt": "2019-05-16 09:59:17",
                        "trans_out": "208811****111111",
                        "trans_in": "208811****111111",
                        "amount": 10,
                        "ori_trans_out": "2088111111111111",
                        "ori_trans_in": "2088111111111111"
                    }
                ]
            },
            "auth_trade_pay_mode": "CREDIT_PREAUTH_PAY",
            "buyer_user_type": "PRIVATE",
            "mdiscount_amount": "88.88",
            "discount_amount": "88.88",
            "buyer_user_name": "菜鸟网络有限公司",
            "subject": "Iphone6 16G",
            "body": "Iphone6 16G",
            "alipay_sub_merchant_id": "2088301372182171",
            "ext_infos": "{\"action\":\"cancel\"}",
            "passback_params": "merchantBizType%3d3C%26merchantBizNo%3d2016010101111",
            "hb_fq_pay_info": {
                "user_install_num": "3",
                "fq_amount": "10.05"
            },
            "receipt_currency_type": "DC",
            "credit_pay_mode": "creditAdvanceV2",
            "credit_biz_order_id": "ZMCB99202103310000450000041833",
            "enterprise_pay_info": {
                "is_use_enterprise_pay": false,
                "invoice_amount": 80,
                "biz_info": "{\\\"enterprisePayAmount\\\":\\\"0.64\\\"}"
            },
            "hyb_amount": "10.24",
            "bkagent_resp_info": {
                "bindtrx_id": "123412341234",
                "bindclrissr_id": "01",
                "bindpyeracctbk_id": "123123123123",
                "bkpyeruser_code": "123451234512345",
                "estter_location": "+37.28/-121.268"
            },
            "charge_info_list": [
                {
                    "charge_fee": 0.01,
                    "original_charge_fee": 0.01,
                    "switch_fee_rate": "0.03",
                    "is_rating_on_trade_receiver": "Y",
                    "is_rating_on_switch": "Y",
                    "charge_type": "trade",
                    "sub_fee_detail_list": [
                        {
                            "charge_fee": 0.1,
                            "original_charge_fee": 0.2,
                            "switch_fee_rate": "0.03"
                        }
                    ]
                }
            ]
        },
        "sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
    }
    
  • 我们使用out_trade_no商品订单号去查询,代码如下
/**
 * 支付结果查询测试
 */
@SpringBootTest
public class Test1 {
    @Value("${pay.alipay.APP_ID}")
    String APP_ID;
    @Value("${pay.alipay.APP_PRIVATE_KEY}")
    String APP_PRIVATE_KEY;

    @Value("${pay.alipay.ALIPAY_PUBLIC_KEY}")
    String ALIPAY_PUBLIC_KEY;

    @Test
    public void queryPayResult() throws AlipayApiException {
        System.out.println(APP_ID);
        System.out.println(APP_PRIVATE_KEY);
        System.out.println(ALIPAY_PUBLIC_KEY);
        AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, "json", AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE); //获得初始化的AlipayClient
        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
        JSONObject bizContent = new JSONObject();
        bizContent.put("out_trade_no", "202310100010101002");
        //bizContent.put("trade_no", "2014112611001004680073956707");
        request.setBizContent(bizContent.toString());
        AlipayTradeQueryResponse response = alipayClient.execute(request);
        if (response.isSuccess()) {
            System.out.println("调用成功");
            String resultJson = response.getBody();
            //转map
            Map resultMap = JSON.parseObject(resultJson, Map.class);
            Map alipay_trade_query_response = (Map) resultMap.get("alipay_trade_query_response");
            //支付结果
            System.out.println(alipay_trade_query_response);
        } else {
            System.out.println("调用失败");
        }
    }
}
  • 运行代码,控制台输出如下
调用成功 {
	"msg": "Success",
	"code": "10000",
	"buyer_user_id": "2088722009945224",
	"send_pay_date": "2023-03-18 11:39:51",
	"invoice_amount": "0.00",
	"out_trade_no": "202310100010101002",
	"total_amount": "100000.00",
	"buyer_user_type": "PRIVATE",
	"trade_status": "TRADE_SUCCESS",
	"trade_no": "2023031822001445220502614531",
	"buyer_logon_id": "hex***@sandbox.com",
	"receipt_amount": "0.00",
	"point_amount": "0.00",
	"buyer_pay_amount": "0.00"
}

支付结果通知接口

准备环境
  • 对于手机网站支付产生的交易,支付宝会通知商户支付结果,有两种方式
    1. return_url:使用此方式时,不能保证通知到位,所以建议使用notify_url
    2. notify_url
return_urlnotify_url
支付成功后点击完成会自动跳转回商家页面地址,同时在URL地址上附带支付结果参数,回跳参数可查看本文前台回跳参数说明。在iOS系统中,唤起支付宝客户端支付完成后,不会自动回到浏览器或商家App。用户可手工切回到浏览器或商家App。异步通知地址,用于接收支付宝推送给商户的支付/退款成功的消息。
  • 具体的使用方法是在调用下单接口的API中传入异步通知地址notify_url,通过POST请求的形式将支付结果作为参数通知到商户系统
  • 根据下单执行流程,订单服务收到支付结果需要对内容进行验签,验签过程如下
    1. 在通知返回参数列表中,出去sign、sign_type两个参数外,凡是通知返回胡来的参数均为待验签的参数。将剩下的参数进行url_decode,然后按照字典排序,组成字符串,得到待签名字符串;生活号异步通知组成的待验签串中须保留sign_type参数
    2. 将签名参数(sigin)使用base64解码为字节码串
    3. 使用RSA的验签方法,通过签名字符串、签名参数(通过base64解码)及支付宝公钥验证签名
    4. 验证签名正确后,必须再严格按照如下描述校验通知数据的正确性
      • 商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。
      • 通过验证out_trade_no、total_amount、appid参数的正确性判断通知请求的合法性。
编写测试代码
  1. 在下单请求时,设置通知地址request_setNotifyUrl(“商户自己的notify_url地址”)
    @RequestMapping("/alipaytest")
    public void doPost(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException, AlipayApiException {
        AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, AlipayConfig.FORMAT, AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE);
        //获得初始化的AlipayClient
        AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
//        alipayRequest.setReturnUrl("http://domain.com/CallBack/return_url.jsp");
        alipayRequest.setNotifyUrl("http://7veh8s.natappfree.cc/orders/paynotify"); //在公共参数中设置回跳和通知地址
        alipayRequest.setBizContent("{" +
                "    \"out_trade_no\":\"202310100011601002\"," +
                "    \"total_amount\":100000," +
                "    \"subject\":\"Iphone6 16G\"," +
                "    \"product_code\":\"QUICK_WAP_WAY\"" +
                "  }");//填充业务参数
        String form = alipayClient.pageExecute(alipayRequest).getBody(); //调用SDK生成表单
        httpResponse.setContentType("text/html;charset=" + AlipayConfig.CHARSET);
        httpResponse.getWriter().write(form);//直接将完整的表单html输出到页面
        httpResponse.getWriter().flush();
    }
  • 由于回调地址必须外网可访问的地址,所以这里需要内网穿透工具(我这里用的是NatApp的免费隧道)
  1. 编写接收通知接口,接收参数并验签
    • 详情参考官方的demo
    • 代码如下
    //接收通知
    @PostMapping("/paynotify")
    public void paynotify(HttpServletRequest request, HttpServletResponse response) throws IOException, AlipayApiException {
        Map<String, String> params = new HashMap<String, String>();
        Map requestParams = request.getParameterMap();
        for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
            String name = (String) iter.next();
            String[] values = (String[]) requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            //乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化
            //valueStr = new String(valueStr.getBytes("ISO-8859-1"), "gbk");
            params.put(name, valueStr);
        }
    
    
        //获取支付宝的通知返回参数,可参考技术文档中页面跳转同步通知参数列表(以上仅供参考)//
        //计算得出通知验证结果
        //boolean AlipaySignature.rsaCheckV1(Map<String, String> params, String publicKey, String charset, String sign_type)
        boolean verify_result = AlipaySignature.rsaCheckV1(params, ALIPAY_PUBLIC_KEY, AlipayConfig.CHARSET, "RSA2");
    
        if (verify_result) {//验证成功
            //
            //请在这里加上商户的业务逻辑程序代码
    
            //商户订单号
            String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"), "UTF-8");
            //支付宝交易号
    
            String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"), "UTF-8");
    
            //交易状态
            String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");
    
    
            //——请根据您的业务逻辑来编写程序(以下代码仅作参考)——
    
            if (trade_status.equals("TRADE_FINISHED")) {//交易结束
                //判断该笔订单是否在商户网站中已经做过处理
                //如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
                //请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
                //如果有做过处理,不执行商户的业务程序
    
                //注意:
                //如果签约的是可退款协议,退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知
                //如果没有签约可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。
            } else if (trade_status.equals("TRADE_SUCCESS")) {//交易成功
                System.out.println(trade_status + "交易成功");
                //判断该笔订单是否在商户网站中已经做过处理
                //如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
                //请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
                //如果有做过处理,不执行商户的业务程序
    
                //注意:
                //如果签约的是可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。
            }
            response.getWriter().write("success");
        } else {
            response.getWriter().write("fail");
        }
    }
    
通知接口测试
  1. 修改订单号,重启订单服务,在接收通知接口上打个断点
  2. 配置内网穿透的本地端口为订单服务端口,启动内网穿透客户端
  3. 生成二维码,复制base64串到浏览器中打开
  4. 打开模拟器、支付宝沙箱,扫码进行字符
  5. 观察接受订单数据等是否正常
  • 这里成功进入到了通知接口

生成支付二维码

需求分析

执行流程
  • 打开课程支付引导界面,当我们点击支付宝支付时,需要生成一个二维码,然后用户扫码支付
  • 所以首先需要生成支付二维码,用户扫描二维码开始请求支付宝下单,在向支付宝下单前需要添加选课记录、创建商品订单、生成支付交易记录
  • 生成二维码执行流程如下
    1. 前端调用学习中心服务的添加选课接口
    2. 添加选课成功,请求订单服务生成支付二维码接口
    3. 生成二维码接口:创建商品订单、生成支付交易记录、生成二维码
    4. 将二维码返回到前端,用户扫码
  • 用户扫码支付流程如下
    1. 用户输入支付密码,支付成功
    2. 接收第三方平台通知的支付结果
    3. 根据支付结果,更新支付交易记录的支付状态为支付成功
数据模型
  • 订单支付模式的核心由三张表组成

    1. 订单表:记录订单信息
    2. 订单明细表:记录订单的详细信息
    3. 支付交易记录表:记录每次支付的交易明细
  • 订单号注意唯一性、安全性、尽量短等特点,生成方案常用的如下

    1. 时间戳+随机数
      • 年月日时分秒毫秒+随机数
    2. 高并发场景
      • 年月日时分秒毫秒+随机数+redis自增序列
    3. 订单号中加上业务标识
      • 订单号加上业务标识方便客服提供服务,例如:第10位是业务类型、第11位是用户类型等
    4. 雪花算法
      • 雪花算法是推特内部使用的分布式环境下的唯一ID生成算法,它基于时间戳生成,保证有序递增,加入计算机硬件等元素,可以看组高并发场景下ID不重复
  • 本项目订单号生成策略采用雪花算法,导入黑马提供的IdWorkerUtils.java到base工程的utils包下

接口定义

  • 在订单服务中定义生成支付二维码接口
    • 请求参数:订单信息
    @Data
    @ToString
    public class AddOrderDto {
    
        /**
        * 总价
        */
        private Float totalPrice;
    
        /**
        * 订单类型
        */
        private String orderType;
    
        /**
        * 订单名称
        */
        private String orderName;
        /**
        * 订单描述
        */
        private String orderDescrip;
    
        /**
        * 订单明细json,不可为空
        * [{"goodsId":"","goodsType":"","goodsName":"","goodsPrice":"","goodsDetail":""},{...}]
        */
        private String orderDetail;
    
        /**
        * 外部系统业务id
        */
        private String outBusinessId;
    
    }
    
    • 响应:支付交易记录及二维码信息
    @Data
    @ToString
    public class PayRecordDto extends XcPayRecord {
    
        //二维码
        private String qrcode;
    
    }
    
    • 接口定义如下
    @Api(value = "订单支付接口", tags = "订单支付接口")
    @RestController
    @Slf4j
    public class OrderController {
        @ApiOperation("生成支付二维码")
        @PostMapping("/generatepaycode")
        public PayRecordDto generatePayCode(@RequestBody AddOrderDto addOrderDto) {
    
            // 插入订单信息、 插入支付记录、生成二维码返回
    
            return null;
        }
    }
    

接口实现

  • 在前面的分析中,生成支付二维码操作,其中包含了三个小操作
    1. 插入订单信息
    2. 插入支付记录
    3. 生成二维码返回
  • 那我们这里要做的就是实现这三个接口
保存商品订单
  • 定义保存订单信息接口
public interface OrderService {

    /**
     * 创建商品订单
     * @param userId        用户id
     * @param addOrderDto   订单信息
     * @return  支付交易记录
     */
    PayRecordDto createOrder(String userId, AddOrderDto addOrderDto);
    
}
  • 在保存订单接口中需要完成
    1. 创建商品订单
    2. 创建交易支付记录
    3. 生成二维码
  • 接口实现如下
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    XcOrdersMapper xcOrdersMapper;

    @Autowired
    XcPayRecordMapper xcPayRecordMapper;

    @Autowired
    XcOrdersGoodsMapper xcOrdersGoodsMapper;


    @Override
    public PayRecordDto createOrder(String userId, AddOrderDto addOrderDto) {
        // 1. 添加商品订单
        
        // 2. 添加支付交易记录
        
        // 3. 生成二维码
        
        return null;
    }
}
  • 编写创建订单方法,商品订单的数据来源于选课记录,在订单表需要存入选课记录的ID,这里需要做好幂等性处理,防止用户重复支付
    /**
     * 保存订单信息,保存订单表和订单明细表,需要做幂等性判断
     *
     * @param userId      用户id
     * @param addOrderDto 选课信息
     * @return
     */
    @Transactional
    public XcOrders saveOrders(String userId, AddOrderDto addOrderDto) {
        // 1. 幂等性判断
        XcOrders order = getOrderByBusinessId(addOrderDto.getOutBusinessId());
        if (order != null) {
            return order;
        }
        // 2. 插入订单表
        order = new XcOrders();
        BeanUtils.copyProperties(addOrderDto, order);
        order.setId(IdWorkerUtils.getInstance().nextId());
        order.setCreateDate(LocalDateTime.now());
        order.setUserId(userId);
        order.setStatus("600001");
        int insert = xcOrdersMapper.insert(order);
        if (insert <= 0) {
            XueChengPlusException.cast("插入订单记录失败");
        }
        // 3. 插入订单明细表
        Long orderId = order.getId();
        String orderDetail = addOrderDto.getOrderDetail();
        List<XcOrdersGoods> xcOrdersGoodsList = JSON.parseArray(orderDetail, XcOrdersGoods.class);
        xcOrdersGoodsList.forEach(goods -> {
            goods.setOrderId(orderId);
            int insert1 = xcOrdersGoodsMapper.insert(goods);
            if (insert1 <= 0) {
                XueChengPlusException.cast("插入订单明细失败");
            }
        });
        return order;
    }
创建支付交易记录

{% note info no-icon %}

  • 为什么创建支付交易记录?
    {% endnote %}
    {% note pink no-icon %}
  • 在请求微信或支付宝下单接口时,需要传入商品订单号,在与第三方交付平台对接时发现,当用户支付失败或因为其他原因导致该订单没有支付成功,此时再次调用第三方支付平台的下单接口就会报错订单号已存在
  • 但如果我们此时传入一个新的订单号就可以解决问题,但是商品订单已经创建,因此没有支付成功重新创建一个新订单是不合理的
  • 解决以上问题的方案是
    1. 用户每次发起都创建一个新的支付交易记录,此交易记录与商品订单关联
    2. 将支付交易记录的流水号传给第三方支付系统的下单接口,这样即使没有支付成功,也不会出现上面的问题
    3. 判断订单支付状态,提醒用户不要重复支付

      {% endnote %}
  • 编写创建支付交易记录的方法
    public XcPayRecord createPayRecord(XcOrders orders) {
        if (orders == null) {
            XueChengPlusException.cast("订单不存在");
        }
        if ("600002".equals(orders.getStatus()) {
            XueChengPlusException.cast("订单已支付");
        }
        XcPayRecord payRecord = new XcPayRecord();
        payRecord.setPayNo(IdWorkerUtils.getInstance().nextId());
        payRecord.setOrderId(orders.getId());
        payRecord.setOrderName(orders.getOrderName());
        payRecord.setTotalPrice(orders.getTotalPrice());
        payRecord.setCurrency("CNY");
        payRecord.setCreateDate(LocalDateTime.now());
        payRecord.setStatus("601001");  // 未支付
        payRecord.setUserId(orders.getUserId());
        int insert = xcPayRecordMapper.insert(payRecord);
        if (insert <= 0) {
            XueChengPlusException.cast("插入支付交易记录失败");
        }
        return payRecord;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值