Commit 66ed61c6416e36adcc7227202130b86737bc9dda

Authored by YunaiV
1 parent 7caf8e90

mall + pay:

1. 微信支付配置,补全 apiclient_cert.p12 证书
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
1 1 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
2 2  
3 3 import cn.hutool.core.bean.BeanUtil;
  4 +import cn.hutool.core.codec.Base64;
4 5 import cn.hutool.core.date.LocalDateTimeUtil;
5 6 import cn.hutool.core.date.TemporalAccessorUtil;
6 7 import cn.hutool.core.lang.Assert;
... ... @@ -34,6 +35,7 @@ import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN;
34 35 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
35 36 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
36 37 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
  38 +import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
37 39  
38 40 /**
39 41 * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
... ... @@ -59,12 +61,17 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
59 61 WxPayConfig payConfig = new WxPayConfig();
60 62 BeanUtil.copyProperties(config, payConfig, "keyContent");
61 63 payConfig.setTradeType(tradeType);
  64 +// if (WxPayClientConfig.API_VERSION_V2.equals(config.getApiVersion())) {
  65 +// payConfig.setSignType(WxPayConstants.SignType.MD5);
  66 +// }
  67 + // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
  68 + if (Base64.isBase64(config.getKeyContent())) {
  69 + payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
  70 + }
62 71 if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
63   - // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
64 72 payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
65 73 }
66 74 if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
67   - // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
68 75 payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
69 76 }
70 77  
... ... @@ -77,7 +84,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
77 84 protected PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
78 85 try {
79 86 switch (config.getApiVersion()) {
80   - case WxPayClientConfig.API_VERSION_V2:
  87 + case API_VERSION_V2:
81 88 return doUnifiedOrderV2(reqDTO);
82 89 case WxPayClientConfig.API_VERSION_V3:
83 90 return doUnifiedOrderV3(reqDTO);
... ... @@ -112,7 +119,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
112 119 protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
113 120 try {
114 121 switch (config.getApiVersion()) {
115   - case WxPayClientConfig.API_VERSION_V2:
  122 + case API_VERSION_V2:
116 123 return doUnifiedRefundV2(reqDTO);
117 124 case WxPayClientConfig.API_VERSION_V3:
118 125 return doUnifiedRefundV3(reqDTO);
... ... @@ -160,7 +167,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
160 167 try {
161 168 // 微信支付 v2 回调结果处理
162 169 switch (config.getApiVersion()) {
163   - case WxPayClientConfig.API_VERSION_V2:
  170 + case API_VERSION_V2:
164 171 return parseOrderNotifyV2(body);
165 172 case WxPayClientConfig.API_VERSION_V3:
166 173 return parseOrderNotifyV3(body);
... ...
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 {
37 37 *
38 38 * 只有公众号或小程序需要该字段
39 39 */
  40 + @NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
40 41 private String appId;
41 42 /**
42 43 * 商户号
43 44 */
44   - @NotBlank(message = "商户号 不能为空", groups = {V2.class, V3.class})
  45 + @NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
45 46 private String mchId;
46 47 /**
47 48 * API 版本
48 49 */
49   - @NotBlank(message = "API 版本 不能为空", groups = {V2.class, V3.class})
  50 + @NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
50 51 private String apiVersion;
51 52  
52 53 // ========== V2 版本的参数 ==========
... ... @@ -54,36 +55,31 @@ public class WxPayClientConfig implements PayClientConfig {
54 55 /**
55 56 * 商户密钥
56 57 */
57   - @NotBlank(message = "商户密钥 不能为空", groups = V2.class)
  58 + @NotBlank(message = "商户密钥不能为空", groups = V2.class)
58 59 private String mchKey;
59 60 /**
60   - * apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径.
61   - * 对应的字符串
  61 + * apiclient_cert.p12 证书文件的对应字符串【base64 格式】
62 62 *
63   - * 注意,可通过 {@link #main(String[])} 读取
  63 + * 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
64 64 */
65   - /// private String keyContent;
  65 + @NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
  66 + private String keyContent;
66 67  
67 68 // ========== V3 版本的参数 ==========
68 69 /**
69   - * apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
70   - * 对应的字符串
71   - * 注意,可通过 {@link #main(String[])} 读取
  70 + * apiclient_key.pem 证书文件的对应字符串
72 71 */
73 72 @NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
74 73 private String privateKeyContent;
75 74 /**
76   - * apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
77   - * 对应的字符串
78   - * <p>
79   - * 注意,可通过 {@link #main(String[])} 读取
  75 + * apiclient_cert.pem 证书文件的对应的字符串
80 76 */
81 77 @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
82 78 private String privateCertContent;
83 79 /**
84 80 * apiV3 密钥值
85 81 */
86   - @NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class)
  82 + @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
