diff --git a/pom.xml b/pom.xml index 0e564e3..56193af 100644 --- a/pom.xml +++ b/pom.xml @@ -221,6 +221,11 @@ + + + org.springframework.boot + spring-boot-starter-amqp + 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/config/RabbitMQConfig.java b/src/main/java/com/greenorange/promotion/config/RabbitMQConfig.java new file mode 100644 index 0000000..14ec82b --- /dev/null +++ b/src/main/java/com/greenorange/promotion/config/RabbitMQConfig.java @@ -0,0 +1,25 @@ +package com.greenorange.promotion.config; + +import org.springframework.amqp.support.converter.DefaultClassMapper; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + + @Bean + public MessageConverter jsonToMapMessageConverter() { + DefaultClassMapper defaultClassMapper = new DefaultClassMapper(); + defaultClassMapper.setTrustedPackages("com.greenorange.promotion.utils.MultiDelayMessage"); // trusted packages + Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); + jackson2JsonMessageConverter.setClassMapper(defaultClassMapper); + return jackson2JsonMessageConverter; + } + + @Bean + public MessageConverter messageConverter(){ + return new Jackson2JsonMessageConverter(); + } +} diff --git a/src/main/java/com/greenorange/promotion/config/WxOpenConfig.java b/src/main/java/com/greenorange/promotion/config/WxOpenConfig.java new file mode 100644 index 0000000..6554e50 --- /dev/null +++ b/src/main/java/com/greenorange/promotion/config/WxOpenConfig.java @@ -0,0 +1,44 @@ +package com.greenorange.promotion.config; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Slf4j +@Configuration +@ConfigurationProperties(prefix = "wx.mini") +public class WxOpenConfig { + + private String appId; + + private String appSecret; + + private WxMaService wxMaService; + + /** + * 单例模式 + */ + public WxMaService getWxMaService() { + if (wxMaService != null) { + return wxMaService; + } + synchronized (this) { + if (wxMaService != null) { + return wxMaService; + } + WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl(); + config.setAppid(appId); + config.setSecret(appSecret); + WxMaService service = new WxMaServiceImpl(); + service.setWxMaConfig(config); + wxMaService = service; + return wxMaService; + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/greenorange/promotion/config/WxPayConfig.java b/src/main/java/com/greenorange/promotion/config/WxPayConfig.java new file mode 100644 index 0000000..bdf99bb --- /dev/null +++ b/src/main/java/com/greenorange/promotion/config/WxPayConfig.java @@ -0,0 +1,78 @@ +package com.greenorange.promotion.config; + +import com.wechat.pay.java.core.RSAAutoCertificateConfig; +import com.wechat.pay.java.core.util.IOUtil; +import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension; +import com.wechat.pay.java.service.refund.RefundService; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Data +@Slf4j +@Configuration +@Component("WxPayConfig") +@ConfigurationProperties(prefix = "wx.pay") +public class WxPayConfig { + + private String appId; + + private String apiV3Key; + + private String notifyUrl; + + private String merchantId; + + private String privateKeyPath; + + private String merchantSerialNumber; + + // RSA配置 + private RSAAutoCertificateConfig RSAConfig; + + // JSAPI支付 + private JsapiServiceExtension jsapiServiceExtension; + + // 退款 + private RefundService refundService; + + /** + * 初始化配置 + */ + @Bean + public boolean initWxPayConfig() throws IOException { + this.RSAConfig = buildRSAAutoCertificateConfig(); + this.jsapiServiceExtension = buildJsapiServiceExtension(RSAConfig); + this.refundService = buildRefundService(RSAConfig); + return true; + } + + // 构建并使用自动更新平台证书的RSA配置,一个商户号只能初始化一个配置,否则会因为重复的下载任务报错 + private RSAAutoCertificateConfig buildRSAAutoCertificateConfig() throws IOException { + // 将 resource 目录下的文件转为 InputStream,然后利用 IOUtil.toString(inputStream) 转化为密钥 + String privateKey = IOUtil.toString(new ClassPathResource(privateKeyPath).getInputStream()); + return new RSAAutoCertificateConfig.Builder() + .merchantId(merchantId) + .privateKey(privateKey) + .merchantSerialNumber(merchantSerialNumber) + .apiV3Key(apiV3Key) + .build(); + } + + // 构建JSAPI支付 + private JsapiServiceExtension buildJsapiServiceExtension(RSAAutoCertificateConfig config) { + return new JsapiServiceExtension.Builder().config(config).build(); + } + + // 构建退款 + private RefundService buildRefundService(RSAAutoCertificateConfig config) { + return new RefundService.Builder().config(config).build(); + } + +} diff --git a/src/main/java/com/greenorange/promotion/controller/course/CourseController.java b/src/main/java/com/greenorange/promotion/controller/course/CourseController.java index 531274f..a0ff09c 100644 --- a/src/main/java/com/greenorange/promotion/controller/course/CourseController.java +++ b/src/main/java/com/greenorange/promotion/controller/course/CourseController.java @@ -56,9 +56,6 @@ public class CourseController { @Resource private CommonService commonService; - @Resource - private WechatGetQrcodeService wechatGetQrcodeService; - /** * 小程序端用户查看热门课程列表 diff --git a/src/main/java/com/greenorange/promotion/controller/wechat/WechatPayController.java b/src/main/java/com/greenorange/promotion/controller/wechat/WechatPayController.java new file mode 100644 index 0000000..131d679 --- /dev/null +++ b/src/main/java/com/greenorange/promotion/controller/wechat/WechatPayController.java @@ -0,0 +1,150 @@ +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 = "微信支付") +@RequestMapping("/wxPay") +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/controller/wechat/WechatPayoutsController.java b/src/main/java/com/greenorange/promotion/controller/wechat/WechatPayoutsController.java index e2861cd..12247e0 100644 --- a/src/main/java/com/greenorange/promotion/controller/wechat/WechatPayoutsController.java +++ b/src/main/java/com/greenorange/promotion/controller/wechat/WechatPayoutsController.java @@ -26,7 +26,6 @@ import java.util.Map; public class WechatPayoutsController { - /** * 微信小程序积分提现到银行卡 */ 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/model/vo/userInfo/UserInfoVO.java b/src/main/java/com/greenorange/promotion/model/vo/userInfo/UserInfoVO.java index 4fb4e53..f249e5d 100644 --- a/src/main/java/com/greenorange/promotion/model/vo/userInfo/UserInfoVO.java +++ b/src/main/java/com/greenorange/promotion/model/vo/userInfo/UserInfoVO.java @@ -37,7 +37,6 @@ public class UserInfoVO implements Serializable { @Schema(description = "手机号", example = "15888610253") private String phoneNumber; - /** * 账号 */ diff --git a/src/main/java/com/greenorange/promotion/service/userInfo/impl/UserInfoServiceImpl.java b/src/main/java/com/greenorange/promotion/service/userInfo/impl/UserInfoServiceImpl.java index 024e608..1dd7839 100644 --- a/src/main/java/com/greenorange/promotion/service/userInfo/impl/UserInfoServiceImpl.java +++ b/src/main/java/com/greenorange/promotion/service/userInfo/impl/UserInfoServiceImpl.java @@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.greenorange.promotion.common.ErrorCode; +import com.greenorange.promotion.config.WxOpenConfig; import com.greenorange.promotion.constant.CommonConstant; import com.greenorange.promotion.constant.SystemConstant; import com.greenorange.promotion.constant.UserConstant; @@ -95,6 +96,8 @@ public class UserInfoServiceImpl extends ServiceImpl + + /** * 获取查询条件 */ diff --git a/src/main/java/com/greenorange/promotion/service/wechat/WechatPayService.java b/src/main/java/com/greenorange/promotion/service/wechat/WechatPayService.java new file mode 100644 index 0000000..9de9036 --- /dev/null +++ b/src/main/java/com/greenorange/promotion/service/wechat/WechatPayService.java @@ -0,0 +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 new file mode 100644 index 0000000..515f40a --- /dev/null +++ b/src/main/java/com/greenorange/promotion/service/wechat/impl/WechatPayServiceImpl.java @@ -0,0 +1,252 @@ +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 陈新知 + */ +@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/MultiDelayMessage.java b/src/main/java/com/greenorange/promotion/utils/MultiDelayMessage.java new file mode 100644 index 0000000..b55c419 --- /dev/null +++ b/src/main/java/com/greenorange/promotion/utils/MultiDelayMessage.java @@ -0,0 +1,59 @@ +package com.greenorange.promotion.utils; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Data +@NoArgsConstructor +public class MultiDelayMessage implements Serializable { + + /** + * 消息体 + */ + private T data; + + + /** + * 记录延时时间的集合 + */ + private List delayMillis; + + public MultiDelayMessage(T data, List delayMillis) { + this.data = data; + this.delayMillis = delayMillis; + } + + public MultiDelayMessage(T data, Long...delayMillis) { + this.data = data; + this.delayMillis = new ArrayList<>(Arrays.asList(delayMillis)); + } + + /** + * 获取并移除下一个延迟时间 + * @return 集合中第一个延迟时间 + */ + public Long removeNextDelay() { + return delayMillis.remove(0); + } + + + /** + * 是否有下一个延迟时间 + */ + public boolean hasNextDelay() { + return !delayMillis.isEmpty(); + } + + + + @Serial + private static final long serialVersionUID = 1L; + + +} 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; + } + +}