无关风月
2024-08-16 57cdf0d3794726ad5ec367598022cd005cdab978
Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/java/mx_charging_pile
6个文件已修改
36个文件已添加
3218 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-auth/src/main/java/com/ruoyi/auth/controller/TokenController.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/service/TokenService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/AccessTokenRespBody.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/Code2SessionRespBody.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/RespBody.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resq/Code2SessionResqBody.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/model/WeixinProperties.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletPhoneEncrypteData.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserDecodeData.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserEncrypteData.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/Watermark.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/JsonUtils.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/SHA1.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WebUtils.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxAppletTools.java 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCache.java 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCacheTemplate.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCaffineCache.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxException.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxJsonUtils.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/pom.xml 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/config/WxConfig.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/controller/WxPayController.java 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/RefundEnum.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/TradeStateEnum.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/exception/WxException.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/V3.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WeixinProperties.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentInfoModel.java 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentRefundModel.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/AppletUserDecodeData.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/Watermark.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/resp/NotifyV3PayDecodeRespBody.java 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/SHA1.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxAbstractPay.java 333 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxJsonUtils.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxTimeUtils.java 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxUtils.java 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxV3Pay.java 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java
@@ -25,7 +25,7 @@
    /**
     * 用户名id
     */
    private Integer userid;
    private Long userId;
    /**
     * 用户名
ruoyi-auth/src/main/java/com/ruoyi/auth/controller/TokenController.java
@@ -3,15 +3,16 @@
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.system.api.domain.SysRole;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.feignClient.SysUserClient;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.auth.form.LoginBody;
import com.ruoyi.auth.form.RegisterBody;
import com.ruoyi.auth.service.SysLoginService;
@@ -30,6 +31,7 @@
 * 
 * @author ruoyi
 */
