- 搞懂微信支付 v3 接口规则-【附Java源码】
简介 为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付API v3。 其实还要一个主要因素是「为了符合监管的要求」。 主要是为了符合监管的要求,保证更高的安全级别。《中华人民共和国电子签名法》、《金融电子认证规范》及《非银行支付机构网络支付业务管理办法》中规定 “电子签名需要第三方认证的,由依法设立的电子认证服务提供者提供认证服务。”,所以需使用第三方 CA 来确保数字证书的唯一性、完整性及交易的不可抵赖性。 支付宝支付也是如此,从之前的「普通公钥方式」新增了 「公钥证书方式」。今天的主角是微信支付 Api v3 这里就不展开讲支付宝支付了。 微信支付 Api v3 接口规则 官方文档 v2 与 v3 的区别 V3 规则差异 V2 JSON 参数格式 XML POST、GET 或 DELETE 提交方式 POST AES-256-GCM加密 回调加密 无需加密 RSA 加密 敏感加密 无需加密 UTF-8 编码方式 UTF-8 非对称密钥SHA256-RSA 签名方式 MD5 或 HMAC-SHA256 微信支付 Api-v2 版本详细介绍请参数之前博客 微信支付,你想知道的一切都在这里 干货多,屁话少,下面直接进入主题,读完全文你将 Get 到以下知识点 如何获取证书序列号 非对称密钥 SHA256-RSA 加密与验证签名 AES-256-GCM 如何解密 API 密钥设置 请登录商户平台进入【账户中心】->【账户设置】->【API安全】->【APIv3密钥】中设置 API 密钥。 具体操作步骤请参见:什么是APIv3密钥?如何设置? 获取 API 证书 请登录商户平台进入【账户中心】->【账户设置】->【API安全】根据提示指引下载证书。 具体操作步骤请参见:什么是API证书?如何获取API证书? 按照以上步骤操作后你将获取如下内容: apiKey API 密钥 apiKey3 APIv3 密钥 mchId 商户号 apiclient_key.pem X.509 标准证书的密钥 apiclient_cert.p12 X.509 标准的证书+密钥 apiclient_cert.pem X.509 标准的证书 请求签名 如何生成签名参数?官方文档 描述得非常清楚这里就不啰嗦了。 示例代码 构造签名串 [代码] /** * 构造签名串 * * @param method {@link RequestMethod} GET,POST,PUT等 * @param url 请求接口 /v3/certificates * @param timestamp 获取发起请求时的系统当前时间戳 * @param nonceStr 随机字符串 * @param body 请求报文主体 * @return 待签名字符串 */ public static String buildSignMessage(RequestMethod method, String url, long timestamp, String nonceStr, String body) { return new StringBuilder() .append(method.toString()) .append("\n") .append(url) .append("\n") .append(timestamp) .append("\n") .append(nonceStr) .append("\n") .append(body) .append("\n") .toString(); } [代码] 构造 HTTP 头中的 Authorization [代码]/** * 构建 v3 接口所需的 Authorization * * @param method {@link RequestMethod} 请求方法 * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath key.pem 证书路径 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @return {@link String} 返回 v3 所需的 Authorization * @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 = PayKit.buildSignMessage(method, urlSuffix, timestamp, nonceStr, body); // 获取商户私钥 String key = PayKit.getPrivateKey(keyPath); // 生成签名 String signature = RsaKit.encryptByPrivateKey(buildSignMessage, key); // 根据平台规则生成请求头 authorization return PayKit.getAuthorization(mchId, serialNo, nonceStr, String.valueOf(timestamp), signature, authType); } /** * 获取授权认证信息 * * @param mchId 商户号 * @param serialNo 商户API证书序列号 * @param nonceStr 请求随机串 * @param timestamp 时间戳 * @param signature 签名值 * @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048 * @return 请求头 Authorization */ public static String getAuthorization(String mchId, String serialNo, String nonceStr, String timestamp, String signature, String authType) { 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)); } [代码] 拼接参数 [代码] public static String createLinkString(Map<String, String> params, String connStr, boolean encode, boolean quotes) { List<String> keys = new ArrayList<String>(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 ? urlEncode(value) : value).append('"'); } else { content.append(key).append("=").append(encode ? urlEncode(value) : value); } } else { if (quotes) { content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"').append(connStr); } else { content.append(key).append("=").append(encode ? urlEncode(value) : value).append(connStr); } } } return content.toString(); } [代码] 从上面示例来看我们还差两个参数 serial_no 证书序列号 signature 使用商户私钥对待签名串进行 SHA256 with RSA 签名 如何获取呢?不要着急,容我喝杯 「89年的咖啡」提提神。 获取证书序列号 通过工具获取 openssl x509 -in apiclient_cert.pem -noout -serial 使用证书解析工具 https://myssl.com/cert_decode.html 通过代码获取 [代码]// 获取证书序列号 X509Certificate certificate = PayKit.getCertificate(FileUtil.getInputStream("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()); /** * 获取证书 * * @param inputStream 证书文件 * @return {@link X509Certificate} 获取证书 */ 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); } } [代码] SHA256 with RSA 签名 获取商户私钥 [代码] /** * 获取商户私钥 * * @param keyPath 商户私钥证书路径 * @return 商户私钥 * @throws Exception 解析 key 异常 */ public static String getPrivateKey(String keyPath) throws Exception { String originalKey = FileUtil.readUtf8String(keyPath); String privateKey = originalKey .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s+", ""); return RsaKit.getPrivateKeyStr(RsaKit.loadPrivateKey(privateKey)); } public static String getPrivateKeyStr(PrivateKey privateKey) { return Base64.encode(privateKey.getEncoded()); } /** * 从字符串中加载私钥 * * @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 = java.security.Signature.getInstance("SHA256WithRSA"); signature.initSign(priKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signed = signature.sign(); return StrUtil.str(Base64.encode(signed)); } [代码] 至此微信支付 Api-v3 接口请求参数已封装完成。 执行请求 [代码]/** * V3 接口统一执行入口 * * @param method {@link RequestMethod} 请求方法 * @param urlPrefix 可通过 {@link WxDomain}来获取 * @param urlSuffix 可通过 {@link WxApiType} 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath apiclient_key.pem 证书路径 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @param file 文件 * @return {@link String} 请求返回的结果 * @throws Exception 接口执行异常 */ public static Map<String, Object> v3Execution(RequestMethod method, String urlPrefix, String urlSuffix, String mchId, String serialNo, String keyPath, String body, String nonceStr, long timestamp, String authType, File file) throws Exception { // 构建 Authorization String authorization = WxPayKit.buildAuthorization(method, urlSuffix, mchId, serialNo, keyPath, body, nonceStr, timestamp, authType); if (method == RequestMethod.GET) { return doGet(urlPrefix.concat(urlSuffix), authorization, serialNo, null); } else if (method == RequestMethod.POST) { return doPost(urlPrefix.concat(urlSuffix), authorization, serialNo, body); } else if (method == RequestMethod.DELETE) { return doDelete(urlPrefix.concat(urlSuffix), authorization, serialNo, body); } else if (method == RequestMethod.UPLOAD) { return doUpload(urlPrefix.concat(urlSuffix), authorization, serialNo, body, file); } return null; } [代码] 网络请求库默认是使用的 Hutool 封装的一套 Java 工具集合来实现 GET 请求 [代码]/** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doGet(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.post(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] POST 请求 [代码] /** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doPost(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.post(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] DELETE 请求 [代码]/** * delete 请求 * * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doDelete(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.delete(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] 上传文件 [代码] /** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @param file 上传的文件 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doUpload(String url, String authorization, String serialNumber, String jsonData, File file) { return HttpRequest.post(url) .addHeaders(getUploadHeaders(authorization, serialNumber)) .form("file", file) .form("meta", jsonData) .execute(); } [代码] 构建 Http 请求头 [代码]private Map<String, String> getBaseHeaders(String authorization) { String userAgent = String.format( "WeChatPay-IJPay-HttpClient/%s (%s) Java/%s", getClass().getPackage().getImplementationVersion(), OS, VERSION == null ? "Unknown" : VERSION); Map<String, String> headers = new HashMap<>(3); headers.put("Accept", ContentType.JSON.toString()); headers.put("Authorization", authorization); headers.put("User-Agent", userAgent); return headers; } private Map<String, String> getHeaders(String authorization, String serialNumber) { Map<String, String> headers = getBaseHeaders(authorization); headers.put("Content-Type", ContentType.JSON.toString()); if (StrUtil.isNotEmpty(serialNumber)) { headers.put("Wechatpay-Serial", serialNumber); } return headers; } private Map<String, String> getUploadHeaders(String authorization, String serialNumber) { Map<String, String> headers = getBaseHeaders(authorization); headers.put("Content-Type", "multipart/form-data;boundary=\"boundary\""); if (StrUtil.isNotEmpty(serialNumber)) { headers.put("Wechatpay-Serial", serialNumber); } return headers; } [代码] 构建 Http 请求返回值 从响应的 HttpResponse 中获取微信响应头信息、状态码以及 body [代码]/** * 构建返回参数 * * @param httpResponse {@link HttpResponse} * @return {@link Map} */ private Map<String, Object> buildResMap(HttpResponse httpResponse) { Map<String, Object> map = new HashMap<>(); String timestamp = httpResponse.header("Wechatpay-Timestamp"); String nonceStr = httpResponse.header("Wechatpay-Nonce"); String serialNo = httpResponse.header("Wechatpay-Serial"); String signature = httpResponse.header("Wechatpay-Signature"); String body = httpResponse.body(); int status = httpResponse.getStatus(); map.put("timestamp", timestamp); map.put("nonceStr", nonceStr); map.put("serialNumber", serialNo); map.put("signature", signature); map.put("body", body); map.put("status", status); return map; } [代码] 至此已完成构建请求参数,执行请求。接下来我们就要实现响应数据的解密以及响应结果的验证签名 对应的官方文档 证书和回调报文解密 签名验证 验证签名 构建签名参数 [代码]/** * 构造签名串 * * @param timestamp 应答时间戳 * @param nonceStr 应答随机串 * @param body 应答报文主体 * @return 应答待签名字符串 */ 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(); } [代码] 证书和回调报文解密 官方文档文末有完整的源码这里就不贴了。贴一个示例大家参数一下 [代码]try { String associatedData = "certificate"; String nonce = "80d28946a64a"; String cipherText = "DwAqW4+4TeUaOEylfKEXhw+XqGh/YTRhUmLw/tBfQ5nM9DZ9d+9aGEghycwV1jwo52vXb/t6ueBvBRHRIW5JgDRcXmTHw9IMTrIK6HxTt2qiaGTWJU9whsF+GGeQdA7gBCHZm3AJUwrzerAGW1mclXBTvXqaCl6haE7AOHJ2g4RtQThi3nxOI63/yc3WaiAlSR22GuCpy6wJBfljBq5Bx2xXDZXlF2TNbDIeodiEnJEG2m9eBWKuvKPyUPyClRXG1fdOkKnCZZ6u+ipb4IJx28n3MmhEtuc2heqqlFUbeONaRpXv6KOZmH/IdEL6nqNDP2D7cXutNVCi0TtSfC7ojnO/+PKRu3MGO2Z9q3zyZXmkWHCSms/C3ACatPUKHIK+92MxjSQDc1E/8faghTc9bDgn8cqWpVKcL3GHK+RfuYKiMcdSkUDJyMJOwEXMYNUdseQMJ3gL4pfxuQu6QrVvJ17q3ZjzkexkPNU4PNSlIBJg+KX61cyBTBumaHy/EbHiP9V2GeM729a0h5UYYJVedSo1guIGjMZ4tA3WgwQrlpp3VAMKEBLRJMcnHd4pH5YQ/4hiUlHGEHttWtnxKFwnJ6jHr3OmFLV1FiUUOZEDAqR0U1KhtGjOffnmB9tymWF8FwRNiH2Tee/cCDBaHhNtfPI5129SrlSR7bZc+h7uzz9z+1OOkNrWHzAoWEe3XVGKAywpn5HGbcL+9nsEVZRJLvV7aOxAZBkxhg8H5Fjt1ioTJL+qXgRzse1BX1iiwfCR0fzEWT9ldDTDW0Y1b3tb419MhdmTQB5FsMXYOzqp5h+Tz1FwEGsa6TJsmdjJQSNz+7qPSg5D6C2gc9/6PkysSu/6XfsWXD7cQkuZ+TJ/Xb6Q1Uu7ZB90SauA8uPQUIchW5zQ6UfK5dwMkOuEcE/141/Aw2rlDqjtsE17u1dQ6TCax/ZQTDQ2MDUaBPEaDIMPcgL7fCeijoRgovkBY92m86leZvQ+HVbxlFx5CoPhz4a81kt9XJuEYOztSIKlm7QNfW0BvSUhLmxDNCjcxqwyydtKbLzA+EBb2gG4ORiH8IOTbV0+G4S6BqetU7RrO+/nKt21nXVqXUmdkhkBakLN8FUcHygyWnVxbA7OI2RGnJJUnxqHd3kTbzD5Wxco4JIQsTOV6KtO5c960oVYUARZIP1SdQhqwELm27AktEN7kzg/ew/blnTys/eauGyw78XCROb9F1wbZBToUZ7L+8/m/2tyyyqNid+sC9fYqJoIOGfFOe6COWzTI/XPytCHwgHeUxmgk7NYfU0ukR223RPUOym6kLzSMMBKCivnNg68tbLRJHEOpQTXFBaFFHt2qpceJpJgw5sKFqx3eQnIFuyvA1i8s2zKLhULZio9hpsDJQREOcNeHVjEZazdCGnbe3Vjg7uqOoVHdE/YbNzJNQEsB3/erYJB+eGzyFwFmdAHenG5RE6FhCutjszwRiSvW9F7wvRK36gm7NnVJZkvlbGwh0UHr0pbcrOmxT81xtNSvMzT0VZNLTUX2ur3AGLwi2ej8BIC0H41nw4ToxTnwtFR1Xy55+pUiwpB7JzraA08dCXdFdtZ72Tw/dNBy5h1P7EtQYiKzXp6rndfOEWgNOsan7e1XRpCnX7xoAkdPvy40OuQ5gNbDKry5gVDEZhmEk/WRuGGaX06CG9m7NfErUsnQYrDJVjXWKYuARd9R7W0aa5nUXqz/Pjul/LAatJgWhZgFBGXhNr9iAoade/0FPpBj0QWa8SWqKYKiOqXqhfhppUq35FIa0a1Vvxcn3E38XYpVZVTDEXcEcD0RLCu/ezdOa6vRcB7hjgXFIRZQAka0aXnQxwOZwE2Rt3yWXqc+Q1ah2oOrg8Lg3ETc644X9QP4FxOtDwz/A=="; AesUtil aesUtil = new AesUtil(wxPayV3Bean.getApiKey3().getBytes(StandardCharsets.UTF_8)); // 平台证书密文解密 // encrypt_certificate 中的 associated_data nonce ciphertext String publicKey = aesUtil.decryptToString( associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), cipherText ); // 保存证书 FileWriter writer = new FileWriter(wxPayV3Bean.getPlatformCertPath()); writer.write(publicKey); // 获取平台证书序列号 X509Certificate certificate = PayKit.getCertificate(new ByteArrayInputStream(publicKey.getBytes())); return certificate.getSerialNumber().toString(16).toUpperCase(); } catch (Exception e) { e.printStackTrace(); } [代码] 验证签名 [代码]/** * 验证签名 * * @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 = PayKit.buildSignMessage(timestamp, nonce, body); // 获取证书 X509Certificate certificate = PayKit.getCertificate(certInputStream); PublicKey publicKey = certificate.getPublicKey(); return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); } /** * 公钥验证签名 * * @param data 需要加密的数据 * @param sign 签名 * @param publicKey 公钥 * @return 验证结果 * @throws Exception 异常信息 */ 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))); } [代码] 至此微信支付 Api-v3 接口已介绍完,如有疑问欢迎留言一起探讨。 完整示例 SpringBoot 参考资料 你真的了解 HTTPS 吗? WechatPay-API-v3
2021-03-02 - 微信支付V3版java整合
1.引入依赖 [代码]<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.6.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.4</version> <classifier>jdk15</classifier> </dependency> <!-- huTool 工具包 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-crypto</artifactId> <version>5.5.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-http</artifactId> <version>5.5.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-json</artifactId> <version>5.5.0</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.12</version> <scope>compile</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.github.xkzhangsan</groupId> <artifactId>xk-time</artifactId> <version>2.1.2</version> </dependency> </dependencies> [代码] 2.创建时间工具类 DateTimeZoneUtil.class [代码]import cn.hutool.core.util.StrUtil; import com.xkzhangsan.time.converter.DateTimeConverterUtil; import com.xkzhangsan.time.formatter.DateTimeFormatterUtil; import java.io.Serializable; import java.time.ZonedDateTime; import java.util.Date; /** * 〈一句话功能简述〉<br> * 〈时间转换工具类〉 * * @author Gym * @create 2020/12/18 * @since 1.0.0 */ public class DateTimeZoneUtil implements Serializable { /** * 时间转 TimeZone * * @param time * @return * @throws Exception */ public static String dateToTimeZone(long time) throws Exception { return dateToTimeZone(new Date(time)); } /** * 时间转 TimeZone * * @param date * @return * @throws Exception */ public static String dateToTimeZone(Date date) throws Exception { String time; if (date == null) { throw new Exception("date is not null"); } ZonedDateTime zonedDateTime = DateTimeConverterUtil.toZonedDateTime(date); time = DateTimeFormatterUtil.format(zonedDateTime, DateTimeFormatterUtil.YYYY_MM_DD_T_HH_MM_SS_XXX_FMT); return time; } /** * TimeZone 时间转标准时间 * * @param str * @return * @throws Exception */ public static String timeZoneDateToStr(String str) throws Exception { String time; if (StrUtil.isBlank(str)) { throw new Exception("str is not null"); } ZonedDateTime zonedDateTime = DateTimeFormatterUtil.parseToZonedDateTime(str, DateTimeFormatterUtil.YYYY_MM_DD_T_HH_MM_SS_XXX_FMT); if (zonedDateTime == null) { throw new Exception("str to zonedDateTime fail"); } time = zonedDateTime.format(DateTimeFormatterUtil.YYYY_MM_DD_HH_MM_SS_FMT); return time; } } [代码] 3.解密工具类 AesUtil.class [代码] import cn.hutool.core.codec.Base64; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; /** * 〈一句话功能简述〉<br> * 〈〉 * * @author Gym * @create 2020/12/18 * @since 1.0.0 */ public 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 associated_data * @param nonce nonce * @param cipherText ciphertext * @return {String} 平台证书明文 * @throws GeneralSecurityException 异常 */ public String decryptToString(byte[] associatedData, byte[] nonce, String cipherText) throws GeneralSecurityException { 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.decode(cipherText)), StandardCharsets.UTF_8); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new IllegalStateException(e); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { throw new IllegalArgumentException(e); } } } [代码] 4.对外暴露方法 公共参数 [代码] //请求网关 private static final String url_prex = "https://api.mch.weixin.qq.com/"; //编码 private static final String charset = "UTF-8"; [代码] 4-1.支付下单 V3PayGet() [代码] /** * 微信支付下单 * * @param url 请求地址(只需传入域名之后的路由地址) * @param jsonStr 请求体 json字符串 此参数与微信官方文档一致 * @param mercId 商户ID * @param serial_no 证书序列号 * @param privateKeyFilePath 私钥的路径 * @return 订单支付的参数 * @throws Exception */ public static String V3PayGet(String url, String jsonStr, String mercId, String serial_no, String privateKeyFilePath) throws Exception { String body = ""; //创建httpclient对象 CloseableHttpClient client = HttpClients.createDefault(); //创建post方式请求对象 HttpPost httpPost = new HttpPost(url_prex + url); //装填参数 StringEntity s = new StringEntity(jsonStr, charset); s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, "application/json")); //设置参数到请求对象中 httpPost.setEntity(s); String post = getToken("POST", HttpUrl.parse(url_prex + url), mercId, serial_no, privateKeyFilePath, jsonStr); //设置header信息 //指定报文头【Content-type】、【User-Agent】 httpPost.setHeader("Content-type", "application/json"); httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)"); httpPost.setHeader("Accept", "application/json"); httpPost.setHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 " + post); //执行请求操作,并拿到结果(同步阻塞) CloseableHttpResponse response = client.execute(httpPost); //获取结果实体 HttpEntity entity = response.getEntity(); if (entity != null) { //按指定编码转换结果实体为String类型 body = EntityUtils.toString(entity, charset); } EntityUtils.consume(entity); //释放链接 response.close(); switch (url) { case "v3/pay/transactions/app"://返回APP支付所需的参数 return JSONObject.fromObject(body).getString("prepay_id"); case "v3/pay/transactions/jsapi"://返回JSAPI支付所需的参数 return JSONObject.fromObject(body).getString("prepay_id"); case "v3/pay/transactions/native"://返回native的请求地址 return JSONObject.fromObject(body).getString("code_url"); case "v3/pay/transactions/h5"://返回h5支付的链接 return JSONObject.fromObject(body).getString("h5_url"); } return null; } [代码] 4-2.微信调起支付参数 WxTuneUp() [代码] /** * 微信调起支付参数 * 返回参数如有不理解 请访问微信官方文档 * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml * * @param prepayId 微信下单返回的prepay_id * @param appId 应用ID(appid) * @param privateKeyFilePath 私钥的地址 * @return 当前调起支付所需的参数 * @throws Exception */ public static JSONObject WxTuneUp(String prepayId, String appId, String privateKeyFilePath) throws Exception { String time = System.currentTimeMillis() / 1000 + ""; String nonceStr = UUID.randomUUID().toString().replace("-", ""); String packageStr = "prepay_id=" + prepayId; ArrayList<String> list = new ArrayList<>(); list.add(appId); list.add(time); list.add(nonceStr); list.add(packageStr); //加载签名 String packageSign = sign(buildSignMessage(list).getBytes(), privateKeyFilePath); JSONObject jsonObject = new JSONObject(); jsonObject.put("appid", appId); jsonObject.put("timeStamp", time); jsonObject.put("nonceStr", nonceStr); jsonObject.put("packages", packageStr); jsonObject.put("signType", "RSA"); jsonObject.put("paySign", packageSign); return jsonObject; } [代码] 4-3.处理微信异步回调 notify() [代码] /** * 处理微信异步回调 * * @param request * @param response * @param privateKey 32的秘钥 */ public static String notify(HttpServletRequest request, HttpServletResponse response, String privateKey) throws Exception { Map<String, String> map = new HashMap<>(12); String result = readData(request); // 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号 String plainText = verifyNotify(result, privateKey); if (StrUtil.isNotEmpty(plainText)) { response.setStatus(200); map.put("code", "SUCCESS"); map.put("message", "SUCCESS"); } else { response.setStatus(500); map.put("code", "ERROR"); map.put("message", "签名错误"); } response.setHeader("Content-type", ContentType.JSON.toString()); response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8)); response.flushBuffer(); String out_trade_no = JSONObject.fromObject(plainText).getString("out_trade_no"); return out_trade_no; } [代码] 5. 同类类方法 5-1.生成组装请求头 getToken() [代码]/** * 生成组装请求头 * * @param method 请求方式 * @param url 请求地址 * @param mercId 商户ID * @param serial_no 证书序列号 * @param privateKeyFilePath 私钥路径 * @param body 请求体 * @return 组装请求的数据 * @throws Exception */ static String getToken(String method, HttpUrl url, String mercId, String serial_no, String privateKeyFilePath, String body) throws Exception { String nonceStr = UUID.randomUUID().toString().replace("-", ""); long timestamp = System.currentTimeMillis() / 1000; String message = buildMessage(method, url, timestamp, nonceStr, body); String signature = sign(message.getBytes("UTF-8"), privateKeyFilePath); return "mchid=\"" + mercId + "\"," + "nonce_str=\"" + nonceStr + "\"," + "timestamp=\"" + timestamp + "\"," + "serial_no=\"" + serial_no + "\"," + "signature=\"" + signature + "\""; } [代码] 5-2.生成签名 sign() [代码] /** * 生成签名 * * @param message 请求体 * @param privateKeyFilePath 私钥的路径 * @return 生成base64位签名信息 * @throws Exception */ static String sign(byte[] message, String privateKeyFilePath) throws Exception { Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(getPrivateKey(privateKeyFilePath)); sign.update(message); return Base64.getEncoder().encodeToString(sign.sign()); } [代码] 5-3.组装签名加载 buildMessage() [代码] /** * 组装签名加载 * * @param method 请求方式 * @param url 请求地址 * @param timestamp 请求时间 * @param nonceStr 请求随机字符串 * @param body 请求体 * @return 组装的字符串 */ static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) { String canonicalUrl = url.encodedPath(); if (url.encodedQuery() != null) { canonicalUrl += "?" + url.encodedQuery(); } return method + "\n" + canonicalUrl + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n"; } [代码] 5-4.获取私钥 getPrivateKey() [代码] /** * 获取私钥。 * * @param filename 私钥文件路径 (required) * @return 私钥对象 */ static PrivateKey getPrivateKey(String filename) throws IOException { String content = new String(Files.readAllBytes(Paths.get(filename)), "UTF-8"); try { String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s+", ""); KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePrivate( new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("当前Java环境不支持RSA", e); } catch (InvalidKeySpecException e) { throw new RuntimeException("无效的密钥格式"); } } [代码] 5-5.构造签名串 buildSignMessage() [代码] /** * 构造签名串 * * @param signMessage 待签名的参数 * @return 构造后带待签名串 */ static String buildSignMessage(ArrayList<String> signMessage) { if (signMessage == null || signMessage.size() <= 0) { return null; } StringBuilder sbf = new StringBuilder(); for (String str : signMessage) { sbf.append(str).append("\n"); } return sbf.toString(); } [代码] 5-6.v3支付异步通知验证签名 verifyNotify() [代码] /** * v3 支付异步通知验证签名 * * @param body 异步通知密文 * @param key api 密钥 * @return 异步通知明文 * @throws Exception 异常信息 */ static String verifyNotify(String body, String key) throws Exception { // 获取平台证书序列号 cn.hutool.json.JSONObject resultObject = JSONUtil.parseObj(body); cn.hutool.json.JSONObject resource = resultObject.getJSONObject("resource"); String cipherText = resource.getStr("ciphertext"); String nonceStr = resource.getStr("nonce"); String associatedData = resource.getStr("associated_data"); AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8)); // 密文解密 return aesUtil.decryptToString( associatedData.getBytes(StandardCharsets.UTF_8), nonceStr.getBytes(StandardCharsets.UTF_8), cipherText ); } [代码] 5-7.处理返回对象 readData() [代码] /** * 处理返回对象 * * @param request * @return */ static String readData(HttpServletRequest request) { BufferedReader br = null; try { StringBuilder result = new StringBuilder(); br = request.getReader(); for (String line; (line = br.readLine()) != null; ) { if (result.length() > 0) { result.append("\n"); } result.append(line); } return result.toString(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } [代码] 6.接口调用实例 6-1支付请求 [代码]@GetMapping("/wxPay") public Object wxPay() throws Exception { //支付的请求参数信息(此参数与微信支付文档一致,文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml) WxPayRequestBo wxPayRequestBo = new WxPayRequestBo() .setAppid(appId) .setMchid(mercId) .setDescription("商品描述") .setOut_trade_no(out_trade_no) .setTime_expire(time_expire) .setNotify_url(notify_url) .setAmount(new Amount().setTotal(1)) .setPayer(new Payer().setOpenid(openId)) .setScene_info(new SceneInfo().setPayer_client_ip(client_ip)); String wxPayRequestJsonStr = JSONUtil.toJsonStr(wxPayRequestBo); //第一步获取prepay_id String prepayId = WxPayV3Util.V3PayGet(url, wxPayRequestJsonStr, mercId, serial_no, privateKeyFilePath); //第二步获取调起支付的参数 JSONObject object = JSONObject.fromObject(WxPayV3Util.WxTuneUp(prepayId, appId, privateKeyFilePath)); return object; } [代码] 6-2.异步回调 [代码] @RequestMapping(value = "/wxnoty", method = {org.springframework.web.bind.annotation.RequestMethod.POST, org.springframework.web.bind.annotation.RequestMethod.GET}) public void wxnoty(HttpServletRequest request, HttpServletResponse response) throws Exception { System.err.println("小程序支付异步消息"); String out_trade_no = WxPayV3Util.notify(request, response, privateKey); System.out.println(out_trade_no); } [代码] 文件秘钥截图: [图片]
2021-01-12 - 云开发web页面公众号授权登录,真机checkLogin一直失败,开发者工具却可以成功?
授权地址: https://wechat.mklaus.cn 效果对比,开发者工具成功 [图片] 真机一直请求授权 [图片] 页面就一个html文件,没有其他。代码如下 ```html <html> <head><script src="https://res.wx.qq.com/open/js/cloudbase/1.1.0/cloud.js"></script></head> <body><p>hello world</p></body> <script> window.login = async function() { const checkLoginOptions = { provider: 'OfficialAccount', appid: 'wx1801955d58387cb4', } var urlSearch = new URLSearchParams(location.search) var accessToken = urlSearch.get('access_token') var refreshToken = urlSearch.get('refresh_token') if (urlSearch.get('oauthredirect') === '1') { checkLoginOptions.accessToken = accessToken checkLoginOptions.refreshToken = refreshToken } const result = await cloud.checkLogin(checkLoginOptions) if (result.errCode === 0 && result.loggedIn) { alert('login success') } else { try { cloud.startLogin({ provider: 'OfficialAccount', appid: 'wx1801955d58387cb4', scope: 'snsapi_base', redirectUri: 'https://wechat.mklaus.cn/' }) } catch (e) { console.error(`startLogin fail: ${e}`) } } } window.login() </script> </html> ```
2020-12-22 - subscribeMessage.send errCode: -504002报错?
[图片]Error: cloud.callFunction:fail Error: errCode: -504002 functions execute fail | errMsg: TypeError: Do not know how to serialize a BigInt 调用订阅消息后测试消息可以发送成功,但是返回值报错Do not know how to serialize a BigInt,无法判断是否发送成功。 2022-05-01 解决方法:云函数的返回值不要返回result,直接返回result.errCode,就不会报错。原因是直接返回result被序列化导致报错(其实就是bug),所以返回result.errCode足以用于判断!
2022-05-01 - 小程序中如何修改 svg 图片的颜色
已知小程序 <image /> 支持 svg 图片渲染;但是在开发场景中,如果我们需要修改 svg 图片原有的颜色,往往需要去修改 svg 文件本身。这样修改既不优雅,也不利于 svg 图片的复用。有没有一种方法可以更加优雅地去修改 svg 图片的颜色呢? 一、可行性探讨 svg 源码修改 既然要在原 svg 文件的基础上修改颜色,让我们先看一下 svg 源码是如何的,下面是一个三角形的 svg 源码: [图片] 我们可以发现在源码中,<path> 中的 fill 属性便是我们需要修改的颜色。如果我们能读取源码,修改对应属性,便能修改 svg 图片的颜色了;但开发过程中,我们的 svg 源文件往往是网络资源,并不能直接修改,有没有一种方法可以将 svg 源码直接在小程序进行渲染呢? svg 源码渲染 小程序原生虽然不支持 <svg> 渲染的,但我们可以通过 background-image 样式属性对 URL 资源进行加载;我们只需要将修改后 svg 源码进行 URL 编码,即可将我们想要的效果渲染出来。 方案总结 1、读取 svg 文件 2、匹配 Hex 字段并进行修改 3、将修改后的 svg 数据进行 URL 编码 4、将 URL 数据通过 background-image 样式属性进行渲染 二、技术实现 调用方式:组件 为方便调用,将其封装自定义组件,组件命名 svg,承接 svg 渲染能力,后续可在此基础上丰富 svg 的能力。 入参:src <string>,colors <string[]> 通过 src 参数传递 svg 图片链接; svg 图片可能包括多个元素,这个时候就需要我们对不同的元素定义各自的颜色。 默认属性 由于 svg 自身不存在尺寸,我们可以将其宽高同时设置成 100%,这样就可以通过承载其的父元素决定 <svg /> 的渲染尺寸。 三、<svg /> 组件代码 代码片段,点击进入 四、更多功能扩展 自定义 svg 渲染 前面我们修改 svg 图片是通过修改其 fill 属性实现的;更进一步我们完全可以不依赖外部资源,自定义 svg 节点,按照 svg 的规范进行图片的绘制,感兴趣可以尝试尝试。
2022-06-22 - 云开发环境计量收费环境可以删除不?
我首次尝试小程序云开发,因为数据库超过500次/日限制,然后用微信云服务助手乱点,无意中又新建了一环境:按量计费。 请问,这个环境如何删除?它会收取我费用吗?我一直没有使用它。 新建环境及首页如下: [图片][图片] 下面是云开发时默认免费送的: [图片]
2022-05-04 - 交易类小程序纠纷投诉能力处理指引
为进一步保障小程序平台用户的合法权益,督促开发者向用户提供更好的服务,平台将针对小程序交易投诉处理规则做出以下更新: 1、和解环节开发者处理时效由72h改为48h,执行环节开发者处理时效由72h改为48h 2、和解环节开发者超时未处理,平台将判定开发者责任并做出相应处理,包括但不限于采取扣除保证金、先行垫付、扣减交易体验分等措施。 该时效调整将在2024年3月25日正式生效,请开发者及时关注。 一、一、交易类纠纷处理通知机制 1、为便于小程序开发者及时获知小程序交易投诉情况,平台将于每天早上10:00向小程序的管理员及运营者推送通知,通知内容为截至前一天24时该小程序账号内待开发者处理的投诉单详情。 2、通知查看路径: 1)小程序管理后台:右上角「通知」查询通知记录; [图片] 2)微信移动端:通知下发给小程序管理员与运营者,可搜索「微信公众平台」查看通知记录。 [图片] 二、二、交易类投诉处理操作指引 1、登陆小程序管理后台并核实投诉 登录小程序管理后台—功能—交易保障—交易投诉;查看待处理的投诉单,并尽快完成投诉单详情的核实与处理。 [图片] 2、和解环节:同意和解/拒绝和解 核实投诉后,若同意与用户和解,请在时效内上传与用户的沟通说明、物流信息或者退款凭证等,平台将推送和解详情给用户。 核实投诉后,若双方未达成和解,请选择拒绝和解并上传相关凭证,平台将推送不和解详情给用户。 若开发者未在处理时效内作任何处理,纠纷单将自动流转至平台介入处理状态,平台将判定开发者责任并做出相应处理,包括但不限于平台先行垫付,扣除开发者保证金,扣减交易体验分等措施。 [图片] [图片] 注:开发者和解时效为72h。选择同意和解或拒绝和解后,用户会收到确认投诉处理结果的通知,若用户认可开发者的处理结果,投诉将完结;若不同意,用户可申请平台客服协助处理。(2024年3月25日后发起的投诉单和解时效更改为48h) 3、平台处理环节:下发举证&判定责任 若用户不认可开发者投诉处理结果并申请平台介入,平台将根据现有纠纷凭证是否有效,要求开发者/用户补充相关凭证;若平台要求开发者补充纠纷凭证,投诉单状态将流转至【待开发者补充凭证】,处理时效为48h(特殊场景为24h),请务必如实填写并上传沟通说明、物流信息或退款凭证等信息;开发者/用户补充凭证后,平台将根据已有材料与凭证进行判责。 [图片] [图片] 4、开发者执行环节:上传处理凭证 若平台判为开发者责任,投诉单状态流转为【待开发者上传处理凭证】,处理时效为72h(特殊场景为48h),请尽快提交微信支付退款单号、交易流水、转账单号或物流单号等凭证,供平台核实是否已执行平台判责。(2024年3月25日后发起的投诉单处理时效更改为48h) [图片] [图片] 5、平台核实环节 若平台根据开发者上传的处理凭证判断开发者已执行平台判责,该投诉单将完结; 若开发者上传的处理凭证信息不准确,平台无法判断其已执行平台判责/超时未上传处理凭证,平台将根据实际情况对开发者进行交易体验分的扣减,对用户进行先行赔付等措施。 6、商家申诉环节 若平台判定商家责任后,开发者不认可平台的判责结果,可在投诉单完结后的72h内发起申诉,平台会根据申诉环节上传的相关材料进行申诉判定;每个投诉单仅支持一次申诉,申诉超时/申诉失败后不支持再次发起申诉。 若申诉成功,则开发者无需按照《微信小程序交易服务违规管理规则》中的第三章违规处理措施缴纳违约金; 若申诉失败,则开发者仍需按照《微信小程序交易服务违规管理规则》中的第三章违规处理措施缴纳违约金。 [图片] [图片] 三、小程序实物交易纠纷处理规则 平台介入处理争议时,开发者应遵守以下约定: 开发者及用户就订单产生交易争议时,双方可以选择自行协商。如用户向平台提起交易投诉,双方协商未果或开发者未作处理,平台有权根据本规则及相关法律法规介入对争议的处理。 当开发者因自身系统、管理、人力等原因出现异常大量维权或舆情事件,且开发者不具备及时处理能力,为保障双方交易安全,平台可主动介入处理。 平台处理交易争议期间,开发者及用户应当按照平台下发的站内信、模版消息、短信、电话或邮件通知及时提供凭证。 平台处理争议期间,若任何一方无正当理由,未按照举证要求提供凭证的,平台有权按照实际收集到的凭证做出处理。 平台做出判责后,开发者应当按照要求及时履行相应义务。 如开发者行为违反相关法律法规或平台规则,因此而可能产生的用户损失或额外赔偿费用将由开发者自行承担。 详情可查看微信小程序实物交易争议处理规则 四、常见问题 1、用户交易投诉的入口在哪? 小程序—页面右上角“…”—反馈与投诉—交易投诉 [图片] 2、如何更改通知接收者? 目前,交易投诉待处理通知会发送给当前小程序的管理员及运营者。若希望取消或者增加推送成员,可登录微信公众平台—管理—成员管理,更换或添加管理员及运营者。 [图片] 3、上传凭证需要注意什么?如果超时未处理怎么办? 上传前务必确保提交的证据材料的真实性、完整性、及时性和准确性;平台处理争议期间,请按照举证要求的内容提供凭证。 为保障用户的体验,请在规定时间内完成核实和处理;否则,开发者应承担超时未处理、举证超时的后果,平台有权按照实际收集到的凭证做出判责,同时根据平台规则对开发者进行扣减交易体验分等措施。 4、除了在小程序管理后台处理投诉,是否有其他方式处理交易投诉? 若开发者在网页端登陆小程序管理后台处理投诉存在困难,平台提供移动端工作台—小程序助手处理投诉,投诉处理流程与PC端一致;同时平台提供线上接口,开发者可通过接口接入自己内部系统处理投诉。 移动端工作台:微信主页搜索「小程序助手」—登陆小程序—管理功能—交易投诉 [图片] 接口文档:可点击查看交易投诉处理接口文档:投诉信息推送 | 微信开放文档 5、交易体验分是什么?为什么被扣分了?扣分后会受到什么处罚? 为保障小程序平台用户的合法权益,平台将对开发者在其小程序的违规行为进行判定,根据违规行为的严重程度对该小程序扣减不同分值的交易体验分,并在小程序交易体验分扣减至相应节点时,对开发者采取相应违规处理措施。 具体规则与案例解析可点击《微信小程序交易服务违规管理规则》、微信小程序交易体验分常见问题指引查看。 6、申诉需要提供哪些凭证才算有效凭证?哪类申诉不支持通过? 开发者需针对用户投诉问题的有效反驳凭证,建议同时提供相关图片进行佐证。 开发者在投诉单处理环节中,包含和解环节/举证环节/商家执行环节,若任意一环节存在未回复或以话术、无效字符回复的情况,不予申诉通过;用户投诉开发者服务态度问题,被平台判定问题属实,不予申诉通过;用户投诉开发者服务意愿类问题,如强制退款、或未按订单页面承诺进行服务履约(如配送超时,商品无货强制退款等),被平台判定投诉属实,不予申诉通过。 7、为什么要收取违约金? 当开发者存在违反《微信小程序交易服务违规管理规则》的行为,需要按规则3.1缴纳违约金;开发者有申诉的权利,可在投诉单完结后的72h内发起申诉,平台要求缴纳的违约金来源为"已过申诉时效或平台认定申诉不成立的违规投诉单"。 8、不缴纳是否有限制措施? 若开发者未按期足额支付违约金的,平台将采取限制小程序搜索、限制支付能力等处理措施。请参考《微信小程序交易服务违规管理规则》的违规行为及对应处理措施。 9、违约金是否可以开具发票? 小程序mp后台(mp.weixin.qq.com-功能-交易保障-消费者资金保障-违约金)模块,支持对单笔缴纳流水进行发票开具,请开发者仔细阅读开票的注意事项并填写准确信息。
03-22 - FileSystemManager.writeFile用这个生成文件后需要清理吗
请问FileSystemManager.writeFile方法创建文件后,是存放在哪里?需要remove掉这个新建的文件吗?只有removesaveFile方法啊,应该调哪个方法删除,感谢回答
2019-01-31 - 使用微剪插件编辑视频时,蓝牙麦克风无法收音?
realme X 手机使用微信小程序拍摄视频无法收音问题
2021-02-02 - 刚注册的域名被微信拦截,怎么解决?
我的域名www.hmskw.com被微信拦截,但是网页都停用了啊,甚至是之前网页内容都还没放上去,仅仅是点击域名就会进入网页已被停止访问页面。网站备案刚通过!申诉了好多次都不通过!
2021-01-26 - 小程序注销了,身份证绑定还在占用,无法再注册帐号?
[图片]
2020-12-23 - 如何突破一次只能获取20条记录的limit限制?只需要一行代码。
笔者刚遇到需要一次性拉取超过100条(云函数里超过1000条)记录的这种需求。 一般情况下,会有下面两种处理方式: 1、先获取总数,再for循环,每次拉取limit条记录;(可结合Promise.all并发) 2、递归拉取,每次拉取limit条记录,直到拉取的记录数量小于limit。 以上两种方式都比较麻烦,于是动了一脑筋,以最简单的方式实现上面的需求。 极简代码如下: db.collection('order').aggregate() .match({ status:'已付费' }) .addFields({ tempTag:1 //增加一个临时标签;也可以不要addFields这个阶段; }) .group({ _id:'$tempTag', orders:$.push('$$ROOT') //一次性拉取超过100条或者1000条记录 }) .end() .then(res=>{ let orders = res.list[0].orders console.log(orders) }) 一个临时标签,搞定。 小心数据量太大搞崩了,崩溃的极限是多少,需要各位自行摸索了。 需要注意的是,如果是云函数里执行以上代码(比如lookup),返回小程序端的数据量不要超过1M。
2021-03-15 - 只有三行代码的神奇云函数的功能之三:100%成功获取unionid
这是一个神奇的网站,哦不,神奇的云函数,它只有三行代码:(真的只有三行哦) 云函数:login index.js: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event) => { return { ...event, ...cloud.getWXContext() } } 神奇功能之三:100%成功获取unionid: 保证100%成功获取unionid,需要用户信息授权。 强调一下:这个100%是指必须绑定了开放平台,那么不管用户是什么情况,不管有没有关注公众号,一定100%能获取到unionid。 依然需要符合unionid机制:第1条。 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html js: getUserInfo: function (e) { app.globalData.userInfo = e.detail.userInfo if (!app.globalData.unionid ) { wx.cloud.callFunction({ name: 'login', data: { weRunData: wx.cloud.CloudID(e.detail.cloudID) } }).then(res => { app.globalData.unionid = res.result.weRunData.data.unionId }) } }, 其他功能: 神奇功能之四:获取电话号码: 还是这三行代码,获取用户的电话号码。 https://developers.weixin.qq.com/community/develop/article/doc/0006a8ec7ac860c94bf90a34f5d813 神奇功能之五:获取群id: 将小程序分享到某群里,可获得该群的群id, https://developers.weixin.qq.com/community/develop/article/doc/000ea802c00f70894cf9fe72556013 神奇功能之一:获取openid: https://developers.weixin.qq.com/community/develop/article/doc/00080c6e3746d8a940f9b43e55fc13 神奇功能之二:不用授权获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/000a0c6b580338e947f9db0c65b813 [图片]
2020-10-25