新增微信支付所需要的基础代码

This commit is contained in:
wanggeng 2021-08-19 14:51:14 +08:00
parent 938d8d4208
commit b1bafbc6a3
17 changed files with 1341 additions and 3 deletions

View File

@ -0,0 +1,96 @@
package ink.wgink.module.wechat.enums;
/**
* @ClassName: PayErrorMsgEnum
* @Description: 支付错误码
* @Author: wanggeng
* @Date: 2021/8/19 9:21 上午
* @Version: 1.0
*/
public enum PayErrorMsgEnum {
USERPAYING_200(202, "USERPAYING", "用户支付中,需要输入密码", "等待5秒然后调用被扫订单结果查询API查询当前订单的不同状态决定下一步的操作"),
APPID_MCHID_NOT_MATCH_400(400, "APPID_MCHID_NOT_MATCH", "appid和mch_id不匹配", "请确认appid和mch_id是否匹配"),
PARAM_ERROR_400(400, "PARAM_ERROR", "业务规则限制", "请根据接口返回的详细信息检查请求参数"),
ORDER_CLOSED(400, "ORDER_CLOSED", "订单已关闭", "当前订单已关闭,请重新下单"),
MCH_NOT_EXISTS_400(400, "MCH_NOT_EXISTS", "商户号不存在", "请检查商户号是否正确"),
INVALID_REQUEST_400(400, "INVALID_REQUEST", "无效请求", "请根据接口返回的详细信息检查"),
PHONE_NOT_EXIST_400(400, "PHONE_NOT_EXIST", "手机号不存在", "请检查手机号码是否正确"),
AUTH_CODE_INVALID_400(400, "AUTH_CODE_INVALID", "收银员扫描的不是微信支付的条码", "请扫描微信支付被扫条码/二维码"),
NO_STATEMENT_EXIST_400(400, "NO_STATEMENT_EXIST", "账单文件不存在", "请检查当前商户号是否在指定日期有交易或退款发生"),
STATEMENT_CREATING_400(400, "STATEMENT_CREATING", "账单生成中", "请先检查当前商户号在指定日期内是否有成功的交易或退款若有则在T+1日上午8点后再重新下载"),
RESOURCE_ALREADY_EXISTS_400(400, "RESOURCE_ALREADY_EXISTS", "用户已签约该商户,不可重复签约", "请通过查询用户接口获取用户的签约信息"),
ALREADY_EXISTS_400(400, "ALREADY_EXISTS", "资源已存在", "尝试创建的资源已存在,无需重复创建"),
USER_NOT_REGISTERED_400(400, "USER_NOT_REGISTERED", "服务未开通或账号未注册", "该用户尚未注册或开通当前服务,请开通后再试"),
ORDER_PAID_400(400, "ORDER_PAID", "订单已支付", "请确认该订单号是否重复支付,如果是新单,请使用新订单号提交"),
ORDER_REVERSED_400(400, "ORDER_REVERSED", "订单已撤销", "当前订单状态为“订单已撤销”,请提示用户重新支付"),
ORDERCLOSED_400(400, "ORDERCLOSED", "订单已关闭", "商户订单号异常,请重新下单支付"),
ORDERPAID_400(400, "ORDERPAID", "订单已支付", "请确认该订单号是否重复支付,如果是新单,请使用新订单号提交"),
ORDERREVERSED_400(400, "ORDERREVERSED", "订单已撤销", "当前订单状态为“订单已撤销”,请提示用户重新支付"),
SIGN_ERROR_401(401, "SIGN_ERROR", "签名错误", "请检查签名参数和方法是否都符合签名算法要求"),
TRADE_ERROR_403(403, "TRADE_ERROR", "交易错误", "因业务原因交易失败,请查看接口返回的详细信息"),
OUT_TRADE_NO_USED_403(403, "OUT_TRADE_NO_USED", "商户订单号重复", "请核实商户订单号是否重复提交"),
ACCOUNT_ERROR_403(403, "ACCOUNT_ERROR", "账号异常", "用户账号异常,无需更多操作"),
RULE_LIMIT_403(403, "RULE_LIMIT", "业务规则限制", "因业务规则限制请求频率,请查看接口返回的详细信息"),
NOT_ENOUGH_403(403, "NOT_ENOUGH", "余额不足", "用户账号余额不足,请用户充值或更换支付卡后再支付"),
NO_AUTH_403(403, "NO_AUTH", "商户无权限", "请商户前往申请此接口相关权限"),
REQUEST_BLOCKED_403(403, "REQUEST_BLOCKED", "请求受阻", "此状态代表退款申请失败,商户可根据具体的错误提示做相应的处理"),
CONTRACT_NOT_EXIST_403(403, "CONTRACT_NOT_EXIST", "签约协议不存在", "请检查签约协议号是否正确,是否已解约"),
CONTRACT_EXISTED_403(403, "CONTRACT_EXISTED", "协议已存在", "已开通自动扣费服务功能,无需重复开通"),
AUTH_CODE_EXPIRE_403(403, "AUTH_CODE_EXPIRE", "用户的条码已经过期", "请收银员提示用户,请用户在微信上刷新条码,然后请收银员重新扫码。 直接将错误展示给收银员"),
USER_ACCOUNT_ABNORMAL_403(403, "USER_ACCOUNT_ABNORMAL", "用户账户异常", "该确认用户账号是否正常,商家可联系微信支付或让用户联系微信支付客服处理"),
CONTRACT_ERROR_403(403, "CONTRACT_ERROR", "当前用户签约状态失效", "请通过查询用户接口核实签约状态"),
CONTRACT_NOT_CONFIRMED_403(403, "CONTRACT_NOT_CONFIRMED", "二级商户未开启手动提现权限", "二级商户号提现权限已关闭,无法发起提现"),
ACCOUNT_NOT_VERIFIED_403(403, "ACCOUNT_NOT_VERIFIED", "二级商户下行打款未成功", "二级商户号结算银行卡信息有误,修改后重试"),
REFUND_NOT_EXISTS_404(404, "REFUND_NOT_EXISTS", "订单号错误或订单状态不正确", "请检查订单号是否有误以及订单状态是否正确,如:未支付、已支付未退款"),
ORDER_NO_TEXIST_404(404, "ORDER_NO_TEXIST", "订单不存在", "请检查订单是否发起过交易"),
ORDER_NOT_EXIST_404(404, "ORDER_NOT_EXIST", "请求的资源不存在", "请商户检查需要查询的id或者请求URL是否正确"),
USER_NOT_EXIST_404(404, "USER_NOT_EXIST", "用户账户注销", "请检查用户账户是否正确"),
USER_NOT_EXISTS_404(404, "USER_NOT_EXISTS", "openid不正确", "请确认传入的openid是否正确"),
MCH_NOT_EXISTS_404(404, "MCH_NOT_EXISTS", "商户号不存在", "请确认传入的商户号是否正确"),
RESOURCE_NOT_EXISTS_404(404, "RESOURCE_NOT_EXISTS", "查询的资源不存在", "请检查查询资源的对应id是否填写正确"),
NOT_FOUND_404(404, "NOT_FOUND", "请求的资源不存在", "请商户检查需要查询的id或者请求URL是否正确"),
FREQUENCY_LIMITED_429(429, "FREQUENCY_LIMITED", "频率超限", "请求量不要超过接口调用频率限制"),
RATELIMIT_EXCEEDED_429(429, "RATELIMIT_EXCEEDED", "频率限制", "请降低频率后重试"),
FREQUENCY_LIMIT_EXCEED_429(429, "FREQUENCY_LIMIT_EXCEED", "接口限频", "请降低调用频率"),
ERROR_500(500, "ERROR", "业务错误", "该错误都会返回具体的错误原因,请根据实际返回做相应处理"),
OPENID_MISMATCH_500(500, "OPENID_MISMATCH", "openid和appid不匹配", "请确认openid和appid是否匹配"),
BANK_ERROR_500(500, "BANK_ERROR", "银行系统异常", "银行系统异常,请用相同参数重新调用"),
INVALID_TRANSACTIONID_500(500, "INVALID_TRANSACTIONID", "订单号非法", "请检查微信支付订单号是否正确"),
BIZ_ERR_NEED_RETRY_500(500, "BIZ_ERR_NEED_RETRY", "退款业务流程错误", "请不要更换商户退款单号请使用相同参数再次调用API"),
SYSTEM_ERROR_500(500, "SYSTEM_ERROR", "系统错误", "系统异常,请用相同参数重新调用");
private int code;
private String errorCode;
private String summary;
private String solution;
PayErrorMsgEnum(int code, String errorCode, String summary, String solution) {
this.code = code;
this.errorCode = errorCode;
this.summary = summary;
this.solution = solution;
}
public int getCode() {
return code;
}
public String getErrorCode() {
return errorCode == null ? "" : errorCode.trim();
}
public String getSummary() {
return summary == null ? "" : summary.trim();
}
public String getSolution() {
return solution == null ? "" : solution.trim();
}
}

