中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁 > news >正文

wordpress 標(biāo)簽手冊關(guān)鍵詞優(yōu)化包年推廣

wordpress 標(biāo)簽手冊,關(guān)鍵詞優(yōu)化包年推廣,wordpress 自定義分類,湖南高速通app下載微服務(wù)實戰(zhàn)項目-學(xué)成在線-選課學(xué)習(xí)(支付與學(xué)習(xí)中心)模塊 1 模塊需求分析 1.1 模塊介紹 本模塊實現(xiàn)了學(xué)生選課、下單支付、學(xué)習(xí)的整體流程。 網(wǎng)站的課程有免費和收費兩種,對于免費課程學(xué)生選課后可直接學(xué)習(xí),對于收費課程學(xué)生需要下單且支付成功方可選…

微服務(wù)實戰(zhàn)項目-學(xué)成在線-選課學(xué)習(xí)(支付與學(xué)習(xí)中心)模塊

1 模塊需求分析

1.1 模塊介紹

本模塊實現(xiàn)了學(xué)生選課、下單支付、學(xué)習(xí)的整體流程。

網(wǎng)站的課程有免費和收費兩種,對于免費課程學(xué)生選課后可直接學(xué)習(xí),對于收費課程學(xué)生需要下單且支付成功方可選課、學(xué)習(xí)。

選課:是將課程加入我的課程表的過程。

我的課程表:記錄我在網(wǎng)站學(xué)習(xí)的課程,我的課程表中有免費課程和收費課程兩種,對于免費課程可直接添加到我的課程表,對于收費課程需要下單、支付成功后自動加入我的課程表。

模塊整體流程如下:

1.2 業(yè)務(wù)流程

1.2.1 學(xué)習(xí)引導(dǎo)

用戶通過搜索課程、課程推薦等信息進入課程詳情頁面,點擊"馬上學(xué)習(xí)"
引導(dǎo)進入學(xué)習(xí)界面去學(xué)習(xí)。

流程如下:

1、進入課程詳情點擊馬上學(xué)習(xí)

2、課程免費時引導(dǎo)加入我的課程表、或進入學(xué)習(xí)界面。

3、課程收費時引導(dǎo)去支付、或試學(xué)。

1.2.2 選課流程

選課是將課程加入我的課程表的過程。

對免費課程選課后可直接加入我的課程表,對收費課程選課后需要下單支付成功系統(tǒng)自動加入我的課程表。

流程如下:

1.2.3 支付流程

本項目與第三方支付平臺對接完成支付操作。

流程如下:

1.2.4 在線學(xué)習(xí)

選課成功用戶可以在線學(xué)習(xí),對于免費課程無需選課即可在線學(xué)習(xí)。

流程如下:

1.2.5 免費課程續(xù)期

免費課程加入我的課程表默認為1年有效期,到期用戶可申請續(xù)期,流程如下:

2 添加選課

2.1 需求分析

2.1.1 數(shù)據(jù)模型

選課是將課程加入我的課程表的過程,根據(jù)選課的業(yè)務(wù)流程進行詳細分析,業(yè)務(wù)流程如下:

選課信息存入選課記錄表,免費課程被選課除了進入選課記錄表同時進入我的課程表,收費課程進入選課記錄表后需要經(jīng)過下單、支付成功才可以進入我的課程表。

我的課程表記錄了用戶學(xué)習(xí)的課程,包括免費課程、收費課程(已經(jīng)支付)。

1、選課記錄表

當(dāng)用戶將課程添加到課程表時需要先創(chuàng)建選課記錄。

結(jié)構(gòu)如下:

選課類型:免費課程、收費課程。

選課狀態(tài):選課成功、待支付、選課刪除。

對于免費課程:課程價格為0,有效期默認365,開始服務(wù)時間為選課時間,結(jié)束服務(wù)時間為選課時間加1年后的時間,選課狀態(tài)為選課成功。

對于收費課程:按課程的現(xiàn)價、有效期確定開始服務(wù)時間、結(jié)束服務(wù)時間,選課狀態(tài)為待支付。

收費課程的選課記錄需要支付成功后選課狀態(tài)為成功。

2、我的課程表

我的課程表中記錄了用戶選課成功的課程,所以我的課程表的數(shù)據(jù)來源于選課記錄表。

對于免費課程創(chuàng)建選課記錄后同時向我的課程表添加記錄。

對于收費課程創(chuàng)建選課記錄后需要下單支付成功后自動向我的課程表添加記錄。

2.1.2 執(zhí)行流程

在學(xué)習(xí)引導(dǎo)處,可以直接將免費課程加入我的課程表,如下圖:

對于收費課程先創(chuàng)建選課記錄表,支付成功后,收到支付結(jié)果由系統(tǒng)自動加入我的課程表。

執(zhí)行流程如下:

2.2 接口開發(fā)

2.2.1 部署學(xué)習(xí)中心工程

從課程資料拷貝學(xué)習(xí)中心服務(wù)工程到自己的工程目錄,結(jié)構(gòu)如下:

注意去修改nacos的命名空間。

創(chuàng)建數(shù)據(jù)庫xc_learning,并導(dǎo)入數(shù)據(jù)

修改數(shù)據(jù)庫的連接,改成自己的數(shù)據(jù)庫。

nacos配置文件:learning-api-dev.yaml

server:servlet:context-path: /learningport: 63020

learning-service-dev.yaml

spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.101.65:3306/xc1010_learning?serverTimezone=UTC&userUnicode=true&useSSL=false&username: rootpassword: mysql

2.2.2 添加查詢課程接口

內(nèi)容管理服務(wù)提供查詢課程信息接口,此接口從課程發(fā)布表查詢。

此接口主要提供其它微服務(wù)遠程調(diào)用,所以此接口不用授權(quán),本項目標(biāo)記此類接口統(tǒng)一以
/r開頭。

在課程發(fā)布controller類中定義課程發(fā)布信息查詢接口。

@ApiOperation("查詢課程發(fā)布信息")
@ResponseBody
@GetMapping("/r/coursepublish/{courseId}")
public CoursePublish getCoursepublish(@PathVariable("courseId") Long courseId) {CoursePublish coursePublish = coursePublishService.getCoursePublish(courseId);return coursePublish;
}

Service如下:

如果課程發(fā)布狀態(tài)正常則正常返回,否則返回空。

public CoursePublish getCoursePublish(Long courseId){CoursePublish coursePublish = coursePublishMapper.selectById(courseId);return coursePublish ;
}

測試:

啟動內(nèi)容管理服務(wù),使用httpclient測試

### 查詢課程發(fā)布信息
GET {{content_host}}/content/r/coursepublish/2

由于是在網(wǎng)關(guān)處進行令牌校驗,所以在微服務(wù)處不再校驗令牌的合法性,修改內(nèi)容管理content-api工程的ResouceServerConfig類,屏蔽authenticated()。

 @Overridepublic void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests()
//          .antMatchers("/r/**","/course/**").authenticated()//所有/r/**的請求必須認證通過.anyRequest().permitAll();}

2.2.3 測試查詢課程信息接口

學(xué)生中心服務(wù)遠程調(diào)用內(nèi)容管理服務(wù)的查詢課程發(fā)布信息接口。

導(dǎo)入的學(xué)習(xí)中心工程已經(jīng)存在ContentServiceClient
接口,通過此接口遠程調(diào)用課程查詢接口。

編寫測試接口

package com.xuecheng.learning;import com.xuecheng.content.model.po.CoursePublish;
import com.xuecheng.learning.feignclient.ContentServiceClient;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;/*** @description Feign接口測試類* @author Mr.M* @date 2022/10/24 17:15* @version 1.0*/@SpringBootTest
public class FeignClientTest {@AutowiredContentServiceClient contentServiceClient;@Testpublic void testContentServiceClient(){CoursePublish coursepublish = contentServiceClient.getCoursepublish(18L);Assertions.assertNotNull(coursepublish);}
}

在進行feign遠程調(diào)用時會將字符串轉(zhuǎn)成LocalDateTime,在CoursePublish
類中LocalDateTime的屬性上邊添加如下代碼:

@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd HH:mm:ss")

2.2.4 添加選課接口

2.2.4.1 接口分析

本接口支持免費課程選課、收費課程選課。

免費課程選課:添加選課記錄、添加我的課程表。

收費課程選課:添加選課記錄。

2.2.4.2 接口定義

1、請求參數(shù):課程id、當(dāng)前用戶id

2、響應(yīng)結(jié)果:選課記錄信息、學(xué)習(xí)資格

學(xué)習(xí)資格:[{“code”:“702001”,“desc”:“正常學(xué)習(xí)”},{“code”:“702002”,“desc”:“沒有選課或選課后沒有支付”},{“code”:“702003”,“desc”:“已過期需要申請續(xù)期或重新支付”}]

接口定義如下:

package com.xuecheng.learning.api;import com.xuecheng.base.execption.XueChengPlusException;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.base.model.XcUser;
import com.xuecheng.learning.model.dto.XcChooseCourseDto;
import com.xuecheng.learning.service.MyCourseTablesService;
import com.xuecheng.learning.util.SecurityUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;/*** @author Mr.M* @version 1.0* @description 我的課程表接口* @date 2022/10/2 14:52*/
@Api(value = "我的課程表接口", tags = "我的課程表接口")
@Slf4j
@RestController
public class MyCourseTablesController {@ApiOperation("添加選課")@PostMapping("/choosecourse/{courseId}")public XcChooseCourseDto addChooseCourse(@PathVariable("courseId") Long courseId)  {}}

Service接口定義:

package com.xuecheng.learning.service;import com.xuecheng.learning.model.dto.XcChooseCourseDto;
import com.xuecheng.learning.model.po.XcChooseCourse;
import com.xuecheng.learning.model.po.XcCourseTables;/*** @description 我的課程表service接口* @author Mr.M* @date 2022/10/2 16:07* @version 1.0*/
public interface MyCourseTablesService {/*** @description 添加選課* @param userId 用戶id* @param courseId 課程id* @return com.xuecheng.learning.model.dto.XcChooseCourseDto* @author Mr.M* @date 2022/10/24 17:33
*/public XcChooseCourseDto addChooseCourse(String userId, Long courseId);}

Service接口執(zhí)行流程