87 83 private String apiV3Key;
88 84  
89 85 /**
... ...
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 {
44 44 .totalFee(1) // 单位分
45 45 .timeExpire(formatDateV2(LocalDateTimeUtils.addTime(Duration.ofMinutes(2))))
46 46 .spbillCreateIp("127.0.0.1")
47   - .authCode("131276541518138032")
  47 + .authCode("134298744426278497")
48 48 .build();
49 49 System.out.println("========= request ==========");
50 50 System.out.println(JsonUtils.toJsonPrettyString(request));
... ... @@ -63,7 +63,7 @@ public class WxBarPayClientIntegrationTest {
63 63  
64 64 // 执行发起退款
65 65 WxPayRefundRequest request = new WxPayRefundRequest()
66   - .setOutTradeNo("1689504162805")
  66 + .setOutTradeNo("1689545667276")
67 67 .setOutRefundNo(String.valueOf(System.currentTimeMillis()))
68 68 .setRefundFee(1)
69 69 .setRefundDesc("就是想退了")
... ... @@ -103,8 +103,6 @@ public class WxBarPayClientIntegrationTest {
103 103 config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim");
104 104 config.setSignType(WxPayConstants.SignType.MD5);
105 105 config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12");
106   - config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem");
107   - config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem");
108 106 return config;
109 107 }
110 108  
... ...
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 {
58 58  
59 59 // 执行发起退款
60 60 WxPayRefundV3Request request = new WxPayRefundV3Request()
61   - .setOutTradeNo("1689506153043")
  61 + .setOutTradeNo("1689545729695")
62 62 .setOutRefundNo(String.valueOf(System.currentTimeMillis()))
63 63 .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY"))
64 64 .setReason("就是想退了");
... ... @@ -73,10 +73,8 @@ public class WxNativePayClientIntegrationTest {
73 73 WxPayConfig config = new WxPayConfig();
74 74 config.setAppId("wx62056c0d5e8db250");
75 75 config.setMchId("1545083881");
76   - config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim");
77 76 config.setApiV3Key("459arNsYHl1mgkiO6H9ZH5KkhFXSxaA4");
78 77 // config.setCertSerialNo(serialNo);
79   - config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12");
80 78 config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem");
81 79 config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem");
82 80 return config;
... ...
yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue
... ... @@ -8,8 +8,8 @@
8 8 <template slot="append">%</template>
9 9 </el-input>
10 10 </el-form-item>
11   - <el-form-item label-width="180px" label="公众号APPID" prop="weChatConfig.appId">
12   - <el-input v-model="form.weChatConfig.appId" placeholder="请输入公众号APPID" clearable :style="{width: '100%'}">
  11 + <el-form-item label-width="180px" label="公众号 APPID" prop="weChatConfig.appId">
  12 + <el-input v-model="form.weChatConfig.appId" placeholder="请输入公众号 APPID" clearable :style="{width: '100%'}">
13 13 </el-input>
14 14 </el-form-item>
15 15 <el-form-item label-width="180px" label="商户号" prop="weChatConfig.mchId">
... ... @@ -29,29 +29,41 @@
29 29 </el-radio>
30 30 </el-radio-group>
31 31 </el-form-item>
32   - <el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey"
33   - v-if="form.weChatConfig.apiVersion === 'v2'">
34   - <el-input v-model="form.weChatConfig.mchKey" placeholder="请输入商户密钥" clearable
35   - :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
36   - </el-form-item>
  32 + <div v-if="form.weChatConfig.apiVersion === 'v2'">
  33 + <el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey">
  34 + <el-input v-model="form.weChatConfig.mchKey" placeholder="请输入商户密钥" clearable
  35 + :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
  36 + </el-form-item>
  37 + <el-form-item label-width="180px" label="apiclient_cert.p12 证书" prop="weChatConfig.keyContent">
  38 + <el-input v-model="form.weChatConfig.keyContent" type="textarea"
  39 + placeholder="请上传 apiclient_cert.p12 证书"
  40 + readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
  41 + </el-form-item>
  42 + <el-form-item label-width="180px" label="">
  43 + <el-upload :limit="1" accept=".p12" action=""
  44 + :before-upload="p12FileBeforeUpload"
  45 + :http-request="keyContentUpload">
  46 + <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
  47 + </el-upload>
  48 + </el-form-item>
  49 + </div>
37 50 <div v-if="form.weChatConfig.apiVersion === 'v3'">
38   - <el-form-item label-width="180px" label="API V3密钥" prop="weChatConfig.apiV3Key">
39   - <el-input v-model="form.weChatConfig.apiV3Key" placeholder="请输入API V3密钥" clearable
  51 + <el-form-item label-width="180px" label="API V3 密钥" prop="weChatConfig.apiV3Key">
  52 + <el-input v-model="form.weChatConfig.apiV3Key" placeholder="请输入 API V3 密钥" clearable
40 53 :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
41 54 </el-form-item>
42   - <el-form-item label-width="180px" label="apiclient_key.perm证书" prop="weChatConfig.privateKeyContent">
  55 + <el-form-item label-width="180px" label="apiclient_key.perm 证书" prop="weChatConfig.privateKeyContent">
43 56 <el-input v-model="form.weChatConfig.privateKeyContent" type="textarea"
44   - placeholder="请上传apiclient_key.perm证书"
  57 + placeholder="请上传 apiclient_key.perm 证书"
45 58 readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
46 59 </el-form-item>
47 60 <el-form-item label-width="180px" label="" prop="privateKeyContentFile">
48 61 <el-upload ref="privateKeyContentFile"
49 62 :limit="1"
50   - :accept="fileAccept"
51   - :headers="header"
  63 + accept=".pem"
52 64 action=""
53 65 :before-upload="pemFileBeforeUpload"
54   - :http-request="privateKeyUpload"
  66 + :http-request="privateKeyContentUpload"
55 67 >
56 68 <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
57 69 </el-upload>
... ... @@ -64,18 +76,17 @@
64 76 <el-form-item label-width="180px" label="" prop="privateCertContentFile">
65 77 <el-upload ref="privateCertContentFile"
66 78 :limit="1"
67   - :accept="fileAccept"
68   - :headers="header"
  79 + accept=".pem"
69 80 action=""
70 81 :before-upload="pemFileBeforeUpload"
71   - :http-request="privateCertUpload"
  82 + :http-request="privateCertContentUpload"
72 83 >
73 84 <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
74 85 </el-upload>
75 86 </el-form-item>
76 87 </div>
77 88 <el-form-item label-width="180px" label="备注" prop="remark">
78   - <el-input v-model="form.remark" :style="{width: '100%'}"></el-input>
  89 + <el-input v-model="form.remark" :style="{width: '100%'}" />
79 90 </el-form-item>
80 91 </el-form>
81 92 <div slot="footer" class="dialog-footer">
... ... @@ -100,6 +111,7 @@ const defaultForm = {
100 111 mchId: '',
101 112 apiVersion: '',
102 113 mchKey: '',
  114 + keyContent: '',
103 115 privateKeyContent: '',
104 116 privateCertContent: '',
105 117 apiV3Key:'',
... ... @@ -159,27 +171,27 @@ export default {
159 171 message: '请输入商户密钥',
160 172 trigger: 'blur'
161 173 }],
  174 + 'weChatConfig.keyContent': [{
  175 + required: true,
  176 + message: '请上传 apiclient_cert.p12 证书',
  177 + trigger: 'blur'
  178 + }],
162 179 'weChatConfig.privateKeyContent': [{
163 180 required: true,
164   - message: '请上传apiclient_key.perm证书',
  181 + message: '请上传 apiclient_key.perm 证书',
165 182 trigger: 'blur'
166 183 }],
167 184 'weChatConfig.privateCertContent': [{
168 185 required: true,
169   - message: '请上传apiclient_cert.perm证书',
  186 + message: '请上传 apiclient_cert.perm证 书',
170 187 trigger: 'blur'
171 188 }],
172 189 'weChatConfig.apiV3Key': [{
173 190 required: true,
174   - message: '请上传apiV3密钥值',
  191 + message: '请上传 api V3 密钥值',
175 192 trigger: 'blur'
176 193 }],
177 194 },
178   - // 文件上传的header
179   - header: {
180   - "Authorization": null
181   - },
182   - fileAccept: ".pem",
183 195 // 渠道状态 数据字典
184 196 statusDictDatas: getDictDatas(DICT_TYPE.COMMON_STATUS),
185 197 versionDictDatas: getDictDatas(DICT_TYPE.PAY_CHANNEL_WECHAT_VERSION),
... ... @@ -218,6 +230,7 @@ export default {
218 230 this.form.weChatConfig.apiVersion = config.apiVersion;
219 231 this.form.weChatConfig.mchId = config.mchId;
220 232 this.form.weChatConfig.mchKey = config.mchKey;
  233 + this.form.weChatConfig.keyContent = config.keyContent;
221 234 this.form.weChatConfig.privateKeyContent = config.privateKeyContent;
222 235 this.form.weChatConfig.privateCertContent = config.privateCertContent;
223 236 this.form.weChatConfig.apiV3Key = config.apiV3Key;
... ... @@ -241,7 +254,6 @@ export default {
241 254 this.$modal.msgSuccess("修改成功");
242 255 this.close();
243 256 }
244   -
245 257 })
246 258 } else {
247 259  
... ... @@ -255,10 +267,14 @@ export default {
255 267 }
256 268 });
257 269 },
258   - pemFileBeforeUpload(file) {
  270 + /**
  271 + * apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem 上传前的校验
  272 + */
  273 + fileBeforeUpload(file, fileAccept) {
259 274 let format = '.' + file.name.split(".")[1];
260   - if (format !== this.fileAccept) {
261   - this.$message.error('请上传指定格式"' + this.fileAccept + '"文件');
  275 + if (format !== fileAccept) {
  276 + debugger
  277 + this.$message.error('请上传指定格式"' + fileAccept + '"文件');
262 278 return false;
263 279 }
264 280 let isRightSize = file.size / 1024 / 1024 < 2
... ... @@ -267,19 +283,41 @@ export default {
267 283 }
268 284 return isRightSize
269 285 },
270   - privateKeyUpload(event) {
  286 + p12FileBeforeUpload(file) {
  287 + this.fileBeforeUpload(file, '.p12')
  288 + },
  289 + pemFileBeforeUpload(file) {
  290 + this.fileBeforeUpload(file, '.pem')
  291 + },
  292 + /**
  293 + * 读取 apiclient_key.pem 到 privateKeyContent 字段
  294 + */
  295 + privateKeyContentUpload(event) {
271 296 const readFile = new FileReader()
272 297 readFile.onload = (e) => {
273 298 this.form.weChatConfig.privateKeyContent = e.target.result
274 299 }
275 300 readFile.readAsText(event.file);
276 301 },
277   - privateCertUpload(event) {
  302 + /**
  303 + * 读取 apiclient_cert.pem 到 privateCertContent 字段
  304 + */
  305 + privateCertContentUpload(event) {
278 306 const readFile = new FileReader()
279 307 readFile.onload = (e) => {
280 308 this.form.weChatConfig.privateCertContent = e.target.result
281 309 }
282 310 readFile.readAsText(event.file);
  311 + },
  312 + /**
  313 + * 读取 apiclient_cert.p12 到 keyContent 字段
  314 + */
  315 + keyContentUpload(event) {
  316 + const readFile = new FileReader()
  317 + readFile.onload = (e) => {
  318 + this.form.weChatConfig.keyContent = e.target.result.split(',')[1]
  319 + }
  320 + readFile.readAsDataURL(event.file); // 读成 base64
283 321 }
284 322 }
285 323 }
... ...
yudao-ui-admin/src/views/pay/app/index.vue
... ... @@ -143,6 +143,30 @@
143 143 </el-button>
144 144 </template>
145 145 </el-table-column>
  146 + <el-table-column :label="payChannelEnum.WX_NATIVE.name" align="center">
  147 + <template v-slot="scope">
  148 + <el-button type="success" icon="el-icon-check" circle
  149 + v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_NATIVE.code)"
  150 + @click="handleUpdateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
  151 + </el-button>
  152 + <el-button v-else
  153 + type="danger" icon="el-icon-close" circle
  154 + @click="handleCreateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
  155 + </el-button>
  156 + </template>
  157 + </el-table-column>
  158 + <el-table-column :label="payChannelEnum.WX_BAR.name" align="center">
  159 + <template v-slot="scope">
  160 + <el-button type="success" icon="el-icon-check" circle
  161 + v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_BAR.code)"
  162 + @click="handleUpdateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
  163 + </el-button>
  164 + <el-button v-else
  165 + type="danger" icon="el-icon-close" circle
  166 + @click="handleCreateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
  167 + </el-button>
  168 + </template>
  169 + </el-table-column>
146 170 </el-table-column>
147 171 <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
148 172 <template v-slot="scope">
... ...