View File

@ -0,0 +1,25 @@
package ink.wgink.module.wechat.manager.pay.v3.miniapp;
/**
* @ClassName: PayMiniappManager
* @Description: 小程序支付管理
* @Author: wanggeng
* @Date: 2021/8/19 11:10 上午
* @Version: 1.0
*/
public class PayMiniappManager {
private static final PayMiniappManager payMiniappManager = PayMiniappManagerBuilder.payMiniappManager;
private PayMiniappManager() {
}
public PayMiniappManager getInstance() {
return payMiniappManager;
}
private static class PayMiniappManagerBuilder {
public static PayMiniappManager payMiniappManager = new PayMiniappManager();
}
}

View File

@ -0,0 +1,494 @@
package ink.wgink.module.wechat.pojo.bos.pay.v3;
import ink.wgink.annotation.*;
import java.util.List;
/**
* @ClassName: PlaceOrderBO
* @Description: 下单
* @Author: wanggeng
* @Date: 2021/8/19 1:52 下午
* @Version: 1.0
*/
public class PlaceOrderBO {
@CheckEmptyAnnotation(name = "应用ID")
private String appid;
@CheckEmptyAnnotation(name = "直连商户号")
private String mchid;
@CheckEmptyAnnotation(name = "商品描述")
private String description;
@CheckEmptyAnnotation(name = "商户订单号")
private String outTradeNo;
/**
* 交易结束时间
*/
private String timeExpire;
/**
* 附加数据
*/
private String attach;
@CheckEmptyAnnotation(name = "通知地址")
private String notifyUrl;
/**
* 订单优惠标记
*/
private String goodsTag;
@CheckNullAnnotation(name = "订单金额")
@CheckBeanAnnotation
private Amount amount;
@CheckNullAnnotation(name = "支付者")
@CheckBeanAnnotation
private Payer payer;
/**
* 优惠功能
*/
@CheckBeanAnnotation
private Detail detail;
/**
* 场景信息
*/
@CheckBeanAnnotation
private SceneInfo sceneInfo;
/**
* 结算信息
*/
@CheckBeanAnnotation
private SettleInfo settleInfo;
public String getAppid() {
return appid == null ? "" : appid.trim();
}
public void setAppid(String appid) {
this.appid = appid;
}
public String getMchid() {
return mchid == null ? "" : mchid.trim();
}
public void setMchid(String mchid) {
this.mchid = mchid;
}
public String getDescription() {
return description == null ? "" : description.trim();
}
public void setDescription(String description) {
this.description = description;
}
public String getOutTradeNo() {
return outTradeNo == null ? "" : outTradeNo.trim();
}
public void setOutTradeNo(String outTradeNo) {
this.outTradeNo = outTradeNo;
}
public String getTimeExpire() {
return timeExpire == null ? "" : timeExpire.trim();
}
public void setTimeExpire(String timeExpire) {
this.timeExpire = timeExpire;
}
public String getAttach() {
return attach == null ? "" : attach.trim();
}
public void setAttach(String attach) {
this.attach = attach;
}
public String getNotifyUrl() {
return notifyUrl == null ? "" : notifyUrl.trim();
}
public void setNotifyUrl(String notifyUrl) {
this.notifyUrl = notifyUrl;
}
public String getGoodsTag() {
return goodsTag == null ? "" : goodsTag.trim();
}
public void setGoodsTag(String goodsTag) {
this.goodsTag = goodsTag;
}
public Amount getAmount() {
return amount;
}
public void setAmount(Amount amount) {
this.amount = amount;
}
public Payer getPayer() {
return payer;
}
public void setPayer(Payer payer) {
this.payer = payer;
}
public Detail getDetail() {
return detail;
}
public void setDetail(Detail detail) {
this.detail = detail;
}
public SceneInfo getSceneInfo() {
return sceneInfo;
}
public void setSceneInfo(SceneInfo sceneInfo) {
this.sceneInfo = sceneInfo;
}
public SettleInfo getSettleInfo() {
return settleInfo;
}
public void setSettleInfo(SettleInfo settleInfo) {
this.settleInfo = settleInfo;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("{");
sb.append("\"appid\":\"")
.append(appid).append('\"');
sb.append(",\"mchid\":\"")
.append(mchid).append('\"');
sb.append(",\"description\":\"")
.append(description).append('\"');
sb.append(",\"outTradeNo\":\"")
.append(outTradeNo).append('\"');
sb.append(",\"timeExpire\":\"")
.append(timeExpire).append('\"');
sb.append(",\"attach\":\"")
.append(attach).append('\"');
sb.append(",\"notifyUrl\":\"")
.append(notifyUrl).append('\"');
sb.append(",\"goodsTag\":\"")
.append(goodsTag).append('\"');
sb.append(",\"amount\":")
.append(amount);
sb.append(",\"payer\":")
.append(payer);
sb.append(",\"detail\":")
.append(detail);
sb.append(",\"sceneInfo\":")
.append(sceneInfo);
sb.append(",\"settleInfo\":")
.append(settleInfo);
sb.append('}');
return sb.toString();
}
/**
* 订单金额
*/
public static class Amount {
@CheckNumberAnnotation(name = "总金额", min = 0)
private Integer total;
/**
* 货币类型
*/
private String currency;
public Integer getTotal() {
return total == null ? 0 : total;
}
public void setTotal(Integer total) {
this.total = total;
}
public String getCurrency() {
return currency == null ? "" : currency.trim();
}
public void setCurrency(String currency) {
this.currency = currency;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("{");
sb.append("\"total\":")
.append(total);
sb.append(",\"currency\":\"")
.append(currency).append('\"');
sb.append('}');
return sb.toString();
}
}
/**
* 支付者
*/
public static class Payer {
@CheckEmptyAnnotation(name = "用户标识")
private String openid;
public String getOpenid() {
return openid == null ? "" : openid.trim();
}
public void setOpenid(String openid) {
this.openid = openid;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("{");
sb.append("\"openid\":\"")
.append(openid).append('\"');
sb.append('}');
return sb.toString();
}
}
/**
* 优惠功能
*/
public static class Detail {
/**
* 订单原价
*/
private Integer costPrice;
/**
* 商品小票ID
*/
private String invoiceId;
@CheckListBeanAnnotation
private List<Goods> goodsDetail;
public Integer getCostPrice() {
return costPrice == null ? 0 : costPrice;
}
public void setCostPrice(Integer costPrice) {
this.costPrice = costPrice;
}
public String getInvoiceId() {
return invoiceId == null ? "" : invoiceId.trim();
}
public void setInvoiceId(String invoiceId) {
this.invoiceId = invoiceId;
}
public List<Goods> getGoodsDetail() {
return goodsDetail;
}
public void setGoodsDetail(List<Goods> goodsDetail) {
this.goodsDetail = goodsDetail;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("{");
sb.append("\"costPrice\":")
.append(costPrice);
sb.append(",\"invoiceId\":\"")
.append(invoiceId).append('\"');
sb.append(",\"goodsDetail\":")
.append(goodsDetail);
sb.append('}');
return sb.toString();
}
public static class Goods {
@CheckEmptyAnnotation(name = "商户侧商品编码")
private String merchantGoodsId;
/**
* 微信侧商品编码
*/
private String wechatpayGoodsId;
/**
* 商品名称
*/
private String goodsName;
@CheckNumberAnnotation(name = "商品数量", min = 0)
private Integer quantity;
@CheckNumberAnnotation(name = "商品单价", min = 0)
private Integer unitPrice;
public String getMerchantGoodsId() {
return merchantGoodsId == null ? "" : merchantGoodsId.trim();
}
public void setMerchantGoodsId(String merchantGoodsId) {
this.merchantGoodsId = merchantGoodsId;
}
public String getWechatpayGoodsId() {
return wechatpayGoodsId == null ? "" : wechatpayGoodsId.trim();
}
public void setWechatpayGoodsId(String wechatpayGoodsId) {
this.wechatpayGoodsId = wechatpayGoodsId;
}
public String getGoodsName() {
return goodsName == null ? "" : goodsName.trim();
}
public void setGoodsName(String goodsName) {
this.goodsName = goodsName;
}
public Integer getQuantity() {
return quantity == null ? 0 : quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public Integer getUnitPrice() {
return unitPrice == null ? 0 : unitPrice;
}
public void setUnitPrice(Integer unitPrice) {
this.unitPrice = unitPrice;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("{");
sb.append("\"merchantGoodsId\":\"")
.append(merchantGoodsId).append('\"');
sb.append(",\"wechatpayGoodsId\":\"")
.append(wechatpayGoodsId).append('\"');
sb.append(",\"goodsName\":\"")
.append(goodsName).append('\"');
sb.append(",\"quantity\":")
.append(quantity);
sb.append(",\"unitPrice\":")
.append(unitPrice);
sb.append('}');
return sb.toString();
}
}
}
/**
* 场景信息
*/
public static class SceneInfo {
@CheckEmptyAnnotation(name = "用户终端IP")
private String payerClientIp;
/**
* 商户端设备号
*/
private String deviceId;
/**
* 商户门店信息
*/
public static class StoreInfo {
@CheckEmptyAnnotation(name = "门店编号")
private String id;
/**
* 门店名称
*/
private String name;
/**
* 地区编码
*/
private String areaCode;
/**
* 详细地址
*/
private String address;
public String getId() {
return id == null ? "" : id.trim();
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name == null ? "" : name.trim();
}
public void setName(String name) {
this.name = name;
}
public String getAreaCode() {
return areaCode == null ? "" : areaCode.trim();
}
public void setAreaCode(String areaCode) {
this.areaCode = areaCode;
}
public String getAddress() {
return address == null ? "" : address.trim();
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("{");
sb.append("\"id\":\"")
.append(id).append('\"');
sb.append(",\"name\":\"")
.append(name).append('\"');
sb.append(",\"areaCode\":\"")
.append(areaCode).append('\"');
sb.append(",\"address\":\"")
.append(address).append('\"');
sb.append('}');
return sb.toString();
}
}
}
/**
* 结算信息
*/
public static class SettleInfo {
/**
* 是否指定分账
*/
private Boolean profitSharing;
public Boolean getProfitSharing() {
return profitSharing;
}
public void setProfitSharing(Boolean profitSharing) {
this.profitSharing = profitSharing;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("{");
sb.append("\"profitSharing\":")
.append(profitSharing);
sb.append('}');
return sb.toString();
}
}
}

View File

@ -100,4 +100,20 @@ public interface IMiniappUserService extends IMiniappUserBaseService {
*/
MiniappUserPO getPO(String appId, String openId);
/**
* 用户码获取用户列表
*
* @param userCode 用户码
* @return
*/
MiniappUserPO getPOByUserCode(String userCode);
/**
* 用户码获取用户列表
*
* @param userCodes 用户码列表
* @return
*/
List<MiniappUserPO> listPOByUserCodes(List<String> userCodes);
}

View File

@ -159,6 +159,20 @@ public class MiniappUserServiceImpl extends DefaultBaseService implements IMinia
return getPO(params);
}
@Override
public MiniappUserPO getPOByUserCode(String userCode) {
Map<String, Object> params = getHashMap(2);
params.put("userCode", userCode);
return getPO(params);
}
@Override
public List<MiniappUserPO> listPOByUserCodes(List<String> userCodes) {
Map<String, Object> params = getHashMap(2);
params.put("userCodes", userCodes);
return listPO(params);
}
private void updateUserCode(String userCode, String openId) {
Map<String, Object> params = getHashMap(6);
params.put("userCode", userCode);

View File

@ -120,9 +120,17 @@ public interface IOfficialAccountUserService extends IOfficialAccountBaseService
/**
* 用户码获取用户
*
* @param userCode
* @param userCode 用户码
* @return
*/
OfficialAccountUserPO getPOByUserCode(String userCode);
/**
* 用户码获取用户列表
*
* @param userCodes 用户码列表
* @return
*/
List<OfficialAccountUserPO> listPOByUserCodes(List<String> userCodes);
}