package com.xuecheng.learning.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuecheng.base.execption.XueChengPlusException;
import com.xuecheng.content.model.po.CoursePublish;
import com.xuecheng.learning.feignclient.ContentServiceClient;
import com.xuecheng.learning.mapper.XcChooseCourseMapper;
import com.xuecheng.learning.mapper.XcCourseTablesMapper;
import com.xuecheng.learning.model.dto.XcChooseCourseDto;
import com.xuecheng.learning.model.po.XcChooseCourse;
import com.xuecheng.learning.model.po.XcCourseTables;
import com.xuecheng.learning.service.MyCourseTablesService;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;/*** @author Mr.M* @version 1.0* @description TODO* @date 2022/10/2 16:12*/
@Slf4j
@Service
public class MyCourseTablesServiceImpl implements MyCourseTablesService {@AutowiredXcChooseCourseMapper xcChooseCourseMapper;@AutowiredXcCourseTablesMapper xcCourseTablesMapper;@AutowiredContentServiceClient contentServiceClient;@AutowiredMyCourseTablesService myCourseTablesService;@AutowiredMyCourseTablesServiceImpl currentProxy;@Transactional@Overridepublic XcChooseCourseDto addChooseCourse(String userId,Long courseId) {//查詢課程信息CoursePublish coursepublish = contentServiceClient.getCoursepublish(courseId);//課程收費標(biāo)準(zhǔn)String charge = coursepublish.getCharge();//選課記錄XcChooseCourse chooseCourse = null;if("201000".equals(charge)){//課程免費//添加免費課程chooseCourse  = addFreeCoruse(userId, coursepublish);//添加到我的課程表XcCourseTables xcCourseTables = addCourseTabls(chooseCourse);}else{//添加收費課程chooseCourse  = addChargeCoruse(userId, coursepublish);}//獲取學(xué)習(xí)資格...return null;}//添加免費課程,免費課程加入選課記錄表、我的課程表
public XcChooseCourse addFreeCoruse(String userId, CoursePublish coursepublish) {return null;
}//添加收費課程
public XcChooseCourse addChargeCoruse(String userId,CoursePublish coursepublish){return null;
}
//添加到我的課程表
public XcCourseTables addCourseTabls(XcChooseCourse xcChooseCourse){return null;
}
2.2.4.3 添加免費課程

//添加免費課程,免費課程加入選課記錄表、我的課程表
public XcChooseCourse addFreeCoruse(String userId, CoursePublish coursepublish) {//查詢選課記錄表是否存在免費的且選課成功的訂單LambdaQueryWrapper<XcChooseCourse> queryWrapper = new LambdaQueryWrapper<>();queryWrapper = queryWrapper.eq(XcChooseCourse::getUserId, userId).eq(XcChooseCourse::getCourseId, coursepublish.getId()).eq(XcChooseCourse::getOrderType, "700001")//免費課程.eq(XcChooseCourse::getStatus, "701001");//選課成功List<XcChooseCourse> xcChooseCourses = xcChooseCourseMapper.selectList(queryWrapper);if (xcChooseCourses != null && xcChooseCourses.size()>0) {return xcChooseCourses.get(0);}//添加選課記錄信息XcChooseCourse xcChooseCourse = new XcChooseCourse();xcChooseCourse.setCourseId(coursepublish.getId());xcChooseCourse.setCourseName(coursepublish.getName());xcChooseCourse.setCoursePrice(0f);//免費課程價格為0xcChooseCourse.setUserId(userId);xcChooseCourse.setCompanyId(coursepublish.getCompanyId());xcChooseCourse.setOrderType("700001");//免費課程xcChooseCourse.setCreateDate(LocalDateTime.now());xcChooseCourse.setStatus("701001");//選課成功xcChooseCourse.setValidDays(365);//免費課程默認365xcChooseCourse.setValidtimeStart(LocalDateTime.now());xcChooseCourse.setValidtimeEnd(LocalDateTime.now().plusDays(365));xcChooseCourseMapper.insert(xcChooseCourse);return xcChooseCourse;}
2.2.4.4 添加我的課程表

我的課程表的記錄來源于選課記錄,選課記錄成功將課程信息添加到我的課程表。

如果我的課程表已存在課程可能已經(jīng)過期,如果有新的選課記錄則需要更新我的課程表中的現(xiàn)有信息。

/*** @description 添加到我的課程表* @param xcChooseCourse 選課記錄* @return com.xuecheng.learning.model.po.XcCourseTables* @author Mr.M* @date 2022/10/3 11:24
*/
public XcCourseTables addCourseTabls(XcChooseCourse xcChooseCourse){//選課記錄完成且未過期可以添加課程到課程表String status = xcChooseCourse.getStatus();if (!"701001".equals(status)){XueChengPlusException.cast("選課未成功,無法添加到課程表");}//查詢我的課程表XcCourseTables xcCourseTables = getXcCourseTables(xcChooseCourse.getUserId(), xcChooseCourse.getCourseId());if(xcCourseTables!=null){return xcCourseTables;}XcCourseTables xcCourseTablesNew = new XcCourseTables();xcCourseTablesNew.setChooseCourseId(xcChooseCourse.getId());xcCourseTablesNew.setUserId(xcChooseCourse.getUserId());xcCourseTablesNew.setCourseId(xcChooseCourse.getCourseId());xcCourseTablesNew.setCompanyId(xcChooseCourse.getCompanyId());xcCourseTablesNew.setCourseName(xcChooseCourse.getCourseName());xcCourseTablesNew.setCreateDate(LocalDateTime.now());xcCourseTablesNew.setValidtimeStart(xcChooseCourse.getValidtimeStart());xcCourseTablesNew.setValidtimeEnd(xcChooseCourse.getValidtimeEnd());xcCourseTablesNew.setCourseType(xcChooseCourse.getOrderType());xcCourseTablesMapper.insert(xcCourseTablesNew);return xcCourseTablesNew;}/*** @description 根據(jù)課程和用戶查詢我的課程表中某一門課程* @param userId* @param courseId* @return com.xuecheng.learning.model.po.XcCourseTables* @author Mr.M* @date 2022/10/2 17:07
*/
public XcCourseTables getXcCourseTables(String userId,Long courseId){XcCourseTables xcCourseTables = xcCourseTablesMapper.selectOne(new LambdaQueryWrapper<XcCourseTables>().eq(XcCourseTables::getUserId, userId).eq(XcCourseTables::getCourseId, courseId));return xcCourseTables;}
2.2.4.5 添加收費課程

//添加收費課程
public XcChooseCourse addChargeCoruse(String userId,CoursePublish coursepublish){//如果存在待支付交易記錄直接返回LambdaQueryWrapper<XcChooseCourse> queryWrapper = new LambdaQueryWrapper<>();queryWrapper = queryWrapper.eq(XcChooseCourse::getUserId, userId).eq(XcChooseCourse::getCourseId, coursepublish.getId()).eq(XcChooseCourse::getOrderType, "700002")//收費訂單.eq(XcChooseCourse::getStatus, "701002");//待支付List<XcChooseCourse> xcChooseCourses = xcChooseCourseMapper.selectList(queryWrapper);if (xcChooseCourses != null && xcChooseCourses.size()>0) {return xcChooseCourses.get(0);}XcChooseCourse xcChooseCourse = new XcChooseCourse();xcChooseCourse.setCourseId(coursepublish.getId());xcChooseCourse.setCourseName(coursepublish.getName());xcChooseCourse.setCoursePrice(coursepublish.getPrice());xcChooseCourse.setUserId(userId);xcChooseCourse.setCompanyId(coursepublish.getCompanyId());xcChooseCourse.setOrderType("700002");//收費課程xcChooseCourse.setCreateDate(LocalDateTime.now());xcChooseCourse.setStatus("701002");//待支付xcChooseCourse.setValidDays(coursepublish.getValidDays());xcChooseCourse.setValidtimeStart(LocalDateTime.now());xcChooseCourse.setValidtimeEnd(LocalDateTime.now().plusDays(coursepublish.getValidDays()));xcChooseCourseMapper.insert(xcChooseCourse);return xcChooseCourse;
}
2.2.4.6 獲取學(xué)習(xí)資格

定義獲取學(xué)習(xí)資格接口

public interface MyCourseTablesService {public XcChooseCourseDto addChooseCourse(String userId, Long courseId);/*** @description 判斷學(xué)習(xí)資格* @param userId* @param courseId* @return XcCourseTablesDto 學(xué)習(xí)資格狀態(tài) [{"code":"702001","desc":"正常學(xué)習(xí)"},{"code":"702002","desc":"沒有選課或選課后沒有支付"},{"code":"702003","desc":"已過期需要申請續(xù)期或重新支付"}]* @author Mr.M* @date 2022/10/3 7:37*/public XcCourseTablesDto getLearningStatus(String userId, Long courseId);
}

接口實現(xiàn)如下:

/*** @description 判斷學(xué)習(xí)資格* @param userId* @param courseId* @return XcCourseTablesDto 學(xué)習(xí)資格狀態(tài) [{"code":"702001","desc":"正常學(xué)習(xí)"},{"code":"702002","desc":"沒有選課或選課后沒有支付"},{"code":"702003","desc":"已過期需要申請續(xù)期或重新支付"}]* @author Mr.M* @date 2022/10/3 7:37
*/
public 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){//正常學(xué)習(xí)xcCourseTablesDto.setLearnStatus("702001");return xcCourseTablesDto;}else{//已過期xcCourseTablesDto.setLearnStatus("702003");return xcCourseTablesDto;}}
2.2.4.7 service接口完善

完善Service接口

@Transactional
@Override
public XcChooseCourseDto addChooseCourse(String userId, Long courseId) {//查詢課程信息CoursePublish coursepublish = contentServiceClient.getCoursepublish(courseId);//課程收費標(biāo)準(zhǔn)String charge = coursepublish.getCharge();//選課記錄XcChooseCourse chooseCourse = null;if ("201000".equals(charge)) {//課程免費//添加免費課程chooseCourse = addFreeCoruse(userId, coursepublish);//添加到我的課程表XcCourseTables xcCourseTables = addCourseTabls(chooseCourse);} else {//添加收費課程chooseCourse = addChargeCoruse(userId, coursepublish);}XcChooseCourseDto xcChooseCourseDto = new XcChooseCourseDto();BeanUtils.copyProperties(chooseCourse,xcChooseCourseDto);//獲取學(xué)習(xí)資格XcCourseTablesDto xcCourseTablesDto = getLearningStatus(userId, courseId);xcChooseCourseDto.setLearnStatus(xcCourseTablesDto.getLearnStatus());return xcChooseCourseDto;
}
2.2.4.8 完善controller
@Autowired
MyCourseTablesService courseTablesService;@ApiOperation("添加選課")
@PostMapping("/choosecourse/{courseId}")
public XcChooseCourseDto addChooseCourse(@PathVariable("courseId") Long courseId) {//登錄用戶SecurityUtil.XcUser user = SecurityUtil.getUser();if(user == null){XueChengPlusException.cast("請登錄后繼續(xù)選課");}String userId = user.getId();return  courseTablesService.addChooseCourse(userId, courseId);}@ApiOperation("查詢學(xué)習(xí)資格")
@PostMapping("/choosecourse/learnstatus/{courseId}")
public XcCourseTablesDto getLearnstatus(@PathVariable("courseId") Long courseId) {//登錄用戶SecurityUtil.XcUser user = SecurityUtil.getUser();if(user == null){XueChengPlusException.cast("請登錄后繼續(xù)選課");}String userId = user.getId();return  courseTablesService.getLeanringStatus(userId, courseId);}

2.3 接口測試

2.3.1 單元測試

1、準(zhǔn)備測試環(huán)境

發(fā)布兩門課程,一門為免費,一門為收費。

小技巧:可以更改課程發(fā)布表已有課程的收費標(biāo)準(zhǔn)進行測試。

2、測試添加免費課程

成功:選課記錄表一條記錄、我的課程表一條記錄。

3、測試添加收費課程

成功:選課記錄表一條記錄

4、重復(fù)添加選課

重復(fù)添加相同的課程,觀察是否存在異常。

5、生成令牌,為方便生成令牌暫時將PasswordAuthServiceImpl類中的驗證碼屏蔽

6、使用httpclient測試如下:

### 添加選課
POST {{learning_host}}/learning/choosecourse/2
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ7XCJiaXJ0aGRheVwiOlwiMjAyMi0wOS0yOFQxOToyODo0NlwiLFwiY3JlYXRlVGltZVwiOlwiMjAyMi0wOS0yOFQwODozMjowM1wiLFwiaWRcIjpcIjUwXCIsXCJuYW1lXCI6XCLlrabnlJ8xXCIsXCJuaWNrbmFtZVwiOlwi5aSn5rC054mbXCIsXCJwZXJtaXNzaW9uc1wiOltcInhjX3N5c21hbmFnZXJcIixcInhjX3N5c21hbmFnZXJfdXNlclwiLFwieGNfc3lzbWFuYWdlcl91c2VyX2FkZFwiLFwieGNfc3lzbWFuYWdlcl91c2VyX2VkaXRcIixcInhjX3N5c21hbmFnZXJfdXNlcl92aWV3XCIsXCJ4Y19zeXNtYW5hZ2VyX3VzZXJfZGVsZXRlXCIsXCJ4Y19zeXNtYW5hZ2VyX2RvY1wiLFwieGNfc3lzbWFuYWdlcl9sb2dcIixcInhjX3RlYWNobWFuYWdlcl9jb3Vyc2VcIixcInhjX3RlYWNobWFuYWdlcl9jb3Vyc2VfYWRkXCIsXCJ4Y190ZWFjaG1hbmFnZXJfY291cnNlX2Jhc2VcIixcInhjX3N5c21hbmFnZXJfY29tcGFueVwiLFwieGNfdGVhY2htYW5hZ2VyX2NvdXJzZV9saXN0XCJdLFwic2V4XCI6XCIxXCIsXCJzdGF0dXNcIjpcIjFcIixcInVzZXJuYW1lXCI6XCJzdHUxXCIsXCJ1c2VycGljXCI6XCJodHRwOi8vZmlsZS54dWVjaGVuZy1wbHVzLmNvbS9kZGRmXCIsXCJ1dHlwZVwiOlwiMTAxMDAxXCJ9Iiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTY2NzI5OTQwNiwiYXV0aG9yaXRpZXMiOlsieGNfc3lzbWFuYWdlcl9kb2MiLCJ4Y19zeXNtYW5hZ2VyX3VzZXJfdmlldyIsInhjX3RlYWNobWFuYWdlcl9jb3Vyc2UiLCJ4Y19zeXNtYW5hZ2VyX3VzZXJfYWRkIiwieGNfc3lzbWFuYWdlcl9jb21wYW55IiwieGNfc3lzbWFuYWdlcl91c2VyX2RlbGV0ZSIsInhjX3N5c21hbmFnZXJfdXNlciIsInhjX3RlYWNobWFuYWdlcl9jb3Vyc2VfYmFzZSIsInhjX3RlYWNobWFuYWdlcl9jb3Vyc2VfbGlzdCIsInhjX3N5c21hbmFnZXIiLCJ4Y19zeXNtYW5hZ2VyX2xvZyIsInhjX3N5c21hbmFnZXJfdXNlcl9lZGl0IiwieGNfdGVhY2htYW5hZ2VyX2NvdXJzZV9hZGQiXSwianRpIjoiOTYyOTYzMWQtYjRiMC00NTlkLTgzYzktM2Q4MmRiNmI4NDEzIiwiY2xpZW50X2lkIjoiWGNXZWJBcHAifQ.b77ZreiNlPoN-_dnAWxuBfH32tPIoRwg2ePgKn_aZ8c

2.3.2 前后端聯(lián)調(diào)

測試流程:

1、啟動認證服務(wù)、網(wǎng)關(guān)服務(wù)、驗證碼服務(wù)、學(xué)習(xí)中心服務(wù)、內(nèi)容管理服務(wù)。

2、發(fā)布一門免費課程、一門收費課程

3、進入課程詳情界面,點擊"馬上學(xué)習(xí)"

4、報名成功,自動跳轉(zhuǎn)到學(xué)習(xí)界面。

5、觀察選課記錄表、我的課程表數(shù)據(jù)是否正確。

對于免費課程在課程詳情頁面點擊"馬上學(xué)習(xí)",通過引導(dǎo)界面添加選課。

1、進入課程詳情點擊馬上學(xué)習(xí)

2、課程免費時引導(dǎo)加入我的課程表、或進入學(xué)習(xí)界面。

3 支付

3.1 需求分析

3.1.1 執(zhí)行流程

用戶去學(xué)習(xí)收費課程時引導(dǎo)其去支付,如下圖:

當(dāng)用戶點擊"微信支付"或支付寶支付時執(zhí)行流程如下:

1、請求學(xué)習(xí)中心服務(wù)創(chuàng)建選課記錄

2、請求訂單服務(wù)創(chuàng)建商品訂單、生成支付二維碼。

3、用戶掃碼請求訂單支付服務(wù),訂單支付服務(wù)請求第三方支付平臺生成支付訂單。

4、前端喚起支付客戶端,用戶輸入密碼完成支付。

5、第三方支付平臺支付完成發(fā)起支付通知。

6、訂單支付服務(wù)接收第三方支付通知結(jié)果。

7、用戶在前端查詢支付結(jié)果,請求訂單支付服務(wù)查詢支付結(jié)果。

8、訂單支付服務(wù)向?qū)W習(xí)中心服務(wù)通知支付結(jié)果。

9、學(xué)習(xí)中心服務(wù)收到支付結(jié)果,如果支付成功則更新選課記錄,并添加到我的課程表。

3.1.2 通用訂單服務(wù)設(shè)計

在本項目中不僅選課需要下單、購買學(xué)習(xí)資料、老師一對一答疑等所以收費項目都需要下單支付。

所以本項目設(shè)計通用的訂單服務(wù),通用的訂單服務(wù)承接各業(yè)務(wù)模塊的收費支付需求,當(dāng)用戶需要交費時統(tǒng)一生成商品訂單并進行支付。

所有收費業(yè)務(wù)最終轉(zhuǎn)換為商品訂單記錄在訂單服務(wù)的商品訂單表。

以選課為例,選課記錄表的ID記錄在商品訂單表的out_business_id字段。

3.2 支付接口調(diào)研

3.2.1 微信支付接口調(diào)研

一般情況下,一個網(wǎng)站要支持在線支付功能通常接入第三方支付平臺,比如:微信支付、支付寶、其它的聚合支付平臺。

本項目的需求實現(xiàn)手機掃碼支付,現(xiàn)在對微信、支付寶的支付接口進行調(diào)研。

微信目前提供的支付方式如下:

地址:https://pay.weixin.qq.com/static/product/product_index.shtml

1、付款碼支付是指用戶展示微信錢包內(nèi)的"付款碼"給商戶系統(tǒng)掃描后直接完成支付,適用于線下場所面對面收銀的場景,例如商超、便利店、餐飲、醫(yī)院、學(xué)校、電影院和旅游景區(qū)等具有明確經(jīng)營地址的實體場所。

2、JSAPI支付是指商戶通過調(diào)用微信支付提供的JSAPI接口,在支付場景中調(diào)起微信支付模塊完成收款

線下場所:調(diào)用接口生成二維碼,用戶掃描二維碼后在微信瀏覽器中打開頁面后完成支付

公眾號場景:用戶在微信公眾賬號內(nèi)進入商家公眾號,打開某個主頁面,完成支付

PC網(wǎng)站場景:在網(wǎng)站中展示二維碼,用戶掃描二維碼后在微信瀏覽器中打開頁面后完成支付

3、小程序支付是指商戶通過調(diào)用微信支付小程序支付接口,在微信小程序平臺內(nèi)實現(xiàn)支付功能;用戶打開商家助手小程序下單,輸入支付密碼并完成支付后,返回商家小程序。

4、Native支付是指商戶系統(tǒng)按微信支付協(xié)議生成支付二維碼,用戶再用微信"掃一掃"完成支付的模式。該模式適用于PC網(wǎng)站、實體店單品或訂單、媒體廣告支付等場景。

5、APP支付是指商戶通過在移動端應(yīng)用APP中集成開放SDK調(diào)起微信支付模塊來完成支付。適用于在移動端APP中集成微信支付功能的場景。

6、刷臉支付是指用戶在刷臉設(shè)備前通過攝像頭刷臉、識別身份后進行的一種支付方式,安全便捷。適用于線下實體場所的收銀場景,如商超、餐飲、便利店、醫(yī)院、學(xué)校等。

以上接口native和JSAPI都可以實現(xiàn)pc網(wǎng)站實現(xiàn)掃碼支付,兩者區(qū)別是什么?怎么選擇?

JSAPI除了在pc網(wǎng)站掃碼支付還可以實現(xiàn)公眾號頁面內(nèi)支付,可以實現(xiàn)在手機端H5頁面喚起微信客戶端完成支付。

本項目選擇JSAPI支付接口。

接口文檔:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml

如何開通JSAPI支付接口?

以企業(yè)身份注冊微信公眾號https://mp.weixin.qq.com/

登錄公眾號,點擊左側(cè)菜單"微信支付"開通微信支付,如下:

需要提供營業(yè)執(zhí)照、身份證等信息。

點擊申請接入,需要注冊微信商戶號。

注冊微信商戶號的過程請參考官方文檔,本文檔略。參考地址如下:

https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal#none

開通微信支付后即可在微信商戶平臺(pay.weixin.qq.com)開通JSAPI支付。

登錄商品平臺,進入產(chǎn)品中心,開通JSAPI支付:

注意:JSAPI支付方式需要在公眾號配置回調(diào)域名,此域名為已經(jīng)備案的外網(wǎng)域名。

最后在公眾號開發(fā)信息中獲取:開發(fā)者id、開發(fā)者密碼。

3.2.2 支付寶接口調(diào)研

支付寶支付產(chǎn)品如下:

文檔:https://b.alipay.com/signing/productSetV2.htm

與本項目需求相關(guān)的接口:電腦網(wǎng)站支付、手機網(wǎng)站支付。

1、電腦網(wǎng)站支付

PC網(wǎng)站輕松收款,資金馬上到賬:用戶在商家PC網(wǎng)站消費,自動跳轉(zhuǎn)支付寶PC網(wǎng)站收銀臺完成付款。
交易資金直接打入商家支付寶賬戶,實時到賬。

2、手機網(wǎng)站支付

用戶在商家手機網(wǎng)站消費,通過瀏覽器自動跳轉(zhuǎn)支付寶APP或支付寶網(wǎng)頁完成付款。
輕松實現(xiàn)和APP支付相同的支付體驗。

對比兩種支付方式:手機網(wǎng)站支付方式可以在H5網(wǎng)頁喚起支付寶,手機掃碼支付可以使用手機網(wǎng)站支付方式來完成,相比電腦網(wǎng)站支付形式更靈活。

本項目選擇手機網(wǎng)站支付方式。

文檔:https://opendocs.alipay.com/open/02ivbt

如何開通支付寶手機網(wǎng)站支付接口?

進入網(wǎng)址:https://b.alipay.com/signing/productDetailV2.htm?productId=I1011000290000001001

點擊:立即開通

上傳營業(yè)執(zhí)照等資料,提交審核,根據(jù)提示進行開通。

3.3 準(zhǔn)備開發(fā)環(huán)境

3.3.1 支付寶開發(fā)環(huán)境

第三方支付接口流程大同小異,考慮開發(fā)及教學(xué)的方便性,支付寶提供支付寶沙箱環(huán)境開發(fā)支付接口,在教學(xué)中接入支付寶手機網(wǎng)站支付接口。

1、配置沙箱環(huán)境

沙箱環(huán)境是支付寶開放平臺為開發(fā)者提供的與生產(chǎn)環(huán)境完全隔離的聯(lián)調(diào)測試環(huán)境,開發(fā)者在沙箱環(huán)境中完成的接口調(diào)用不會對生產(chǎn)環(huán)境中的數(shù)據(jù)造成任何影響。

接入手機網(wǎng)站支付需要具備如下條件:

申請前必須擁有經(jīng)過實名認證的支付寶賬戶;

企業(yè)或個體工商戶可申請;

需提供真實有效的營業(yè)執(zhí)照,且支付寶賬戶名稱需與營業(yè)執(zhí)照主體一致;

網(wǎng)站能正常訪問且頁面顯示完整,網(wǎng)站需要明確經(jīng)營內(nèi)容且有完整的商品信息;

網(wǎng)站必須通過ICP備案。如為個體工商戶,網(wǎng)站備案主體需要與支付寶賬戶主體名稱一致;

如為個體工商戶,則團購不開放,且古玩、珠寶等奢侈品、投資類行業(yè)無法申請本產(chǎn)品。

詳細參見:https://docs.open.alipay.com/203

本文檔使用支付寶沙箱進行開發(fā)測試,這里主要介紹支付寶沙箱環(huán)境配置。

詳細參見:https://docs.open.alipay.com/200/105311/

2、模擬器

下載模擬器:http://mumu.163.com/

安裝模擬器,安裝在沒有空格和中文的目錄。

安裝成功,啟動模擬器

下一步在模擬器安裝支付寶:

選擇課程資料中支付寶安裝包wallet_101521226_client_release_201812261416.apk(沙箱版本)

安裝成功后支付寶客戶端的快捷方式出現(xiàn)在桌面上。

使用沙箱環(huán)境的買家賬號登錄沙箱版本的支付寶。

查看沙箱環(huán)境的賬號:

3.3.2 創(chuàng)建訂單服務(wù)

拷貝課程資料目錄下的訂單服務(wù)工程xuecheng-plus-orders到自己的工程目錄。

創(chuàng)建xc_orders數(shù)據(jù)庫,并導(dǎo)入xcplus_orders.sql

修改nacos中orders-service-dev.yaml的數(shù)據(jù)庫連接參數(shù)。

3.4 支付接口測試

3.4.1 閱讀接口定義

手機網(wǎng)站支付接入流程詳細參見:https://docs.open.alipay.com/203/105285/

1、接口交互流程如下:

1)用戶在商戶的H5網(wǎng)站下單支付后,商戶系統(tǒng)按照手機網(wǎng)站支付接口alipay.trade.wap.payAPI的參數(shù)規(guī)范生成訂單數(shù)據(jù)

2)前端頁面通過Form表單的形式請求到支付寶。此時支付寶會自動將頁面跳轉(zhuǎn)至支付寶H5收銀臺頁面,如果用戶手機上安裝了支付寶APP,則自動喚起支付寶APP。

3)輸入支付密碼完成支付。

4)用戶在支付寶APP或H5收銀臺完成支付后,會根據(jù)商戶在手機網(wǎng)站支付API中傳入的前臺回跳地址return_url自動跳轉(zhuǎn)回商戶頁面,同時在URL請求中以Query
String的形式附帶上支付結(jié)果參數(shù),詳細回跳參數(shù)見"手機網(wǎng)站支付接口alipay.trade.wap.pay"前臺回跳參數(shù)。

5)支付寶還會根據(jù)原始支付API中傳入的異步通知地址notify_url,通過POST請求的形式將支付結(jié)果作為參數(shù)通知到商戶系統(tǒng),詳情見支付結(jié)果異步通知。

