wordpress 標(biāo)簽手冊關(guān)鍵詞優(yōu)化包年推廣
微服務(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)前用戶所選課程。