View File

@ -1,5 +1,7 @@
package ink.wgink.module.wechat.service.official.account.impl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import ink.wgink.common.base.DefaultBaseService;
import ink.wgink.interfaces.consts.ISystemConstant;
import ink.wgink.module.wechat.dao.official.account.IOfficialAccountUserDao;
@ -166,7 +168,10 @@ public class OfficialAccountUserServiceImpl extends DefaultBaseService implement
@Override
public SuccessResultList<List<OfficialAccountUserDTO>> listPage(ListPage page) {
return null;
PageHelper.startPage(page.getPage(), page.getRows());
List<OfficialAccountUserDTO> officialAccountUserDTOs = officialAccountUserDao.list(page.getParams());
PageInfo<OfficialAccountUserDTO> pageInfo = new PageInfo<>(officialAccountUserDTOs);
return new SuccessResultList<>(officialAccountUserDTOs, pageInfo.getPageNum(), pageInfo.getTotal());
}
@Override
@ -182,6 +187,13 @@ public class OfficialAccountUserServiceImpl extends DefaultBaseService implement
return getPO(params);
}
@Override
public List<OfficialAccountUserPO> listPOByUserCodes(List<String> userCodes) {
Map<String, Object> params = getHashMap(2);
params.put("userCodes", userCodes);
return listPO(params);
}
/**
* 更新用户码邀请码
*

View File

@ -0,0 +1,11 @@
package ink.wgink.module.wechat.service.pay.v3.jsapi;
/**
* @ClassName: IJsapiService
* @Description: Jsapi业务
* @Author: wanggeng
* @Date: 2021/8/19 10:42 上午
* @Version: 1.0
*/
public interface IJsapiService {
}