2、接口定義

文檔:https://opendocs.alipay.com/open/203/107090

接口定義:外部商戶請求支付寶創(chuàng)建訂單并支付

公共參數(shù)

請求地址

開發(fā)中使用沙箱地址:https://openapi.alipaydev.com/gateway.do

請求參數(shù):

詳細查閱https://opendocs.alipay.com/open/203/107090

一部分由sdk設(shè)置,一部分需要編寫程序時指定。

其它擴展參數(shù)參見接口文檔。

3、示例代碼

public void doPost(HttpServletRequest httpRequest,HttpServletResponse httpResponse) throws ServletException, IOException {AlipayClient alipayClient = ... //獲得初始化的AlipayClientAlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//創(chuàng)建API對應(yīng)的requestalipayRequest.setReturnUrl("http://domain.com/CallBack/return_url.jsp");alipayRequest.setNotifyUrl("http://domain.com/CallBack/notify_url.jsp");//在公共參數(shù)中設(shè)置回跳和通知地址alipayRequest.setBizContent("{" +"    \"out_trade_no\":\"20150320010101002\"," +"    \"total_amount\":88.88," +"    \"subject\":\"Iphone6 16G\"," +"    \"product_code\":\"QUICK_WAP_WAY\"" +"  }");//填充業(yè)務(wù)參數(shù)String form = alipayClient.pageExecute(alipayRequest).getBody(); //調(diào)用SDK生成表單httpResponse.setContentType("text/html;charset=" + AlipayServiceEnvConstants.CHARSET);httpResponse.getWriter().write(form);//直接將完整的表單html輸出到頁面httpResponse.getWriter().flush();
}

3.4.2 下單執(zhí)行流程

根據(jù)接口描述,支付寶下單接口的執(zhí)行流程如下:

3.4.3 支付接口測試

3.4.3.1 編寫下單代碼

根據(jù)接口流程,首先在訂單服務(wù)編寫測試類請求支付寶下單的接口。

在訂單服務(wù)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>

下載示例代碼https://opendocs.alipay.com/open/203/105910

拷貝示例代碼,修改、測試。

拷貝AlipayConfig.java到訂單服務(wù)的service工程。

package com.xuecheng.orders.api;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;/*** @author Mr.M* @version 1.0* @description 測試支付寶接口* @date 2022/10/20 22:19*/
@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);//獲得初始化的AlipayClientAlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//創(chuàng)建API對應(yīng)的request
//        alipayRequest.setReturnUrl("http://domain.com/CallBack/return_url.jsp");
//        alipayRequest.setNotifyUrl("http://domain.com/CallBack/notify_url.jsp");//在公共參數(shù)中設(shè)置回跳和通知地址alipayRequest.setBizContent("{" +"    \"out_trade_no\":\"202210100010101002\"," +"    \"total_amount\":0.1," +"    \"subject\":\"Iphone6 16G\"," +"    \"product_code\":\"QUICK_WAP_WAY\"" +"  }");//填充業(yè)務(wù)參數(shù)String form = alipayClient.pageExecute(alipayRequest).getBody(); //調(diào)用SDK生成表單httpResponse.setContentType("text/html;charset=" + AlipayConfig.CHARSET);httpResponse.getWriter().write(form);//直接將完整的表單html輸出到頁面httpResponse.getWriter().flush();}}
3.4.3.2 生成二維碼

用戶在前端使用支付寶沙箱通過掃碼請求下單接口,我們需要生成訂單服務(wù)的下單接口的二維碼。