@Slf4j
@RestController
public class TokenController
{
@@ -67,10 +69,6 @@
        userClient.updateSysUser(sysUser);
        return R.ok(map);
    }
    @DeleteMapping("logout")
    public R<?> logout(HttpServletRequest request) {
@@ -86,6 +84,17 @@
        return R.ok();
    }
    @DeleteMapping("logoutApplet")
    public R<?> logoutApplet(HttpServletRequest request) {
        String token = SecurityUtils.getToken(request);
        if (StringUtils.isNotEmpty(token))
        {
            // 删除用户缓存记录
            AuthUtil.logoutByToken(token);
        }
        return R.ok();
    }
    @PostMapping("refresh")
    public R<?> refresh(HttpServletRequest request)
    {
ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java
@@ -11,6 +11,7 @@
     * 缓存有效期,默认720(分钟)
     */
    public final static long EXPIRATION = 720;
    public final static long EXPIRATION_APPLET = 7*24*60*60;
    /**
     * 缓存刷新时间,默认120(分钟)
ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/service/TokenService.java
@@ -36,6 +36,7 @@
    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
    private final static long expireTime = CacheConstants.EXPIRATION;
    private final static long expireAppletTime = CacheConstants.EXPIRATION_APPLET;
    private final static String ACCESS_TOKEN = CacheConstants.LOGIN_TOKEN_KEY;
@@ -72,7 +73,7 @@
     */
    public Map<String, Object> createTokenApplet(LoginUserApplet loginUser) {
        String token = IdUtils.fastUUID();
        Integer userId = loginUser.getUserid();
        Long userId = loginUser.getUserId();
        String name = loginUser.getName();
        loginUser.setToken(token);
        loginUser.setIpaddr(IpUtils.getIpAddr());
@@ -85,7 +86,7 @@
        // 接口返回信息
        Map<String, Object> rspMap = new HashMap<String, Object>();
        rspMap.put("access_token", JwtUtils.createToken(claimsMap));
        rspMap.put("expires_in", expireTime);
        rspMap.put("expires_in", expireAppletTime);
        return rspMap;
    }
    public LoginUserApplet getLoginUserApplet() {
@@ -109,8 +110,8 @@
        LoginUserApplet user = null;
        try {
            if (StringUtils.isNotEmpty(token)) {
                String userkey = JwtUtils.getUserKeyApplet(token);
                user = redisService.getCacheObject(getTokenKey(userkey));
                String userKey = JwtUtils.getUserKeyApplet(token);
                user = redisService.getCacheObject(getTokenKey(userKey));
                return user;
            }
        } catch (Exception e) {
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
@@ -3,18 +3,25 @@
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.api.dto.*;
import com.ruoyi.account.api.model.*;
import com.ruoyi.account.api.vo.CouponListVOVO;
import com.ruoyi.account.service.*;
import com.ruoyi.account.wx.body.resp.Code2SessionRespBody;
import com.ruoyi.account.wx.body.resq.Code2SessionResqBody;
import com.ruoyi.account.wx.model.WeixinProperties;
import com.ruoyi.account.wx.tools.WxAppletTools;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.bean.BeanUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.domain.BasePojo;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.order.api.feignClient.ChargingOrderClient;
import com.ruoyi.order.api.feignClient.ExchangeOrderClient;
import com.ruoyi.order.api.model.TChargingOrder;
@@ -22,18 +29,20 @@
import com.ruoyi.other.api.domain.TCompany;
import com.ruoyi.other.api.domain.TUserTag;
import com.ruoyi.other.api.feignClient.OtherClient;
import com.ruoyi.system.api.domain.SysRole;
import com.ruoyi.system.api.model.LoginUserApplet;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
/**
@@ -44,6 +53,7 @@
 * @author luodangjia
 * @since 2024-08-06
 */
@Slf4j
@RestController
@RequestMapping("/t-app-user")
public class TAppUserController {
@@ -68,8 +78,41 @@
    @Resource
    private ExchangeOrderClient exchangeOrderClient;
    @Autowired
    private TokenService tokenService;
    @Autowired
    private RedisService redisService;
    @Autowired
    private WeixinProperties wxConfig;
    @Autowired
    private RestTemplate wxRestTemplate;
    @ApiOperation(value = "通过code获得openid,  1 --->对应的appid:wx4c405fa42539fc21  2---->对应的appid:wx02d9f6c92e6d3c86")
    @GetMapping("openId-by-jscode2session/{code}")
    public AjaxResult<Map<String, Object>> jscode2session(@PathVariable String code) {
        log.info("<<<<<<<<换取openid开始<<<<<<<<:{}", code);
        WxAppletTools appletTools = new WxAppletTools(wxRestTemplate, wxConfig);
        Code2SessionRespBody body = appletTools.getOpenIdByJscode2session(new Code2SessionResqBody().build(code));
        String openid = body.getOpenid();
        String sessionKey = body.getSessionKey();
        TAppUser appUser = appUserService.getOne(Wrappers.lambdaQuery(TAppUser.class).eq(TAppUser::getWxOpenid, openid).last("limit 1"));
        if (Objects.isNull(appUser)) {
            appUser = new TAppUser();
            appUser.setWxOpenid(openid);
            appUserService.save(appUser);
        }
        // 提前对sessionKey进行删除
        log.info("换取sessionKey:{}", sessionKey);
        // 将sessionKey进行存储,后续获取信息需要
        redisService.setCacheObject(openid, sessionKey);
        LoginUserApplet loginUserApplet = new LoginUserApplet();
        if(ObjectUtils.isNotNull(appUser)){
            loginUserApplet.setUserId(appUser.getId());
        }
        HashMap<String, Object> tokenInfos = new HashMap<>();
        tokenInfos.put("token",tokenService.createTokenApplet(loginUserApplet));
        tokenInfos.put("info",loginUserApplet);
        return AjaxResult.ok(tokenInfos);
    }
    @ApiOperation(value = "管理后台-根据手机号查询用户ids", tags = {"管理后台-活动费用统计"})
    @PostMapping(value = "/user/getUserIdsByPhone")
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/AccessTokenRespBody.java
New file
@@ -0,0 +1,28 @@
package com.ruoyi.account.wx.body.resp;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
/**
 * AccessToken 全局唯一
 *
 * @author liheng
 */
@Data
public class AccessTokenRespBody extends RespBody implements Serializable {
    /**
     * 获取到的凭证
     */
    @JsonProperty("access_token")
    private String accessToken;
    /**
     * 凭证有效时间,单位:秒
     */
    @JsonProperty("expires_in")
    private int expiresIn;
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/Code2SessionRespBody.java
New file
@@ -0,0 +1,29 @@
package com.ruoyi.account.wx.body.resp;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
 * @author liheng
 * @ClassName Code2SessionRespBody
 * @Description
 * @date 2021-07-28 12:35
 */
@Data
public class Code2SessionRespBody extends RespBody {
    /**
     * 用户唯一标识
     */
    @JsonProperty("openid")
    private String openid;
    /**
     * 会话密钥
     */
    @JsonProperty("session_key")
    private String sessionKey;
    /**
     * 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。
     */
    @JsonProperty("unionid")
    private String unionid;
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resp/RespBody.java
New file
@@ -0,0 +1,19 @@
package com.ruoyi.account.wx.body.resp;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
 * @author liheng
 * @ClassName RespBody
 * @Description
 * @date 2021-07-28 11:44
 */
@Data
public class RespBody {
    @JsonProperty("errcode")
    private Integer errorCode;
    @JsonProperty("errmsg")
    private String errorMsg;
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/body/resq/Code2SessionResqBody.java
New file
@@ -0,0 +1,21 @@
package com.ruoyi.account.wx.body.resq;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
 * @author liheng
 * @ClassName Code2SessionResqBody
 * @Description
 * @date 2021-07-28 11:47
 */
@Data
public class Code2SessionResqBody {
    @JsonProperty("js_code")
    private String jsCode;
    public Code2SessionResqBody build(String jsCode) {
        this.jsCode = jsCode;
        return this;
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/model/WeixinProperties.java
New file
@@ -0,0 +1,80 @@
package com.ruoyi.account.wx.model;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.stereotype.Component;
/**
 * @author xiaochen
 * @ClassName WeixinProperties
 * @Description
 * @date 2024-08-14 13:55
 */
@ToString
@Component
@ConfigurationProperties(prefix = "wx.conf")
public class WeixinProperties {
    /**
     * 默认开启
     */
    private boolean enabled = true;
    /**
     * 获取 App ID
     *
     * @return App ID
     */
    private String appId;
    /**
     * 获取 Mch ID
     *
     * @return Mch ID
     */
    private String mchId;
    /**
     * 获取 secret ID
     *
     * @return secret ID
     */
    private String secretId;
    public String getSecretId() {
        return secretId;
    }
    public void setSecretId(String secretId) {
        this.secretId = secretId;
    }
    /**
     * HTTP(S) 连接超时时间,单位毫秒
     *
     */
    public int getHttpConnectTimeoutMs() {
        return 6 * 1000;
    }
    /**
     * HTTP(S) 读数据超时时间,单位毫秒
     */
    public int getHttpReadTimeoutMs() {
        return 8 * 1000;
    }
    public String getAppId() {
        return appId;
    }
    public void setAppId(String appId) {
        this.appId = appId;
    }
    public String getMchId() {
        return mchId;
    }
    public void setMchId(String mchId) {
        this.mchId = mchId;
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletPhoneEncrypteData.java
New file
@@ -0,0 +1,19 @@
package com.ruoyi.account.wx.pojo;
import lombok.Data;
/**
 * @author liheng
 * @ClassName AppletUserDecodeData
 * @Description
 * @date 2021-08-13 17:46
 * 小程序加密数据体
 *
 */
@Data
public class AppletPhoneEncrypteData {
    private String encryptedData;
    private String openid;
    private String unionid;
    private String iv;
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserDecodeData.java
New file
@@ -0,0 +1,53 @@
package com.ruoyi.account.wx.pojo;
import lombok.Data;
/**
 * @author liheng
 * @ClassName AppletUserDecodeData
 * @Description
 * @date 2021-08-13 17:46
 * 用户主体信息部分
 * {
 *     "openId": "OPENID",
 *     "nickName": "NICKNAME",
 *     "gender": GENDER,
 *     "city": "CITY",
 *     "province": "PROVINCE",
 *     "country": "COUNTRY",
 *     "avatarUrl": "AVATARURL",
 *     "unionId": "UNIONID",
 *     "watermark":
 *     {
 *         "appid":"APPID",
 *         "timestamp":TIMESTAMP
 *     }
 * }
 * 电话部分
 * {
 *     "phoneNumber": "13580006666",
 *     "purePhoneNumber": "13580006666",
 *     "countryCode": "86",
 *     "watermark":
 *     {
 *         "appid":"APPID",
 *         "timestamp": TIMESTAMP
 *     }
 * }
 *
 */
@Data
public class AppletUserDecodeData {
    private String openId;
    private String unionId;
    private String nickName;
    private int gender;
    private String city;
    private String province;
    private String country;
    private String avatarUrl;
    private Watermark watermark;
    private String phoneNumber;
    private String purePhoneNumber;
    private String countryCode;
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/AppletUserEncrypteData.java
New file
@@ -0,0 +1,17 @@
package com.ruoyi.account.wx.pojo;
import lombok.Data;
/**
 * @author liheng
 * @ClassName AppletUserDecodeData
 * @Description
 * @date 2021-08-13 17:46
 * 小程序加密数据体
 *
 */
@Data
public class AppletUserEncrypteData extends AppletPhoneEncrypteData {
    private String rawData;
    private String signature;
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/pojo/Watermark.java
New file
@@ -0,0 +1,9 @@
package com.ruoyi.account.wx.pojo;
import lombok.Data;
@Data
public class Watermark {
    private String appid;
    private String timestamp;
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/JsonUtils.java
New file
@@ -0,0 +1,110 @@
package com.ruoyi.account.wx.tools;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.hotel.config.JacksonConfig;
import com.hotel.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
 * Json转换工具类
 * 参考:https://blog.csdn.net/weixin_38413579/article/details/82562634
 * @author madman
 */
@Slf4j
public final class JsonUtils {
    private static final ObjectMapper OM = new ObjectMapper();
    private static final JavaTimeModule timeModule = new JavaTimeModule();
    /**
     * 转换LocalDateTime
     */
    static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
        @Override
        public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeString(localDateTime.format(DateTimeFormatter.ofPattern(JacksonConfig.dateTimeFormat)));
        }
    }
    /**
     * 转换LocalDate
     */
    static class LocalDateSerializer extends JsonSerializer<LocalDate> {
        @Override
        public void serialize(LocalDate localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeString(localDate.format(DateTimeFormatter.ofPattern(JacksonConfig.dateFormat)));
        }
    }
    /**
     * 设置 ObjectMapper
     *
     * @return
     */
    private static ObjectMapper getObjectMapper() {
        // 序列化
        timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
        timeModule.addSerializer(LocalDate.class, new LocalDateSerializer());
        // 反序列化
        timeModule.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(JacksonConfig.dateTimeFormat)));
        timeModule.addDeserializer(LocalDate.class,
                new LocalDateDeserializer(DateTimeFormatter.ofPattern(JacksonConfig.dateFormat)));
        // 允许对象忽略json中不存在的属性
        OM.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        OM.registerModule(timeModule);
        return OM;
    }
    /**
     * 将对象序列化
     */
    public static <T> String toJsonString(T obj) {
        try {
            ObjectMapper om = getObjectMapper();
            return om.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            log.error("转json字符串失败:{}", obj);
            return null;
        }
    }
    /**
     * 反序列化对象字符串
     */
    public static <T> T parseObject(String json, Class<T> clazz) {
        try {
            ObjectMapper om = getObjectMapper();
            return om.readValue(json, clazz);
        } catch (JsonProcessingException e) {
            throw new ServiceException("反序列化对象字符串失败");
        }
    }
    /**
     * 反序列化字符串成为对象
     */
    public static <T> T parseObject(String json, TypeReference<T> valueTypeRef) {
        try {
            ObjectMapper om = getObjectMapper();
            return om.readValue(json, valueTypeRef);
        } catch (JsonProcessingException e) {
            throw new ServiceException("反序列化字符串成为对象失败");
        }
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/SHA1.java
New file
@@ -0,0 +1,36 @@
package com.ruoyi.account.wx.tools;
import java.security.MessageDigest;
public class SHA1 {
    /**
     * 用SHA1算法生成安全签名
     *
     * @param str
     * @return
     * @throws WxException
     */
    public static String getSHA1(String str) throws WxException {
        try {
            // SHA1签名生成
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();
            StringBuffer hexstr = new StringBuffer();
            String shaHex;
            for (int i = 0; i < digest.length; i++) {
                shaHex = Integer.toHexString(digest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexstr.append(0);
                }
                hexstr.append(shaHex);
            }
            return hexstr.toString();
        } catch (Exception e) {
            throw new WxException(WxException.ComputeSignatureError);
        }
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WebUtils.java
New file
@@ -0,0 +1,48 @@
package com.ruoyi.account.wx.tools;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
 * @Author liheng
 * @Date 2019/08/26 10:28 AM
 * @Description
 */
public final class WebUtils {
    private WebUtils() {
    }
    /**
     * 当前请求
     */
    public static HttpServletRequest request() {
        return contextHolder() == null ? null : contextHolder().getRequest();
    }
    /**
     * 当前响应
     */
    public static HttpServletResponse response() {
        return contextHolder() == null ? null : contextHolder().getResponse();
    }
    /**
     * 当前session
     */
    public static HttpSession session() {
        return request() == null ? null : request().getSession();
    }
    /**
     * 当前ServletRequest
     */
    public static ServletRequestAttributes contextHolder() {
        return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxAppletTools.java
New file
@@ -0,0 +1,125 @@
package com.ruoyi.account.wx.tools;
import com.ruoyi.account.wx.body.resp.AccessTokenRespBody;
import com.ruoyi.account.wx.body.resp.Code2SessionRespBody;
import com.ruoyi.account.wx.body.resq.Code2SessionResqBody;
import com.ruoyi.account.wx.model.WeixinProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import java.text.MessageFormat;
/**
 * @author xiaochen
 * @ClassName WxAppletTools
 * @Description
 * @date 2024-8-04 13:55
 */
@Slf4j
public class WxAppletTools {
    private final static String ACCESSTOKEN_CACHE_KEY = "accessToken";
    /**
     * 请求参数
     * 属性    类型    默认值    必填    说明
     * appid    string        是    小程序 appId
     * secret    string        是    小程序 appSecret
     * js_code    string        是    登录时获取的 code
     * grant_type    string        是    授权类型,此处只需填写 authorization_cod
     * <p>
     * 返回值:
     * <p>
     * 属性    类型    说明
     * openid    string    用户唯一标识
     * session_key    string    会话密钥
     * unionid    string    用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。
     * errcode    number    错误码
     * errmsg    string    错误信息
     */
    private static final String JSCODE_2_SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
    /**
     * 请求参数
     * 属性    类型    默认值    必填    说明
     * grant_type    string        是    填写 client_credential
     * appid    string        是    小程序唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得。(需要已经成为开发者,且帐号没有异常状态)
     * secret    string        是    小程序唯一凭证密钥,即 AppSecret,获取方式同 appid
     * 返回值
     * Object
     * 返回的 JSON 数据包
     * <p>
     * 属性    类型    说明
     * access_token    string    获取到的凭证
     * expires_in    number    凭证有效时间,单位:秒。目前是7200秒之内的值。
     * errcode    number    错误码
     * errmsg    string    错误信息
     */
    public static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
    private WeixinProperties wxConfig;
    private RestTemplate wxRestTemplate;
    private WxCacheTemplate<String> wxCacheTemplate;
    public WxAppletTools(RestTemplate wxRestTemplate, WeixinProperties wxConfig, WxCaffineCache wxCacheTemplate) {
        this.wxRestTemplate = wxRestTemplate;
        this.wxCacheTemplate = wxCacheTemplate;
        this.wxConfig = wxConfig;
    }
    public WxAppletTools(RestTemplate wxRestTemplate, WeixinProperties wxConfig) {
        this.wxRestTemplate = wxRestTemplate;
        this.wxConfig = wxConfig;
    }
    /**
     * 自定义部分数据
     *
     * @param wxConfig
     * @return
     */
    public WxAppletTools build(WeixinProperties wxConfig) {
        this.wxConfig = wxConfig;
        return this;
    }
    /**
     * @param resqBody
     * @return
     */
    public Code2SessionRespBody getOpenIdByJscode2session(Code2SessionResqBody resqBody) {
        long start = System.currentTimeMillis();
        String requestUrl = MessageFormat.format(JSCODE_2_SESSION_URL, wxConfig.getAppId(), wxConfig.getSecretId(), resqBody.getJsCode());
        long end = System.currentTimeMillis();
        log.info("code换取sessionKey时间:{}", (end - start));
        String respBody = wxRestTemplate.getForEntity(requestUrl, String.class).getBody();
        end = System.currentTimeMillis();
        log.info("code换取sessionKey时间:{}", (end - start));
        log.info("Jscode2session:{}", respBody);
        Code2SessionRespBody code2SessionRespBody = WxJsonUtils.parseObject(respBody, Code2SessionRespBody.class);
        // 判断有误异常
        if (StringUtils.hasLength(code2SessionRespBody.getErrorMsg())) {
            // 抛出错误
            throw new WxException(code2SessionRespBody.getErrorCode() + ":" + code2SessionRespBody.getErrorMsg());
        }
        return code2SessionRespBody;
    }
    /**
     * @return
     */
    public String getAccessToken(String version) {
        String accessToken = wxCacheTemplate.getKey(ACCESSTOKEN_CACHE_KEY + version);
        if (StringUtils.hasLength(accessToken)) {
            return accessToken;
        }
        String requestUrl = MessageFormat.format(ACCESS_TOKEN_URL, wxConfig.getAppId(), wxConfig.getSecretId());
        String respBody = wxRestTemplate.getForEntity(requestUrl, String.class).getBody();
        AccessTokenRespBody accessTokenRespBody = WxJsonUtils.parseObject(respBody, AccessTokenRespBody.class);
        // 判断有误异常
        if (StringUtils.hasLength(accessTokenRespBody.getErrorMsg())) {
            // 抛出错误
            throw new WxException(accessTokenRespBody.getErrorCode() + ":" + accessTokenRespBody.getErrorMsg());
        }
        wxCacheTemplate.setKey(ACCESSTOKEN_CACHE_KEY + version, accessTokenRespBody.getAccessToken());
        return accessTokenRespBody.getAccessToken();
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCache.java
New file
@@ -0,0 +1,117 @@
package com.ruoyi.account.wx.tools;
import java.util.concurrent.TimeUnit;
/**
 * 缓存
 *
 * @author liheng
 */
class WxCache {
    /**
     * 缓存的初始化容量
     */
    private int initialCapacity = 50;
    /**
     * 缓存最大容量
     */
    private long maximumSize = 200L;
    /**
     * 缓存时长
     */
    private long duration = 7000L;
    /**
     * 时长单位,自动转换
     * 支持:
     * 时
     * 分
     * 秒
     * 天
     */
    private TimeUnit timeunit = TimeUnit.SECONDS;
    public int getInitialCapacity() {
        return initialCapacity;
    }
    public void setInitialCapacity(int initialCapacity) {
        this.initialCapacity = initialCapacity;
    }
    public long getMaximumSize() {
        return maximumSize;
    }
    public void setMaximumSize(long maximumSize) {
        this.maximumSize = maximumSize;
    }
    public long getDuration() {
        return duration;
    }
    public void setDuration(long duration) {
        this.duration = duration;
    }
    public TimeUnit getTimeunit() {
        return timeunit;
    }
    public void setTimeunit(TimeUnit timeunit) {
        this.timeunit = timeunit;
    }
    public static class Builder {
        private int initialCapacity;
        private long maximumSize;
        private long duration;
        private TimeUnit timeunit;
        public Builder setInitialCapacity(int initialCapacity) {
            this.initialCapacity = initialCapacity;
            return this;
        }
        public Builder setMaximumSize(long maximumSize) {
            this.maximumSize = maximumSize;
            return this;
        }
        public Builder setDuration(long duration) {
            this.duration = duration;
            return this;
        }
        public Builder setTimeUnit(TimeUnit timeunit) {
            this.timeunit = timeunit;
            return this;
        }
        public WxCache build() {
            return new WxCache(this);
        }
    }
    public static Builder options() {
        return new Builder();
    }
    private WxCache(Builder builder) {
        this.initialCapacity = 0 == builder.initialCapacity ? this.initialCapacity : builder.initialCapacity;
        this.maximumSize = 0L == builder.maximumSize ? this.maximumSize : builder.maximumSize;
        this.duration = 0L == builder.duration ? this.duration : builder.duration;
        this.timeunit = null == builder.timeunit ? this.timeunit : builder.timeunit;
    }
    @Override
    public String toString() {
        return "WxCache{" +
                "initialCapacity=" + initialCapacity +
                ", maximumSize=" + maximumSize +
                ", duration=" + duration +
                ", timeunit=" + timeunit +
                '}';
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCacheTemplate.java
New file
@@ -0,0 +1,34 @@
package com.ruoyi.account.wx.tools;
/**
 * @author liheng
 * @ClassName WxCacheTemplate
 * @Description
 * @date 2021-01-11 11:27
 */
public interface WxCacheTemplate<T> {
    /**
     * 保存key
     *
     * @param key
     * @param value
     * @return
     */
     boolean setKey(String key, T value);
    /**
     * 获取缓存
     *
     * @param key
     * @return
     */
    T getKey(String key);
    /**
     * 删除
     *
     * @param key
     * @return
     */
    boolean delKey(String key);
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxCaffineCache.java
New file
@@ -0,0 +1,122 @@
package com.ruoyi.account.wx.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
 * @author liheng
 * @ClassName AbstractCaffineCache
 * @Description
 * @date 2021-01-11 11:27
 */
@Slf4j
class WxCaffineCache<T>  implements WxCacheTemplate<T> {
    /**
     * 缓存环境
     */
    private String env = "wx";
    /**
     * 本地缓存实例
     */
    private LoadingCache<String, Object> loadingCache;
    /**
     * 构造函数
     *
     */
    public WxCaffineCache() {
        WxCache cache = WxCache.options().setTimeUnit(TimeUnit.SECONDS).build();
        // 构建本地缓存实例
        this.loadingCache = caffineCacheManage(cache);
    }
    @Override
    public boolean setKey(String key, T value) {
        if (Objects.isNull(this.loadingCache)) {
            return Boolean.FALSE;
        }
        if (StringUtils.hasLength(this.env)) {
            this.loadingCache.put(this.env + ":" + key, value);
        } else {
            this.loadingCache.put(key, value);
        }
        return Boolean.TRUE;
    }
    @Override
    public T getKey(String key) {
        if (Objects.isNull(this.loadingCache)) {
            return null;
        }
        try {
            if (StringUtils.hasLength(this.env)) {
                return (T) this.loadingCache.get(this.env + ":" + key);
            } else {
                return (T) this.loadingCache.get(key);
            }
        } catch (Exception e) {
            return null;
        }
    }
    @Override
    public boolean delKey(String key) {
        if (Objects.isNull(this.loadingCache)) {
            return Boolean.FALSE;
        }
        if (StringUtils.hasLength(this.env)) {
            this.loadingCache.invalidate(this.env + ":" + key);
        } else {
            this.loadingCache.invalidate(key);
        }
        return Boolean.TRUE;
    }
    /**
     * 缓存管理
     *
     * @param cache
     * @param <T>
     * @return
     */
    private static <T> LoadingCache<String, T> caffineCacheManage(WxCache cache) {
        log.info("初始化缓存的实体数据:{}", cache);
        if (Objects.isNull(cache)) {
            throw new NullPointerException("请实例化一个Cache对象!");
        }
        LoadingCache<String, T> localcache =
                // 构建本地缓存,调用链的方式
                // ,1000是设置缓存的初始化容量,maximumSize是设置缓存最大容量,当超过了最大容量,guava将使用LRU算法(最少使用算法),来移除缓存项
                // expireAfterAccess(12,TimeUnit.HOURS)设置缓存有效期为12个小时
                Caffeine.newBuilder().initialCapacity(cache.getInitialCapacity()).maximumSize(cache.getMaximumSize())
                        // 设置写缓存后n秒钟过期
                        // .expireAfterWrite(30, TimeUnit.SECONDS)
                        .expireAfterWrite(cache.getDuration(), cache.getTimeunit())
                        // 设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite
                        //.expireAfterAccess(googleCache.getDuration(), googleCache.getTimeunit())
                        // 只阻塞当前数据加载线程,其他线程返回旧值
                        //.refreshAfterWrite(10, TimeUnit.SECONDS)
                        // 设置缓存的移除通知//用户手动移除EXPLICIT,
                        // //用户手动替换REPLACED,//被垃圾回收COLLECTED,//超时过期EXPIRED,//SIZE由于缓存大小限制
                        .removalListener(new RemovalListener<String, T>() {
                            @Override
                            public void onRemoval(String key, Object value, RemovalCause cause) {
                                log.info(key + ":" + value + ":" + cause.name());
                            }
                        })
                        // build里面要实现一个匿名抽象类
                        .build(new CacheLoader<String, T>() {
                            // 这个方法是默认的数据加载实现,get的时候,如果key没有对应的值,就调用这个方法进行加载。此处是没有默认值则返回null
                            @Override
                            public T load(String key) throws Exception {
                                return null;
                            }
                        });
        return localcache;
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxException.java
New file
@@ -0,0 +1,55 @@
package com.ruoyi.account.wx.tools;
/**
 * @author lihen
 */
public class WxException extends RuntimeException {
    private final static int OK = 0;
    private final static int ValidateSignatureError = -40001;
    private final static int ParseXmlError = -40002;
    public final static int ComputeSignatureError = -40003;
    private final static int IllegalAesKey = -40004;
    private final static int ValidateAppidError = -40005;
    private final static int EncryptAESError = -40006;
    private final static int DecryptAESError = -40007;
    private final static int IllegalBuffer = -40008;
    private int code;
    private static String getMessage(int code) {
        switch (code) {
            case ValidateSignatureError:
                return "签名验证错误";
            case ParseXmlError:
                return "xml解析失败";
            case ComputeSignatureError:
                return "sha加密生成签名失败";
            case IllegalAesKey:
                return "SymmetricKey非法";
            case ValidateAppidError:
                return "appid校验失败";
            case EncryptAESError:
                return "aes加密失败";
            case DecryptAESError:
                return "aes解密失败";
            case IllegalBuffer:
                return "解密后得到的buffer非法";
            default:
                return null;
        }
    }
    public int getCode() {
        return code;
    }
    WxException(int code) {
        super(getMessage(code));
        this.code = code;
    }
    public WxException(String message) {
        super(message);
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/wx/tools/WxJsonUtils.java
New file
@@ -0,0 +1,109 @@
package com.ruoyi.account.wx.tools;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
 * Json转换工具类
 * 参考:https://blog.csdn.net/weixin_38413579/article/details/82562634
 * @author madman
 */
@Slf4j
public final class WxJsonUtils {
    public static final String dateFormat = "yyyy-MM-dd";
    public static final String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
    private static final ObjectMapper OM = new ObjectMapper();
    private static final JavaTimeModule timeModule = new JavaTimeModule();
    /**
     * 转换LocalDateTime
     */
    static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
        @Override
        public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeString(localDateTime.format(DateTimeFormatter.ofPattern(dateTimeFormat)));
        }
    }
    /**
     * 转换LocalDate
     */
    static class LocalDateSerializer extends JsonSerializer<LocalDate> {
        @Override
        public void serialize(LocalDate localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeString(localDate.format(DateTimeFormatter.ofPattern(dateFormat)));
        }
    }
    /**
     * 设置 ObjectMapper
     *
     * @return
     */
    private static ObjectMapper getObjectMapper() {
        // 序列化
        timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
        timeModule.addSerializer(LocalDate.class, new LocalDateSerializer());
        // 反序列化
        timeModule.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(dateTimeFormat)));
        timeModule.addDeserializer(LocalDate.class,
                new LocalDateDeserializer(DateTimeFormatter.ofPattern(dateFormat)));
        // 允许对象忽略json中不存在的属性
        OM.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        OM.registerModule(timeModule);
        return OM;
    }
    /**
     * 将对象序列化
     */
    public static <T> String toJsonString(T obj) {
        try {
            ObjectMapper om = getObjectMapper();
            return om.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            log.error("转json字符串失败:{}", obj);
            return null;
        }
    }
    /**
     * 反序列化对象字符串
     */
    public static <T> T parseObject(String json, Class<T> clazz) {
        try {
            ObjectMapper om = getObjectMapper();
            return om.readValue(json, clazz);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("反序列化对象字符串失败");
        }
    }
    /**
     * 反序列化字符串成为对象
     */
    public static <T> T parseObject(String json, TypeReference<T> valueTypeRef) {
        try {
            ObjectMapper om = getObjectMapper();
            return om.readValue(json, valueTypeRef);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("反序列化字符串成为对象失败");
        }
    }
}
ruoyi-service/ruoyi-payment/pom.xml
@@ -100,7 +100,13 @@
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- wx sdk -->
        <!-- https://mvnrepository.com/artifact/com.github.wechatpay-apiv3/wechatpay-apache-httpclient -->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.3</version>
        </dependency>
    </dependencies>
    <build>
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/config/WxConfig.java
New file
@@ -0,0 +1,33 @@
package com.ruoyi.payment.wx.config;
import com.ruoyi.payment.wx.model.WeixinProperties;
import com.ruoyi.payment.wx.utils.WxV3Pay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * 项目中需继承此类
 *
 * @author lihen
 */
@ConditionalOnProperty(name = "wx.conf.enabled")
@Configuration
public class WxConfig {
    private final WeixinProperties weixinProperties;
    @Autowired
    public WxConfig(WeixinProperties weixinProperties) {
        this.weixinProperties = weixinProperties;
    }
    @Bean
    @ConditionalOnMissingBean(name = "wxV3Pay")
    public WxV3Pay wxSpV3Pay() {
        return new WxV3Pay(weixinProperties);
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/controller/WxPayController.java
New file
@@ -0,0 +1,142 @@
package com.ruoyi.payment.wx.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.payment.wx.enums.RefundEnum;
import com.ruoyi.payment.wx.model.WxPaymentRefundModel;
import com.ruoyi.payment.wx.utils.WxV3Pay;
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.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
/**
 * 微信相关接口
 */
@Slf4j
@RestController
@CrossOrigin
@RequestMapping("/wx/")
@Api(tags = {"微信支付相关接口"})
public class WxPayController {
    @Autowired
    private WxV3Pay wxV3Pay;
    /**
     * 按实际修改
     */
    @PostMapping("order")
    @ApiOperation("订单支付")
    public AjaxResult<Map<String, Object>> orderPay(@RequestParam Long orderId) {
        // 查询订单
        // 0元订单不走支付
        // 价格
        Integer totalPrice = 0;
        // 生成订单号
        String orderNo = "";
        // 查询用户信息 用户openid
        String openId = "";
        // 订单做修改
        // 调用支付方法
        Map<String, Object> result = wxV3Pay.jsApi(orderNo, totalPrice, openId,"");
        log.info("支付参数:{}", result);
        return AjaxResult.ok(result);
    }
    /**
     * 微信v3支付-订单退款
     *
     * @return
     */
    @ApiOperation("订单退款")
    @PostMapping(value = "refund-order")
    public AjaxResult<String> refundOrder() {
         Map<String, Object> result = wxV3Pay.refund(new WxPaymentRefundModel());
         log.info("退款结果:{}", result);
        // 微信支付退款单号
        String refund_id = result.get("refund_id").toString();
        // 商户退款单号
        String out_refund_no = result.get("out_refund_no").toString();
        // 微信支付订单号
        String transaction_id = result.get("transaction_id").toString();
        // 商户订单号 tradeNo
        String out_trade_no = result.get("out_trade_no").toString();
        // 退款成功时间
        String success_time = Objects.nonNull(result.get("success_time")) ? result.get("success_time").toString() : null;
        // 退款状态 RefundEnum
        String status = result.get("status").toString();
        // TODO 退款业务处理
        return AjaxResult.success();
    }
    /**
     * 支付回调
     */
    @PostMapping("pay/notify")
    public void payNotify(HttpServletRequest request) throws IOException {
        try {
            Map<String, Object> params = wxV3Pay.verifyNotify(request, new TypeReference<Map<String, Object>>() {
            });
            log.info("支付回调:{}", params);
            // 商户订单号
            String tradeNo = params.get("out_trade_no").toString();
            // 交易状态
            String trade_state = params.get("trade_state").toString();
            // 交易状态描述
            String trade_state_desc = params.get("trade_state_desc").toString();
            // 微信支付订单号
            String transaction_id = params.get("transaction_id").toString();
            // 支付完成时间
            // 时间不对的话,可以调用  WxTimeUtils.toRfc3339Date(success_time)转换一下
            String success_time = params.get("success_time").toString();
            // 附加数据
            Integer attach = Integer.parseInt(params.get("attach").toString());
            //  TODO 业务处理
        } catch (Exception e) {
            log.error("支付回调异常:{}", e, e);
            wxV3Pay.ack(false, e.getMessage());
        }
    }
    /**
     * 退款回调
     */
    @PostMapping("refund/notify")
    public void refundNotify(HttpServletRequest request) throws IOException {
        try {
            Map<String, Object> params = wxV3Pay.verifyNotify(request, new TypeReference<Map<String, Object>>() {
            });
            // 商户订单号
            String tradeNo = params.get("out_trade_no").toString();
            // 商户退款单号
            String out_refund_no = params.get("out_refund_no").toString();
            // 微信支付订单号
            String transaction_id = params.get("transaction_id").toString();
            // 微信支付退款单号
            String refund_id = params.get("refund_id").toString();
            // 退款状态
            String tradeState = params.get("refund_status").toString();
            // 退款成功时间
            // 时间不对的话,可以调用  WxTimeUtils.toRfc3339Date(success_time)转换一下
            String success_time = params.get("success_time").toString();
            if (tradeState.equals(RefundEnum.SUCCESS.name())) {
                // TODO 退款成功处理
                wxV3Pay.ack();
            } else {
                wxV3Pay.ack(false, "不是成功的退款状态");
            }
        } catch (Exception e) {
            wxV3Pay.ack(false, e.getMessage());
        }
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/RefundEnum.java
New file
@@ -0,0 +1,53 @@
package com.ruoyi.payment.wx.enums;
import lombok.Getter;
import java.util.stream.Stream;
/**
 * @author xiaochen
 * @ClassName ProfitSharingEnum
 * @Description
 * @date 2021-11-21 11:15
 */
public enum RefundEnum {
    /**
     * 退款成功
     */
    SUCCESS("SUCCESS", "退款成功"),
    /**
     * 退款关闭
     */
    CLOSED("CLOSED", "退款关闭"),
    /**
     * 退款处理中
     */
    PROCESSING("PROCESSING", "退款处理中"),
    /**
     * 退款异常
     */
    ABNORMAL("ABNORMAL", "退款异常"),
    ;
    @Getter
    private String code;
    @Getter
    private String desc;
    RefundEnum(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }
    /**
     * 通过交易类型执行具体的交易方法
     *
     * @param code
     * @return
     */
    public static RefundEnum fromValue(String code) {
        return Stream.of(RefundEnum.values()).filter(fileType ->
                fileType.getCode().toLowerCase().equals(code.toLowerCase())
        ).findFirst().orElse(null);
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/enums/TradeStateEnum.java
New file
@@ -0,0 +1,41 @@
package com.ruoyi.payment.wx.enums;
import lombok.Getter;
import java.util.stream.Stream;
/**
 * @author xiaochen
 * @ClassName AliTradeStateEnum
 * @Description
 * @date 2022-01-07 11:56
 */
public enum TradeStateEnum {
    SUCCESS("支付成功"),
    RETURN("已分账回退"),
    FAIL("已失败"),
    PROCESSING("处理中"),
    FINISHED("分账完成"),
    REFUND("转入退款"),
    PAYERROR("支付失败(其他原因,如银行返回失败)"),
    USERPAYING("用户支付中"),
    CLOSED("已关闭"),
    NOTPAY("未支付"),
    UNKNOWN("未知"),
    DONE("服务订单完成"),
    // ...
    ;
    @Getter
    private String desc;
    TradeStateEnum(String desc) {
        this.desc = desc;
    }
    public static TradeStateEnum tradeState(String code) {
        return Stream.of(TradeStateEnum.values()).filter(fileType ->
                fileType.name().equals(code)
        ).findFirst().orElse(UNKNOWN);
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/exception/WxException.java
New file
@@ -0,0 +1,55 @@
package com.ruoyi.payment.wx.exception;
/**
 * @author lihen
 */
public class WxException extends RuntimeException {
    private final static int OK = 0;
    private final static int ValidateSignatureError = -40001;
    private final static int ParseXmlError = -40002;
    public final static int ComputeSignatureError = -40003;
    private final static int IllegalAesKey = -40004;
    private final static int ValidateAppidError = -40005;
    private final static int EncryptAESError = -40006;
    private final static int DecryptAESError = -40007;
    private final static int IllegalBuffer = -40008;
    private int code;
    private static String getMessage(int code) {
        switch (code) {
            case ValidateSignatureError:
                return "签名验证错误";
            case ParseXmlError:
                return "xml解析失败";
            case ComputeSignatureError:
                return "sha加密生成签名失败";
            case IllegalAesKey:
                return "SymmetricKey非法";
            case ValidateAppidError:
                return "appid校验失败";
            case EncryptAESError:
                return "aes加密失败";
            case DecryptAESError:
                return "aes解密失败";
            case IllegalBuffer:
                return "解密后得到的buffer非法";
            default:
                return null;
        }
    }
    public int getCode() {
        return code;
    }
    public WxException(int code) {
        super(getMessage(code));
        this.code = code;
    }
    public WxException(String message) {
        super(message);
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/V3.java
New file
@@ -0,0 +1,71 @@
package com.ruoyi.payment.wx.model;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.util.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
 * @author xiaochen
 * @ClassName V3
 * @Description
 */
@Slf4j
@Data
public class V3 {
    /**
     * 获取 API 密钥
     *
     * @return API密钥
     */
    private String apiKey;
    /**
     * 秘钥路径,apiclient_key.pem
     */
    private String privateKeyPath;
    /**
     * 商户证书序列号
     */
    private String  mchSerialNo;
    /**
     * 支付回调地址
     *
     * @return
     */
    private String notifyPayUrl;
    /**
     * 退款回调地址
     *
     * @return
     */
    private String notifyRefundUrl;
    /**
     * 退款回调地址
     */
    private String notifyTravelRefundUrl;
    public InputStream getPrivateKeyStream() {
        // 需要证书释放
        byte[] certData;
        InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(this.privateKeyPath);
        try {
            certData = IOUtils.toByteArray(certStream);
        } catch (IOException e) {
            throw new RuntimeException("私钥文件未找到");
        }finally {
            try {
                certStream.close();
            } catch (IOException e) {
                log.error("私钥流关闭异常");
            }
        }
        return new ByteArrayInputStream(certData);
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WeixinProperties.java
New file
@@ -0,0 +1,101 @@
package com.ruoyi.payment.wx.model;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.stereotype.Component;
/**
 * @author xiaochen
 * @ClassName WeixinProperties
 * @Description
 */
@ToString
@Component
@ConfigurationProperties(prefix = "wx.conf")
public class WeixinProperties {
    /**
     * 默认开启
     */
    private boolean enabled = true;
    /**
     * 获取 App ID
     *
     * @return App ID
     */
    private String appId;
    /**
     * 获取 Mch ID
     *
     * @return Mch ID
     */
    private String mchId;
    /**
     * 获取 secret ID
     *
     * @return secret ID
     */
    private String secretId;
    public String getSecretId() {
        return secretId;
    }
    public void setSecretId(String secretId) {
        this.secretId = secretId;
    }
    /**
     * v3
     */
    @NestedConfigurationProperty
    private V3 v3;
    public boolean isEnabled() {
        return enabled;
    }
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
    public V3 getV3() {
        return v3;
    }
    public void setV3(V3 v3) {
        this.v3 = v3;
    }
    /**
     * HTTP(S) 连接超时时间,单位毫秒
     *
     */
    public int getHttpConnectTimeoutMs() {
        return 6 * 1000;
    }
    /**
     * HTTP(S) 读数据超时时间,单位毫秒
     */
    public int getHttpReadTimeoutMs() {
        return 8 * 1000;
    }
    public String getAppId() {
        return appId;
    }
    public void setAppId(String appId) {
        this.appId = appId;
    }
    public String getMchId() {
        return mchId;
    }
    public void setMchId(String mchId) {
        this.mchId = mchId;
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentInfoModel.java
New file
@@ -0,0 +1,201 @@
package com.ruoyi.payment.wx.model;
import lombok.*;
import java.util.List;
/**
 * @author xiaochen
 * @ClassName WxPaymentInfoModel
 * @Description
 */
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class WxPaymentInfoModel {
    /**
     * 合单商户appid
     */
    private String combine_appid;
    /**
     * 合单商户号
     */
    private String combine_mchid;
    /**
     * 合单商户订单号
     */
    private String combine_out_trade_no;
    /**
     * 合单--子单信息
     */
    private List<SubOrders> sub_orders;
    /**
     * 合单--支付者
     */
    private CombinePayerInfo combine_payer_info;
    private String appid;
    private String sp_appid;
    private String mchid;
    private String sp_mchid;
    private String sub_appid;
    private String sub_mchid;
    private String description;
    private String out_trade_no;
    private String time_expire;
    private String attach;
    private String notify_url;
    private String goods_tag;
    private SettleInfo settle_info;
    private Amount amount;
    private Payer payer;
    private Detail detail;
    private SceneInfo scene_info;
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class SettleInfo {
        private Boolean profit_sharing;
        private Integer subsidy_amount;
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class Amount {
        private Integer total;
        /**
         * 合单支付时需要
         */
        private Integer total_amount;
        @Builder.Default
        private String currency = "CNY";
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class Payer {
        private String openid;
        private String sp_openid;
        private String sub_openid;
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class Detail {
        private int cost_price;
        private String invoice_id;
        private List<GoodsDetail> goods_detail;
        @Builder
        @AllArgsConstructor
        @NoArgsConstructor
        @Getter
        @Setter
        @ToString
        public static class GoodsDetail {
            private String merchant_goods_id;
            private String wechatpay_goods_id;
            private String goods_name;
            private int quantity;
            private int unit_price;
        }
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class SceneInfo {
        private String payer_client_ip;
        private String device_id;
        private StoreInfo store_info;
        private H5Info h5_info;
        @Builder
        @AllArgsConstructor
        @NoArgsConstructor
        @Getter
        @Setter
        @ToString
        public static class StoreInfo {
            private String id;
            private String name;
            private String area_code;
            private String address;
        }
        @Builder
        @AllArgsConstructor
        @NoArgsConstructor
        @Getter
        @Setter
        @ToString
        public static class H5Info {
            private String type;
            private String app_name;
            private String app_url;
            private String bundle_id;
            private String package_name;
        }
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class SubOrders {
        private String out_trade_no;
        private Amount amount;
        private String mchid;
        private String sub_mchid;
        private String attach;
        private String description;
        private String goods_tag;
        private SettleInfo settle_info;
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class CombinePayerInfo {
        private String openid;
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/model/WxPaymentRefundModel.java
New file
@@ -0,0 +1,84 @@
package com.ruoyi.payment.wx.model;
import lombok.*;
import java.util.List;
/**
 * @author xiaochen
 * @ClassName WxPaymentRefundModel
 * @Description
 */
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class WxPaymentRefundModel {
    /**
     * 子商户,二级商户号
     */
    private String sub_mchid;
    /**
     * 电商平台APPID
     */
    private String sp_appid;
    private String transaction_id;
    private String out_trade_no;
    /**
     * 商户退款单号
     */
    private String out_refund_no;
    /**
     * 退款原因
     */
    private String reason;
    private String notify_url;
    /**
     * 资金账户,否
     */
    private String funds_account;
    /**
     * 退款金额信息
     */
    private RefundAmount amount;
    /**
     * 退款商品
     */
    private List<RefundGoodsDetail> goods_detail;
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class RefundAmount {
        /**
         * 原订单金额
         */
        private int total;
        @Builder.Default
        private String currency = "CNY";
        /**
         * 退款金额
         */
        private int refund;
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class RefundGoodsDetail {
        private String merchant_goods_id;
        private String wechatpay_goods_id;
        private String goods_name;
        private int unit_price;
        private int refund_amount;
        private int refund_quantity;
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/AppletUserDecodeData.java
New file
@@ -0,0 +1,52 @@
package com.ruoyi.payment.wx.pojo;
import lombok.Data;
/**
 * @author xiaochen
 * @ClassName AppletUserDecodeData
 * @Description
 * 用户主体信息部分
 * {
 *     "openId": "OPENID",
 *     "nickName": "NICKNAME",
 *     "gender": GENDER,
 *     "city": "CITY",
 *     "province": "PROVINCE",
 *     "country": "COUNTRY",
 *     "avatarUrl": "AVATARURL",
 *     "unionId": "UNIONID",
 *     "watermark":
 *     {
 *         "appid":"APPID",
 *         "timestamp":TIMESTAMP
 *     }
 * }
 * 电话部分
 * {
 *     "phoneNumber": "13580006666",
 *     "purePhoneNumber": "13580006666",
 *     "countryCode": "86",
 *     "watermark":
 *     {
 *         "appid":"APPID",
 *         "timestamp": TIMESTAMP
 *     }
 * }
 *
 */
@Data
public class AppletUserDecodeData {
    private String openId;
    private String unionId;
    private String nickName;
    private int gender;
    private String city;
    private String province;
    private String country;
    private String avatarUrl;
    private Watermark watermark;
    private String phoneNumber;
    private String purePhoneNumber;
    private String countryCode;
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/pojo/Watermark.java
New file
@@ -0,0 +1,9 @@
package com.ruoyi.payment.wx.pojo;
import lombok.Data;
@Data
public class Watermark {
    private String appid;
    private String timestamp;
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/resp/NotifyV3PayDecodeRespBody.java
New file
@@ -0,0 +1,222 @@
package com.ruoyi.payment.wx.resp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
 * @author xiaochen
 * @ClassName FacilV3PayNotifyRespBody
 * @Description
 */
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class NotifyV3PayDecodeRespBody implements Serializable {
    // 合单--开始
    private String combine_appid;
    private String combine_mchid;
    private String combine_out_trade_no;
    private List<SubOrders> sub_orders;
    // 合单--结束
    /**
     * 服务商应用ID
     */
    private String sp_appid;
    /**
     * 服务商户号
     */
    private String sp_mchid;
    /**
     * 商户号
     */
    private String mchid;
    /**
     * 子商户应用ID
     */
    private String sub_appid;
    /**
     * 子商户号
     */
    private String sub_mchid;
    /**
     * 商户订单号
     */
    private String out_trade_no;
    /**
     * 交易状态描述
     */
    private String trade_state_desc;
    /**
     * 交易类型,枚举值:
     * JSAPI:公众号支付
     * NATIVE:扫码支付
     * APP:APP支付
     * MICROPAY:付款码支付
     * MWEB:H5支付
     * FACEPAY:刷脸支付
     */
    private String trade_type;
    /**
     * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
     */
    private String attach;
    /**
     * 微信支付订单号
     */
    private String transaction_id;
    /**
     * 交易状态,枚举值:
     * SUCCESS:支付成功
     * REFUND:转入退款
     * NOTPAY:未支付
     * CLOSED:已关闭
     * REVOKED:已撤销(付款码支付)
     * USERPAYING:用户支付中(付款码支付)
     * PAYERROR:支付失败(其他原因,如银行返回失败)
     */
    private String trade_state;
    /**
     * 银行类型,采用字符串类型的银行标识。银行标识请参考《银行类型对照表》
     * https://pay.weixin.qq.com/wiki/doc/apiv3_partner/terms_definition/chapter1_1_3.shtml#part-6
     */
    private String bank_type;
    /**
     * 支付完成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,
     * YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,
     * TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。
     * 例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
     * 示例值:2018-06-08T10:34:56+08:00
     */
    private String success_time;
    /**
     * 支付者信息
     */
    private Payer payer;
    /**
     * 支付者
     */
    private Payer combine_payer_info;
    /**
     * 订单金额信息
     */
    private Amount amount;
    /**
     * 场景信息
     */
    private SceneInfo scene_info;
    /**
     * 优惠功能,享受优惠时返回该字段
     */
    private List<PromotionDetail> promotion_detail;
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class Amount implements Serializable{
        /**
         * 用户支付金额
         */
        private int payer_total;
        /**
         * 总金额
         */
        private int total;
        /**
         * 标价金额
         */
        private int total_amount;
        /**
         * 现金支付金额
         */
        private int payer_amount;
        private String currency;
        private String payer_currency;
    }
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class GoodsDetail implements Serializable{
        private String goods_id;
        private int quantity;
        private int unit_price;
        private int discount_amount;
        private String goods_remark;
    }
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class Payer implements Serializable{
        private String openid;
        private String sp_openid;
        private String sub_openid;
    }
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class PromotionDetail implements Serializable{
        private String coupon_id;
        private String name;
        private String scope;
        private String type;
        private int amount;
        private String stock_id;
        private int wechatpay_contribute;
        private int merchant_contribute;
        private int other_contribute;
        private String currency;
        private List<GoodsDetail> goods_detail;
    }
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class SceneInfo implements Serializable{
        /**
         * 商户端设备号
         */
        private String device_id;
    }
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class SubOrders implements Serializable{
        private String mchid;
        private String trade_type;
        private String trade_state;
        private String trade_state_desc;
        private String bank_type;
        private String attach;
        private String success_time;
        private String transaction_id;
        private String out_trade_no;
        private String sub_mchid;
        private Amount amount;
        /**
         * 优惠功能,享受优惠时返回该字段
         */
        private List<PromotionDetail> promotion_detail;
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/SHA1.java
New file
@@ -0,0 +1,39 @@
package com.ruoyi.payment.wx.utils;
import com.ruoyi.payment.wx.exception.WxException;
import java.security.MessageDigest;
public class SHA1 {
    /**
     * 用SHA1算法生成安全签名
     *
     * @param str
     * @return
     * @throws WxException
     */
    public static String getSHA1(String str) throws WxException {
        try {
            // SHA1签名生成
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();
            StringBuffer hexstr = new StringBuffer();
            String shaHex;
            for (int i = 0; i < digest.length; i++) {
                shaHex = Integer.toHexString(digest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexstr.append(0);
                }
                hexstr.append(shaHex);
            }
            return hexstr.toString();
        } catch (Exception e) {
            throw new WxException(WxException.ComputeSignatureError);
        }
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxAbstractPay.java
New file
@@ -0,0 +1,333 @@
package com.ruoyi.payment.wx.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.ruoyi.common.core.utils.WebUtils;
import com.ruoyi.payment.wx.model.WxPaymentInfoModel;
import com.ruoyi.payment.wx.model.WxPaymentRefundModel;
import com.ruoyi.payment.wx.resp.NotifyV3PayDecodeRespBody;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
/**
 * @author xiaochen
 * @ClassName WxWifiV3Pay
 * @Description
 * @date 2021-11-13 21:10
 */
@Slf4j
public abstract class WxAbstractPay {
    /**
     * 请求成功相应码
     */
    private static final int STATUS_CODE = 200;
    /**
     * 请求成功相应码
     */
    private static final int OTHER_STATUS_CODE = 204;
    /**
     * 请求根地址
     */
    private static final String HOST = "https://api.mch.weixin.qq.com";
    private static RuntimeException parameterError(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("parameter error: " + message);
    }
    /**
     * 封装基础数据
     *
     * @param requestBody
     * @param notifyUrl
     * @return
     */
    protected String buildBaseParam(WxPaymentInfoModel requestBody, String notifyUrl) {
        // 封装基础数据
        requestBody.setNotify_url(notifyUrl);
        String reqBody = WxJsonUtils.toJsonString(requestBody);
        return reqBody;
    }
    /**
     * 微信调起支付参数
     * 返回参数如有不理解 请访问微信官方文档
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
     *
     * @param prepayId 微信下单返回的prepay_id
     * @param appId    应用ID(appid)
     * @return 当前调起支付所需的参数
     * @throws Exception
     */
    protected Map<String, Object> wxTuneUp(PrivateKeySigner privateKeySigner, String appId, String prepayId) {
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonceStr = WxUtils.generateNonceStr();
        String packageStr = "prepay_id=" + prepayId;
        //加载签名
        String signStr = Stream.of(appId, timeStamp, nonceStr, packageStr).collect(Collectors.joining("\n", "", "\n"));
        String packageSign = privateKeySigner.sign(signStr.getBytes(StandardCharsets.UTF_8)).getSign();
        Map<String, Object> map = new HashMap<>(6);
        map.put("appId", appId);
        map.put("timeStamp", timeStamp);
        map.put("nonceStr", nonceStr);
        map.put("package", packageStr);
        map.put("signType", "RSA");
        map.put("paySign", packageSign);
        return map;
    }
    /**
     * 构建方法请求
     *
     * @param uri
     * @param socketTimeout
     * @param connectTimeout
     * @return
     */
    protected HttpGet requestGet(String uri, int socketTimeout, int connectTimeout) {
        //请求URL
        HttpGet httpGet = new HttpGet(HOST + uri);
        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
                .setConnectTimeout(connectTimeout).build();
        httpGet.setConfig(requestConfig);
        httpGet.setHeader("Content-type", "application/json");
        httpGet.setHeader("Accept", "application/json");
        return httpGet;
    }
    /**
     * 构建方法请求
     *
     * @param uri
     * @param socketTimeout
     * @param connectTimeout
     * @param reqdata
     * @return
     */
    protected HttpPost requestPost(String uri, int socketTimeout, int connectTimeout, String reqdata) {
        //请求URL
        HttpPost httpPost = new HttpPost(HOST + uri);
        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
                .setConnectTimeout(connectTimeout).build();
        httpPost.setConfig(requestConfig);
        StringEntity entity = new StringEntity(reqdata, StandardCharsets.UTF_8);
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");
        return httpPost;
    }
    public abstract <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception;
    /**
     * 接收回调
     *
     * @param request
     * @return
     * @throws Exception
     */
    public <T> T verifyNotify(HttpServletRequest request, Verifier verifier, String apiKey, TypeReference<T> valueTypeRef) throws Exception {
        String body = WxUtils.streamBodyByReceive(request);
        String requestId = request.getHeader(REQUEST_ID);
        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
        String value;
        // 验签必须参数检验
        for (String headerName : headers) {
            value = request.getHeader(headerName);
            if (value == null || "".equals(value)) {
                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
            }
        }
        String serial = request.getHeader(WECHAT_PAY_SERIAL);
        String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        // 构建request,传入必要参数
        NotificationRequest notificationRequest = new NotificationRequest.Builder().withSerialNumber(serial)
                .withNonce(nonce)
                .withTimestamp(timestamp)
                .withSignature(signature)
                .withBody(body)
                .build();
        NotificationHandler handler = new NotificationHandler(verifier, apiKey.getBytes(StandardCharsets.UTF_8));
        // 验签和解析请求体
        Notification notification = handler.parse(notificationRequest);
        assert notification != null;
        T respBody = WxJsonUtils.parseObject(notification.getDecryptData(), valueTypeRef);
        return respBody;
    }
    /**
     * 订单查询
     *
     * @param httpClient
     * @param socketTimeout
     * @param connectTimeout
     * @param url
     * @return com.abl.biz.center.payment.wx.v3.NotifyV3PayDecodeRespBody
     * @author xiaochen
     * @date 2021-12-20 17:12
     */
    protected NotifyV3PayDecodeRespBody query(CloseableHttpClient httpClient, int socketTimeout, int connectTimeout, String url) {
        //请求URL
        HttpGet httpGet = requestGet(
                url
                , socketTimeout
                , connectTimeout);
        String repBody = result(httpClient, httpGet);
        NotifyV3PayDecodeRespBody body = WxJsonUtils.parseObject(repBody, NotifyV3PayDecodeRespBody.class);
        return body;
    }
    /**
     * 子级实现
     *
     * @param out_trade_no
     * @param mchid
     * @return
     */
    public abstract NotifyV3PayDecodeRespBody query(String out_trade_no, String mchid);
    /**
     * 订单退款
     *
     * @param refundModel
     * @return
     */
    public abstract Map<String, Object> refund(WxPaymentRefundModel refundModel);
    /**
     * 订单退款
     *
     * @param httpClient
     * @param uri
     * @param httpReadTimeoutMs
     * @param httpConnectTimeoutMs
     * @param refundModel
     * @return
     */
    public Map<String, Object> refund(CloseableHttpClient httpClient,
                                      String uri,
                                      int httpReadTimeoutMs,
                                      int httpConnectTimeoutMs,
                                      WxPaymentRefundModel refundModel) {
        String reqBody = WxJsonUtils.toJsonString(refundModel);
        //请求URL
        HttpEntityEnclosingRequestBase httpPost = requestPost(
                uri
                , httpReadTimeoutMs
                , httpConnectTimeoutMs, reqBody);
        String repBody = result(httpClient, httpPost);
        Map<String, Object> body = WxJsonUtils.parseObject(repBody, Map.class);
        return body;
    }
    /**
     * 请求结果
     *
     * @param request
     * @return
     */
    protected String result(CloseableHttpClient httpClient, HttpRequestBase request) {
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(request);
            int statusCode = response.getStatusLine().getStatusCode();
            String respBodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            if (WxUtils.getLogger().isDebugEnabled()) {
                WxUtils.debug("请求成功:{}", respBodyStr);
            }
            // 成功相应
            if (STATUS_CODE == statusCode || OTHER_STATUS_CODE == statusCode) {
                return respBodyStr;
            } else {
                WxUtils.error("failed,resp code = {},return body = {}", statusCode, respBodyStr);
                throw new RuntimeException(respBodyStr);
            }
        } catch (ConnectTimeoutException e) {
            e.printStackTrace();
            throw new RuntimeException("接口超时");
        } catch (SocketTimeoutException e) {
            e.printStackTrace();
            throw new RuntimeException("读取接口数据超时");
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("接口请求失败,请尝试检查网络环境或请求接口是否能正常访问");
        } finally {
            // 关闭响应
            try {
                if (response != null) {
                    //关闭结果集
                    response.getEntity().getContent().close();
                    response.close();
                }
            } catch (IOException e) {
                throw new RuntimeException("关闭流异常");
            }
        }
    }
    /**
     * 微信结果确认应答
     * 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
     *
     * @param
     * @throws IOException
     */
    public void ack() throws IOException {
        ack(true, null);
    }
    /**
     * 微信结果确认应答
     * 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
     *
     * @param
     * @throws IOException
     */
    public void ack(boolean ackSucc, String erroMsg) throws IOException {
        HttpServletResponse response = WebUtils.response();
        PrintWriter writer = response.getWriter();
        if (ackSucc) {
            log.info("响应微信回调成功!");
            response.setStatus(200);
            writer.write("{\"code\": \"SUCCESS\",\"message\": \"成功\"}");
        } else {
            log.info("响应微信回调失败:{}!", erroMsg);
            response.setStatus(500);
            writer.write("{\"code\": \"FAIL\",\"message\": " + (StringUtils.hasLength(erroMsg) ? erroMsg : "业务处理失败") + "}");
        }
        // 关闭流
        if (Objects.nonNull(writer)) {
            writer.close();
        }
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxJsonUtils.java
New file
@@ -0,0 +1,73 @@
package com.ruoyi.payment.wx.utils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
/**
 * Json转换工具类
 *
 * @author madman
 */
@Slf4j
public final class WxJsonUtils {
    private static final ObjectMapper OM = new ObjectMapper();
    private static final JavaTimeModule timeModule = new JavaTimeModule();
    /**
     * 设置 ObjectMapper
     *
     * @return
     */
    private static ObjectMapper getObjectMapper() {
        // 允许对象忽略json中不存在的属性
        OM.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        OM.registerModule(timeModule);
        return OM;
    }
    /**
     * 将对象序列化
     */
    public static <T> String toJsonString(T obj) {
        try {
            ObjectMapper om = getObjectMapper();
            // 忽略空值
            om.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
            return om.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            log.error("转json字符串失败:{}", obj);
            return null;
        }
    }
    /**
     * 反序列化对象字符串
     */
    public static <T> T parseObject(String json, Class<T> clazz) {
        try {
            ObjectMapper om = getObjectMapper();
            return om.readValue(json, clazz);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("反序列化对象字符串失败");
        }
    }
    /**
     * 反序列化字符串成为对象
     */
    public static <T> T parseObject(String json, TypeReference<T> valueTypeRef) {
        try {
            ObjectMapper om = getObjectMapper();
            return om.readValue(json, valueTypeRef);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("反序列化字符串成为对象失败");
        }
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxTimeUtils.java
New file
@@ -0,0 +1,164 @@
package com.ruoyi.payment.wx.utils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.util.StringUtils;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Objects;
import java.util.TimeZone;
/**
 * @author xiaochen
 * @ClassName WxTimeUtils
 * @Description
 * @date 2021-12-16 16:07
 */
public class WxTimeUtils {
    /**
     * 系统默认时区
     */
    private static final ZoneId ZONE = ZoneId.systemDefault();
    /**
     * yyyy-MM-dd'T'HH:mm:ssxxx 比如:2020-05-23T17:06:30+08:00 0时区时末尾 为+00:00
     */
    public static final DateTimeFormatter YYYY_MM_DD_T_HH_MM_SS_XXX_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssxxx");
    /**
     * yyyy-MM-dd HH:mm:ss 比如:2020-05-23 17:06:30
     */
    public static final DateTimeFormatter YYYY_MM_DD_HH_MM_SS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZONE);
    /**
     * 时间转 TimeZone
     *
     * @param date
     * @return
     * @throws Exception
     */
    public static String dateToTimeZone(Date date) throws Exception {
        String time;
        if (date == null) {
            throw new Exception("date is not null");
        }
        ZonedDateTime zonedDateTime = toZonedDateTime(date);
        time = format(zonedDateTime, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
        return time;
    }
    /**
     * Date转ZonedDateTime,时区为系统默认时区
     *
     * @param date Date
     * @return ZonedDateTime
     */
    public static ZonedDateTime toZonedDateTime(Date date) {
        Objects.requireNonNull(date, "date");
        return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault());
    }
    /**
     * 根据 formatter格式化 zonedDateTime
     *
     * @param zonedDateTime ZonedDateTime
     * @param formatter     DateTimeFormatter
     * @return String
     */
    public static String format(ZonedDateTime zonedDateTime, DateTimeFormatter formatter) {
        Objects.requireNonNull(zonedDateTime, "zonedDateTime");
        Objects.requireNonNull(formatter, "formatter");
        return zonedDateTime.format(formatter);
    }
    /**
     * TimeZone 时间转标准时间
     *
     * @param date
     * @return
     * @throws Exception
     */
    public static String toTimeZoneStr(String date) {
        String time;
        if (!StringUtils.hasLength(date)) {
            throw new RuntimeException("str is not null");
        }
        ZonedDateTime zonedDateTime = parseToZonedDateTime(date, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
        if (zonedDateTime == null) {
            throw new RuntimeException("str to zonedDateTime fail");
        }
        time = zonedDateTime.format(YYYY_MM_DD_HH_MM_SS_FMT);
        return time;
    }
    /**
     * 转date
     *
     * @param date
     * @return
     * @throws Exception
     */
    public static Date toDate(String date) {
        String time;
        if (!StringUtils.hasLength(date)) {
            throw new RuntimeException("str is not null");
        }
        ZonedDateTime zonedDateTime = parseToZonedDateTime(date, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
        if (zonedDateTime == null) {
            throw new RuntimeException("str to zonedDateTime fail");
        }
        return Date.from(zonedDateTime.toInstant());
    }
    /**
     * str --> Date
     *
     * @param date
     * @return java.util.Date
     * @author xiaochen
     * @date 2022-01-20 18:20
     */
    public static Date toRfc3339Date(String date) {
        DateTime dt2 = new DateTime(date);
        return dt2.toDate();
    }
    /**
     * 将 Date 转为 LocalDateTime
     *
     * @param date
     * @return java.time.LocalDateTime;
     */
    public static LocalDateTime dateToLocalDateTime(Date date) {
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
    }
    /**
     * str --> Date
     *
     * @param date
     * @return java.util.Date
     * @author xiaochen
     * @date 2022-01-20 18:20
     */
    public static String toRfc3339Str(Date date) {
        DateTime dt1 = new DateTime(new Date(), DateTimeZone.forTimeZone(TimeZone.getTimeZone("Asia/Shanghai")));
        return dt1.toString();
    }
    /**
     * 根据 formatter解析为 ZonedDateTime
     *
     * @param text      待解析字符串
     * @param formatter DateTimeFormatter
     * @return ZonedDateTime
     */
    public static ZonedDateTime parseToZonedDateTime(String text, DateTimeFormatter formatter) {
        return ZonedDateTime.parse(text, formatter);
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxUtils.java
New file
@@ -0,0 +1,217 @@
package com.ruoyi.payment.wx.utils;
import com.ruoyi.payment.wx.pojo.AppletUserDecodeData;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.AlgorithmParameters;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;
import java.util.Random;
/**
 * @Description 获取用户信息工具类
 * @Author xiaochen
 * @Date 2021/8/12 15:45
 */
@Slf4j
public class WxUtils {
    /**
     * 随机字符
     */
    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final Random RANDOM = new SecureRandom();
    /**
     * 微信小程序API 用户数据的解密
     * @param encryptedData
     * @param sessionKey
     * @param iv
     * @return
     */
    public static AppletUserDecodeData encryptedData(String encryptedData, String sessionKey, String iv) {
        // 被加密的数据
        byte[] dataByte = Base64.decode(encryptedData);
        // 加密秘钥
        byte[] keyByte = Base64.decode(sessionKey);
        // 偏移量
        byte[] ivByte = Base64.decode(iv);
        try {
            // 如果密钥不足16位,那么就补足.  这个if 中的内容很重要
            int base = 16;
            if (keyByte.length % base != 0) {
                int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
                byte[] temp = new byte[groups * base];
                Arrays.fill(temp, (byte) 0);
                System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
                keyByte = temp;
            }
            // 初始化
            Security.addProvider(new BouncyCastleProvider());
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
            SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
            AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
            parameters.init(new IvParameterSpec(ivByte));
            cipher.init(Cipher.DECRYPT_MODE, spec, parameters);
            byte[] resultByte = cipher.doFinal(dataByte);
            if (null != resultByte && resultByte.length > 0) {
                String result = new String(resultByte, "UTF-8");
                log.info("解密原串:{}",result);
                return WxJsonUtils.parseObject(result, AppletUserDecodeData.class);
            }
            throw new RuntimeException("解密的数据为空");
        } catch (Exception e) {
            log.error("解密失败. error = {}", e.getMessage(), e);
            throw new RuntimeException(e.getMessage());
        }
    }
    /**
     * 微信小程序API 用户数据的签名验证
     * signature = sha1( rawData + session_key )
     *
     * @param rawData    不包括敏感信息的原始数据字符串,用于计算签名。
     * @param sessionKey
     */
    public static void verifySignature(String rawData, String sessionKey, String signature) {
        String serverSignature = SHA1.getSHA1(rawData + sessionKey);
        System.out.println(rawData + sessionKey);
        log.info(rawData + ">>>>>>:" + sessionKey + " === " + serverSignature + "  ======" + signature);
        if (!signature.equals(serverSignature)) {
            throw new RuntimeException("数据验签不通过");
        }
    }
    /**
     * 根据流接收请求数据
     *
     * @param request
     * @return
     */
    public static String streamBodyByReceive(HttpServletRequest request) throws IOException {
        BufferedReader reader = null;
        StringBuffer sb = new StringBuffer();
        try {
            ServletInputStream stream = request.getInputStream();
            // 获取响应
            reader = new BufferedReader(new InputStreamReader(stream));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            throw new RuntimeException("读取微信支付接口数据流出现异常!");
        } finally {
            reader.close();
            WxUtils.info(sb.toString());
        }
        return sb.toString();
    }
    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }
    /**
     * 获取当前时间戳,单位秒
     *
     * @return
     */
    public static long getCurrentTimestamp() {
        return System.currentTimeMillis() / 1000;
    }
    /**
     * 获取当前时间戳,单位毫秒
     *
     * @return
     */
    public static long getCurrentTimestampMs() {
        return System.currentTimeMillis();
    }
    /**
     * 日志
     *
     * @return
     */
    public static Logger getLogger() {
        Logger logger = LoggerFactory.getLogger("wxpay java sdk  --->");
        return logger;
    }
    /**
     * debug
     *
     * @param msg
     * @param args
     */
    public static void debug(String msg, Object... args) {
        Logger log = getLogger();
        if (log.isDebugEnabled()) {
            log.debug(msg, args);
        }
    }
    /**
     * info
     *
     * @param msg
     * @param args
     */
    public static void info(String msg, Object... args) {
        Logger log = getLogger();
        if (log.isInfoEnabled()) {
            log.info(msg, args);
        }
    }
    /**
     * warn
     *
     * @param msg
     * @param args
     */
    public static void warn(String msg, Object... args) {
        Logger log = getLogger();
        if (log.isWarnEnabled()) {
            log.warn(msg, args);
        }
    }
    /**
     * error
     *
     * @param msg
     * @param args
     */
    public static void error(String msg, Object... args) {
        Logger log = getLogger();
        if (log.isErrorEnabled()) {
            log.error(msg, args);
        }
    }
}
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/wx/utils/WxV3Pay.java
New file
@@ -0,0 +1,197 @@
package com.ruoyi.payment.wx.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.payment.wx.model.WeixinProperties;
import com.ruoyi.payment.wx.model.WxPaymentInfoModel;
import com.ruoyi.payment.wx.model.WxPaymentRefundModel;
import com.ruoyi.payment.wx.resp.NotifyV3PayDecodeRespBody;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.impl.client.CloseableHttpClient;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.Map;
/**
 * @author xiaochen
 * @ClassName WxWifiV3Pay
 * @Description
 * @date 2021-11-13 21:10
 */
@Slf4j
public class WxV3Pay extends WxAbstractPay {
    @Getter
    private WeixinProperties config;
    @Getter
    private Verifier verifier;
    private WechatPayHttpClientBuilder builder;
    @Getter
    private CloseableHttpClient httpClient;
    private PrivateKeySigner privateKeySigner;
    private WechatPay2Validator validator;
    @Getter
    private PrivateKey privateKey;
    /**
     * 初始化
     *
     * @param config
     */
    public WxV3Pay(WeixinProperties config) {
        // 检查v3支付配置信息
        if (WxUtils.getLogger().isDebugEnabled()) {
            WxUtils.debug("开始检查v3支付配置信息....");
        }
        try {
            this.config = config;
            checkWxConfig();
            this.privateKey = PemUtil.loadPrivateKey(config.getV3().getPrivateKeyStream());
            this.privateKeySigner = new PrivateKeySigner(config.getV3().getMchSerialNo(), privateKey);
            // 获取证书管理器实例
            CertificatesManager certificatesManager = CertificatesManager.getInstance();
            // 向证书管理器增加需要自动更新平台证书的商户信息
            certificatesManager.putMerchant(config.getMchId(), new WechatPay2Credentials(config.getMchId(),
                    this.privateKeySigner), config.getV3().getApiKey().getBytes(StandardCharsets.UTF_8));
            // 从证书管理器中获取verifier
            this.verifier = certificatesManager.getVerifier(config.getMchId());
            this.validator = new WechatPay2Validator(verifier);
            this.builder = WechatPayHttpClientBuilder.create()
                    .withMerchant(config.getMchId(), config.getV3().getMchSerialNo(), this.privateKey)
                    .withValidator(this.validator);
            this.httpClient = this.builder.build();
        } catch (Exception e) {
            // 打印异常信息
            e.printStackTrace();
            WxUtils.warn("检查v3支付配置信息出现错误,直连商户商户号:{},{}", config.getMchId(), e.getMessage());
            return;
        }
        if (WxUtils.getLogger().isDebugEnabled()) {
            WxUtils.debug("检查v3支付配置信息完成,未出现异常....");
        }
    }
    /**
     * 检查支付配置信息
     *
     * @throws Exception
     */
    private void checkWxConfig() throws Exception {
        if (this.config == null) {
            throw new Exception("config is null");
        }
        if (config.getMchId() == null || config.getMchId().trim().length() == 0) {
            throw new Exception("MchID in config is empty");
        }
        if (config.getV3().getMchSerialNo() == null) {
            throw new Exception("mchSerialNo in config is empty");
        }
        if (config.getV3().getPrivateKeyStream() == null) {
            throw new Exception("cert stream in config is empty");
        }
        if (this.config.getHttpConnectTimeoutMs() < 10) {
            throw new Exception("http connect timeout is too small");
        }
        if (this.config.getHttpReadTimeoutMs() < 10) {
            throw new Exception("http read timeout is too small");
        }
    }
    /**
     * jsApi下单
     *
     * @param tradeNo     订单号
     * @param amount      金额 分
     * @param openid      openid
     * @param description 订单描述
     * @return java.util.Map<java.lang.String, java.lang.Object>
     * @author xiaochen
     * @date 2022-03-22 12:47
     */
    public Map<String, Object> jsApi(String tradeNo, Integer amount, String openid, String description) {
        WxPaymentInfoModel requestBody = WxPaymentInfoModel.builder()
                .mchid(this.config.getMchId())
                .appid(this.config.getAppId())
                .description(description)
                .out_trade_no(tradeNo)
//                .attach("")
                .amount(WxPaymentInfoModel.Amount.builder().total(amount).build())
                .payer(WxPaymentInfoModel.Payer.builder().openid(openid).build())
                // 分不分账
                //.settle_info(WxPaymentInfoModel.SettleInfo.builder().profit_sharing(true).build())
                .build();
        // 封装基础数据
        String reqBody = buildBaseParam(requestBody
                , this.config.getV3().getNotifyPayUrl());
        //请求URL
        HttpEntityEnclosingRequestBase httpPost = requestPost(
                "/v3/pay/transactions/jsapi"
                , this.config.getHttpReadTimeoutMs()
                , this.config.getHttpConnectTimeoutMs()
                , reqBody);
        String repBody = result(httpClient, httpPost);
        ObjectMapper om = new ObjectMapper();
        try {
            JsonNode rootNode = om.readTree(repBody);
            String prepayId = rootNode.path("prepay_id").asText();
            return wxTuneUp(this.privateKeySigner, requestBody.getAppid(), prepayId);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("获取支付数据错误!");
        }
    }
    /**
     * 接收回调
     *
     * @param request
     * @return
     * @throws Exception
     */
    @Override
    public <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception {
        return verifyNotify(request, this.verifier, this.config.getV3().getApiKey(), valueTypeRef);
    }
    /**
     * 订单查询
     *
     * @param out_trade_no
     * @param mchid
     * @return com.abl.biz.center.payment.wx.v3.NotifyV3PayDecodeRespBody
     * @author xiaochen
     * @date 2021-12-20 16:47
     */
    @Override
    public NotifyV3PayDecodeRespBody query(String out_trade_no, String mchid) {
        String url =
                String.format("/v3/pay/transactions/out-trade-no/%s", out_trade_no) + String.format("?mchid=%s", mchid);
        return query(this.httpClient, this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), url);
    }
    /**
     * 退款
     *
     * @param refundModel
     * @return java.util.Map<java.lang.String, java.lang.Object>
     * @author xiaochen
     */
    @Override
    public Map<String, Object> refund(WxPaymentRefundModel refundModel) {
//        refundModel.setNotify_url(this.config.getV3().getNotifyRefundUrl());
        return refund(this.httpClient, "/v3/refund/domestic/refunds", this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), refundModel);
    }
}