View File

@ -0,0 +1,19 @@
package ink.wgink.module.wechat.service.pay.v3.jsapi.impl;
import ink.wgink.common.base.DefaultBaseService;
import ink.wgink.module.wechat.service.pay.v3.jsapi.IJsapiService;
import org.springframework.stereotype.Service;
/**
* @ClassName: JsapiServiceImpl
* @Description: Jsapi业务
* @Author: wanggeng
* @Date: 2021/8/19 10:42 上午
* @Version: 1.0
*/
@Service
public class JsapiServiceImpl extends DefaultBaseService implements IJsapiService {
}

View File

@ -0,0 +1,209 @@
package ink.wgink.module.wechat.utils.pay;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.*;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* @ClassName: PayRequestUtil
* @Description: 支付请求
* @Author: wanggeng
* @Date: 2021/8/18 9:41 下午
* @Version: 1.0
*/
public abstract class AbstractPayRequest {
public static final String PAY_SERIAL = "Wechatpay-Serial";
private RestTemplate restTemplate;
public AbstractPayRequest() {
SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();
simpleClientHttpRequestFactory.setConnectTimeout(20 * 1000);
simpleClientHttpRequestFactory.setReadTimeout(60 * 1000);
this.restTemplate = new RestTemplate(simpleClientHttpRequestFactory);
}
public abstract void response(ResponseEntity<String> responseEntity);
/**
* GET 请求
*
* @param url 请求地址
* @param authorization 授权信息
* @param serialNumber 公钥证书序列号
*/
public void get(String url, String authorization, String serialNumber) {
HttpEntity<String> httpEntity = new HttpEntity(getHttpHeaders(authorization, serialNumber));
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class);
response(responseEntity);
}
/**
* POST 请求
*
* @param url 请求地址
* @param authorization 授权信息
* @param serialNumber 公钥证书序列号
* @param jsonBody 请求参数
*/
public void post(String url, String authorization, String serialNumber, String jsonBody) {
HttpEntity<String> httpEntity = new HttpEntity(jsonBody, getHttpHeaders(authorization, serialNumber));
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, String.class);
response(responseEntity);
}
/**
* Put 请求
*
* @param url 请求地址
* @param authorization 授权信息
* @param serialNumber 公钥证书序列号
* @param jsonBody 请求参数
*/
public void put(String url, String authorization, String serialNumber, String jsonBody) {
HttpEntity<String> httpEntity = new HttpEntity(jsonBody, getHttpHeaders(authorization, serialNumber));
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, String.class);
response(responseEntity);
}
/**
* Delete 请求
*
* @param url 请求地址
* @param authorization 授权信息
* @param serialNumber 公钥证书序列号
*/
public void delete(String url, String authorization, String serialNumber) {
HttpEntity<String> httpEntity = new HttpEntity(getHttpHeaders(authorization, serialNumber));
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.DELETE, httpEntity, String.class);
response(responseEntity);
}
/**
* 上传文件
*
* @param url 请求地址
* @param authorization 授权信息
* @param serialNumber 公钥证书序列号
* @param jsonData 请求参数
* @param uploadFile 上传文件
*/
public void upload(String url, String authorization, String serialNumber, String jsonData, File uploadFile) {
FileSystemResource fileSystemResource = new FileSystemResource(uploadFile);
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
form.add("file", fileSystemResource);
form.add("meta", jsonData);
HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(form, getUploadHeaders(authorization, serialNumber));
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, String.class);
response(responseEntity);
}
/**
* 请求头
*
* @param authorization 授权信息
* @return
*/
private HttpHeaders getHttpBaseHeaders(String authorization) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Accept", MediaType.APPLICATION_JSON_UTF8_VALUE);
httpHeaders.add("Authorization", authorization);
return httpHeaders;
}
/**
* JSON请求头
*
* @param authorization 授权信息
* @param serialNumber 公钥证书序列号
* @return
*/
private HttpHeaders getHttpHeaders(String authorization, String serialNumber) {
HttpHeaders httpHeaders = getHttpBaseHeaders(authorization);
httpHeaders.add("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE);
if (!StringUtils.isBlank(serialNumber)) {
httpHeaders.add(PAY_SERIAL, serialNumber);
}
return httpHeaders;
}
/**
* 上传文件请求头
*
* @param authorization 授权信息
* @param serialNumber 公钥证书序列号
* @return
*/
private HttpHeaders getUploadHeaders(String authorization, String serialNumber) {
HttpHeaders httpHeaders = getHttpBaseHeaders(authorization);
httpHeaders.add("Content-Type", "multipart/form-data;boundary=\"boundary\"");
if (!StringUtils.isBlank(serialNumber)) {
httpHeaders.add(PAY_SERIAL, serialNumber);
}
return httpHeaders;
}
/**
* Aes 解密
*/
public static class AesUtil {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] aesKey;
/**
* @param key APIv3秘钥
*/
public AesUtil(byte[] key) {
if (key.length != KEY_LENGTH_BYTE) {
throw new IllegalArgumentException("无效的ApiV3Key长度必须为32个字节");
}
this.aesKey = key;
}
/**
* 解密
*
* @param associatedData 关联数据
* @param nonce 加密使用的随机串初始化向量
* @param ciphertext 密文
* @return
* @throws GeneralSecurityException
* @throws IOException
*/
public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) throws GeneralSecurityException, IOException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
}
}

