pom.xml
@@ -28,6 +28,11 @@ </properties> <dependencies> <dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-java</artifactId> <version>0.2.12</version> </dependency> <!-- 图片压缩--> <dependency> <groupId>net.coobird</groupId> src/main/java/cc/mrbird/febs/common/configure/WebMvcConfigure.java
@@ -31,5 +31,6 @@ registration.excludePathPatterns("/api/leader/noLoginLeaderTitle"); registration.excludePathPatterns("/api/xcxPay/wxpayCallback"); registration.excludePathPatterns("/api/xcxPay/rechargeCallBack"); registration.excludePathPatterns("/api/xcxPay/fapiaoCallBack"); } } src/main/java/cc/mrbird/febs/common/enumerates/DataDictionaryEnum.java
@@ -5,6 +5,9 @@ @Getter public enum DataDictionaryEnum { // 发票的通知回调路径 FP_CALLBACK_URL("FP_CALLBACK_URL", "FP_CALLBACK_URL"), //微信订阅模板ID, // 微信订单通知 // WX_TEMPLATE_ID_ONE("WX_TEMPLATE", "WX_TEMPLATE_ID_ONE"), src/main/java/cc/mrbird/febs/common/properties/XcxProperties.java
@@ -26,6 +26,8 @@ private String wecharpayMchid; //支付秘钥 private String wecharpaySecret; //支付秘钥V3 private String wecharpaySecretV3; //高德KEY private String gaodeKey; //高德serviceName src/main/java/cc/mrbird/febs/mall/controller/AdminSystemController.java
@@ -3,18 +3,22 @@ import cc.mrbird.febs.common.annotation.ControllerEndpoint; import cc.mrbird.febs.common.entity.FebsResponse; import cc.mrbird.febs.common.enumerates.DataDictionaryEnum; import cc.mrbird.febs.mall.dto.AdminAgentAmountDto; import cc.mrbird.febs.mall.dto.AdminAgentDetailDto; import cc.mrbird.febs.mall.dto.AdminIndexVideoDto; import cc.mrbird.febs.mall.dto.CashOutSettingDto; import cc.mrbird.febs.mall.dto.*; import cc.mrbird.febs.mall.entity.DataDictionaryCustom; import cc.mrbird.febs.mall.mapper.DataDictionaryCustomMapper; import cc.mrbird.febs.mall.service.ICommonService; import cc.mrbird.febs.mall.service.ISystemService; import cc.mrbird.febs.pay.model.HeaderDto; import cc.mrbird.febs.pay.service.WxFaPiaoService; import cn.hutool.core.util.ObjectUtil; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSONObject; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.RequestEntity; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; @@ -22,7 +26,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.math.BigDecimal; import java.security.KeyPair; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -39,6 +45,7 @@ private final ICommonService commonService; private final DataDictionaryCustomMapper dataDictionaryCustomMapper; private final WxFaPiaoService wxFaPiaoService; @PostMapping(value = "/bonusSystemSetting") public FebsResponse bonusSystemSetting(@RequestBody Map<String, Object> map) { @@ -122,6 +129,48 @@ return new FebsResponse().success().message("操作成功"); } @PostMapping(value = "/faPiaoSet") public FebsResponse faPiaoSet(FaPiaoDto faPiaoDto) { dataDictionaryCustomMapper.updateDicValueByTypeAndCode( DataDictionaryEnum.FP_CALLBACK_URL.getType(), DataDictionaryEnum.FP_CALLBACK_URL.getCode(), faPiaoDto.getCallbackUrl() ); KeyPair privateKey = wxFaPiaoService.getPrivateKey(); HeaderDto headerDto = new HeaderDto(); headerDto.setCallback_url(faPiaoDto.getCallbackUrl()); headerDto.setShow_fapiao_cell(false); String parseObj = JSONUtil.parseObj(headerDto).toString(); String baseUrl = "https://api.mch.weixin.qq.com"; String canonicalUrl = "/v3/new-tax-control-fapiao/merchant/development-config"; String postStr = wxFaPiaoService.createAuthorization( "POST", canonicalUrl, parseObj, privateKey ); // 创建httppost try { HttpClient httpClient = new HttpClient(); PostMethod post = new PostMethod(baseUrl+canonicalUrl); post.setRequestHeader("Accept", "application/json"); post.setRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36"); post.setRequestHeader("Content-Type", "application/json"); post.setRequestHeader("Connection", "keep-alive"); post.setRequestHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 "+postStr); RequestEntity entity = new StringRequestEntity(parseObj, "text/html", "utf-8"); post.setRequestEntity(entity); httpClient.executeMethod(post); String responseBodyAsString = post.getResponseBodyAsString(); cn.hutool.json.JSONObject maps = JSONUtil.parseObj(responseBodyAsString); System.out.println(maps); } catch (IOException e) { e.printStackTrace(); } return new FebsResponse().success().message("操作成功"); } @PostMapping(value = "/agentDetail") public FebsResponse agentDetail(AdminAgentDetailDto adminAgentDetailDto) { DataDictionaryCustom dic = dataDictionaryCustomMapper.selectDicDataByTypeAndCode( src/main/java/cc/mrbird/febs/mall/controller/ViewSystemController.java
@@ -99,4 +99,19 @@ model.addAttribute("indexVideoSet", adminIndexVideoDto); return FebsUtil.view("modules/system/indexVideo"); } @GetMapping("faPiao") @RequiresPermissions("faPiao:update") public String faPiao(Model model) { FaPiaoDto faPiaoDto = new FaPiaoDto(); DataDictionaryCustom dic = dataDictionaryCustomMapper.selectDicDataByTypeAndCode(DataDictionaryEnum.FP_CALLBACK_URL.getType(), DataDictionaryEnum.FP_CALLBACK_URL.getCode()); if (dic != null) { faPiaoDto.setCallbackUrl(dic.getValue()); } model.addAttribute("faPiaoDto", faPiaoDto); return FebsUtil.view("modules/system/faPiao"); } } src/main/java/cc/mrbird/febs/mall/dto/FaPiaoDto.java
New file @@ -0,0 +1,10 @@ package cc.mrbird.febs.mall.dto; import io.swagger.annotations.ApiModel; import lombok.Data; @Data @ApiModel(value = "DeliverGoodsDto", description = "接收参数类") public class FaPiaoDto { private String callbackUrl; } src/main/java/cc/mrbird/febs/pay/controller/XcxPayController.java
@@ -12,12 +12,7 @@ import cc.mrbird.febs.mall.service.IApiMallMemberWalletService; import cc.mrbird.febs.mall.service.IMallMoneyFlowService; import cc.mrbird.febs.pay.model.NotifyData; import cc.mrbird.febs.pay.model.OrderStateDto; import cc.mrbird.febs.pay.model.OrderStateMsgVo; import cc.mrbird.febs.pay.model.WxTemplateData; import cc.mrbird.febs.pay.service.IPayService; import cc.mrbird.febs.pay.service.IXcxPayService; import cc.mrbird.febs.pay.util.PayThreadPool; import cc.mrbird.febs.pay.util.Signature; import cc.mrbird.febs.pay.util.Util; import cc.mrbird.febs.pay.util.WechatConfigure; @@ -26,17 +21,12 @@ import cn.hutool.core.util.ObjectUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.thymeleaf.engine.TemplateData; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; @@ -247,6 +237,19 @@ // System.out.println(rechargeNo); // } /** * 微信电子发票回调接口 * @return */ @Transactional(rollbackFor = Exception.class) @RequestMapping(value = "/fapiaoCallBack") public Map<Object, Object> fapiaoCallBack(HttpServletResponse response, HttpServletRequest request) throws IOException { log.info("微信电子发票回调接口...."); Map<Object, Object> objectObjectHashMap = new HashMap<>(); objectObjectHashMap.put("code","SUCCESS"); objectObjectHashMap.put("message",""); return objectObjectHashMap; } /** * 微信支付回调接口 */ @Transactional(rollbackFor = Exception.class) src/main/java/cc/mrbird/febs/pay/model/HeaderDto.java
New file @@ -0,0 +1,12 @@ package cc.mrbird.febs.pay.model; import com.sun.org.apache.xpath.internal.operations.Bool; import lombok.Data; @Data public class HeaderDto { private String callback_url; private boolean show_fapiao_cell; } src/main/java/cc/mrbird/febs/pay/service/WxFaPiaoService.java
New file @@ -0,0 +1,11 @@ package cc.mrbird.febs.pay.service; import java.security.KeyPair; public interface WxFaPiaoService { String createAuthorization(String method, String canonicalUrl, String body, KeyPair keyPair);//生成Token KeyPair getPrivateKey();//通过证书获取私钥公钥 } src/main/java/cc/mrbird/febs/pay/service/impl/WxFaPiaoServiceImpl.java
New file @@ -0,0 +1,108 @@ package cc.mrbird.febs.pay.service.impl; import cc.mrbird.febs.common.properties.XcxProperties; import cc.mrbird.febs.common.utils.SpringContextHolder; import cc.mrbird.febs.pay.service.WxFaPiaoService; import cc.mrbird.febs.pay.util.RandomStringGenerator; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import okhttp3.HttpUrl; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Service; import org.springframework.util.Base64Utils; import java.io.*; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.cert.X509Certificate; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @Slf4j @Service @RequiredArgsConstructor public class WxFaPiaoServiceImpl implements WxFaPiaoService { private final XcxProperties xcxProperties = SpringContextHolder.getBean(XcxProperties.class); @Override public String createAuthorization(String method, String canonicalUrl, String body, KeyPair keyPair) { String nonceStr = RandomStringGenerator.getRandomStringByLength(32);//随机字符串 long timestamp = System.currentTimeMillis() / 1000;//时间戳 String signature = sign(method, canonicalUrl, timestamp, nonceStr, body, keyPair);//签名加密 return "mchid=\"" + xcxProperties.getWecharpayMchid() + "\"," + "nonce_str=\"" + nonceStr + "\"," + "timestamp=\"" + timestamp + "\"," + "serial_no=\"" + "50F37206347BCC9E6AC9860DAACE52AC035F7C24" + "\","//证书序列号 + "signature=\"" + signature + "\""; } @Override public KeyPair getPrivateKey() { return createPKCS12("Tenpay Certificate", "1658958205"); } /** * V3 SHA256withRSA 签名. * * @param method 请求方法 GET POST PUT DELETE 等 * @param canonicalUrl 例如 https://api.mch.weixin.qq.com/v3/pay/transactions/app?version=1 ——> /v3/pay/transactions/app?version=1 * @param timestamp 当前时间戳 因为要配置到TOKEN 中所以 签名中的要跟TOKEN 保持一致 * @param nonceStr 随机字符串 要和TOKEN中的保持一致 * @param body 请求体 GET 为 "" POST 为JSON * @param keyPair 商户API 证书解析的密钥对 实际使用的是其中的私钥 * @return the string */ @SneakyThrows public String sign(String method, String canonicalUrl, long timestamp, String nonceStr, String body, KeyPair keyPair) { String signatureStr = Stream.of(method, canonicalUrl, String.valueOf(timestamp), nonceStr, body) .collect(Collectors.joining("\n", "", "\n")); Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(keyPair.getPrivate()); sign.update(signatureStr.getBytes(StandardCharsets.UTF_8)); return Base64Utils.encodeToString(sign.sign()); } /** * 获取公私钥.通过证书 */ private KeyStore store; private final Object lock = new Object(); public KeyPair createPKCS12(String keyAlias, String keyPass) { ClassPathResource resource = new ClassPathResource(xcxProperties.getCertLocalPath()); // File file = new File("src/main/resources/wxP12/apiclient_cert.p12"); char[] pem = keyPass.toCharArray(); try { synchronized (lock) { if (store == null) { synchronized (lock) { store = KeyStore.getInstance("PKCS12"); store.load(resource.getInputStream(), pem); // store.load(new FileInputStream(file), pem); } } } X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias); certificate.checkValidity(); // 证书的序列号 也有用 50F37206347BCC9E6AC9860DAACE52AC035F7C24 String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase(); // 证书的 公钥 PublicKey publicKey = certificate.getPublicKey(); // 证书的私钥 PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem); return new KeyPair(publicKey, storeKey); } catch (Exception e) { throw new IllegalStateException("Cannot load keys from store: " , e); } } public static void main(String[] args) { try { System.out.println(new ClassPathResource("wxP12/apiclient_cert.p12").getFile().exists()); } catch (IOException e) { e.printStackTrace(); } } } src/main/resources/application-prod.yml
@@ -69,6 +69,7 @@ certLocalPath: /home/blnkaCert/apiclient_cert.p12 wecharpayMchid: 1658958205 wecharpaySecret: daL341aN5orDt13puXadsAf2rpuXdq4r wecharpaySecretV3: daL341aN5orDt13puXadsAf2rpuX12v3 gaodeKey: 95ede7157929f5f6b6c758971be924b1 serviceName: yiyuanshucai src/main/resources/application-test.yml
@@ -78,6 +78,7 @@ certLocalPath: /home/blnkaCert/apiclient_cert.p12 wecharpayMchid: 1658958205 wecharpaySecret: daL341aN5orDt13puXadsAf2rpuXdq4r wecharpaySecretV3: daL341aN5orDt13puXadsAf2rpuX12v3 gaodeKey: 95ede7157929f5f6b6c758971be924b1 serviceName: yiyuanshucai src/main/resources/templates/febs/views/modules/system/faPiao.html
New file @@ -0,0 +1,75 @@ <div class="layui-fluid layui-anim febs-anim" id="fa-piao-set" lay-title="发票回调地址设置"> <div class="layui-row layui-col-space8 febs-container"> <form class="layui-form" action="" lay-filter="fa-piao-set-form"> <div class="layui-card"> <div class="layui-card-body"> <!-- <div class="layui-form-item">--> <!-- <blockquote class="layui-elem-quote blue-border">是否</blockquote>--> <!-- <label class="layui-form-label">充值送金额:</label>--> <!-- <div class="layui-input-block">--> <!-- <input type="radio" name="giveState" value="1" title="开启" lay-filter="giveStateOpen" />--> <!-- <input type="radio" name="giveState" value="2" title="关闭" lay-filter="giveStateOpen" checked/>--> <!-- </div>--> <!-- </div>--> <div class="layui-form-item activityBulletin-input febs-hide"> <label class="layui-form-label">回调地址:</label> <div class="layui-input-block"> <input id="callbackUrl" type="text" name="callbackUrl" placeholder="" autocomplete="off" class="layui-input"> <div class="layui-form-mid layui-word-aux">收取微信的授权通知、开票通知、插卡通知等相关通知。</div> </div> </div> </div> <div class="layui-card-footer"> <button class="layui-btn layui-btn-normal" lay-submit="" lay-filter="fa-piao-set-form-submit" id="submit">保存</button> </div> </div> </form> </div> </div> <style> .layui-form-label { width: 120px; } .layui-form-item .layui-input-block { margin-left: 150px; } .layui-table-form .layui-form-item { margin-bottom: 20px !important; } </style> <script data-th-inline="javascript" type="text/javascript"> layui.use(['dropdown', 'jquery', 'validate', 'febs', 'form', 'eleTree'], function () { var $ = layui.jquery, febs = layui.febs, form = layui.form, faPiaoDto = [[${faPiaoDto}]], validate = layui.validate, $view = $('#fa-piao-set'); form.verify(validate); initFPValue(); form.render(); function initFPValue() { form.val("fa-piao-set-form", { "callbackUrl": faPiaoDto.callbackUrl, }); } form.on('submit(fa-piao-set-form-submit)', function (data) { console.log(data); febs.post(ctx + 'admin/system/faPiaoSet', data.field, function (res) { if (res.code == 200) { febs.alert.success(res.message); } else { febs.alert.warn(res.message); } }); // window.location.reload(); return false; }); }); </script> src/main/resources/wxP12/apiclient_cert.p12Binary files differ
src/test/java/cc/mrbird/febs/ProfitTest.java
@@ -12,8 +12,11 @@ import cc.mrbird.febs.mall.mapper.*; import cc.mrbird.febs.mall.service.*; import cc.mrbird.febs.mall.vo.MallMemberCouponVo; import cc.mrbird.febs.pay.configure.WxPayUtil; import cc.mrbird.febs.pay.model.BrandWCPayRequestData; import cc.mrbird.febs.pay.model.HeaderDto; import cc.mrbird.febs.pay.service.IXcxPayService; import cc.mrbird.febs.pay.service.WxFaPiaoService; import cc.mrbird.febs.pay.util.WechatConfigure; import cc.mrbird.febs.rabbit.consumer.AgentConsumer; import cn.hutool.core.collection.CollUtil; @@ -22,10 +25,17 @@ import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import okhttp3.HttpUrl; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.RequestEntity; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; @@ -33,6 +43,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ResourceLoader; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; @@ -40,6 +52,7 @@ import java.math.BigDecimal; import java.net.URLConnection; import java.nio.charset.Charset; import java.security.KeyPair; import java.util.*; import java.io.ByteArrayOutputStream; import java.io.InputStream; @@ -89,18 +102,81 @@ private MallGoodsMapper mallGoodsMapper; @Autowired private CouponGoodsMapper couponGoodsMapper; @Autowired private WxFaPiaoService wxFaPiaoService; @Autowired ResourceLoader resourceLoader; @Test public void rankProfit() { // memberProfitService.rankProfit(); MallMemberCouponDto mallMemberCouponDto = new MallMemberCouponDto(); mallMemberCouponDto.setMemberId(72L); mallMemberCouponDto.setExpireTime(DateUtil.date()); List<Long> couponIds = couponGoodsMapper.selectByGoodId(10L); List<MallMemberCouponVo> mallMemberCouponVos = new ArrayList<>(); if(CollUtil.isNotEmpty(couponIds)){ mallMemberCouponVos = mallMemberCouponMapper.selectListCreateInPage(mallMemberCouponDto,couponIds); System.out.println(mallMemberCouponVos); public void rankProfit() throws IOException { // System.out.println(new ClassPathResource("wxP12/apiclient_cert.p12").getFile().exists()); // System.out.println(new File("src/main/resources/wxP12/apiclient_cert.p12").exists()); // // InputStream inputStream = new FileInputStream(file); // System.out.println(resourceLoader.getResource("classpath:/wxP12/apiclient_cert.p12").exists()); KeyPair privateKey = wxFaPiaoService.getPrivateKey(); HeaderDto headerDto = new HeaderDto(); headerDto.setCallback_url("https://api.blnka.cn/api/xcxPay/fapiaoCallBack"); headerDto.setShow_fapiao_cell(true); String parseObj = JSONUtil.parseObj(headerDto).toString(); String baseUrl = "https://api.mch.weixin.qq.com"; String canonicalUrl = "/v3/new-tax-control-fapiao/merchant/development-config"; String postStr = wxFaPiaoService.createAuthorization( "POST", canonicalUrl, parseObj, privateKey ); // 创建httppost try { HttpClient httpClient = new HttpClient(); PostMethod post = new PostMethod("https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/merchant/development-config"); post.setRequestHeader("Accept", "application/json"); post.setRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36"); post.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); post.setRequestHeader("Connection", "keep-alive"); post.setRequestHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 "+postStr); RequestEntity entity = new StringRequestEntity(parseObj, "text/html", "utf-8"); post.setRequestEntity(entity); httpClient.executeMethod(post); String responseBodyAsString = post.getResponseBodyAsString(); cn.hutool.json.JSONObject maps = JSONUtil.parseObj(responseBodyAsString); System.out.println(maps); } catch (IOException e) { e.printStackTrace(); } } @Test public void rankProfit2() throws IOException { KeyPair privateKey = wxFaPiaoService.getPrivateKey(); String baseUrl = "https://api.mch.weixin.qq.com"; String canonicalUrl = "/v3/new-tax-control-fapiao/merchant/development-config"; String postStr = wxFaPiaoService.createAuthorization( "GET", canonicalUrl, "", privateKey ); try { HttpClient httpClient = new HttpClient(); GetMethod method = new GetMethod(baseUrl+canonicalUrl); method.setRequestHeader("Accept", "application/json"); method.setRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36"); method.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); method.setRequestHeader("Connection", "keep-alive"); method.setRequestHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 "+postStr); httpClient.executeMethod(method); System.out.println(method); String responseBodyAsString = method.getResponseBodyAsString(); cn.hutool.json.JSONObject maps = JSONUtil.parseObj(responseBodyAsString); System.out.println(maps); } catch (IOException e) { e.printStackTrace(); } }