Просмотр исходного кода

mall + pay:
1. 微信支付配置,补全 apiclient_cert.p12 证书

YunaiV 1 год назад
Родитель
Сommit
66ed61c641

+ 12 - 5
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.codec.Base64;
 import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.date.TemporalAccessorUtil;
 import cn.hutool.core.lang.Assert;
@@ -34,6 +35,7 @@ import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
 
 /**
  * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
@@ -59,12 +61,17 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         WxPayConfig payConfig = new WxPayConfig();
         BeanUtil.copyProperties(config, payConfig, "keyContent");
         payConfig.setTradeType(tradeType);
+//        if (WxPayClientConfig.API_VERSION_V2.equals(config.getApiVersion())) {
+//            payConfig.setSignType(WxPayConstants.SignType.MD5);
+//        }
+        // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
+        if (Base64.isBase64(config.getKeyContent())) {
+            payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
+        }
         if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
-            // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
             payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
         }
         if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
-            // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
             payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
         }
 
@@ -77,7 +84,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     protected PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
         try {
             switch (config.getApiVersion()) {
-                case WxPayClientConfig.API_VERSION_V2:
+                case API_VERSION_V2:
                     return doUnifiedOrderV2(reqDTO);
                 case WxPayClientConfig.API_VERSION_V3:
                     return doUnifiedOrderV3(reqDTO);
@@ -112,7 +119,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
         try {
             switch (config.getApiVersion()) {
-                case WxPayClientConfig.API_VERSION_V2:
+                case API_VERSION_V2:
                     return doUnifiedRefundV2(reqDTO);
                 case WxPayClientConfig.API_VERSION_V3:
                     return doUnifiedRefundV3(reqDTO);
@@ -160,7 +167,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         try {
             // 微信支付 v2 回调结果处理
             switch (config.getApiVersion()) {
-                case WxPayClientConfig.API_VERSION_V2:
+                case API_VERSION_V2:
                     return parseOrderNotifyV2(body);
                 case WxPayClientConfig.API_VERSION_V3:
                     return parseOrderNotifyV3(body);

+ 11 - 15
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java

@@ -37,16 +37,17 @@ public class WxPayClientConfig implements PayClientConfig {
      *
      * 只有公众号或小程序需要该字段
      */
+    @NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
     private String appId;
     /**
      * 商户号
      */
-    @NotBlank(message = "商户号 不能为空", groups = {V2.class, V3.class})
+    @NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
     private String mchId;
     /**
      * API 版本
      */
-    @NotBlank(message = "API 版本 不能为空", groups = {V2.class, V3.class})
+    @NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
     private String apiVersion;
 
     // ========== V2 版本的参数 ==========