View File

@ -0,0 +1,53 @@
package ink.wgink.module.wechat.utils.pay;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.security.cert.*;
/**
* @ClassName: PayCertificateUtil
* @Description: 证书工具
* @Author: wanggeng
* @Date: 2021/8/18 8:38 下午
* @Version: 1.0
*/
public class PayCertificateUtil {
/**
* 获取证书
*
* @param inputStream
* @return
*/
public static X509Certificate getCertificate(InputStream inputStream) {
try {
CertificateFactory cf = CertificateFactory.getInstance("X509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
cert.checkValidity();
return cert;
} catch (CertificateExpiredException e) {
throw new RuntimeException("证书已过期", e);
} catch (CertificateNotYetValidException e) {
throw new RuntimeException("证书尚未生效", e);
} catch (CertificateException e) {
throw new RuntimeException("无效的证书", e);
}
}
public static void main(String[] args) throws FileNotFoundException {
X509Certificate certificate = PayCertificateUtil.getCertificate(new FileInputStream(new File("/Users/wanggeng/tencent/tenlion/cert/1609461201_20210818_cert/apiclient_cert.pem")));
System.out.println("输出证书信息:\n" + certificate.toString());
System.out.println("证书序列号:" + certificate.getSerialNumber().toString(16));
System.out.println("版本号:" + certificate.getVersion());
System.out.println("签发者:" + certificate.getIssuerDN());
System.out.println("有效起始日期:" + certificate.getNotBefore());
System.out.println("有效终止日期:" + certificate.getNotAfter());
System.out.println("主体名:" + certificate.getSubjectDN());
System.out.println("签名算法:" + certificate.getSigAlgName());
System.out.println("签名:" + certificate.getSignature().toString());
}
}

View File

@ -0,0 +1,109 @@
package ink.wgink.module.wechat.utils.pay;
import ink.wgink.interfaces.consts.ISystemConstant;
import org.bouncycastle.util.encoders.Base64;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
/**
* @ClassName: PayPrivateKeyUtil
* @Description: 商户私钥工具
* @Author: wanggeng
* @Date: 2021/8/18 8:43 下午
* @Version: 1.0
*/
public class PayPrivateKeyUtil {
/**
* 算法
*/
public static final String KEY_ALGORITHM = "RSA";
/**
* 获取商户私钥
*
* @param keyPath 商户私钥证书路径
* @return 商户私钥
* @throws Exception 解析 key 异常
*/
public static String getPrivateKey(String keyPath) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new FileReader(keyPath));
StringBuilder apiClientKeySB = new StringBuilder();
for (String line; (line = bufferedReader.readLine()) != null; ) {
apiClientKeySB.append(line);
}
String originalKey = apiClientKeySB.toString();
System.out.println(originalKey);
String privateKey = originalKey
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
return PayPrivateKeyUtil.getPrivateKeyStr(PayPrivateKeyUtil.loadPrivateKey(privateKey));
}
/**
* 私钥字符串
*
* @param privateKey
* @return
* @throws UnsupportedEncodingException
*/
public static String getPrivateKeyStr(PrivateKey privateKey) throws UnsupportedEncodingException {
return new String(Base64.encode(privateKey.getEncoded()), ISystemConstant.CHARSET_UTF8);
}
/**
* 从字符串中加载私钥
*
* @param privateKeyStr 私钥
* @return {@link PrivateKey}
* @throws Exception 异常信息
*/
public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception {
try {
byte[] buffer = Base64.decode(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
return keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new Exception("无此算法");
} catch (InvalidKeySpecException e) {
throw new Exception("私钥非法");
} catch (NullPointerException e) {
throw new Exception("私钥数据为空");
}
}
/**
* 私钥签名
*
* @param data 需要加密的数据
* @param privateKey 私钥
* @return 加密后的数据
* @throws Exception 异常信息
*/
public static String encryptByPrivateKey(String data, String privateKey) throws Exception {
PKCS8EncodedKeySpec priPkcs8 = new PKCS8EncodedKeySpec(Base64.decode(privateKey));
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PrivateKey priKey = keyFactory.generatePrivate(priPkcs8);
java.security.Signature signature = Signature.getInstance("SHA256WithRSA");
signature.initSign(priKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
byte[] signed = signature.sign();
return new String(Base64.encode(signed), ISystemConstant.CHARSET_UTF8);
}
}

View File

@ -0,0 +1,124 @@
package ink.wgink.module.wechat.utils.pay;
import ink.wgink.interfaces.consts.ISystemConstant;
import org.springframework.web.bind.annotation.RequestMethod;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
/**
* @ClassName: PaySignAuthorizationUtil
* @Description: 支付签名和认证工具
* @Author: wanggeng
* @Date: 2021/8/18 5:45 下午
* @Version: 1.0
*/
public class PaySignAuthorizationUtil {
// 商户号1609461201
// APIv3秘钥_TSKJ_0471_TSkj_0471_tsKJ_0471__
/**
* @param requestMethod 请求方法
* @param url 请求接口
* @param timestamp 时间戳
* @param nonceStr 随机字符串
* @param body 请求主体
* @return
*/
public static String getSign(RequestMethod requestMethod, String url, long timestamp, String nonceStr, String body) {
return new StringBuilder()
.append(requestMethod.toString())
.append("\n")
.append(url)
.append("\n")
.append(timestamp)
.append("\n")
.append(nonceStr)
.append("\n")
.append(body)
.append("\n")
.toString();
}
/**
* @param method 方法
* @param urlSuffix 可通过 WxApiType 来获取URL挂载参数需要自行拼接
* @param mchId 商户Id
* @param serialNo 商户 API 证书序列号
* @param keyPath key.pem 证书路径
* @param body 接口请求参数
* @param nonceStr 随机字符库
* @param timestamp 时间戳
* @param authType 认证类型
* @return
* @throws Exception
*/
public static String buildAuthorization(RequestMethod method, String urlSuffix, String mchId,
String serialNo, String keyPath, String body, String nonceStr,
long timestamp, String authType) throws Exception {
// 构建签名参数
String buildSignMessage = getSign(method, urlSuffix, timestamp, nonceStr, body);
// 获取商户私钥
String key = PayPrivateKeyUtil.getPrivateKey(keyPath);
// 生成签名
String signature = PayPrivateKeyUtil.encryptByPrivateKey(buildSignMessage, key);
// 根据平台规则生成请求头 authorization
return getAuthorization(mchId, serialNo, nonceStr, String.valueOf(timestamp), signature, authType);
}
/**
* @param mchId 方法
* @param serialNo 商户 API 证书序列号
* @param nonceStr 随机字符库
* @param timestamp 时间戳
* @param signature 签名值
* @param authType 认证类型
* @return
*/
private static String getAuthorization(String mchId, String serialNo, String nonceStr, String timestamp, String signature, String authType) throws UnsupportedEncodingException {
Map<String, String> params = new HashMap<>(5);
params.put("mchid", mchId);
params.put("serial_no", serialNo);
params.put("nonce_str", nonceStr);
params.put("timestamp", timestamp);
params.put("signature", signature);
return authType.concat(" ").concat(createLinkString(params, ",", false, true));
}
/**
* 链接字符串
*
* @param params map
* @param connStr 链接值
* @param encode 是否编码
* @param quotes 是否加引号
* @return
*/
private static String createLinkString(Map<String, String> params, String connStr, boolean encode, boolean quotes) throws UnsupportedEncodingException {
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
StringBuilder content = new StringBuilder();
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
// 拼接时不包括最后一个&字符
if (i == keys.size() - 1) {
if (quotes) {
content.append(key).append("=").append('"').append(encode ? URLEncoder.encode(value, ISystemConstant.CHARSET_UTF8) : value).append('"');
} else {
content.append(key).append("=").append(encode ? URLEncoder.encode(value, ISystemConstant.CHARSET_UTF8) : value);
}
} else {
if (quotes) {
content.append(key).append("=").append('"').append(encode ? URLEncoder.encode(value, ISystemConstant.CHARSET_UTF8) : value).append('"').append(connStr);
} else {
content.append(key).append("=").append(encode ? URLEncoder.encode(value, ISystemConstant.CHARSET_UTF8) : value).append(connStr);
}
}
}
return content.toString();
}
}

