Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/java/mx_charging_pile
| | |
| | | /** |
| | | * 用户名id |
| | | */ |
| | | private Integer userid; |
| | | private Long userId; |
| | | |
| | | /** |
| | | * 用户名 |
| | |
| | | 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; |
| | |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | | @Slf4j |
| | | @RestController |
| | | public class TokenController |
| | | { |
| | |
| | | userClient.updateSysUser(sysUser); |
| | | return R.ok(map); |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | @DeleteMapping("logout") |
| | | public R<?> logout(HttpServletRequest request) { |
| | |
| | | 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) |
| | | { |
| | |
| | | * 缓存有效期,默认720(分钟) |
| | | */ |
| | | public final static long EXPIRATION = 720; |
| | | public final static long EXPIRATION_APPLET = 7*24*60*60; |
| | | |
| | | /** |
| | | * 缓存刷新时间,默认120(分钟) |
| | |
| | | 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; |
| | | |
| | |
| | | */ |
| | | 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()); |
| | |
| | | // 接口返回信息 |
| | | 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() { |
| | |
| | | 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) { |
| | |
| | | |
| | | 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; |
| | |
| | | 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; |
| | | |
| | | /** |
| | |
| | | * @author luodangjia |
| | | * @since 2024-08-06 |
| | | */ |
| | | @Slf4j |
| | | @RestController |
| | | @RequestMapping("/t-app-user") |
| | | public class TAppUserController { |
| | |
| | | |
| | | @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") |
New file |
| | |
| | | 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; |
| | | |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | package com.ruoyi.account.wx.pojo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public class Watermark { |
| | | private String appid; |
| | | private String timestamp; |
| | | } |
New file |
| | |
| | | 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("反序列化字符串成为对象失败"); |
| | | } |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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(); |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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(); |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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 + |
| | | '}'; |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
New file |
| | |
| | | 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("反序列化字符串成为对象失败"); |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | <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> |
New file |
| | |
| | | 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); |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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()); |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | package com.ruoyi.payment.wx.pojo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public class Watermark { |
| | | private String appid; |
| | | private String timestamp; |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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("反序列化字符串成为对象失败"); |
| | | } |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | |
| | | } |