diff --git a/src/main/java/com/greenorange/promotion/aop/PermissionCheck.java b/src/main/java/com/greenorange/promotion/aop/PermissionCheck.java index cb345ba..bd91b9d 100644 --- a/src/main/java/com/greenorange/promotion/aop/PermissionCheck.java +++ b/src/main/java/com/greenorange/promotion/aop/PermissionCheck.java @@ -72,11 +72,15 @@ public class PermissionCheck { String userRole = userInfo.getUserRole(); UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(userRole); - // 接口权限只能是 USER,ADMIN,BOSS,用户权限是 ADMIN,BOSS,USER,BAN + // 接口权限只能是 USER,ADMIN,BOSS,用户权限是 ADMIN,BOSS,USER,BAN,MANAGER,SUPERVISOR,STAFF // 校验角色 - ThrowUtils.throwIf(UserRoleEnum.USER.equals(userRoleEnum) && !UserRoleEnum.USER.equals(interfaceRoleEnum), ErrorCode.NO_AUTH_ERROR); - ThrowUtils.throwIf(UserRoleEnum.BAN.equals(userRoleEnum), ErrorCode.NO_AUTH_ERROR, "用户已被封禁"); - ThrowUtils.throwIf(UserRoleEnum.ADMIN.equals(userRoleEnum) && UserRoleEnum.BOSS.equals(interfaceRoleEnum), ErrorCode.NO_AUTH_ERROR); + ThrowUtils.throwIf(UserRoleEnum.BOSS.equals(userRoleEnum) && UserRoleEnum.USER.equals(interfaceRoleEnum), ErrorCode.NO_AUTH_ERROR); + ThrowUtils.throwIf(UserRoleEnum.ADMIN.equals(userRoleEnum) && !UserRoleEnum.ADMIN.equals(interfaceRoleEnum), ErrorCode.NO_AUTH_ERROR); + ThrowUtils.throwIf(UserRoleEnum.BAN.equals(userRoleEnum), ErrorCode.PARAMS_ERROR, "用户已被封禁"); + ThrowUtils.throwIf((UserRoleEnum.USER.equals(userRoleEnum) + || UserRoleEnum.STAFF.equals(userRoleEnum) + || UserRoleEnum.SUPERVISOR.equals(userRoleEnum) + || UserRoleEnum.MANAGER.equals(userRoleEnum)) && !UserRoleEnum.USER.equals(interfaceRoleEnum), ErrorCode.NO_AUTH_ERROR); return joinPoint.proceed(); } diff --git a/src/main/java/com/greenorange/promotion/controller/wechat/WechatPayController.java b/src/main/java/com/greenorange/promotion/controller/wechat/WechatPayController.java index 8c3b8fe..131d679 100644 --- a/src/main/java/com/greenorange/promotion/controller/wechat/WechatPayController.java +++ b/src/main/java/com/greenorange/promotion/controller/wechat/WechatPayController.java @@ -1,10 +1,44 @@ package com.greenorange.promotion.controller.wechat; +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; +import com.greenorange.promotion.annotation.RequiresPermission; +import com.greenorange.promotion.common.BaseResponse; +import com.greenorange.promotion.common.ErrorCode; +import com.greenorange.promotion.common.ResultUtils; +import com.greenorange.promotion.config.WxOpenConfig; +import com.greenorange.promotion.constant.OrderStatusConstant; +import com.greenorange.promotion.constant.UserConstant; +import com.greenorange.promotion.exception.BusinessException; +import com.greenorange.promotion.exception.ThrowUtils; +import com.greenorange.promotion.model.dto.CommonRequest; +import com.greenorange.promotion.model.dto.wxPay.WechatPayRequest; +import com.greenorange.promotion.model.entity.CourseOrder; +import com.greenorange.promotion.model.entity.UserInfo; +import com.greenorange.promotion.service.course.CourseOrderService; +import com.greenorange.promotion.service.userInfo.UserInfoService; +import com.greenorange.promotion.service.wechat.WechatPayService; +import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse; +import com.wechat.pay.java.service.payments.model.Transaction; +import com.wechat.pay.java.service.refund.model.Refund; +import com.wechat.pay.java.service.refund.model.RefundNotification; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.io.IOException; +import java.math.BigDecimal; + @Slf4j @RestController @Tag(name = "微信支付") @@ -12,6 +46,105 @@ import org.springframework.web.bind.annotation.RestController; public class WechatPayController { + @Resource + private WechatPayService weChatService; + + @Resource + private UserInfoService userInfoService; + + @Resource + private CourseOrderService courseOrderService; + + @Resource + private WxOpenConfig wxOpenConfig; + + + /** + * JSAPI 下单(商品类) + */ + @PostMapping("/payment/create") + @Operation(summary = "JSAPI 下单(商品类)", description = "参数:订单id, 权限:所有人, 方法名:createPayment") + @RequiresPermission(mustRole = UserConstant.DEFAULT_ROLE) + public BaseResponse createPayment(@Valid @RequestBody WechatPayRequest wechatPayRequest, HttpServletRequest request) { + + String code = wechatPayRequest.getCode(); + WxMaJscode2SessionResult sessionInfo; + String miniOpenId; + try { + WxMaService wxMaService = wxOpenConfig.getWxMaService(); + sessionInfo = wxMaService.jsCode2SessionInfo(code); + miniOpenId = sessionInfo.getOpenid(); + if (StringUtils.isAnyBlank(miniOpenId)) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR); + } + } catch (WxErrorException e) { + log.error("userLoginByWxOpen error", e); + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,系统错误"); + } + Long userId = (Long) request.getAttribute("userId"); + UserInfo userInfo = userInfoService.getById(userId); + + Long orderId = wechatPayRequest.getOrderId(); + CourseOrder courseOrder = courseOrderService.getById(orderId); + ThrowUtils.throwIf(courseOrder == null, ErrorCode.NOT_FOUND_ERROR, "订单不存在"); + ThrowUtils.throwIf(!courseOrder.getOrderStatus().equals(OrderStatusConstant.PENDING), ErrorCode.OPERATION_ERROR, "订单状态错误"); + if (!userInfo.getId().equals(courseOrder.getUserId())) { + throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "你不是该订单用户!"); + } + PrepayWithRequestPaymentResponse response = weChatService.createPayment(String.valueOf(orderId), miniOpenId, courseOrder.getTotalAmount()); + return ResultUtils.success(response); + } + + + + /** + * JSAPI 下单回调(商品类) + */ + @Hidden + @PostMapping("/payment/callback") + @Operation(summary = "JSAPI 下单回调(商品类)", description = "参数:订单id, 权限:所有人, 方法名:callbackPayment") + public synchronized BaseResponse callbackPayment(HttpServletRequest request) throws IOException { + // 获取下单信息 + Transaction transaction = weChatService.getTransactionInfo(request); + System.out.println("下单信息:" + transaction); + // 支付回调 + boolean result = weChatService.paymentCallback(transaction); + ThrowUtils.throwIf(!result, ErrorCode.SYSTEM_ERROR, "微信支付回调失败"); + return ResultUtils.success(true); + } + + + /** + * Web管理员部分退款 + * @param commonRequest 订单id + */ + @PostMapping("/refund/part/create") + @Operation(summary = "Web管理员部分退款", description = "参数:订单id, 权限:web端管理员, 方法名:createPartRefund") + @RequiresPermission(mustRole = UserConstant.ADMIN_ROLE) + public BaseResponse createPartRefund(@Valid @RequestBody CommonRequest commonRequest) { + Long courseOrderId = commonRequest.getId(); + CourseOrder courseOrder = courseOrderService.getById(courseOrderId); + ThrowUtils.throwIf(courseOrder == null, ErrorCode.OPERATION_ERROR, "订单不存在"); + + Refund refund = weChatService.refundPartPayment(String.valueOf(courseOrderId), courseOrder.getTotalAmount()); + return ResultUtils.success(refund); + } + + + /** + * 部分退款回调 + */ + @Hidden + @PostMapping("/refund/part/callback") + @Operation(summary = "部分退款回调", description = "参数:订单id, 权限:web端管理员, 方法名:callbackRefundPart") + public BaseResponse callbackRefundPart(HttpServletRequest request) { + // 获取退款信息 + RefundNotification refundNotification = weChatService.getRefundInfo(request); + // 退款回调 + boolean result = weChatService.refundPartCallback(refundNotification); + ThrowUtils.throwIf(!result, ErrorCode.SYSTEM_ERROR, "退款回调失败"); + return ResultUtils.success(true); + } } diff --git a/src/main/java/com/greenorange/promotion/model/dto/wxPay/WechatPayRequest.java b/src/main/java/com/greenorange/promotion/model/dto/wxPay/WechatPayRequest.java new file mode 100644 index 0000000..c0e1ba5 --- /dev/null +++ b/src/main/java/com/greenorange/promotion/model/dto/wxPay/WechatPayRequest.java @@ -0,0 +1,33 @@ +package com.greenorange.promotion.model.dto.wxPay; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@Schema(description = "微信支付请求体", requiredProperties = {"orderId", "code"}) +public class WechatPayRequest implements Serializable { + + /** + * 订单id + */ + @NotNull(message = "id不能为null") + @Min(value = 1L, message = "id不能小于1") + @Schema(description = "订单id", example = "1") + private Long orderId; + + /** + * 用户登录凭证 + */ + @NotBlank(message = "用户登录凭证不能为空") + @Schema(description = "用户登录凭证", example = "23nm5jfds22a2324rr32rr") + private String code; + + @Serial + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/greenorange/promotion/service/wechat/WechatPayService.java b/src/main/java/com/greenorange/promotion/service/wechat/WechatPayService.java index f85e412..9de9036 100644 --- a/src/main/java/com/greenorange/promotion/service/wechat/WechatPayService.java +++ b/src/main/java/com/greenorange/promotion/service/wechat/WechatPayService.java @@ -1,7 +1,53 @@ package com.greenorange.promotion.service.wechat; +import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse; +import com.wechat.pay.java.service.payments.model.Transaction; +import com.wechat.pay.java.service.refund.model.Refund; +import com.wechat.pay.java.service.refund.model.RefundNotification; +import jakarta.servlet.http.HttpServletRequest; + +import java.io.IOException; +import java.math.BigDecimal; + /** * @author 陈新知 */ public interface WechatPayService { + + /** + * 微信支付 + */ + PrepayWithRequestPaymentResponse createPayment(String orderId, String miniOpenId, BigDecimal amount); + + + /** + * 支付回调 + */ + boolean paymentCallback(Transaction transaction) throws IOException; + + + /** + * 部分退款申请 + */ + Refund refundPartPayment(String orderId, BigDecimal refundAmount); + + + /** + * 部分退款回调 + */ + boolean refundPartCallback(RefundNotification refundNotification); + + + /** + * 获取支付回调信息 + */ + Transaction getTransactionInfo(HttpServletRequest request); + + + /** + * 获取退款回调信息 + */ + RefundNotification getRefundInfo(HttpServletRequest request); + + } diff --git a/src/main/java/com/greenorange/promotion/service/wechat/impl/WechatPayServiceImpl.java b/src/main/java/com/greenorange/promotion/service/wechat/impl/WechatPayServiceImpl.java index 6d55bff..515f40a 100644 --- a/src/main/java/com/greenorange/promotion/service/wechat/impl/WechatPayServiceImpl.java +++ b/src/main/java/com/greenorange/promotion/service/wechat/impl/WechatPayServiceImpl.java @@ -1,8 +1,39 @@ package com.greenorange.promotion.service.wechat.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.greenorange.promotion.common.ErrorCode; +import com.greenorange.promotion.config.WxPayConfig; +import com.greenorange.promotion.constant.OrderStatusConstant; +import com.greenorange.promotion.exception.ThrowUtils; +import com.greenorange.promotion.model.entity.Course; +import com.greenorange.promotion.model.entity.CourseOrder; +import com.greenorange.promotion.service.course.CourseOrderService; +import com.greenorange.promotion.service.course.CourseService; import com.greenorange.promotion.service.wechat.WechatPayService; +import com.greenorange.promotion.utils.RefundUtils; +import com.wechat.pay.java.core.notification.NotificationParser; +import com.wechat.pay.java.core.notification.RequestParam; +import com.wechat.pay.java.service.payments.jsapi.model.Amount; +import com.wechat.pay.java.service.payments.jsapi.model.Payer; +import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest; +import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse; +import com.wechat.pay.java.service.payments.model.Transaction; +import com.wechat.pay.java.service.refund.model.AmountReq; +import com.wechat.pay.java.service.refund.model.CreateRequest; +import com.wechat.pay.java.service.refund.model.Refund; +import com.wechat.pay.java.service.refund.model.RefundNotification; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import lombok.SneakyThrows; import org.springframework.stereotype.Service; +import java.io.BufferedReader; +import java.io.IOException; +import java.math.BigDecimal; + /** * @author 陈新知 */ @@ -10,4 +41,212 @@ import org.springframework.stereotype.Service; public class WechatPayServiceImpl implements WechatPayService { + @Resource + private WxPayConfig wxPayConfig; + + + @Resource + private CourseOrderService courseOrderService; + + + @Resource + private CourseService courseService; + + + /** + * 请求参数 + */ + public static RequestParam requestParam = null; + + + /** + * 微信支付 + */ + @Override + public PrepayWithRequestPaymentResponse createPayment(String orderId, String miniOpenId, BigDecimal amount) { + // request.setXxx(val)设置所需参数,具体参数可见Request定义 + PrepayRequest request = new PrepayRequest(); + // 金额 + Amount WxAmount = new Amount(); + WxAmount.setTotal(amount.movePointRight(2).intValue()); + WxAmount.setCurrency("CNY"); + request.setAmount(WxAmount); + // 公众号id + request.setAppid(wxPayConfig.getAppId()); + // 商户号 + request.setMchid(wxPayConfig.getMerchantId()); + // 支付者信息 + Payer payer = new Payer(); + payer.setOpenid(miniOpenId); + request.setPayer(payer); + // 获取订单号 + CourseOrder courseOrder = courseOrderService.getById(orderId); + String orderNumber = courseOrder.getOrderNumber(); + // 描述 + request.setDescription("订单号:" + orderNumber); + // 微信回调地址 + request.setNotifyUrl(wxPayConfig.getNotifyUrl() + "/wxPay/payment/callback"); + // 商户订单号 + request.setOutTradeNo(orderNumber); + //返回数据,前端调起支付 + return wxPayConfig.getJsapiServiceExtension().prepayWithRequestPayment(request); + } + + + /** + * 支付回调 + */ + @Override + public boolean paymentCallback(Transaction transaction) throws IOException { + System.out.println("---------------------------微信支付回调(开始)-------------------------------"); + // 获取订单信息 + String orderNumber = transaction.getOutTradeNo(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(CourseOrder::getOrderNumber, orderNumber); + CourseOrder courseOrder = courseOrderService.getOne(queryWrapper); + + // 修改订单状态 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(CourseOrder::getId, courseOrder.getId()) + .set(CourseOrder::getOrderStatus, OrderStatusConstant.SUCCESS); + courseOrderService.update(updateWrapper); + + // 修改当前课程下单人数 + Long courseId = courseOrder.getCourseId(); + Course course = courseService.getById(courseId); + if (course != null) { + course.setOrderCount(course.getOrderCount() + 1); + courseService.updateById(course); + } + System.out.println("---------------------------微信支付回调(结束)-------------------------------"); + return true; + } + + + /** + * 部分退款申请 + */ + @Override + public Refund refundPartPayment(String orderId, BigDecimal refundAmount) { + // 获取订单 + CourseOrder courseOrder = courseOrderService.getById(orderId); + ThrowUtils.throwIf(courseOrder == null, ErrorCode.OPERATION_ERROR, "订单不存在"); + // 判断该订单是否已经退款 + ThrowUtils.throwIf(courseOrder.getOrderStatus().equals(OrderStatusConstant.REFUNDED), ErrorCode.OPERATION_ERROR, "订单已退款"); + + String orderNumber = courseOrder.getOrderNumber(); + // 退款请求 + CreateRequest createRequest = new CreateRequest(); + // 商户订单号 + createRequest.setOutTradeNo(orderNumber); + // 商户退款单号 + String outRefundNo = RefundUtils.generateRefundNo(); + createRequest.setOutRefundNo(outRefundNo); + // 退款结果回调 + createRequest.setNotifyUrl(wxPayConfig.getNotifyUrl() + "/wxPay/refund/part/callback"); + // 退款金额 + AmountReq amountReq = new AmountReq(); + + amountReq.setRefund(refundAmount.movePointRight(2).longValue()); + amountReq.setTotal(courseOrder.getTotalAmount().movePointRight(2).longValue()); + amountReq.setCurrency("CNY"); + createRequest.setAmount(amountReq); + + //TODO 生成退款记录 + + // 申请退款 + System.out.println("退款请求:" + createRequest); + Refund refund = wxPayConfig.getRefundService().create(createRequest); + System.out.println("退款申请结果:" + refund); + + return refund; + } + + + /** + * 部分退款回调 + */ + @Override + public boolean refundPartCallback(RefundNotification refundNotification) { + System.out.println("---------------------------微信退款回调(开始)-------------------------------"); + // 获取订单信息 + String orderNumber = refundNotification.getOutTradeNo(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(CourseOrder::getOrderNumber, orderNumber); + CourseOrder courseOrder = courseOrderService.getOne(queryWrapper); + + // 修改订单状态 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(CourseOrder::getId, courseOrder.getId()) + .set(CourseOrder::getOrderStatus, OrderStatusConstant.REFUNDED); + courseOrderService.update(updateWrapper); + + // 修改课程下单人数 + Long courseId = courseOrder.getCourseId(); + Course course = courseService.getById(courseId); + if (course != null) { + course.setOrderCount(course.getOrderCount() - 1); + courseService.updateById(course); + } + + System.out.println("---------------------------微信退款回调(结束)-------------------------------"); + return true; + } + + + /** + * 获取支付回调信息 + */ + @Override + public Transaction getTransactionInfo(HttpServletRequest request) { + NotificationParser notificationParser = getNotificationParser(request); + return notificationParser.parse(requestParam, Transaction.class); + } + + + /** + * 获取退款回调信息 + */ + @Override + public RefundNotification getRefundInfo(HttpServletRequest request) { + NotificationParser notificationParser = getNotificationParser(request); + return notificationParser.parse(requestParam, RefundNotification.class); + } + + + /** + * 根据微信官方发送的请求获取信息 + */ + @SneakyThrows + public NotificationParser getNotificationParser(HttpServletRequest request) { + System.out.println("---------------------------获取信息-------------------------------"); + // 获取RSA配置 + NotificationParser notificationParser = new NotificationParser(wxPayConfig.getRSAConfig()); + // 构建请求 + StringBuilder bodyBuilder = new StringBuilder(); + BufferedReader reader = request.getReader(); + String line; + while ((line = reader.readLine()) != null) { + bodyBuilder.append(line); + } + String body = bodyBuilder.toString(); + String timestamp = request.getHeader("Wechatpay-Timestamp"); + String nonce = request.getHeader("Wechatpay-Nonce"); + String signature = request.getHeader("Wechatpay-Signature"); + String singType = request.getHeader("Wechatpay-Signature-Type"); + String wechatPayCertificateSerialNumber = request.getHeader("Wechatpay-Serial"); + requestParam = new RequestParam.Builder() + .serialNumber(wechatPayCertificateSerialNumber) + .nonce(nonce) + .signature(signature) + .timestamp(timestamp) + .signType(singType) + .body(body) + .build(); + System.out.println(requestParam.toString()); + System.out.println("---------------------------信息获取完毕-------------------------------"); + return notificationParser; + } + + } diff --git a/src/main/java/com/greenorange/promotion/utils/RefundUtils.java b/src/main/java/com/greenorange/promotion/utils/RefundUtils.java new file mode 100644 index 0000000..e33f017 --- /dev/null +++ b/src/main/java/com/greenorange/promotion/utils/RefundUtils.java @@ -0,0 +1,21 @@ +package com.greenorange.promotion.utils; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Random; + +public class RefundUtils { + + // 生成唯一的全额退款单号,格式为 yyyyMMddHHmmssSSS + 随机数 + public static String generateRefundNo() { + // 获取当前时间的时间戳 + String timestamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); + + // 生成一个 4 位随机数,保证每次退款单号不同 + int randomNum = new Random().nextInt(9000) + 1000; // 生成1000到9999之间的随机数 + + // 拼接退款单号 + return timestamp + randomNum; + } + +}