View File

@ -0,0 +1,56 @@
package ink.wgink.module.wechat.utils.pay;
import org.bouncycastle.util.encoders.Base64;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
/**
* @ClassName: PayVerifyUtil
* @Description: 验证工具
* @Author: wanggeng
* @Date: 2021/8/18 11:20 下午
* @Version: 1.0
*/
public class PayVerifyUtil {
/**
* 验证签名
*
* @param signature
* @param body
* @param nonce
* @param timestamp
* @param certInputStream
* @return
* @throws Exception
*/
public static boolean verifySignature(String signature, String body, String nonce, String timestamp, InputStream certInputStream) throws Exception {
String buildSignMessage = buildSignMessage(timestamp, nonce, body);
// 获取证书
X509Certificate certificate = PayCertificateUtil.getCertificate(certInputStream);
PublicKey publicKey = certificate.getPublicKey();
return checkByPublicKey(buildSignMessage, signature, publicKey);
}
public static boolean checkByPublicKey(String data, String sign, PublicKey publicKey) throws Exception {
java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA");
signature.initVerify(publicKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
return signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8)));
}
public static String buildSignMessage(String timestamp, String nonceStr, String body) {
return new StringBuilder()
.append(timestamp)
.append("\n")
.append(nonceStr)
.append("\n")
.append(body)
.append("\n")
.toString();
}
}

