选课:
1、执行流程
2、接口定义
需要取用户信息,若为空说明没有登录,抛出异常要求登录。
/**
* @author Mr.M
* @version 1.0
* @description 我的课程表接口
* @date 2022/10/25 9:40
*/
@Api(value = "我的课程表接口", tags = "我的课程表接口")
@Slf4j
@RestController
public class MyCourseTablesController {
@Autowired
private MyCourseTables myCourseTables;
@ApiOperation("添加选课")
@PostMapping("/choosecourse/{courseId}")
public XcChooseCourseDto addChooseCourse(@PathVariable("courseId") Long courseId) {
String userId = null;
try {
SecurityUtil.XcUser user = SecurityUtil.getUser();
userId = user.getId();
} catch (Exception e) {
XueChengPlusException.cast("请登录后再选课!");
}
return myCourseTables.addChooseCourse(courseId,userId);
}
@ApiOperation("查询学习资格")
@PostMapping("/choosecourse/learnstatus/{courseId}")
public XcCourseTablesDto getLearnstatus(@PathVariable("courseId") Long courseId) {
SecurityUtil.XcUser user = SecurityUtil.getUser();
if (ObjectUtils.isEmpty(user)){
XueChengPlusException.cast("请登录后再查询!");
}
return myCourseTables.getLearnstatus(courseId);
}
@ApiOperation("我的课程表")
@GetMapping("/mycoursetable")
public PageResult<XcCourseTables> mycoursetable(MyCourseTableParams params) {
return null;
}
}
3、业务层定义
/**
* @ClassName : MyCourseTables
* @Description : 课程表业务处理层
* @Author : abc123
* @Date: 2024-02-17 10:13
*/
public interface MyCourseTables {
/**
* 添加选课
* @param courseId 课程id
* @param userId
* @return 统一返回类
*/
XcChooseCourseDto addChooseCourse(Long courseId, String userId);
/**
* 初始化选课信息表
* @param coursepublish
*/
XcChooseCourse initChooseCourseTableData(CoursePublish coursepublish, String orderType, String status, String userId);
/**
* 初始化选课信息表和课程表
* @param coursepublish
*/
XcChooseCourse initChooseCourseAndCourseTableData(CoursePublish coursepublish,String userId);
/**
* 查询学习资格
* @param courseId
* @return
*/
XcCourseTablesDto getLearnstatus(Long courseId);
}
添加选课业务实现:
(1)定义feign接口远程调用xuecheng-plus-content服务,查询course_publish表中是否存在该课程。(只有发布的课程才能被添加)
(2)判断收费标准。如果免费就直接加入xc_choose_course和xc_course_tables表中。如果收费先加入xc_choose_course表,后续支付完成后再加入xc_course_tables表。
查询学习资格业务实现:
(1)判断该课程是否在xc_choose_course或xc_course_tables表中。
(2)如果该课程是收费课程,判断是否支付。
(3)判断当前日期是否超过课程结束日期。
/**
* @ClassName : MyCourseTablesImpl
* @Description : 课程表业务处理层实现类
* @Author : abc123
* @Date: 2024-02-17 10:14
*/
@Service
@Transactional
@Slf4j
public class MyCourseTablesImpl implements MyCourseTables {
@Autowired
private ContentServiceClient contentServiceClient;
@Autowired
private XcChooseCourseMapper chooseCourseMapper;
@Autowired
private XcCourseTablesMapper courseTablesMapper;
@Autowired
private MyCourseTables myCourseTables;
/**
* 添加选课
*
* @param courseId 课程id
* @param userId
* @return 统一返回类
*/
@Override
public XcChooseCourseDto addChooseCourse(Long courseId, String userId) {
//远程调用内容管理服务查询课程信息
CoursePublish coursepublish = contentServiceClient.getCoursepublish(courseId);
//只有课程发布了才能被选择
if (ObjectUtils.isEmpty(coursepublish)) {
throw new XueChengPlusException("您添加的课程不存在,请检查!");
}
//判断收费标准
String charge = coursepublish.getCharge();
XcChooseCourse xcChooseCourse = null;
switch (charge) {
case "201000"://免费
xcChooseCourse = myCourseTables.initChooseCourseAndCourseTableData(coursepublish, userId);
break;
case "201001"://收费
xcChooseCourse = myCourseTables.initChooseCourseTableData(coursepublish, "700002", "701002", userId);
break;
}
XcChooseCourseDto dto = new XcChooseCourseDto();
BeanUtils.copyProperties(xcChooseCourse, dto);
return dto;
}
public XcChooseCourse initChooseCourseAndCourseTableData(CoursePublish coursepublish, String userId) {
XcChooseCourse xcChooseCourseRes = this.chooseCourseMapper.selectOne(new LambdaQueryWrapper<XcChooseCourse>().eq(XcChooseCourse::getCourseId, coursepublish.getId()));
XcCourseTables xcCourseTablesRes = this.courseTablesMapper.selectOne(new LambdaQueryWrapper<XcCourseTables>().eq(XcCourseTables::getCourseId, coursepublish.getId()));
if (!ObjectUtils.isEmpty(xcChooseCourseRes) || !ObjectUtils.isEmpty(xcCourseTablesRes)) {
throw new XueChengPlusException("该课程已存在,请勿重复添加!");
}
//添加课程到选课记录表
XcChooseCourse xcChooseCourse = myCourseTables.initChooseCourseTableData(coursepublish, "700001", "701001", userId);
// 添加课程到课程表 先要判断是否过期
if (LocalDateTime.now().isAfter(xcChooseCourse.getValidtimeEnd())) {
throw new XueChengPlusException("该课程已过期,请勿添加!");
}
XcCourseTables xcCourseTables = new XcCourseTables();
BeanUtils.copyProperties(xcChooseCourse, xcCourseTables);
xcCourseTables.setChooseCourseId(xcChooseCourse.getId());
this.courseTablesMapper.insert(xcCourseTables);
return xcChooseCourse;
}
/**
* 查询学习资格
*
* @param courseId
* @return
*/
@Override
public XcCourseTablesDto getLearnstatus(Long courseId) {
XcCourseTablesDto xcCourseTablesDto = new XcCourseTablesDto();
//无论是免费还是收费都要是选课成功
XcChooseCourse xcChooseCourse = null;
XcCourseTables xcCourseTables = null;
try {
//选课记录表
xcChooseCourse = chooseCourseMapper.selectOne(new LambdaQueryWrapper<XcChooseCourse>().eq(XcChooseCourse::getCourseId, courseId));
//课程表
xcCourseTables = courseTablesMapper.selectOne(new LambdaQueryWrapper<XcCourseTables>().eq(XcCourseTables::getCourseId, courseId));
} catch (Exception e) {
log.error("查询选课信息错误,课程ID:{}", courseId);
XueChengPlusException.cast("查询选课信息错误!");
}
//判断是否在选课记录表或课程表中
if (ObjectUtils.isEmpty(xcChooseCourse) && ObjectUtils.isEmpty(xcCourseTables)) {
xcCourseTablesDto.setLearnStatus("702002");
return xcCourseTablesDto;
}
switch (xcChooseCourse.getStatus()) {
case "701001":
xcCourseTablesDto.setLearnStatus("702001");
return xcCourseTablesDto;
case "701002":
xcCourseTablesDto.setLearnStatus("702002");
return xcCourseTablesDto;
}
//然后判断有效期是否过期
if (xcChooseCourse.getValidtimeEnd().isBefore(LocalDateTime.now())) {
xcCourseTablesDto.setLearnStatus("702003");
return xcCourseTablesDto;
}
xcCourseTablesDto.setLearnStatus("702001");
return xcCourseTablesDto;
}
/**
* 添加课程到选课记录表
*
* @param coursepublish 课程发布实体
* @param orderType 收费方式
* @param status 选课状态
* @param userId 用户id
*/
public XcChooseCourse initChooseCourseTableData(CoursePublish coursepublish, String orderType, String status, String userId) {
//添加前先查询,如果课程存在并且状态是未支付,就直接返回
XcChooseCourse xcChooseCourseRes = this.chooseCourseMapper.selectOne(new LambdaQueryWrapper<XcChooseCourse>().eq(XcChooseCourse::getCourseId, coursepublish.getId()));
if (!ObjectUtils.isEmpty(xcChooseCourseRes) && xcChooseCourseRes.getStatus().equals("701002")) {
return xcChooseCourseRes;
}
//否则抛出异常
if (!ObjectUtils.isEmpty(xcChooseCourseRes)) {
throw new XueChengPlusException("该课程已存在,请勿重复添加!");
}
//不存在则添加
XcChooseCourse xcChooseCourse = new XcChooseCourse();
BeanUtils.copyProperties(coursepublish, xcChooseCourse);
xcChooseCourse.setCourseId(coursepublish.getId());
xcChooseCourse.setOrderType(orderType);
xcChooseCourse.setStatus(status);
xcChooseCourse.setUserId(userId);
xcChooseCourse.setCoursePrice(coursepublish.getPrice() == null ? 0F : coursepublish.getPrice());
LocalDateTime now = LocalDateTime.now();
xcChooseCourse.setValidtimeStart(now);
xcChooseCourse.setValidtimeEnd(DateUtil.getFutureDateTimePlusDays(now, coursepublish.getValidDays()));
xcChooseCourse.setCourseName(coursepublish.getName());
this.chooseCourseMapper.insert(xcChooseCourse);
return xcChooseCourse;
}
}
4、功能测试
先调用一次查询学习资格接口
### 查询学习资格
POST http://localhost:63020/learning/choosecourse/learnstatus/121
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ7XCJjZWxscGhvbmVcIjpcIjE1NjUxODIwMDg4XCIsXCJjcmVhdGVUaW1lXCI6XCIyMDI0LTAyLTE2VDE5OjMyOjA4XCIsXCJlbWFpbFwiOlwiOTgwNjgwNTQ0QHFxLmNvbVwiLFwiaWRcIjpcImUzMzVjMTk4LTYyMzgtNDc2NS04ZTA3LTA5MDk1ZDUyODU2Y1wiLFwibmFtZVwiOlwiZWxkZXdpc3NcIixcIm5pY2tuYW1lXCI6XCJlbGRld2lzc1wiLFwicGVybWlzc2lvbnNcIjpbXSxcInN0YXR1c1wiOlwiMVwiLFwidXNlcm5hbWVcIjpcImVsZGV3aXNzXCIsXCJ1dHlwZVwiOlwiMTAxMDAxXCJ9Iiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTcwODE3ODMwMCwiYXV0aG9yaXRpZXMiOlsieGNfc3lzbWFuYWdlcl9kb2MiLCJ4Y190ZWFjaG1hbmFnZXJfY291cnNlX2xpc3QiLCJ4Y19zeXNtYW5hZ2VyIiwieGNfc3lzbWFuYWdlcl9jb21wYW55IiwieGNfc3lzbWFuYWdlcl9sb2ciXSwianRpIjoiOGNkYmQxZmUtOTI4Yi00NmZlLThiYmQtZTBlNTFjMmZmYzFiIiwiY2xpZW50X2lkIjoiWGNXZWJBcHAifQ.9JY_3fapNgsHfefSf9aejHRTgqLWpUFUIyEy0smaCGc
由于未选课,返回702002
{
"id": null,
"chooseCourseId": null,
"userId": null,
"courseId": null,
"companyId": null,
"courseName": null,
"courseType": null,
"createDate": null,
"validtimeStart": null,
"validtimeEnd": null,
"updateDate": null,
"remarks": null,
"learnStatus": "702002"
}
再调用添加课程接口
## 添加课程
POST http://localhost:63020/learning/choosecourse/121
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ7XCJjZWxscGhvbmVcIjpcIjE1NjUxODIwMDg4XCIsXCJjcmVhdGVUaW1lXCI6XCIyMDI0LTAyLTE2VDE5OjMyOjA4XCIsXCJlbWFpbFwiOlwiOTgwNjgwNTQ0QHFxLmNvbVwiLFwiaWRcIjpcImUzMzVjMTk4LTYyMzgtNDc2NS04ZTA3LTA5MDk1ZDUyODU2Y1wiLFwibmFtZVwiOlwiZWxkZXdpc3NcIixcIm5pY2tuYW1lXCI6XCJlbGRld2lzc1wiLFwicGVybWlzc2lvbnNcIjpbXSxcInN0YXR1c1wiOlwiMVwiLFwidXNlcm5hbWVcIjpcImVsZGV3aXNzXCIsXCJ1dHlwZVwiOlwiMTAxMDAxXCJ9Iiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTcwODE3ODMwMCwiYXV0aG9yaXRpZXMiOlsieGNfc3lzbWFuYWdlcl9kb2MiLCJ4Y190ZWFjaG1hbmFnZXJfY291cnNlX2xpc3QiLCJ4Y19zeXNtYW5hZ2VyIiwieGNfc3lzbWFuYWdlcl9jb21wYW55IiwieGNfc3lzbWFuYWdlcl9sb2ciXSwianRpIjoiOGNkYmQxZmUtOTI4Yi00NmZlLThiYmQtZTBlNTFjMmZmYzFiIiwiY2xpZW50X2lkIjoiWGNXZWJBcHAifQ.9JY_3fapNgsHfefSf9aejHRTgqLWpUFUIyEy0smaCGc
由于该课程是收费课程,暂时未支付,所以只在 xc_choose_course表中初始化一条记录。
因为没有支付,所以查询学习资格依旧是返回702002。
支付:
1、集成支付宝环境
<!-- 支付宝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>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>
<!-- 二维码生成&识别组件 -->
<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>
public class AlipayConfig {
// 商户appid
// public static String APPID = "";
// 私钥 pkcs8格式的
// public static String RSA_PRIVATE_KEY = "";
// 服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
public static String notify_url = "http://商户网关地址/alipay.trade.wap.pay-JAVA-UTF-8/notify_url.jsp";
// 页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址
public static String return_url = "http://商户网关地址/alipay.trade.wap.pay-JAVA-UTF-8/return_url.jsp";
// 请求网关地址
public static String URL = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
// 编码
public static String CHARSET = "UTF-8";
// 返回格式
public static String FORMAT = "json";
// 支付宝公钥
// public static String ALIPAY_PUBLIC_KEY = "";
// 日志记录目录
public static String log_path = "/log";
// RSA2
public static String SIGNTYPE = "RSA2";
}
下载秘钥工具
重要:如果使用的是新版支付宝沙箱,请求网关地址为"https://openapi-sandbox.dl.alipaydev.com/gateway.do"
APP_PRIVATE_KEY对应的是查看秘钥-应用私钥。
ALIPAY_PUBLIC_KEY对应的是查看秘钥-支付宝公钥,非应用公钥!
我这里采用的是下载手机版沙箱支付宝进行测试:
2、测试代码
2.1、测试支付宝接口:
官网文档:小程序文档 - 支付宝文档中心
/**
* @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);
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
//异步接收地址,仅支持http/https,公网可访问
request.setNotifyUrl("");
//同步跳转地址,仅支持http/https
request.setReturnUrl("");
/******必传参数******/
JSONObject bizContent = new JSONObject();
//商户订单号,商家自定义,保持唯一性
bizContent.put("out_trade_no", "20210817010101005");
//支付金额,最小值0.01元
bizContent.put("total_amount", 0.01);
//订单标题,不可使用特殊符号
bizContent.put("subject", "测试商品");
/******可选参数******/
//手机网站支付默认传值FAST_INSTANT_TRADE_PAY
bizContent.put("product_code", "QUICK_WAP_WAY");
//bizContent.put("time_expire", "2022-08-01 22:00:00");
商品明细信息,按需传入
//JSONArray goodsDetail = new JSONArray();
//JSONObject goods1 = new JSONObject();
//goods1.put("goods_id", "goodsNo1");
//goods1.put("goods_name", "子商品1");
//goods1.put("quantity", 1);
//goods1.put("price", 0.01);
//goodsDetail.add(goods1);
//bizContent.put("goods_detail", goodsDetail);
扩展信息,按需传入
//JSONObject extendParams = new JSONObject();
//extendParams.put("sys_service_provider_id", "2088511833207846");
//bizContent.put("extend_params", extendParams);
request.setBizContent(bizContent.toString());
AlipayTradeWapPayResponse response = alipayClient.pageExecute(request, "POST");
// 如果需要返回GET请求,请使用
// AlipayTradeWapPayResponse response = alipayClient.pageExecute(request,"GET");
String pageRedirectionData = response.getBody();
System.out.println(pageRedirectionData);
if (response.isSuccess()) {
System.out.println("调用成功");
String form = alipayClient.pageExecute(request).getBody(); //调用SDK生成表单
httpResponse.setContentType("text/html;charset=" + AlipayConfig.CHARSET);
httpResponse.getWriter().write(form);//直接将完整的表单html输出到页面
httpResponse.getWriter().flush();
return; // 添加此行,结束方法执行,确保只返回 HTML 表单代码而不会继续执行下面的代码
} else {
System.out.println("调用失败");
}
}
}
生成二维码:
/**
* @author Mr.M
* @version 1.0
* @description 二维码生成工具
* @date 2022/10/3 0:03
*/
public class QRCodeUtil {
/**
* 生成二维码
*
* @param content 二维码对应的URL
* @param width 二维码图片宽度
* @param height 二维码图片高度
* @return
*/
public String createQRCode(String content, int width, int height) throws IOException {
String resultImage = "";
//除了尺寸,传入内容不能为空
if (!StringUtils.isEmpty(content)) {
ServletOutputStream stream = null;
ByteArrayOutputStream os = new ByteArrayOutputStream();
//二维码参数
@SuppressWarnings("rawtypes")
HashMap<EncodeHintType, Comparable> hints = new HashMap<>();
//指定字符编码为“utf-8”
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
//L M Q H四个纠错等级从低到高,指定二维码的纠错等级为M
//纠错级别越高,可以修正的错误就越多,需要的纠错码的数量也变多,相应的二维吗可储存的数据就会减少
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
//设置图片的边距
hints.put(EncodeHintType.MARGIN, 1);
try {
//zxing生成二维码核心类
QRCodeWriter writer = new QRCodeWriter();
//把输入文本按照指定规则转成二维吗
BitMatrix bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints);
//生成二维码图片流
BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
//输出流
ImageIO.write(bufferedImage, "png", os);
/**
* 原生转码前面没有 data:image/png;base64 这些字段,返回给前端是无法被解析,所以加上前缀
*/
resultImage = new String("data:image/png;base64," + EncryptUtil.encodeBase64(os.toByteArray()));
return resultImage;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("生成二维码出错");
} finally {
if (stream != null) {
stream.flush();
stream.close();
}
}
}
return null;
}
public static void main(String[] args) throws IOException {
QRCodeUtil qrCodeUtil = new QRCodeUtil();
System.out.println(qrCodeUtil.createQRCode("http://"换成你自己的":63030/orders/alipaytest", 200, 200));
}
}
重要:端口号非localhost,而是本地网卡分配的局域网ip地址。我这里填的是wifi地址,手机电脑连同一个wifi进行测试。
复制控制台的BASE64字符串至浏览器,用手机沙箱版支付宝扫描。
用浏览器打开
选择沙箱版支付宝,输入密码支付即可。
注意:同一out_trade_no只能使用一次,相当于支付宝系统数据库已存该条记录!
2.2、测试交易结果查询接口:
官网文档:小程序文档 - 支付宝文档中心
关键入参:out_trade_no-订单号
/**
* @ClassName : Test
* @Description :
* @Author : abc123
* @Date: 2024-02-17 19:28
*/
public class Test {
public static void main(String[] args) throws AlipayApiException {
AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, "填自己的appid",
"填自己的APP_PRIVATE_KEY",
"json", AlipayConfig.CHARSET, "填自己的ALIPAY_PUBLIC_KEY", AlipayConfig.SIGNTYPE); //获得初始化的AlipayClient
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", "20210817010101005");
//bizContent.put("trade_no", "2014112611001004680073956707");
request.setBizContent(bizContent.toString());
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
System.out.println("调用成功");
String resultJson = response.getBody();
//转map
Map resultMap = JSON.parseObject(resultJson, Map.class);
Map alipay_trade_query_response = (Map) resultMap.get("alipay_trade_query_response");
//支付结果
String trade_status = (String) alipay_trade_query_response.get("trade_status");
System.out.println(trade_status);
} else {
System.out.println("调用失败");
}
}
}
附录:
sign check fail: check Sign and Data Fail问题官方解决方案:小程序文档 - 支付宝文档中心