@@ -54,36 +55,31 @@ public class WxPayClientConfig implements PayClientConfig {
     /**
      * 商户密钥
      */
-    @NotBlank(message = "商户密钥 不能为空", groups = V2.class)
+    @NotBlank(message = "商户密钥不能为空", groups = V2.class)
     private String mchKey;
     /**
-     * apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径.
-     * 对应的字符串
+     * apiclient_cert.p12 证书文件的对应字符串【base64 格式】
      *
-     * 注意,可通过 {@link #main(String[])} 读取
+     * 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
      */
-    /// private String keyContent;
+    @NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
+    private String keyContent;
 
     // ========== V3 版本的参数 ==========
     /**
-     * apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
-     * 对应的字符串
-     * 注意,可通过 {@link #main(String[])} 读取
+     * apiclient_key.pem 证书文件的对应字符串
      */
     @NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
     private String privateKeyContent;
     /**
-     * apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
-     * 对应的字符串
-     * <p>
-     * 注意,可通过 {@link #main(String[])} 读取
+     * apiclient_cert.pem 证书文件的对应的字符串
      */
     @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
     private String privateCertContent;
     /**
      * apiV3 密钥值
      */
-    @NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class)
+    @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
     private String apiV3Key;
 
     /**

+ 2 - 4
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java

@@ -44,7 +44,7 @@ public class WxBarPayClientIntegrationTest {
                 .totalFee(1) // 单位分
                 .timeExpire(formatDateV2(LocalDateTimeUtils.addTime(Duration.ofMinutes(2))))
                 .spbillCreateIp("127.0.0.1")
-                .authCode("131276541518138032")
+                .authCode("134298744426278497")
                 .build();
         System.out.println("========= request ==========");
         System.out.println(JsonUtils.toJsonPrettyString(request));
@@ -63,7 +63,7 @@ public class WxBarPayClientIntegrationTest {
 
         // 执行发起退款
         WxPayRefundRequest request = new WxPayRefundRequest()
-                .setOutTradeNo("1689504162805")
+                .setOutTradeNo("1689545667276")
                 .setOutRefundNo(String.valueOf(System.currentTimeMillis()))
                 .setRefundFee(1)
                 .setRefundDesc("就是想退了")
@@ -103,8 +103,6 @@ public class WxBarPayClientIntegrationTest {
         config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim");
         config.setSignType(WxPayConstants.SignType.MD5);
         config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12");
-        config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem");
-        config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem");
         return config;
     }
 

+ 1 - 3
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java

@@ -58,7 +58,7 @@ public class WxNativePayClientIntegrationTest {
 
         // 执行发起退款
         WxPayRefundV3Request request = new WxPayRefundV3Request()
-                .setOutTradeNo("1689506153043")
+                .setOutTradeNo("1689545729695")
                 .setOutRefundNo(String.valueOf(System.currentTimeMillis()))
                 .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY"))
                 .setReason("就是想退了");
@@ -73,10 +73,8 @@ public class WxNativePayClientIntegrationTest {
         WxPayConfig config = new WxPayConfig();
         config.setAppId("wx62056c0d5e8db250");
         config.setMchId("1545083881");
-        config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim");
         config.setApiV3Key("459arNsYHl1mgkiO6H9ZH5KkhFXSxaA4");
 //        config.setCertSerialNo(serialNo);
-        config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12");
         config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem");
         config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem");
         return config;

+ 70 - 32
yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue

@@ -8,8 +8,8 @@
             <template slot="append">%</template>
           </el-input>
         </el-form-item>
-        <el-form-item label-width="180px" label="公众号APPID" prop="weChatConfig.appId">
-          <el-input v-model="form.weChatConfig.appId" placeholder="请输入公众号APPID" clearable :style="{width: '100%'}">
+        <el-form-item label-width="180px" label="公众号 APPID" prop="weChatConfig.appId">
+          <el-input v-model="form.weChatConfig.appId" placeholder="请输入公众号 APPID" clearable :style="{width: '100%'}">
           </el-input>
         </el-form-item>
         <el-form-item label-width="180px" label="商户号" prop="weChatConfig.mchId">
@@ -29,29 +29,41 @@
             </el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey"
-                      v-if="form.weChatConfig.apiVersion === 'v2'">
-          <el-input v-model="form.weChatConfig.mchKey" placeholder="请输入商户密钥" clearable
-                    :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
-        </el-form-item>
+        <div v-if="form.weChatConfig.apiVersion === 'v2'">
+          <el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey">
+            <el-input v-model="form.weChatConfig.mchKey" placeholder="请输入商户密钥" clearable
+                      :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
+          </el-form-item>
+          <el-form-item label-width="180px" label="apiclient_cert.p12 证书" prop="weChatConfig.keyContent">
+            <el-input v-model="form.weChatConfig.keyContent" type="textarea"
+                      placeholder="请上传 apiclient_cert.p12 证书"
+                      readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
+          </el-form-item>
+          <el-form-item label-width="180px" label="">
+            <el-upload :limit="1" accept=".p12" action=""
+                       :before-upload="p12FileBeforeUpload"
+                       :http-request="keyContentUpload">
+              <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
+            </el-upload>
+          </el-form-item>
+        </div>
         <div v-if="form.weChatConfig.apiVersion === 'v3'">
-          <el-form-item label-width="180px" label="API V3密钥" prop="weChatConfig.apiV3Key">
-            <el-input v-model="form.weChatConfig.apiV3Key" placeholder="请输入API V3密钥" clearable
+          <el-form-item label-width="180px" label="API V3 密钥" prop="weChatConfig.apiV3Key">
+            <el-input v-model="form.weChatConfig.apiV3Key" placeholder="请输入 API V3 密钥" clearable
                       :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
           </el-form-item>
-          <el-form-item label-width="180px" label="apiclient_key.perm证书" prop="weChatConfig.privateKeyContent">
+          <el-form-item label-width="180px" label="apiclient_key.perm 证书" prop="weChatConfig.privateKeyContent">
             <el-input v-model="form.weChatConfig.privateKeyContent" type="textarea"
-                      placeholder="请上传apiclient_key.perm证书"
+                      placeholder="请上传 apiclient_key.perm 证书"
                       readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
           </el-form-item>
           <el-form-item label-width="180px" label="" prop="privateKeyContentFile">
             <el-upload ref="privateKeyContentFile"
                        :limit="1"
-                       :accept="fileAccept"
-                       :headers="header"
+                       accept=".pem"
                        action=""
                        :before-upload="pemFileBeforeUpload"
-                       :http-request="privateKeyUpload"
+                       :http-request="privateKeyContentUpload"
             >
               <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
             </el-upload>
@@ -64,18 +76,17 @@
           <el-form-item label-width="180px" label="" prop="privateCertContentFile">
             <el-upload ref="privateCertContentFile"
                        :limit="1"
-                       :accept="fileAccept"
-                       :headers="header"
+                       accept=".pem"
                        action=""
                        :before-upload="pemFileBeforeUpload"
-                       :http-request="privateCertUpload"
+                       :http-request="privateCertContentUpload"
             >
               <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
             </el-upload>
           </el-form-item>
         </div>
         <el-form-item label-width="180px" label="备注" prop="remark">
-          <el-input v-model="form.remark" :style="{width: '100%'}"></el-input>
+          <el-input v-model="form.remark" :style="{width: '100%'}" />
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
@@ -100,6 +111,7 @@ const defaultForm = {
     mchId: '',
     apiVersion: '',
     mchKey: '',
+    keyContent: '',
     privateKeyContent: '',
     privateCertContent: '',
     apiV3Key:'',
@@ -159,27 +171,27 @@ export default {
           message: '请输入商户密钥',
           trigger: 'blur'
         }],
+        'weChatConfig.keyContent': [{
+          required: true,
+          message: '请上传 apiclient_cert.p12 证书',
+          trigger: 'blur'
+        }],
         'weChatConfig.privateKeyContent': [{
           required: true,
-          message: '请上传apiclient_key.perm证书',
+          message: '请上传 apiclient_key.perm 证书',
           trigger: 'blur'
         }],
         'weChatConfig.privateCertContent': [{
           required: true,
-          message: '请上传apiclient_cert.perm证书',
+          message: '请上传 apiclient_cert.perm证 书',
           trigger: 'blur'
         }],
         'weChatConfig.apiV3Key': [{
           required: true,
-          message: '请上传apiV3密钥值',
+          message: '请上传 api V3 密钥值',
           trigger: 'blur'
         }],
       },
-      // 文件上传的header
-      header: {
-        "Authorization": null
-      },
-      fileAccept: ".pem",
       // 渠道状态 数据字典
       statusDictDatas: getDictDatas(DICT_TYPE.COMMON_STATUS),
       versionDictDatas: getDictDatas(DICT_TYPE.PAY_CHANNEL_WECHAT_VERSION),
@@ -218,6 +230,7 @@ export default {
           this.form.weChatConfig.apiVersion = config.apiVersion;
           this.form.weChatConfig.mchId = config.mchId;
           this.form.weChatConfig.mchKey = config.mchKey;
+          this.form.weChatConfig.keyContent = config.keyContent;
           this.form.weChatConfig.privateKeyContent = config.privateKeyContent;
           this.form.weChatConfig.privateCertContent = config.privateCertContent;
           this.form.weChatConfig.apiV3Key = config.apiV3Key;
@@ -241,7 +254,6 @@ export default {
               this.$modal.msgSuccess("修改成功");
               this.close();
             }
-
           })
         } else {
 
@@ -255,10 +267,14 @@ export default {
         }
       });
     },
-    pemFileBeforeUpload(file) {
+    /**
+     * apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem 上传前的校验
+     */
+    fileBeforeUpload(file, fileAccept) {
       let format = '.' + file.name.split(".")[1];
-      if (format !== this.fileAccept) {
-        this.$message.error('请上传指定格式"' + this.fileAccept + '"文件');
+      if (format !== fileAccept) {
+        debugger
+        this.$message.error('请上传指定格式"' + fileAccept + '"文件');
         return false;
       }
       let isRightSize = file.size / 1024 / 1024 < 2
@@ -267,19 +283,41 @@ export default {
       }
       return isRightSize
     },
-    privateKeyUpload(event) {
+    p12FileBeforeUpload(file) {
+      this.fileBeforeUpload(file, '.p12')
+    },
+    pemFileBeforeUpload(file) {
+      this.fileBeforeUpload(file, '.pem')
+    },
+    /**
+     * 读取 apiclient_key.pem 到 privateKeyContent 字段
+     */
+    privateKeyContentUpload(event) {
       const readFile = new FileReader()
       readFile.onload = (e) => {
         this.form.weChatConfig.privateKeyContent = e.target.result
       }
       readFile.readAsText(event.file);
     },
-    privateCertUpload(event) {
+    /**
+     * 读取 apiclient_cert.pem 到 privateCertContent 字段
+     */
+    privateCertContentUpload(event) {
       const readFile = new FileReader()
       readFile.onload = (e) => {
         this.form.weChatConfig.privateCertContent = e.target.result
       }
       readFile.readAsText(event.file);
+    },
+    /**
+     * 读取 apiclient_cert.p12 到 keyContent 字段
+     */
+    keyContentUpload(event) {
+      const readFile = new FileReader()
+      readFile.onload = (e) => {
+        this.form.weChatConfig.keyContent = e.target.result.split(',')[1]
+      }
+      readFile.readAsDataURL(event.file); // 读成 base64
     }
   }
 }

+ 24 - 0
yudao-ui-admin/src/views/pay/app/index.vue

@@ -143,6 +143,30 @@
             </el-button>
           </template>
         </el-table-column>
+        <el-table-column :label="payChannelEnum.WX_NATIVE.name" align="center">
+          <template v-slot="scope">
+            <el-button type="success" icon="el-icon-check" circle
+                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_NATIVE.code)"
+                       @click="handleUpdateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
+            </el-button>
+            <el-button v-else
+                       type="danger" icon="el-icon-close" circle
+                       @click="handleCreateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
+            </el-button>
+          </template>
+        </el-table-column>
+        <el-table-column :label="payChannelEnum.WX_BAR.name" align="center">
+          <template v-slot="scope">
+            <el-button type="success" icon="el-icon-check" circle
+                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_BAR.code)"
+                       @click="handleUpdateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
+            </el-button>
+            <el-button v-else
+                       type="danger" icon="el-icon-close" circle
+                       @click="handleCreateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
+            </el-button>
+          </template>
+        </el-table-column>
       </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template v-slot="scope">