View File

@ -0,0 +1,74 @@
package ink.wgink.module.wechat.utils.pay;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Base64;
/**
* @ClassName: SensitiveDataUtil
* @Description: 敏感信息工具
* @Author: wanggeng
* @Date: 2021/8/18 11:54 下午
* @Version: 1.0
*/
public class SensitiveDataUtil {
/**
* 加密
*
* @param message 加密内容
* @param certificate 证书
* @return
* @throws IllegalBlockSizeException
* @throws IOException
*/
public static String rsaEncryptOAEP(String message, X509Certificate certificate) throws IllegalBlockSizeException, IOException {
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
byte[] data = message.getBytes("utf-8");
byte[] cipherdata = cipher.doFinal(data);
return Base64.getEncoder().encodeToString(cipherdata);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("无效的证书", e);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
}
}
/**
* 解密
*
* @param ciphertext 密文
* @param privateKey 私钥
* @return
* @throws BadPaddingException
* @throws IOException
*/
public static String rsaDecryptOAEP(String ciphertext, PrivateKey privateKey) throws BadPaddingException, IOException {
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] data = Base64.getDecoder().decode(ciphertext);
return new String(cipher.doFinal(data), "utf-8");
} catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("无效的私钥", e);
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new BadPaddingException("解密失败");
}
}
}

View File

@ -121,7 +121,14 @@
<if test="userIds != null and userIds.size > 0">
user_id IN
<foreach collection="userIds" index="index" open="(" separator="," close=")">
#{userIds[${index}]}
#{userIds[${index}]}
</foreach>
</if>
<if test="userCodes != null and userCodes.size > 0">
AND
user_code IN
<foreach collection="userCodes" index="index" open="(" separator="," close=")">
#{userCodes[${index}]}
</foreach>
</if>
</where>
@ -152,6 +159,10 @@
AND
user_id = #{userId}
</if>
<if test="userCode != null and userCode != ''">
AND
user_code = #{userCode}
</if>
</where>
</select>

View File

@ -220,6 +220,13 @@
<if test="status != null and status != ''">
status = #{status}
</if>
<if test="userCodes != null and userCodes.size > 0">
AND
user_code IN
<foreach collection="userCodes" index="index" open="(" separator="," close=")">
#{userCodes[${index}]}
</foreach>
</if>
</where>
</select>