ZXing是一個開源的類庫,是用Java編寫的多格式的1D /
2D條碼圖像處理庫,使用ZXing可以生成、識別QR
Code(二維碼)。常用的二維碼處理庫還有zbar,近幾年已經(jīng)不再更新代碼,下邊介紹ZXing生成二維碼的方法。

1)引入依賴

在base工程pom.xml中添加依賴:

        <!-- 二維碼生成&識別組件 --><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>

2)生成二維碼方法

拷貝課程資料中utils下的QRCodeUtil.java到base工程util包下。

測試根據(jù)內(nèi)容生成二維碼方法,在QRCodeUtil中添加main方法如下:

    public static void main(String[] args) throws IOException {QRCodeUtil qrCodeUtil = new QRCodeUtil();System.out.println(qrCodeUtil.createQRCode("http://www.itcast.cn/", 200, 200));}

運行main方法輸入二維碼圖片的base64串,如下:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIAQAAAACFI5MzAAABQElEQVR42u2YPZKDMAyF5aFIuUfIUThafDSOwhEoUzC8fZKMySSbrVI8ZuICBX8uIvtZPxjeDfuSf8liPi7LFSgrzRTvV3XCKawXYLptFobviz6ZzB2xEfTjhyS9OwXB3A7jbMSngLOQ0I4v2AZf96wqTWJ9+9/dYEHSx2RYqfg/oqUgiX3nFBVfcCepcSbiJP67iwZ1G+5+Am7kyTzW9OcW/kRAX+QJ953+uCl8zO5PV5UsaffUp8rqP5+jiySJU8jtNxcNrysetCNK6A/V4lEQeU+xa0eZREE1tOTpFYod0VKXsKCqvRqMkW5pkza8Ggy3WgEuTvZcz0dcUBc+9MneL1DqkXjQz0eaZA1LqVtmzcMffTKPiPwz1mh2zkGyNwtT9kguTVI7LWv6ul7DCpOjX9iaGV66HDny/ZL1WfILfc/hMHLUpekAAAAASUVORK5CYII=

將base64串復(fù)制到瀏覽器地址后回車將展示一個二維碼,用戶用手機掃此二維碼將請求至http://www.itcast.cn/。

3.4.3.3 接口測試

1、生成訂單服務(wù)下單接口的二維碼

修改二維碼生成的代碼如下:

public static void main(String[] args) throws IOException {QRCodeUtil qrCodeUtil = new QRCodeUtil();System.out.println(qrCodeUtil.createQRCode("http://localhost:63030/orders/alipaytest", 200, 200));}

注意:http://localhost:63030地址用模擬器無法訪問,進入cmd命令狀態(tài),輸入命令ipconfig
-all 查看本地網(wǎng)卡分配的局域網(wǎng)ip地址,將上邊的地址修改如下:

http://192.168.101.1:63030/orders/alipaytest

運行main方法,復(fù)制輸出到控制臺的base64串。

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIAQAAAACFI5MzAAABxUlEQVR42u2YO46EMBBEGxE45AjcBC6GhCUuNtzER3DoANFbZT4zu9KmNavVOLAwj6BV/TXmvy37kL9Oopl1sS+DWV/M2oRzKyWL+95tfXDfenyzJK/vlCTZ0G1WGpzDDlKA9ST2YbfJrMnz2wiE8WQw8A2E/lkc6kQ66afnBKTGKCwaru179ApIXVBnzNb7gy+/ZbCAQJgB5zLCyrD6ZnSSlPAxm1VNyphh28NmKUFI7F3EQ55qrXJfL3VUBOGZJyssF74C45tWSjbEBW2DJslG94QPtQTNYsyzH9WSrgmXOiqCCBlgFmIUhMLwKCVcSFHkxsaagcY1vmSwgkRDZE5WO4YzT6tYSuIprNTEzskBedq5lNS4wEs2TOiEbT1tUxGslaW6OoX98+5ZKhLpmhYFi8LsTNY7T1WE7apNzE5UCgiDD2cpca+T612v0zNGZYQWMVDhJG7h2dE1BOoMSIujUjQcZUYxOWcXBodzeuKmJdddhtNTqZNcc1cxDTnuMmwbxr7dup5wjl8S44J9O75Mdkpyzo1hr/eqV9tUBE+8waBy80p1T3YictxpDyexcQUXk+Muw9m5RohZnWKU5PMX55+RLyKnzJvqtaeFAAAAAElFTkSuQmCC

打開模擬器,在模擬器中打開瀏覽器,將base64串復(fù)制到瀏覽器的地址欄。

使用截屏工具進行截屏,稍后使用支付寶沙箱客戶端掃此圖片。

2、啟動訂單服務(wù)

3、打開模擬器,在模擬器中打開支付寶沙箱客戶端,并使用沙箱客戶端賬號密碼登錄。

點擊掃一掃選擇相冊中剛才截屏的二維碼

掃碼后如果提示系統(tǒng)繁忙再重試

如果提示請求勿重復(fù)提交則需要修改下單測試代碼中指定的out_trade_no商品訂單號,訂單號在每個商戶是唯一的,每次支付前修改out_trade_no
為一個沒有使用過的訂單號。

修改訂單號后重啟訂單服務(wù),再使用沙箱支付寶客戶端掃碼

輸入支付密碼進行支付。

支付密碼為沙箱賬號的支付密碼,此支付所扣款為沙箱賬號的虛擬貨幣余額。

支付成功界面:

3.4.4 支付結(jié)果查詢接口

支付完成可以調(diào)用第三方支付平臺的支付結(jié)果查詢接口 查詢支付結(jié)果。

文檔:https://opendocs.alipay.com/open/02ivbt

示例代碼:

AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.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("調(diào)用成功");
} else {System.out.println("調(diào)用失敗");
}

剛才訂單付款成功,可以使用out_trade_no商品訂單號或支付寶的交易流水號trade_no去查詢支付結(jié)果。

out_trade_no商品訂單號: 是在下單請求時指定的商品訂單號。

支付寶的交易流水號trade_no:是支付完成后支付寶通知支付結(jié)果時發(fā)送的trade_no

我們使用out_trade_no商品訂單號去查詢,代碼如下:

package com.xuecheng.orders.api;import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.response.AlipayTradeQueryResponse;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;/*** @author Mr.M* @version 1.0* @description 支付寶查詢接口* @date 2022/10/4 17:18*/
@SpringBootTest
public class AliPayTest {@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 {AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, "json", AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE); //獲得初始化的AlipayClientAlipayTradeQueryRequest request = new AlipayTradeQueryRequest();JSONObject bizContent = new JSONObject();bizContent.put("out_trade_no", "202210100010101002");//bizContent.put("trade_no", "2014112611001004680073956707");request.setBizContent(bizContent.toString());AlipayTradeQueryResponse response = alipayClient.execute(request);if (response.isSuccess()) {System.out.println("調(diào)用成功");String resultJson = response.getBody();//轉(zhuǎn)mapMap resultMap = JSON.parseObject(resultJson, Map.class);Map alipay_trade_query_response = (Map) resultMap.get("alipay_trade_query_response");//支付結(jié)果String trade_status = (String) alipay_trade_query_response.get("trade_status");System.out.println(trade_status);} else {System.out.println("調(diào)用失敗");}
}
}

運行代碼,輸出如下:

調(diào)用成功
TRADE_SUCCESS

輸出結(jié)果即是調(diào)用支付寶查詢接口查詢到的支付結(jié)果

參考文檔https://opendocs.alipay.com/open/02ivbt 查閱每個參數(shù)的意義。

我們主要需要下邊的參數(shù):

“out_trade_no” : “20220520010101026”,

“trade_no”:“2022100422001422760505740639” : 支付寶交易流水號

“total_amount” : “1.30”

“trade_status” : “TRADE_SUCCESS”: 交易狀態(tài)

交易狀態(tài)類型:

交易狀態(tài):WAIT_BUYER_PAY(交易創(chuàng)建,等待買家付款)

TRADE_CLOSED(未付款交易超時關(guān)閉,或支付完成后全額退款)

TRADE_SUCCESS(交易支付成功)

TRADE_FINISHED(交易結(jié)束,不可退款)

3.4.5 支付結(jié)果通知接口

3.4.5.1 準(zhǔn)備環(huán)境

對于手機網(wǎng)站支付產(chǎn)生的交易,支付寶會通知商戶支付結(jié)果,有兩種通知方式,通過return_url、notify_url進行通知,使用return_url不能保證通知到位,推薦使用notify_url完成支付結(jié)構(gòu)通知。

具體的使用方法是在調(diào)用下單接口的 API 中傳入的異步通知地址
notify_url,通過 POST
請求的形式將支付結(jié)果作為參數(shù)通知到商戶系統(tǒng)。詳情可查看
支付寶異步通知說明 。

文檔:https://opendocs.alipay.com/open/203/105286

根據(jù)下單執(zhí)行流程,訂單服務(wù)收到支付結(jié)果需要對內(nèi)容進行驗簽,驗簽過程如下:

在通知返回參數(shù)列表中,除去sign、sign_type兩個參數(shù)外,凡是通知返回回來的參數(shù)皆是待驗簽的參數(shù)。將剩下參數(shù)進行
url_decode,然后進行字典排序,組成字符串,得到待簽名字符串;
生活號異步通知組成的待驗簽串里需要保留 sign_type 參數(shù)。

將簽名參數(shù)(sign)使用 base64 解碼為字節(jié)碼串;

使用 RSA 的驗簽方法,通過簽名字符串、簽名參數(shù)(經(jīng)過 base64
解碼)及支付寶公鑰驗證簽名。

驗證簽名正確后,必須再嚴格按照如下描述校驗通知數(shù)據(jù)的正確性。

在上述驗證通過后,商戶必須根據(jù)支付寶不同類型的業(yè)務(wù)通知,正確的進行不同的業(yè)務(wù)處理,并且過濾重復(fù)的通知結(jié)果數(shù)據(jù)。

通過驗證out_trade_no、total_amount、appid參數(shù)的正確性判斷通知請求的合法性。

驗證的過程可以參考sdk demo代碼,下載 sdk
demo代碼,https://opendocs.alipay.com/open/203/105910

參考demo中的alipay.trade.wap.pay-java-utf-8\WebContent\ notify_url.jsp

另外,支付寶通知訂單服務(wù)的地址必須為外網(wǎng)域名且備案通過可以正常訪問。

此接口仍然使用內(nèi)網(wǎng)穿透技術(shù)。

3.4.5.2 編寫測試代碼

1、在下單請求時設(shè)置通知地址request.setNotifyUrl(“商戶自己的notify_url地址”);

@GetMapping("/alipaytest")public void alipaytest(HttpServletRequest httpRequest,HttpServletResponse httpResponse) throws ServletException, IOException {//構(gòu)造sdk的客戶端對象AlipayClient alipayClient = new DefaultAlipayClient(serverUrl, APP_ID, APP_PRIVATE_KEY, "json", CHARSET, ALIPAY_PUBLIC_KEY, sign_type); //獲得初始化的AlipayClientAlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//創(chuàng)建API對應(yīng)的request
//        alipayRequest.setReturnUrl("http://domain.com/CallBack/return_url.jsp");alipayRequest.setNotifyUrl("http://tjxt-user-t.itheima.net/xuecheng/orders/paynotify");//在公共參數(shù)中設(shè)置回跳和通知地址.....

2、編寫接收通知接口,接收參數(shù)并驗簽

參考課程資料下的alipay.trade.wap.pay-java-utf-8\WebContent\notify_url.jsp

代碼如下:

//接收通知
@PostMapping("/paynotify")
public void paynotify(HttpServletRequest request,HttpServletResponse response) throws UnsupportedEncodingException, 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] + ",";}//亂碼解決,這段代碼在出現(xiàn)亂碼時使用。如果mysign和sign不相等也可以使用這段代碼轉(zhuǎn)化//valueStr = new String(valueStr.getBytes("ISO-8859-1"), "gbk");params.put(name, valueStr);}//獲取支付寶的通知返回參數(shù),可參考技術(shù)文檔中頁面跳轉(zhuǎn)同步通知參數(shù)列表(以上僅供參考)////計算得出通知驗證結(jié)果//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) {//驗證成功////請在這里加上商戶的業(yè)務(wù)邏輯程序代碼//商戶訂單號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");//交易狀態(tài)String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8");//——請根據(jù)您的業(yè)務(wù)邏輯來編寫程序(以下代碼僅作參考)——if (trade_status.equals("TRADE_FINISHED")) {//交易結(jié)束//判斷該筆訂單是否在商戶網(wǎng)站中已經(jīng)做過處理//如果沒有做過處理,根據(jù)訂單號(out_trade_no)在商戶網(wǎng)站的訂單系統(tǒng)中查到該筆訂單的詳細,并執(zhí)行商戶的業(yè)務(wù)程序//請務(wù)必判斷請求時的total_fee、seller_id與通知時獲取的total_fee、seller_id為一致的//如果有做過處理,不執(zhí)行商戶的業(yè)務(wù)程序//注意://如果簽約的是可退款協(xié)議,退款日期超過可退款期限后(如三個月可退款),支付寶系統(tǒng)發(fā)送該交易狀態(tài)通知//如果沒有簽約可退款協(xié)議,那么付款完成后,支付寶系統(tǒng)發(fā)送該交易狀態(tài)通知。} else if (trade_status.equals("TRADE_SUCCESS")) {//交易成功System.out.println(trade_status);//判斷該筆訂單是否在商戶網(wǎng)站中已經(jīng)做過處理//如果沒有做過處理,根據(jù)訂單號(out_trade_no)在商戶網(wǎng)站的訂單系統(tǒng)中查到該筆訂單的詳細,并執(zhí)行商戶的業(yè)務(wù)程序//請務(wù)必判斷請求時的total_fee、seller_id與通知時獲取的total_fee、seller_id為一致的//如果有做過處理,不執(zhí)行商戶的業(yè)務(wù)程序//注意://如果簽約的是可退款協(xié)議,那么付款完成后,支付寶系統(tǒng)發(fā)送該交易狀態(tài)通知。}response.getWriter().write("success");}else{response.getWriter().write("fail");}}
3.4.5.3 通知接口測試

