在学生选课中,需要学员通过学成在线门户网站实现,查看学生的课程购买记录、学生购买课程的下单、学生支付收费课程、分布式任务调度查询支付结果等操作。本次主要是对学成的学生群体来开发对应的功能。
本次主要针对学生学科的业务操作,主要功能包括:
首页
或课程搜索
找到目标课程,进入某个课程的详情页,需要判断是否收费,如果收费需要查询改学员对此课程的购买记录。学生选课流程如下:
1.通过首页
或课程搜索
找到目标课程,进入课程详情页.
上述的业务主要涉及到两个实体:订单
、学习记录
。
由于订单包含的字段太多了,并且我们将其拆分为两张表,一张表示订单基本信息、另外一张表示订单支付信息
课程记录表表达的是用户对每门的学习进度,通过username和coursepubId可以确定唯一
的一条记录(最开始我误解了业务流程,认为只要用户看过的小节,这张表都会记录,这样username和coursepubId可以找到多条
记录,这样会大大增加表的数据量,所以我们采用只记录用户在该门课上最近看的小节,每当用户访问课程小节时,就修改课程记录表中的teachplanId为该小节id)
controller
@GetMapping("learnedRecords/myCourseRec/{coursePubId}")
public CourseRecordDTO getRecordByCoursePubId(@PathVariable Long coursePubId) {
String username = UAASecurityUtil.getUser().getUsername();
CourseRecordDTO dto = courseRecordService.getRecordByCoursePubId(coursePubId, username);
return dto;
}
service
/*
* 业务分析:
* 1.判断关键数据
* 2.判断业务数据
* 判断课程是否处于发布状态
* 不处于发布状态:若为免费课程则拒绝查看,若为付费课程则根据用户是否付款来给予观看权限
* 3.根据条件查询用户的学习记录
* 4.返回学习记录数据
* 如果有数据返回
* 如果没有返回空数据
* */
public CourseRecordDTO getRecordByCoursePubId(Long coursePubId, String username) {
// 1.判断关键数据
if (ObjectUtils.isEmpty(coursePubId)||
StringUtil.isBlank(username)
) {
ExceptionCast.cast(CommonErrorCode.E_100101);
}
//2.判断该课程是否下架
RestResponse response = coursePubIndexApiAgent.getCoursePubIndexById4s(coursePubId);
CoursePubIndexDTO result = response.getResult();
//课程不曾公开或免费课程下架则抛出异常
if(result == null || (result.getIs_pub() != 0 && result.getCharge().equals(CourseChargeEnum.CHARGE_NO.getCode()))){
ExceptionCast.cast(LearningErrorCode.E_202205);
}
// 3.根据条件查询用户的学习记录(一个人对一门课只有一个学习记录)
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CourseRecord::getCoursePubId, coursePubId);
queryWrapper.eq(CourseRecord::getUserName, username);
CourseRecord courseRecord = this.getOne(queryWrapper);
//付费课程若下架则根据用户是否支付来给予观看权限
if((ObjectUtils.isEmpty(courseRecord) && result.getIs_pub() != 0) || (courseRecord.getPaid() == 0 && result.getIs_pub() != 0)){
ExceptionCast.cast(LearningErrorCode.E_202205);
}
// 4.返回学习记录数据
CourseRecordDTO courseRecordDTO = null;
if (ObjectUtils.isEmpty(courseRecord)) {
// 如果没有返回空数据
courseRecordDTO = new CourseRecordDTO();
} else {
// 如果有数据返回
courseRecordDTO = CourseRecordConvert.INSTANCE.entity2dto(courseRecord);
}
return courseRecordDTO;
}
controller
@GetMapping("orders/create/{coursePubId}")
public OrdersDTO createOrder(@PathVariable Long coursePubId) {
LoginUser user = UAASecurityUtil.getUser();
if (ObjectUtils.isEmpty(user)) {
ExceptionCast.cast(CommonErrorCode.E_100108);
}
OrdersDTO dto = ordersService.createOrder(coursePubId, user.getUsername());
return dto;
}
service
/*
* 业务分析:
* 1.判断关键数据
* coursePubId username
* 2.判断业务数据
* 课程是否发布
* 用户是否已经购买该课程
* 3.保存订单数据
* 判断订单数据是否存在:coursePubId username(一个人对于一门课只有一个订单)
* 根据订单支付状态来判断:初始化态
* 如果没有
* 创建订单:新建的订单状态为--初始态(orderNo,coursePubId,coursePubName,username,companyId,initPrice,price,status)
* 如果有
* 如果订单数据存在,需要获得最新的课程价格并修改订单的价格数据--活动
*
* 4.将结果数据转为DTO并返回
* */
@Transactional
public OrdersDTO createOrder(Long coursePubId, String username) {
// 1.判断关键数据
// coursePubId username
if (ObjectUtils.isEmpty(coursePubId)||
StringUtil.isBlank(username)
) {
ExceptionCast.cast(CommonErrorCode.E_100101);
}
// 2.判断业务数据
// 课程发布数据
RestResponse restResponse = searchApiAgent.getPubIndexById4s(coursePubId);
CoursePubIndexDTO coursePub = restResponse.getResult();
if (ObjectUtils.isEmpty(coursePub)) {
ExceptionCast.castWithCodeAndDesc(restResponse.getCode(),restResponse.getMsg());
}
// 查看用户是否购买过该课程
Orders one = this.lambdaQuery().eq(Orders::getStatus, OrderDealStatusEnum.ORDER_DEAL_CLOSED_STATUS.getCode()).or().eq(Orders::getStatus, OrderDealStatusEnum.ORDER_DEAL_CLOSED_STATUS.getCode()).one();
if(!ObjectUtils.isEmpty(one)){
ExceptionCast.cast(OrderErrorCode.E_160021);
}
// 3.保存订单数据
// 判断订单数据是否存在:coursePubId username(一个人对于一门课只有一个订单)
// 根据订单支付状态来判断:初始化态
// select * from orders where username = ? and coursePubid = ? and status = ?
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Orders::getUserName, username);
queryWrapper.eq(Orders::getCoursePubId, coursePubId);
queryWrapper.eq(Orders::getStatus,new Integer(OrderDealStatusEnum.ORDER_DEAL_INIT_STATUS.getCode()) );
Orders orders = this.getOne(queryWrapper);
boolean result = false;
//若订单不存在则创建订单
if (ObjectUtils.isEmpty(orders)) {
orders = new Orders();
// 订单编号:有学成的标识+时间+唯一标识
orders.setOrderNo(PaymentUtil.genUniquePayOrderNo());
orders.setCoursePubId(coursePubId);
orders.setCoursePubName(coursePub.getName());
orders.setCompanyId(coursePub.getCompanyId());
orders.setUserName(username);
orders.setInitialPrice(coursePub.getPrice());
orders.setPrice(coursePub.getPrice());
orders.setStatus(new Integer(OrderDealStatusEnum.ORDER_DEAL_INIT_STATUS.getCode()));
result = this.save(orders);
} else{
// 修改订单的价格
// 如果订单数据存在,需要获得最新的课程价格并修改订单的价格数据--活动
LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(Orders::getPrice, coursePub.getPrice());
updateWrapper.set(Orders::getChangeDate, LocalDateTime.now());
updateWrapper.eq(Orders::getId, orders.getId());
result = this.update(updateWrapper);
}
if (!result) {
ExceptionCast.cast(OrderErrorCode.E_160015);
}
// 4.将结果数据转为DTO并返回
Orders po = this.getById(orders.getId());
OrdersDTO resultDTO = OrderConvert.INSTANCE.entity2dto(po);
return resultDTO;
}
controller
@GetMapping("orderPay/wxPay/createPay")
public PayCodeUrlResult createPayment(String orderNo) {
LoginUser user = UAASecurityUtil.getUser();
if (ObjectUtils.isEmpty(user)) {
ExceptionCast.cast(CommonErrorCode.E_100108);
}
PayCodeUrlResult payment = ordersService.createPayment(orderNo, user.getUsername());
return payment;
}
service
/*
PS:该方法不管也什么异常,都需要使用PayCodeUrlResult数据进行封装
* 业务分析:
* 1.判断关键数据
* orderNo username
* 2.判断业务数据
* 订单数据
* 判断是否存在:orderId username status :初始态
特别说明:本次的操作由于无法向外抛出异常,使得事务无法回滚:
1.操作本地数据
2.先操作第三方支付系统
3.保存订单支付数据
4.和wx平台进行交互获得支付链接地址
* */
@GlobalTransactional
public PayCodeUrlResult createPayment(String orderNo, String username) {
if(ObjectUtils.isEmpty(orderNo) ){
ExceptionCast.castWithCodeAndDesc(10010,"订单编号不能为空");
}
//2.判断业务数据
// 订单数据
// 判断是否存在:orderNo username status:初始态
Orders order = this.lambdaQuery().eq(Orders::getOrderNo, orderNo).eq(Orders::getUserName, username).eq(Orders::getStatus, OrderDealStatusEnum.ORDER_DEAL_INIT_STATUS.getCode()).one();
if(ObjectUtils.isEmpty(order)){
ExceptionCast.cast(OrderErrorCode.E_160008);
}
//3.保存订单支付数据
LambdaQueryWrapper payQueryWrapper = new LambdaQueryWrapper<>();
payQueryWrapper.eq(Pay::getOrderId, order.getId());
payQueryWrapper.eq(Pay::getUserName, username);
payQueryWrapper.eq(Pay::getCompanyId, order.getCompanyId());
payQueryWrapper.eq(Pay::getStatus, PayCodeUrlResult.NOT_PAY);
//3.1 查询是否存在支付数据
Pay pay = payService.getOne(payQueryWrapper);
if(ObjectUtils.isEmpty(pay)){
pay = new Pay();
pay.setUserName(username);
pay.setCompanyId(order.getCompanyId());
pay.setOrderId(order.getId());
pay.setStatus(PayCodeUrlResult.NOT_PAY);
pay.setPayMethod(PayCodeUrlResult.WX_PAY_FLAG);
// 实收金额和付款金额在支付后的通知中再去赋值,本次秩序赋值订单金额即可
pay.setTotalAmount(new BigDecimal(order.getPrice().toString()));
boolean save = payService.save(pay);
if(!save){
ExceptionCast.cast(OrderErrorCode.E_160009);
}
}
//4.和wx平台进行交互获得支付链接地址
try{
WxPayUnifiedOrderRequest payUnifiedOrderRequest = new WxPayUnifiedOrderRequest();
payUnifiedOrderRequest.setBody(order.getCoursePubName());
payUnifiedOrderRequest.setOutTradeNo(order.getOrderNo());
Integer price = WxPayUnifiedOrderRequest.yuanToFen(order.getPrice().toString());
payUnifiedOrderRequest.setTotalFee(price);
// 4.1获得程序服务所在服务的ip地址
payUnifiedOrderRequest.setSpbillCreateIp(InetAddress.getLocalHost().getHostAddress());
payUnifiedOrderRequest.setProductId("课程id:"+order.getCoursePubId());
// 4.2通过wxPayService获得统一下单的响应数据
WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(payUnifiedOrderRequest);
// 4.3解析响应数据
PayCodeUrlResult codeUrlResult = new PayCodeUrlResult();
if ("SUCCESS".equalsIgnoreCase(result.getReturnCode()) && "SUCCESS".equalsIgnoreCase(result.getResultCode())) {
codeUrlResult = PayCodeUrlResult.success(result.getCodeURL());
} else {
ExceptionCast.cast(OrderErrorCode.E_160011);
}
return codeUrlResult;
}catch (Exception e){
ExceptionCast.cast(OrderErrorCode.E_160011);
}
return null;
}
首先我们需要设置回调的地址
#商户微信公共号或开放平台唯一标识
weixinpay.app-id = wx
#商户号
weixinpay.mch-id = 14
#商户密钥
weixinpay.mch-key = T6m9iK7
#微信回调商户的地址
weixinpay.notify-url = http://6aa17ecf.cpolar.io
#商户的支付类型(NATIVE 为扫码支付)
weixinpay.trade-type = NATIVE
controller
/* 给wx支付平台来调用的方法,没有在Api接口中定义方法 */
@RequestMapping("order-pay/wx-pay/notify-result")
public String notifyPayment(HttpServletRequest request) {
System.out.println("订单支付通知方法开始执行");
try {
String xmlResult = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());
// 加入自己处理订单的业务逻辑,需要判断订单是否已经支付过,否则可能会重复调用
// 调用service层处理业务
payService.notifyPayment(xmlResult);
return WxPayNotifyResponse.success("处理成功!");
} catch (Exception e) {
log.error("微信回调结果异常,异常原因{}", e.getMessage());
return WxPayNotifyResponse.fail(e.getMessage());
}
}
service
/*
* 业务操作
* 1.判断并解析wx通知内容
* 保证wx支付是成功:returncode和resultcode为SUCCESS
* 如果支付通知消息失败,无需完成订单服务的业务逻辑操作,抛出异常,使用controller接受并返回wx错误信息
* 2.修改订单状态
*
* 完成消息幂等(wx支付平台会重复发消息)
* 判断订单是否已经支付
* 通过订单编号:out_trade_no
*
* PS:如果业务数据操作失败,需要让wx支付平台继续通知,继续通知的方式是业务层对外抛出异常
*
* 判断第三方订单金额是否和商户的订单金额是否一致(wx官方特别提醒)
*
* orders:status
* 判断订单是否存在
* 判断订单的状态:支付前状态必须初始态
* 修改内容:
* status
* pay_orders:支付后的结果
* 判断订单支付是否存在
* 判断订单支付的状态:status
*
* 修改内容:
* status
* pay_number
* pay_date
* receipt_amount
* buyer_pay_amount
* pay_response
*
* 3.给用户支付后的内容创建一个默认的学习记录
* 在学习中心完成:Feign远程调用
*
*
* 4.返回通知结果-wx平台
* 如果正常,执行代码就可以
* 如果数据不正常,业务需要抛出异常
* controller会针对service的执行结果来返回数据给wx支付平台
* */
@GlobalTransactional
@GlobalTransactional
public void notifyPayment(String xmlResultString) {
// 1.判断并解析wx通知内容
// 保证wx支付是成功:returncode和resultcode为SUCCESS
// 如果支付失败,无需完成订单服务的业务逻辑操作
if (StringUtil.isBlank(xmlResultString)) {
ExceptionCast.cast(CommonErrorCode.E_100101);
}
WxPayOrderNotifyResult notifyResult = null;
try {
notifyResult = wxPayService.parseOrderNotifyResult(xmlResultString);
} catch (WxPayException e) {
log.error(OrderErrorCode.E_160018.getDesc()+" : errMsg {}",e.getMessage());
ExceptionCast.cast(OrderErrorCode.E_160018);
}
String returnCode = notifyResult.getReturnCode();
String resultCode = notifyResult.getResultCode();
String outTradeNo = notifyResult.getOutTradeNo();
// 支付通知成功
if (PayCodeUrlResult.WX_PAY_SUCCESS_FLAG.equalsIgnoreCase(returnCode)&&
PayCodeUrlResult.WX_PAY_SUCCESS_FLAG.equalsIgnoreCase(resultCode)
) {
// 2.修改订单状态
// 完成消息幂等(wx支付平台会重复发消息)
// 判断订单是否已经支付
// 通过订单编号:out_trade_no-->orderNo
LambdaQueryWrapper ordersQueryWrapper = new LambdaQueryWrapper<>();
// 创建订单没有支付的查询条件
ordersQueryWrapper.eq(Orders::getOrderNo, outTradeNo);
ordersQueryWrapper.eq(Orders::getStatus,new Integer(OrderDealStatusEnum.ORDER_DEAL_INIT_STATUS.getCode()));
Orders order = ordersService.getOne(ordersQueryWrapper);
if (ObjectUtils.isEmpty(order)) {
log.error("订单信息不是初始态,订单编号:{}", outTradeNo);
return;
}
// 判断第三方订单金额是否和商户的订单金额是否一致
// wx订单金额(分)
Integer totalFee = notifyResult.getTotalFee();
String toYuan = BaseWxPayResult.fenToYuan(totalFee);
Float totalFeeFloat = new Float(toYuan);
// 商户订单金额(元)
Float price = order.getPrice();
// 判断金额是否一致
if (!(totalFeeFloat.equals(price))) {
log.error("订单金额与支付金额不一致,订单编号:{}", outTradeNo);
return;
}
// 订单数据:status
// 判断订单是否存在
// 判断订单的状态:支付前状态必须初始态
// 修改内容:
// status
LambdaUpdateWrapper ordersUpdateWrapper = new LambdaUpdateWrapper<>();
ordersUpdateWrapper.set(Orders::getStatus, new Integer(OrderDealStatusEnum.ORDER_DEAL_PAID_STATUS.getCode()));
ordersUpdateWrapper.set(Orders::getChangeDate, LocalDateTime.now());
ordersUpdateWrapper.eq(Orders::getOrderNo,outTradeNo);
boolean orderResult = ordersService.update(ordersUpdateWrapper);
// 如果业务数据操作失败,需要让wx支付平台继续通知,继续通知的方式是业务层对外抛出异常
if (!orderResult) {
ExceptionCast.cast(OrderErrorCode.E_160015);
}
// 订单支付数据:支付后的结果
// 判断订单支付是否存在
// 判断订单支付的状态:status 0
// 修改内容:
// status
// pay_number
// pay_date
// receipt_amount
// buyer_pay_amount
// pay_response
LambdaQueryWrapper payQueryWrapper = new LambdaQueryWrapper<>();
payQueryWrapper.eq(Pay::getOrderId,order.getId());
payQueryWrapper.eq(Pay::getStatus,PayCodeUrlResult.NOT_PAY);
Pay pay = this.getOne(payQueryWrapper);
if (ObjectUtils.isEmpty(pay)) {
log.error("订单支付信息不是未支付,订单编号:{}", outTradeNo);
return ;
}
pay.setStatus(PayCodeUrlResult.PAIED);
// 记录wx的支付编号
pay.setPayNumber(notifyResult.getTransactionId());
// 记录wx支付平台的用户支付时间
String timeEnd = notifyResult.getTimeEnd();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
LocalDateTime payTime = LocalDateTime.parse(timeEnd, dateTimeFormatter);
pay.setPayDate(payTime);
Integer settlementTotalFee = notifyResult.getSettlementTotalFee();
if (!(ObjectUtils.isEmpty(settlementTotalFee))) {
String settlementTotalFeeString = BaseWxPayResult.fenToYuan(settlementTotalFee);
pay.setReceiptAmount(new BigDecimal(settlementTotalFeeString));
}
Integer cashFee = notifyResult.getCashFee();
if (!(ObjectUtils.isEmpty(cashFee))) {
String cashFeeString = BaseWxPayResult.fenToYuan(cashFee);
pay.setBuyerPayAmount(new BigDecimal(cashFeeString));
}
pay.setPayResponse(xmlResultString);
boolean payResult = this.updateById(pay);
if (!payResult) {
ExceptionCast.cast(OrderErrorCode.E_160017);
}
// 3.给用户支付后的内容创建一个默认的学习记录
// 在学习中心完成:Feign远程调用
learningApiAgent.createPaidRecord(order.getUserName(),order.getCoursePubId());
// 支付通知失败
} else {
log.error("支付通过内容失败,订单编号:{}", outTradeNo);
}
}