1、重啟訂單服務(wù),并在接收通知接口中打上斷點

2、配置內(nèi)網(wǎng)穿透的本地端口為訂單服務(wù)端口,啟動內(nèi)網(wǎng)穿透客戶端。

3、打開模擬器、支付寶沙箱,掃碼、支付。

4、觀察接收訂單支付數(shù)據(jù)等是否正常。

3.5 生成支付二維碼

3.5.1 需求分析

3.5.1.1 執(zhí)行流程

再次打開課程支付引導(dǎo)界面,點擊"支付寶支付"按鈕系統(tǒng)該如何處理?

點擊"支付寶支付"此時打開支付二維碼,用戶掃碼支付。

所以首先需要生成支付二維碼,用戶掃描二維碼開始請求支付寶下單,在向支付寶下單前需要添加選課記錄、創(chuàng)建商品訂單、生成支付交易記錄。

生成二維碼執(zhí)行流程如下:

執(zhí)行流程:

1、前端調(diào)用學(xué)習(xí)中心服務(wù)的添加選課接口。

2、添加選課成功請求訂單服務(wù)生成支付二維碼接口。

3、生成二維碼接口:創(chuàng)建商品訂單、生成支付交易記錄、生成二維碼。

4、將二維碼返回到前端,用戶掃碼。

用戶掃碼支付流程如下:

執(zhí)行流程:

1、用戶輸入支付密碼,支付成功。

2、接收第三方平臺通知的支付結(jié)果。

3、根據(jù)支付結(jié)果更新支付交易記錄的支付狀態(tài)為支付成功。

3.5.1.2 數(shù)據(jù)模型

訂單支付模式的核心由三張表組成:訂單表、訂單明細表、支付交易記錄表。

訂單表:記錄訂單信息

訂單明細表記錄訂單的詳細信息

支付交易記錄表記錄每次支付的交易明細

訂單號注意唯一性、安全性、盡量短等特點,生成方案常用的如下:

1、時間戳+隨機數(shù)

年月日時分秒毫秒+隨機數(shù)

2、高并發(fā)場景

年月日時分秒毫秒+隨機數(shù)+redis自增序列

3、訂單號中加上業(yè)務(wù)標(biāo)識

訂單號加上業(yè)務(wù)標(biāo)識方便客服,比如:第10位是業(yè)務(wù)類型,第11位是用戶類型等。

4、雪花算法

雪花算法是推特內(nèi)部使用的分布式環(huán)境下的唯一ID生成算法,它基于時間戳生成,保證有序遞增,加以入計算機硬件等元素,可以滿足高并發(fā)環(huán)境下ID不重復(fù)。

本項目訂單號生成采用雪花算法。

3.5.2 接口定義

在訂單服務(wù)中定義生成支付二維碼接口。

請求:訂單信息

package com.xuecheng.orders.model.dto;import com.xuecheng.orders.model.po.XcOrders;
import lombok.Data;
import lombok.ToString;/*** @author Mr.M* @version 1.0* @description 創(chuàng)建商品訂單* @date 2022/10/4 10:21*/
@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;/*** 外部系統(tǒng)業(yè)務(wù)id*/private String outBusinessId;}

響應(yīng):支付交易記錄信息及二維碼信息

@Data
@ToString
public class PayRecordDto extends XcPayRecord {//二維碼private String qrcode;}

接口定義如下:

@Api(value = "訂單支付接口", tags = "訂單支付接口")
@Slf4j
@Controller
public class OrderController {@ApiOperation("生成支付二維碼")@PostMapping("/generatepaycode")@ResponseBodypublic PayRecordDto generatePayCode(@RequestBody AddOrderDto addOrderDto) {return null;}}

用戶掃碼請求下單,定義下單接口如下:

@ApiOperation("掃碼下單接口")
@GetMapping("/requestpay")
public void requestpay(String payNo,HttpServletResponse httpResponse) throws IOException {}

3.5.3 接口實現(xiàn)

3.5.3.1 保存商品訂單

定義保存訂單信息接口

public interface OrderService {/*** @description 創(chuàng)建商品訂單* @param addOrderDto 訂單信息* @return PayRecordDto 支付交易記錄(包括二維碼)* @author Mr.M* @date 2022/10/4 11:02
*/
public PayRecordDto createOrder(String userId,AddOrderDto addOrderDto);

在保存訂單接口中需要完成創(chuàng)建商品訂單、創(chuàng)建支付交易記錄,接口實現(xiàn)方法如下:

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {@AutowiredXcOrdersMapper ordersMapper;@AutowiredXcOrdersGoodsMapper ordersGoodsMapper;@AutowiredXcPayRecordMapper payRecordMapper;@Transactional@Overridepublic PayRecordDto createOrder(String userId, AddOrderDto addOrderDto) {//添加商品訂單//添加支付交易記錄//生成二維碼return null;}
}

編寫創(chuàng)建商品訂單方法,商品訂單的數(shù)據(jù)來源于選課記錄,在訂單表需要存入選課記錄的ID,這里需要作好冪等處理。

@Transactional
public XcOrders saveXcOrders(String userId,AddOrderDto addOrderDto){//冪等性處理XcOrders order = getOrderByBusinessId(addOrderDto.getOutBusinessId());if(order!=null){return order;}order = new XcOrders();//生成訂單號long orderId = IdWorkerUtils.getInstance().nextId();order.setId(orderId);order.setTotalPrice(addOrderDto.getTotalPrice());order.setCreateDate(LocalDateTime.now());order.setStatus("600001");//未支付order.setUserId(userId);order.setOrderType(addOrderDto.getOrderType());order.setOrderName(addOrderDto.getOrderName());order.setOrderDetail(addOrderDto.getOrderDetail());order.setOrderDescrip(addOrderDto.getOrderDescrip());order.setOutBusinessId(addOrderDto.getOutBusinessId());//選課記錄idordersMapper.insert(order);String orderDetailJson = addOrderDto.getOrderDetail();List<XcOrdersGoods> xcOrdersGoodsList = JSON.parseArray(orderDetailJson, XcOrdersGoods.class);xcOrdersGoodsList.forEach(goods->{XcOrdersGoods xcOrdersGoods = new XcOrdersGoods();BeanUtils.copyProperties(goods,xcOrdersGoods);xcOrdersGoods.setOrderId(orderId);//訂單號ordersGoodsMapper.insert(xcOrdersGoods);});return order;
}//根據(jù)業(yè)務(wù)id查詢訂單
public XcOrders getOrderByBusinessId(String businessId) {XcOrders orders = ordersMapper.selectOne(new LambdaQueryWrapper<XcOrders>().eq(XcOrders::getOutBusinessId, businessId));return orders;
}
3.5.3.2 創(chuàng)建支付交易記錄

為什么創(chuàng)建支付交易記錄?

在請求微信或支付寶下單接口時需要傳入
商品訂單號,在與第三方支付平臺對接時發(fā)現(xiàn),當(dāng)用戶支付失敗或因為其它原因最終該訂單沒有支付成功,此時再次調(diào)用第三方支付平臺的下單接口發(fā)現(xiàn)報錯"訂單號已存在",此時如果我們傳入一個沒有使用過的訂單號就可以解決問題,但是商品訂單已經(jīng)創(chuàng)建,因為沒有支付成功重新創(chuàng)建一個新訂單是不合理的。

解決以上問題的方案是:

1、用戶每次發(fā)起都創(chuàng)建一個新的支付交易記錄 ,此交易記錄與商品訂單關(guān)聯(lián)。

2、將支付交易記錄的流水號傳給第三方支付系統(tǒng)下單接口,這樣就即使沒有支付成功就不會出現(xiàn)上邊的問題。

3、需要提醒用戶不要重復(fù)支付。

編寫創(chuàng)建支付交易記錄的方法:

public XcPayRecord createPayRecord(XcOrders orders){if(order==null){XueChengPlusException.cast("訂單不存在");}if(orders.getStatus().equals("600002")){XueChengPlusException.cast("訂單已支付");}XcPayRecord payRecord = new XcPayRecord();//生成支付交易流水號long payNo = IdWorkerUtils.getInstance().nextId();payRecord.setPayNo(payNo);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());payRecordMapper.insert(payRecord);return payRecord;}
3.5.3.3 生成支付二維碼

1、在nacos中orders-service-dev.yaml配置二維碼的url

pay:qrcodeurl: http://192.168.101.1/api/orders/requestpay?payNo=%s

2、完善創(chuàng)建訂單service方法:

@Value("${pay.qrcodeurl}")
String qrcodeurl;@Transactional
@Override
public PayRecordDto createOrder(String userId, AddOrderDto addOrderDto) {//創(chuàng)建商品訂單XcOrders orders = saveXcOrders(userId, addOrderDto);if(orders==null){XueChengPlusException.cast("訂單創(chuàng)建失敗");}if(orders.getStatus().equals("600002")){XueChengPlusException.cast("訂單已支付");}//生成支付記錄XcPayRecord payRecord = createPayRecord(orders);//生成二維碼String qrCode = null;try {//url要可以被模擬器訪問到,url為下單接口(稍后定義)String url = String.format(qrcodeurl, payRecord.getPayNo());qrCode = new QRCodeUtil().createQRCode(url, 200, 200);} catch (IOException e) {XueChengPlusException.cast("生成二維碼出錯");}PayRecordDto payRecordDto = new PayRecordDto();BeanUtils.copyProperties(payRecord,payRecordDto);payRecordDto.setQrcode(qrCode);return payRecordDto;
}
3.5.3.4 生成二維碼接口完善

完善生成支付二維碼controller接口

@Autowired
OrderService orderService;@ApiOperation("生成支付二維碼")
@PostMapping("/generatepaycode")
@ResponseBody
public PayRecordDto generatePayCode(@RequestBody AddOrderDto addOrderDto) {//登錄用戶SecurityUtil.XcUser user = SecurityUtil.getUser();if(user == null){XueChengPlusException.cast("請登錄后繼續(xù)選課");}return orderService.createOrder(user.getId(), addOrderDto);}
3.5.3.5 掃碼下單接口完善

生成了支付二維碼,用戶掃碼請求第三方支付平臺下單、支付。

1、定義查詢支付交易記錄的Service接口與實現(xiàn)方法

/*** @description 查詢支付交易記錄* @param payNo  交易記錄號* @return com.xuecheng.orders.model.po.XcPayRecord* @author Mr.M* @date 2022/10/20 23:38
*/
public XcPayRecord getPayRecordByPayno(String payNo);

實現(xiàn)如下:

@Override
public XcPayRecord getPayRecordByPayno(String payNo) {XcPayRecord xcPayRecord = payRecordMapper.selectOne(new LambdaQueryWrapper<XcPayRecord>().eq(XcPayRecord::getPayNo, payNo));return xcPayRecord;
}

2 定義下單接口如下:

@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;@ApiOperation("掃碼下單接口")@GetMapping("/requestpay")public void requestpay(String payNo,HttpServletResponse httpResponse) throws IOException {//如果payNo不存在則提示重新發(fā)起支付XcPayRecord payRecord = orderService.getPayRecordByPayno(payNo);if(payRecord == null){XueChengPlusException.cast("請重新點擊支付獲取二維碼");}//支付狀態(tài)String status = payRecord.getStatus();if("601002".equals(status)){XueChengPlusException.cast("訂單已支付,請勿重復(fù)支付。");}//構(gòu)造sdk的客戶端對象AlipayClient client = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, AlipayConfig.FORMAT, AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE);//獲得初始化的AlipayClientAlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//創(chuàng)建API對應(yīng)的request
//        alipayRequest.setReturnUrl("http://domain.com/CallBack/return_url.jsp");
//        alipayRequest.setNotifyUrl("http://tjxt-user-t.itheima.net/xuecheng/orders/paynotify");//在公共參數(shù)中設(shè)置回跳和通知地址alipayRequest.setBizContent("{" +" \"out_trade_no\":\""+payRecord.getPayNo()+"\"," +" \"total_amount\":\""+payRecord.getTotalPrice()+"\"," +" \"subject\":\""+payRecord.getOrderName()+"\"," +" \"product_code\":\"QUICK_WAP_PAY\"" +" }");//填充業(yè)務(wù)參數(shù)String form = "";try {//請求支付寶下單接口,發(fā)起http請求form = client.pageExecute(alipayRequest).getBody(); //調(diào)用SDK生成表單} catch (AlipayApiException e) {e.printStackTrace();}httpResponse.setContentType("text/html;charset=" + AlipayConfig.CHARSET);httpResponse.getWriter().write(form);//直接將完整的表單html輸出到頁面httpResponse.getWriter().flush();httpResponse.getWriter().close();}

3.5.4 支付測試

測試準(zhǔn)備:

1、啟動網(wǎng)關(guān)服務(wù)、認證服務(wù)、驗證碼服務(wù)、學(xué)習(xí)中心服務(wù)、訂單服務(wù)、內(nèi)容管理服務(wù)。

2、發(fā)布一門收費課程。

3、使用資料目錄中的新模板course_template.ftl

測試流程:

1、進入收費課程詳細頁面,點擊馬上學(xué)習(xí)。

2、跟蹤瀏覽器及微服務(wù),觀察選課記錄是否創(chuàng)建成功、商品訂單是否創(chuàng)建成功、支付交易記錄是否創(chuàng)建成功。

3、觀察生成二維碼是否成功

4、使用模擬器掃碼測試,是否可以正常支付。

如果報訂單參數(shù)異常報如下錯誤,需要檢查請求支付寶的下單數(shù)據(jù)是否正確。

3.6 查詢支付結(jié)果

3.6.1 接口定義

根據(jù)前邊我們調(diào)研的獲取支付結(jié)果的接口,包括:主動查詢支付結(jié)果、被動接收支付結(jié)果。

這里先實現(xiàn)主動查詢支付結(jié)果,當(dāng)支付完成用戶點擊"支付結(jié)果"將請求第三方支付平臺查詢支付結(jié)果。

在OrderController類中定義接口如下:

@ApiOperation("查詢支付結(jié)果")
@GetMapping("/payresult")
@ResponseBody
public PayRecordDto payresult(String payNo) throws IOException {//查詢支付結(jié)果return null;}

3.6.2 接口實現(xiàn)

3.6.2.1 service總體接口

1、定義查詢支付結(jié)果的service

/*** 請求支付寶查詢支付結(jié)果* @param payNo 支付記錄id* @return 支付記錄信息*/
public PayRecordDto queryPayResult(String payNo);

2、service實現(xiàn)如下:

@Override
public PayRecordDto queryPayResult(String payNo){XcPayRecord payRecord = getPayRecordByPayno(payNo);if (payRecord == null) {XueChengPlusException.cast("請重新點擊支付獲取二維碼");}//支付狀態(tài)String status = payRecord.getStatus();//如果支付成功直接返回if ("601002".equals(status)) {PayRecordDto payRecordDto = new PayRecordDto();BeanUtils.copyProperties(payRecord, payRecordDto);return payRecordDto;}//從支付寶查詢支付結(jié)果PayStatusDto payStatusDto = queryPayResultFromAlipay(payNo);//保存支付結(jié)果currentProxy.saveAliPayStatus( payStatusDto);//重新查詢支付記錄payRecord = getPayRecordByPayno(payNo);PayRecordDto payRecordDto = new PayRecordDto();BeanUtils.copyProperties(payRecord, payRecordDto);return payRecordDto;}/*** 請求支付寶查詢支付結(jié)果* @param payNo 支付交易號* @return 支付結(jié)果*/
public PayStatusDto queryPayResultFromAlipay(String payNo){}/*** @description 保存支付寶支付結(jié)果* @param payStatusDto  支付結(jié)果信息* @return void* @author Mr.M* @date 2022/10/4 16:52*/
public void saveAliPayStatus(PayStatusDto payStatusDto) ;
3.6.2.2 查詢支付結(jié)果

定義從支付寶查詢支付結(jié)果的方法

/*** 請求支付寶查詢支付結(jié)果* @param payNo 支付交易號* @return 支付結(jié)果*/
public PayStatusDto queryPayResultFromAlipay(String payNo) {//========請求支付寶查詢支付結(jié)果=============AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, "json", AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE); //獲得初始化的AlipayClientAlipayTradeQueryRequest request = new AlipayTradeQueryRequest();JSONObject bizContent = new JSONObject();bizContent.put("out_trade_no", payNo);request.setBizContent(bizContent.toString());AlipayTradeQueryResponse response = null;try {response = alipayClient.execute(request);if (!response.isSuccess()) {XueChengPlusException.cast("請求支付查詢查詢失敗");}} catch (AlipayApiException e) {log.error("請求支付寶查詢支付結(jié)果異常:{}", e.toString(), e);XueChengPlusException.cast("請求支付查詢查詢失敗");}//獲取支付結(jié)果String resultJson = response.getBody();//轉(zhuǎn)mapMap resultMap = JSON.parseObject(resultJson, Map.class);Map alipay_trade_query_response = (Map) resultMap.get("alipay_trade_query_response");//支付結(jié)果String trade_status = (String) alipay_trade_query_response.get("trade_status");String total_amount = (String) alipay_trade_query_response.get("total_amount");String trade_no = (String) alipay_trade_query_response.get("trade_no");//保存支付結(jié)果PayStatusDto payStatusDto = new PayStatusDto();payStatusDto.setOut_trade_no(payNo);payStatusDto.setTrade_status(trade_status);payStatusDto.setApp_id(APP_ID);payStatusDto.setTrade_no(trade_no);payStatusDto.setTotal_amount(total_amount);return payStatusDto;}
3.6.2.3 保存支付結(jié)果

1、定義保存支付結(jié)果的接口

/*** @description 保存支付寶支付結(jié)果* @param payStatusDto  支付結(jié)果信息* @return void* @author Mr.M* @date 2022/10/4 16:52*/
public void saveAliPayStatus(PayStatusDto payStatusDto) ;

2、編寫接口實現(xiàn)

@Transactional
@Override
public void saveAliPayStatus(PayStatusDto payStatusDto) {//支付流水號String payNo = payStatusDto.getOut_trade_no();XcPayRecord payRecord = getPayRecordByPayno(payNo);if (payRecord == null) {XueChengPlusException.cast("支付記錄找不到");}//支付結(jié)果String trade_status = payStatusDto.getTrade_status();log.debug("收到支付結(jié)果:{},支付記錄:{}}", payStatusDto.toString(),payRecord.toString());if (trade_status.equals("TRADE_SUCCESS")) {//支付金額變?yōu)榉?/span>Float totalPrice = payRecord.getTotalPrice() * 100;Float total_amount = Float.parseFloat(payStatusDto.getTotal_amount()) * 100;//校驗是否一致if (!payStatusDto.getApp_id().equals(APP_ID) || totalPrice.intValue() != total_amount.intValue()) {//校驗失敗log.info("校驗支付結(jié)果失敗,支付記錄:{},APP_ID:{},totalPrice:{}" ,payRecord.toString(),payStatusDto.getApp_id(),total_amount.intValue());XueChengPlusException.cast("校驗支付結(jié)果失敗");}log.debug("更新支付結(jié)果,支付交易流水號:{},支付結(jié)果:{}", payNo, trade_status);XcPayRecord payRecord_u = new XcPayRecord();payRecord_u.setStatus("601002");//支付成功payRecord_u.setOutPayChannel("Alipay");payRecord_u.setOutPayNo(payStatusDto.getTrade_no());//支付寶交易號payRecord_u.setPaySuccessTime(LocalDateTime.now());//通知時間int update1 = payRecordMapper.update(payRecord_u, new LambdaQueryWrapper<XcPayRecord>().eq(XcPayRecord::getPayNo, payNo));if (update1 > 0) {log.info("更新支付記錄狀態(tài)成功:{}", payRecord_u.toString());} else {log.info("更新支付記錄狀態(tài)失敗:{}", payRecord_u.toString());XueChengPlusException.cast("更新支付記錄狀態(tài)失敗");}//關(guān)聯(lián)的訂單號Long orderId = payRecord.getOrderId();XcOrders orders = ordersMapper.selectById(orderId);if (orders == null) {log.info("根據(jù)支付記錄[{}}]找不到訂單", payRecord_u.toString());XueChengPlusException.cast("根據(jù)支付記錄找不到訂單");}XcOrders order_u = new XcOrders();order_u.setStatus("600002");//支付成功int update = ordersMapper.update(order_u, new LambdaQueryWrapper<XcOrders>().eq(XcOrders::getId, orderId));if (update > 0) {log.info("更新訂單表狀態(tài)成功,訂單號:{}", orderId);} else {log.info("更新訂單表狀態(tài)失敗,訂單號:{}", orderId);XueChengPlusException.cast("更新訂單表狀態(tài)失敗");}}}

3.6.3 接口測試

1、完善接口

@ApiOperation("查詢支付結(jié)果")
@GetMapping("/payresult")
@ResponseBody
public PayRecordDto payresult(String payNo) throws IOException {//調(diào)用支付寶接口查詢PayRecordDto payRecordDto = orderService.queryPayResult(payNo);return payRecordDto;
}

2、測試流程

完成生成支付二維碼

用支付寶掃碼但不支付

使用httpclient請求查詢支付結(jié)果,查詢失敗

### 查詢支付結(jié)果
GET {{orders_host}}/orders/payresult?payNo=1628648111951941632
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ7XCJiaXJ0aGRheVwiOlwiMjAyMi0wOS0yOFQxOToyODo0NlwiLFwiY3JlYXRlVGltZVwiOlwiMjAyMi0wOS0yOFQwODozMjowM1wiLFwiaWRcIjpcIjUwXCIsXCJuYW1lXCI6XCLlrabnlJ8xXCIsXCJuaWNrbmFtZVwiOlwi5aSn5rC054mbXCIsXCJwZXJtaXNzaW9uc1wiOltdLFwic2V4XCI6XCIxXCIsXCJzdGF0dXNcIjpcIjFcIixcInVzZXJuYW1lXCI6XCJzdHUxXCIsXCJ1c2VycGljXCI6XCJodHRwOi8vZmlsZS41MXh1ZWNoZW5nLmNuL2RkZGZcIixcInV0eXBlXCI6XCIxMDEwMDFcIn0iLCJzY29wZSI6WyJhbGwiXSwiZXhwIjoxNjc3MTQwMDI1LCJhdXRob3JpdGllcyI6WyJwMSJdLCJqdGkiOiJmYThiMmY1OS03ZTQ5LTRmODUtOTBlMC05NzYwNjlkYjE3ODIiLCJjbGllbnRfaWQiOiJYY1dlYkFwcCJ9.89sp5lPdFafZ_HdGhe8Cpv0anMJC3vT4PtaGMHgCkr8

用支付寶掃碼再次完成支付

使用httpclient請求查詢支付結(jié)果,支付結(jié)果為成功,并更新支付記錄狀態(tài)和訂單狀態(tài)。

3、使用前后端聯(lián)調(diào)

拷貝資料目錄中LocalDateTimeConfig.java到base工程下,處理long轉(zhuǎn)string精度損失的問題。

使用最新的門戶代碼xc-ui-pc-static-portal.zip

3.7 接收支付通知

3.7.1 接口定義

支付完成后第三方支付系統(tǒng)會主動通知支付結(jié)果,要實現(xiàn)主動通知需要在請求支付系統(tǒng)下單時傳入NotifyUrl,這里有兩個url:NotifyUrl和ReturnUrl,ReturnUrl是支付完成后支付系統(tǒng)攜帶支付結(jié)果重定向到ReturnUrl地址,NotifyUrl是支付完成后支付系統(tǒng)在后臺定時去通知,使用NotifyUrl比使用ReturnUrl有保證。

根據(jù)接口描述:https://opendocs.alipay.com/open/203/105286的內(nèi)容下邊在訂單服務(wù)定義接收支付結(jié)果通知的接口。

首先在下單時指定NotifyUrl:

alipayRequest.setNotifyUrl("http://tjxt-user-t.itheima.net/xuecheng/orders/receivenotify");

接收支付結(jié)果通知接口如下:

@ApiOperation("接收支付結(jié)果通知")
@PostMapping("/receivenotify")
public void receivenotify(HttpServletRequest request,HttpServletResponse out) throws UnsupportedEncodingException, 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] + ",";}params.put(name, valueStr);}//驗簽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");//交易狀態(tài)String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8");//appidString app_id = new String(request.getParameter("app_id").getBytes("ISO-8859-1"),"UTF-8");//total_amountString total_amount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"),"UTF-8");//交易成功處理if (trade_status.equals("TRADE_SUCCESS")) {//處理邏輯。。。}}}

3.7.2 接口實現(xiàn)

完善contorller接口

@ApiOperation("接收支付結(jié)果通知")
@PostMapping("/receivenotify")
public void receivenotify(HttpServletRequest request) throws UnsupportedEncodingException, 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] + ",";}params.put(name, valueStr);}//驗簽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");//交易狀態(tài)String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8");//appidString app_id = new String(request.getParameter("app_id").getBytes("ISO-8859-1"),"UTF-8");//total_amountString total_amount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"),"UTF-8");//交易成功處理if (trade_status.equals("TRADE_SUCCESS")) {PayStatusDto payStatusDto = new PayStatusDto();payStatusDto.setOut_trade_no(out_trade_no);payStatusDto.setTrade_status(trade_status);payStatusDto.setApp_id(app_id);payStatusDto.setTrade_no(trade_no);payStatusDto.setTotal_amount(total_amount);//處理邏輯。。。orderService.saveAliPayStatus(payStatusDto);}}}

3.7.3 接口測試

測試準(zhǔn)備:

1、啟動網(wǎng)關(guān)服務(wù)、認證服務(wù)、驗證碼服務(wù)、學(xué)習(xí)中心服務(wù)、內(nèi)容管理服務(wù)。

2、發(fā)布一門收費課程。

測試流程:

1、對選課進行支付

2、支付成功跟蹤service方法的日志,支付成功需要更新支付交易表記錄的狀態(tài)、通知時間、支付寶交易號、支付渠道(Alipay)

支付成功更新訂單表的狀態(tài)為空。

4 支付通知

4.1 需求分析

訂單服務(wù)作為通用服務(wù)在訂單支付成功后需要將支付結(jié)果異步通知給其它微服務(wù)。

下圖使用了消息隊列完成支付結(jié)果通知:

學(xué)習(xí)中心服務(wù):對收費課程選課需要支付,與訂單服務(wù)對接完成支付。

學(xué)習(xí)資源服務(wù):對收費的學(xué)習(xí)資料需要購買后下載,與訂單服務(wù)對接完成支付。

訂單服務(wù)完成支付后將支付結(jié)果發(fā)給每一個與訂單服務(wù)對接的微服務(wù),訂單服務(wù)將消息發(fā)給交換機,由交換機廣播消息,每個訂閱消息的微服務(wù)都可以接收到支付結(jié)果.

微服務(wù)收到支付結(jié)果根據(jù)訂單的類型去更新自己的業(yè)務(wù)數(shù)據(jù)。

4.2 技術(shù)方案

使用消息隊列進行異步通知需要保證消息的可靠性,即生產(chǎn)端將消息成功通知到消費端。

消息從生產(chǎn)端發(fā)送到消費端經(jīng)歷了如下過程:

1、消息發(fā)送到交換機

2、消息由交換機發(fā)送到隊列

3、消息者收到消息進行處理

保證消息的可靠性需要保證以上過程的可靠性,本項目使用RabbitMQ可以通過如下方面保證消息的可靠性。

1、生產(chǎn)者確認機制

發(fā)送消息前使用數(shù)據(jù)庫事務(wù)將消息保證到數(shù)據(jù)庫表中

成功發(fā)送到交換機將消息從數(shù)據(jù)庫中刪除

2、mq持久化

mq收到消息進行持久化,當(dāng)mq重啟即使消息沒有消費完也不會丟失。

需要配置交換機持久化、隊列持久化、發(fā)送消息時設(shè)置持久化。

3、消費者確認機制

消費者消費成功自動發(fā)送ack,否則重試消費。

4.3 發(fā)送支付結(jié)果

4.3.1 訂單服務(wù)集成MQ

訂單服務(wù)通過消息隊列將支付結(jié)果發(fā)給學(xué)習(xí)中心服務(wù),消息隊列采用發(fā)布訂閱模式。

1、訂單服務(wù)創(chuàng)建支付結(jié)果通知交換機。

2、學(xué)習(xí)中心服務(wù)綁定隊列到交換機。

項目使用RabbitMQ作為消息隊列,在課前下發(fā)的虛擬上已經(jīng)安裝了RabbitMQ.

執(zhí)行docker start rabbitmq
啟動RabbitMQ。訪問:http://192.168.101.65:15672/

賬戶密碼:guest/guest

交換機為Fanout廣播模式。

首先需要在學(xué)習(xí)中心服務(wù)和訂單服務(wù)工程配置連接消息隊列。

1、首先在訂單服務(wù)添加消息隊列依賴


<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2、在nacos配置rabbitmq-dev.yaml為通用配置文件

spring:rabbitmq:host: 192.168.101.65port: 5672username: guestpassword: guestvirtual-host: /publisher-confirm-type: correlated #correlated 異步回調(diào),定義ConfirmCallback,MQ返回結(jié)果時會回調(diào)這個ConfirmCallbackpublisher-returns: false #開啟publish-return功能,同樣是基于callback機制,需要定義ReturnCallbacktemplate:mandatory: false #定義消息路由失敗時的策略。true,則調(diào)用ReturnCallback;false:則直接丟棄消息listener:simple:acknowledge-mode: none #出現(xiàn)異常時返回unack,消息回滾到mq;沒有異常,返回ack ,manual:手動控制,none:丟棄消息,不回滾到mqretry:enabled: true #開啟消費者失敗重試initial-interval: 1000ms #初識的失敗等待時長為1秒multiplier: 1 #失敗的等待時長倍數(shù),下次等待時長 = multiplier * last-intervalmax-attempts: 3 #最大重試次數(shù)stateless: true #true無狀態(tài);false有狀態(tài)。如果業(yè)務(wù)中包含事務(wù),這里改為false

3、在訂單服務(wù)接口工程引入rabbitmq-dev.yaml配置文件

shared-configs:- data-id: rabbitmq-${spring.profiles.active}.yamlgroup: xuecheng-plus-commonrefresh: true

4、在訂單服務(wù)service工程編寫MQ配置類,配置交換機

package com.xuecheng.orders.config;import com.alibaba.fastjson.JSON;
import com.xuecheng.messagesdk.model.po.MqMessage;
import com.xuecheng.messagesdk.service.MqMessageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @author Mr.M* @version 1.0* @description TODO* @date 2023/2/23 16:59*/
@Slf4j
@Configuration
public class PayNotifyConfig implements ApplicationContextAware {//交換機public static final String PAYNOTIFY_EXCHANGE_FANOUT = "paynotify_exchange_fanout";//支付結(jié)果通知消息類型public static final String MESSAGE_TYPE = "payresult_notify";//支付通知隊列public static final String PAYNOTIFY_QUEUE = "paynotify_queue";//聲明交換機,且持久化@Bean(PAYNOTIFY_EXCHANGE_FANOUT)public FanoutExchange paynotify_exchange_fanout() {// 三個參數(shù):交換機名稱、是否持久化、當(dāng)沒有queue與其綁定時是否自動刪除return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);}//支付通知隊列,且持久化@Bean(PAYNOTIFY_QUEUE)public Queue course_publish_queue() {return QueueBuilder.durable(PAYNOTIFY_QUEUE).build();}//交換機和支付通知隊列綁定@Beanpublic Binding binding_course_publish_queue(@Qualifier(PAYNOTIFY_QUEUE) Queue queue, @Qualifier(PAYNOTIFY_EXCHANGE_FANOUT) FanoutExchange exchange) {return BindingBuilder.bind(queue).to(exchange);}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {// 獲取RabbitTemplateRabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);//消息處理serviceMqMessageService mqMessageService = applicationContext.getBean(MqMessageService.class);// 設(shè)置ReturnCallbackrabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {// 投遞失敗,記錄日志log.info("消息發(fā)送失敗,應(yīng)答碼{},原因{},交換機{},路由鍵{},消息{}",replyCode, replyText, exchange, routingKey, message.toString());MqMessage mqMessage = JSON.parseObject(message.toString(), MqMessage.class);//將消息再添加到消息表mqMessageService.addMessage(mqMessage.getMessageType(),mqMessage.getBusinessKey1(),mqMessage.getBusinessKey2(),mqMessage.getBusinessKey3());});}
}

重啟訂單服務(wù),登錄rabbitmq,查看交換機自動創(chuàng)建成功

查看隊列自動成功

4.3.2 發(fā)送支付結(jié)果

在OrderService中定義接口

/*** 發(fā)送通知結(jié)果* @param message*/
public void notifyPayResult(MqMessage message);

編寫接口實現(xiàn)方法:

@Override
public void notifyPayResult(MqMessage message) {//1、消息體,轉(zhuǎn)jsonString msg = JSON.toJSONString(message);//設(shè)置消息持久化Message msgObj = MessageBuilder.withBody(msg.getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();// 2.全局唯一的消息ID,需要封裝到CorrelationData中CorrelationData correlationData = new CorrelationData(message.getId().toString());// 3.添加callbackcorrelationData.getFuture().addCallback(result -> {if(result.isAck()){// 3.1.ack,消息成功log.debug("通知支付結(jié)果消息發(fā)送成功, ID:{}", correlationData.getId());//刪除消息表中的記錄mqMessageService.completed(message.getId());}else{// 3.2.nack,消息失敗log.error("通知支付結(jié)果消息發(fā)送失敗, ID:{}, 原因{}",correlationData.getId(), result.getReason());}},ex -> log.error("消息發(fā)送異常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage()));// 發(fā)送消息rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT, "", msgObj,correlationData);}

訂單服務(wù)收到第三方平臺的支付結(jié)果時,在saveAliPayStatus方法中添加代碼,向數(shù)據(jù)庫消息表添加消息并進行發(fā)送消息,如下所示:

@Transactional
@Override
public void saveAliPayStatus(PayStatusDto payStatusDto) {.......//保存消息記錄,參數(shù)1:支付結(jié)果通知類型,2: 業(yè)務(wù)id,3:業(yè)務(wù)類型MqMessage mqMessage = mqMessageService.addMessage("payresult_notify", orders.getOutBusinessId(), orders.getOrderType(), null);//通知消息notifyPayResult(mqMessage);}
}

配置交換機和隊列

在order-service工程配置

消息發(fā)送方法

/*** 發(fā)送通知結(jié)果* @param message*/
public void notifyPayResult(MqMessage message);

4.4 接收支付結(jié)果

4.4.1 學(xué)習(xí)中心服務(wù)集成MQ

1、在學(xué)習(xí)中心服務(wù)添加消息隊列依賴

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2、在學(xué)習(xí)中心服務(wù)接口工程引入rabbitmq-dev.yaml配置文件

shared-configs:- data-id: rabbitmq-${spring.profiles.active}.yamlgroup: xuecheng-plus-commonrefresh: true

3、添加配置類

package com.xuecheng.learning.config;import com.alibaba.fastjson.JSON;
import com.xuecheng.messagesdk.model.po.MqMessage;
import com.xuecheng.messagesdk.service.MqMessageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @author Mr.M* @version 1.0* @description TODO* @date 2023/2/23 16:59*/
@Slf4j
@Configuration
public class PayNotifyConfig {//交換機public static final String PAYNOTIFY_EXCHANGE_FANOUT = "paynotify_exchange_fanout";//支付結(jié)果通知消息類型public static final String MESSAGE_TYPE = "payresult_notify";//支付通知隊列public static final String PAYNOTIFY_QUEUE = "paynotify_queue";//聲明交換機,且持久化@Bean(PAYNOTIFY_EXCHANGE_FANOUT)public FanoutExchange paynotify_exchange_fanout() {// 三個參數(shù):交換機名稱、是否持久化、當(dāng)沒有queue與其綁定時是否自動刪除return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);}//支付通知隊列,且持久化@Bean(PAYNOTIFY_QUEUE)public Queue course_publish_queue() {return QueueBuilder.durable(PAYNOTIFY_QUEUE).build();}//交換機和支付通知隊列綁定@Beanpublic Binding binding_course_publish_queue(@Qualifier(PAYNOTIFY_QUEUE) Queue queue, @Qualifier(PAYNOTIFY_EXCHANGE_FANOUT) FanoutExchange exchange) {return BindingBuilder.bind(queue).to(exchange);}}

4.4.2 接收支付結(jié)果

監(jiān)聽MQ,接收支付結(jié)果,定義ReceivePayNotifyService類如下:

package com.xuecheng.learning.service.impl;import com.alibaba.fastjson.JSON;
import com.rabbitmq.client.Channel;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.learning.config.PayNotifyConfig;
import com.xuecheng.learning.service.MyCourseTablesService;
import com.xuecheng.messagesdk.model.po.MqMessage;
import com.xuecheng.messagesdk.service.MqMessageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.io.IOException;/*** @author Mr.M* @version 1.0* @description 接收支付結(jié)果* @date 2023/2/23 19:04*/
@Slf4j
@Service
public class ReceivePayNotifyService {@Autowiredprivate RabbitTemplate rabbitTemplate;@AutowiredMqMessageService mqMessageService;@AutowiredMyCourseTablesService myCourseTablesService;//監(jiān)聽消息隊列接收支付結(jié)果通知@RabbitListener(queues = PayNotifyConfig.PAYNOTIFY_QUEUE)public void receive(Message message, Channel channel) {try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}//獲取消息MqMessage mqMessage = JSON.parseObject(message.getBody(), MqMessage.class);log.debug("學(xué)習(xí)中心服務(wù)接收支付結(jié)果:{}", mqMessage);//消息類型String messageType = mqMessage.getMessageType();//訂單類型,60201表示購買課程String businessKey2 = mqMessage.getBusinessKey2();//這里只處理支付結(jié)果通知if (PayNotifyConfig.MESSAGE_TYPE.equals(messageType) && "60201".equals(businessKey2)) {//選課記錄idString choosecourseId = mqMessage.getBusinessKey1();//添加選課boolean b = myCourseTablesService.saveChooseCourseStauts(choosecourseId);if(!b){//添加選課失敗,拋出異常,消息重回隊列XueChengPlusException.cast("收到支付結(jié)果,添加選課失敗");}}}}

4.5 通知支付結(jié)果測試

測試準(zhǔn)備:

1、找一門已發(fā)布的收費課程。

2、如果在我的課程表存儲則刪除。

3、刪除此課程的選課記錄及訂單信息。

測試流程:

1、進入課程詳細頁面,點擊馬上學(xué)習(xí),生成二維碼進行支付。

2、支付完成點擊"支付完成",觀察訂單服務(wù)控制臺是否發(fā)送消息。

3、觀察學(xué)習(xí)中心服務(wù)控制臺是否接收到消息。

4、觀察數(shù)據(jù)庫中的消息表的相應(yīng)記錄是否已刪除。

消費重試測試:

1、在學(xué)習(xí)中心服務(wù)接收支付結(jié)果方法中制造異常。

2、重新執(zhí)行上邊的測試流程,觀察是否消費重試。

4 在線學(xué)習(xí)

4.1 需求分析

用戶通過課程詳情界面點擊馬上學(xué)習(xí) 進入 視頻插放界面進行視頻點播。

獲取視頻資源時進行學(xué)習(xí)資格校驗,如下圖:

擁有學(xué)習(xí)資格則繼續(xù)播放視頻,不具有學(xué)習(xí)資格則引導(dǎo)去購買、續(xù)期等操作。

如何判斷是否擁有學(xué)習(xí)資格?

首先判斷是否為試學(xué)視頻,如果為試學(xué)視頻則可以正常學(xué)習(xí)。

如果為非試學(xué)課程首先判斷用戶是否登錄,如果已登錄則判斷是否選課,如果已經(jīng)選課且沒有過期可以正常學(xué)習(xí)。

詳細流程如下圖:

4.2 查詢課程信息

在視頻點播頁面需要查詢課程信息,課程上線后也需要訪問/api/content/course/whole/{courseId}

課程預(yù)覽時請求獲取課程的接口為:/open/content/course/whole/{courseId}

在nginx中進行配置:

/open、/api在nginx的配置如下:(已經(jīng)配置的不要重復(fù)配置)

        #apilocation /api/ {proxy_pass http://gatewayserver/;} #openapilocation /open/content/ {proxy_pass http://gatewayserver/content/open/;} location /open/media/ {proxy_pass http://gatewayserver/media/open/;} 

下邊實現(xiàn)/api/content/course/whole/{courseId} 獲取課程發(fā)布信息接口。

進入內(nèi)容管理服務(wù)api工程CoursePublishController
類,定義查詢課程預(yù)覽信息接口如下:

@ApiOperation("獲取課程發(fā)布信息")
@ResponseBody
@GetMapping("/course/whole/{courseId}")
public CoursePreviewDto getCoursePublish(@PathVariable("courseId") Long courseId) {//查詢課程發(fā)布信息CoursePublish coursePublish = coursePublishService.getCoursePublish(courseId);if (coursePublish == null) {return new CoursePreviewDto();}//課程基本信息CourseBaseInfoDto courseBase = new CourseBaseInfoDto();BeanUtils.copyProperties(coursePublish, courseBase);//課程計劃List<TeachplanDto> teachplans = JSON.parseArray(coursePublish.getTeachplan(), TeachplanDto.class);CoursePreviewDto coursePreviewInfo = new CoursePreviewDto();coursePreviewInfo.setCourseBase(courseBase);coursePreviewInfo.setTeachplans(teachplans);return coursePreviewInfo;
}

重啟內(nèi)容管理服務(wù),進入學(xué)習(xí)界面查看課程計劃、課程名稱等信息是否顯示正常。

4.3 獲取視頻

4.3.1 需求分析

4.3.2 接口定義

package com.xuecheng.learning.api;import com.xuecheng.base.execption.XueChengPlusException;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.base.model.XcUser;
import com.xuecheng.learning.model.dto.XcChooseCourseDto;
import com.xuecheng.learning.model.dto.XcCourseTablesDto;
import com.xuecheng.learning.service.LearningService;
import com.xuecheng.learning.service.MyCourseTablesService;
import com.xuecheng.learning.util.SecurityUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;/*** @author Mr.M* @version 1.0* @description 學(xué)習(xí)過程管理接口* @date 2022/10/2 14:52*/
@Api(value = "學(xué)習(xí)過程管理接口", tags = "學(xué)習(xí)過程管理接口")
@Slf4j
@RestController
public class MyLearningController {@AutowiredLearningService learningService;@ApiOperation("獲取視頻")@GetMapping("/open/learn/getvideo/{courseId}/{teachplanId}/{mediaId}")public RestResponse<String> getvideo(@PathVariable("courseId") Long courseId,@PathVariable("courseId") Long teachplanId, @PathVariable("mediaId") String mediaId) {//登錄用戶XcUser user = SecurityUtil.getUser();String userId = null;if(user != null){userId = user.getId();}//獲取視頻}}

定義service接口

package com.xuecheng.learning.service;import com.xuecheng.base.model.RestResponse;
import com.xuecheng.learning.model.dto.XcChooseCourseDto;
import com.xuecheng.learning.model.dto.XcCourseTablesDto;/*** @description 學(xué)習(xí)過程管理service接口* @author Mr.M* @date 2022/10/2 16:07* @version 1.0*/
public interface LearningService {/*** @description 獲取教學(xué)視頻* @param courseId 課程id* @param teachplanId 課程計劃id* @param mediaId 視頻文件id* @return com.xuecheng.base.model.RestResponse<java.lang.String>* @author Mr.M* @date 2022/10/5 9:08
*/
public RestResponse<String> getVideo(String userId,Long courseId,Long teachplanId,String mediaId);
}

4.3.3 獲取視頻遠程接口

在學(xué)習(xí)中心服務(wù)service工程中定義媒資管理Feignclient

package com.xuecheng.learning.feignclient;import com.xuecheng.base.model.RestResponse;
import com.xuecheng.content.model.po.CoursePublish;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;/*** @description 媒資管理服務(wù)遠程接口* @author Mr.M* @date 2022/9/20 20:29* @version 1.0*/@FeignClient(value = "media-api",fallbackFactory = MediaServiceClientFallbackFactory.class)@RequestMapping("/media")
public interface MediaServiceClient {@GetMapping("/open/preview/{mediaId}")public RestResponse<String> getPlayUrlByMediaId(@PathVariable("mediaId") String mediaId);}

FeignClient接口的降級類:

package com.xuecheng.learning.feignclient;import com.xuecheng.base.model.RestResponse;
import com.xuecheng.content.model.po.CoursePublish;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;/*** @author Mr.M* @version 1.0* @description TODO* @date 2022/10/3 8:03*/
@Slf4j
@Component
public class MediaServiceClientFallbackFactory implements FallbackFactory<MediaServiceClient> {@Overridepublic MediaServiceClient create(Throwable throwable) {return new MediaServiceClient() {@Overridepublic RestResponse<String> getPlayUrlByMediaId(String mediaId) {log.error("遠程調(diào)用媒資管理服務(wù)熔斷異常:{}",throwable.getMessage());return null;}};}
}

4.3.4 學(xué)習(xí)資格校驗

編寫獲取視頻的接口實現(xiàn)方法:

@Override
public RestResponse<String> getVideo(String userId,Long courseId,Long teachplanId, String mediaId) {//查詢課程信息CoursePublish coursepublish = contentServiceClient.getCoursepublish(courseId);if(coursepublish==null){XueChengPlusException.cast("課程信息不存在");}//校驗學(xué)習(xí)資格//如果登錄if(StringUtils.isNotEmpty(userId)){//判斷是否選課,根據(jù)選課情況判斷學(xué)習(xí)資格XcCourseTablesDto xcCourseTablesDto = myCourseTablesService.getLeanringStatus(userId, courseId);//學(xué)習(xí)資格狀態(tài) [{"code":"702001","desc":"正常學(xué)習(xí)"},{"code":"702002","desc":"沒有選課或選課后沒有支付"},{"code":"702003","desc":"已過期需要申請續(xù)期或重新支付"}]String learnStatus = xcCourseTablesDto.getLearnStatus();if(learnStatus.equals("702001")){return mediaServiceClient.getPlayUrlByMediaId(mediaId);}else if(learnStatus.equals("702003")){RestResponse.validfail("您的選課已過期需要申請續(xù)期或重新支付");}}//未登錄或未選課判斷是否收費String charge = coursepublish.getCharge();if(charge.equals("201000")){//免費可以正常學(xué)習(xí)return mediaServiceClient.getPlayUrlByMediaId(mediaId);}return RestResponse.validfail("請購買課程后繼續(xù)學(xué)習(xí)");}

4.3.5 測試

1、完善接口

@ApiOperation("獲取視頻")
@GetMapping("/open/learn/getvideo/{courseId}/{teachplanId}/{mediaId}")
public RestResponse<String> getvideo(@PathVariable("courseId") Long courseId, @PathVariable("courseId") Long teachplanId, @PathVariable("mediaId") String mediaId) {//登錄用戶SecurityUtil.XcUser user = SecurityUtil.getUser();String userId = null;if (user != null) {userId = user.getId();}//獲取視頻return learningService.getVideo(userId, courseId, teachplanId, mediaId);}

2、測試準(zhǔn)備

選課成功一門課程。

沒有選課的免費課程、收費課程各一門,其中收費課程具有試學(xué)課程。

3、測試項目

1)選課成功的課程是否可以正常獲取視頻

2)免費課程沒有選課是否可以正常學(xué)習(xí)

可修改選課記錄表中的課程id為不存在進行測試,測試完再恢復(fù)原樣。

3)收費課程沒有選課是否可以正常學(xué)習(xí)

可修改選課記錄表中的課程id為不存在進行測試,測試完再恢復(fù)原樣。

4.4 我的課表

4.4.1 需求分析

4.4.1.1 業(yè)務(wù)流程

登錄網(wǎng)站,點擊"我的學(xué)習(xí)"進入個人中心,

個人中心首頁顯示我的課程表:

我的課表中顯示了選課成功的免費課程、收費課程。最近學(xué)習(xí)課程顯示了當(dāng)前用戶最近學(xué)習(xí)的課程信息。

點擊繼續(xù)學(xué)習(xí)進入當(dāng)前學(xué)習(xí)章節(jié)的視頻繼續(xù)學(xué)習(xí)。

點擊課程評價進入課程評價界面。

4.4.1.2 配置nginx

在nginx配置用戶中心server ,如下:

    server {listen       80;server_name  ucenter.51xuecheng.cn;#charset koi8-r;ssi on;ssi_silent_errors on;#access_log  logs/host.access.log  main;location / {alias   D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal/ucenter/;index  index.html index.htm;}location /include {proxy_pass   http://127.0.0.1;}location /img/ {proxy_pass   http://127.0.0.1/static/img/;}location /api/ {proxy_pass http://gatewayserver/;} }

4.4.2 接口定義

在MyCourseTablesController中定義我的課程表接口:

@ApiOperation("我的課程表")
@GetMapping("/mycoursetable")
public PageResult<XcCourseTables> mycoursetable(MyCourseTableParams params) {}

4.4.3 接口開發(fā)

4.4.3.1DAO

使用自動生成的mapper即可實現(xiàn)分頁查詢。

4.4.3.2 Service

在service中定義我的課程表接口:

/*** @description 我的課程表* @param params* @return com.xuecheng.base.model.PageResult<com.xuecheng.learning.model.po.XcCourseTables>* @author Mr.M* @date 2022/10/27 9:24
*/
public PageResult<XcCourseTables> mycourestabls(MyCourseTableParams params);

編寫接口實現(xiàn):

public PageResult<XcCourseTables> mycourestabls( MyCourseTableParams params){//頁碼long pageNo = params.getPage();//每頁記錄數(shù),固定為4long pageSize = 4;//分頁條件Page<XcCourseTables> page = new Page<>(pageNo, pageSize);//根據(jù)用戶id查詢String userId = params.getUserId();LambdaQueryWrapper<XcCourseTables> lambdaQueryWrapper = new LambdaQueryWrapper<XcCourseTables>().eq(XcCourseTables::getUserId, userId);//分頁查詢Page<XcCourseTables> pageResult = courseTablesMapper.selectPage(page, lambdaQueryWrapper);List<XcCourseTables> records = pageResult.getRecords();//記錄總數(shù)long total = pageResult.getTotal();PageResult<XcCourseTables> courseTablesResult = new PageResult<>(records, total, pageNo, pageSize);return courseTablesResult;}

完善接口:

@ApiOperation("我的課程表")
@GetMapping("/mycoursetable")
public PageResult<XcCourseTables> mycoursetable(MyCourseTableParams params) {//登錄用戶SecurityUtil.XcUser user = SecurityUtil.getUser();if(user == null){XueChengPlusException.cast("請登錄后繼續(xù)選課");}String userId = user.getId();
//設(shè)置當(dāng)前的登錄用戶params.setUserId(userId);return myCourseTablesService.mycourestabls(params);
}

4.4.4 接口測試

登錄網(wǎng)站,點擊"我的學(xué)習(xí)"進入個人中心,查看我的課程表中課程是否是當(dāng)前用戶所選課程。

http://www.risenshineclean.com/news/53150.html

相關(guān)文章:

  • 好看的 網(wǎng)站正在建設(shè)中源碼福州短視頻seo網(wǎng)站
  • 建站排名教育培訓(xùn)網(wǎng)站
  • 網(wǎng)頁網(wǎng)站免費佛山快速排名seo
  • 白城網(wǎng)站建設(shè)網(wǎng)站制作公司高端
  • 美武漢有什么網(wǎng)站建設(shè)公司好磁力鏈
  • 狐表做網(wǎng)站360優(yōu)化大師最新版
  • 網(wǎng)站建設(shè)的基本流程包括網(wǎng)絡(luò)營銷ppt怎么做
  • 球類網(wǎng)站如何做宣傳推廣搜索怎么選關(guān)鍵詞
  • html簡單網(wǎng)頁代碼下載廣東網(wǎng)站seo
  • 免費做公司電子畫冊的網(wǎng)站怎么優(yōu)化網(wǎng)站
  • wordpress標(biāo)簽自動生成插件下載北京谷歌seo公司
  • 什么網(wǎng)站可以做海報aso優(yōu)化報價
  • 找網(wǎng)站建設(shè)企業(yè)常德seo招聘
  • 做證券考試的網(wǎng)站電商seo是什么
  • 小說網(wǎng)站制作模板網(wǎng)絡(luò)營銷分析報告
  • 大連網(wǎng)站建設(shè)選網(wǎng)龍建網(wǎng)站
  • 做網(wǎng)站開發(fā) 用的最多的語言手機系統(tǒng)流暢神器
  • 去除wordpress主題底部信息網(wǎng)站seo綜合查詢
  • 建好的網(wǎng)站怎么用橙子建站
  • 營銷型網(wǎng)站建設(shè)的注意事項灰色行業(yè)推廣平臺
  • 如何組建網(wǎng)站開發(fā)團隊亞馬遜alexa
  • php做的網(wǎng)站好么自己搜20條優(yōu)化措施
  • wordpress插件 圖片上傳采集站seo提高收錄
  • php網(wǎng)站源碼怎么在本地測試網(wǎng)站設(shè)計的基本原則
  • 安徽餐飲網(wǎng)站建設(shè)市場監(jiān)督管理局
  • 織夢做響應(yīng)式網(wǎng)站網(wǎng)站收錄查詢工具
  • 服裝網(wǎng)站建設(shè)價格百度瀏覽器官網(wǎng)入口
  • 專門做二手的網(wǎng)站seo是搜索引擎優(yōu)化
  • 青島做網(wǎng)站建設(shè)的公司手機登錄百度pc端入口
  • 南充網(wǎng)站建設(shè)服務(wù)商現(xiàn)在如何進行網(wǎng)上推廣