从零玩转系列之微信支付实战PC端支付微信回调接口搭建

2023-11-05 08:10

本文主要是介绍从零玩转系列之微信支付实战PC端支付微信回调接口搭建,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、前言

halo各位大佬很久没更新了最近在搞微信支付,因商户号审核了我半个月和小程序认证也找了资料并且将商户号和小程序进行关联,至此微信支付Native支付完成.此篇文章过长我将分几个阶段的文章发布(项目源码都有,小程序和PC端)

在此之前已经更新了微信支付开篇、微信支付安全、微信实战基础框架搭建、本次更新为微信支付实战PC端接口搭建,实战篇分为几个章节因为代码量确实有点多哈.

  • 第一章从零玩转系列之微信支付开篇
  • 第二章从零玩转系列之微信支付安全
  • 第三章从零玩转系列之微信支付实战基础框架搭建
  • 第四章从零玩转系列之微信支付实战PC端支付下单接口搭建
  • 第五章从零玩转系列之微信支付实战PC端支付微信回调接口搭建

本次项目使用技术栈

后端: SpringBoot3.1.x、Mysql8.0、MybatisPlus

前端: Vue3、Vite、ElementPlus

小程序: Uniapp、Uview

问题微信添加: BN_Tang

备注: 微信支付

一、Native模式回调

当用户支付完成时候微信会下发一个回调到我们系统当中

该链接是通过基础下单接口中的请求参数notify_url来设置的,要求必须为https地址。请确保回调URL是外部可正常访问的,且不能携带后缀参数,否则可能导致商户无法接收到微信的回调通知信息。回调URL示例: “https://xxxxxx.com/api/wx-pay/native/notify”

通知规则

用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理该消息,并返回应答。

对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)

OK 我们在下单的时候设置了回调必须是HTTPS的SSL证书的

搭建本地调试 到时候上线的时候就替换域名即可

同学们可以使用免费的内网穿透,使用方式官方文档很详细仔细看我这就不讲解.

  • https://www.ngrok.cc/ Sunny-Ngrok
    • 提供免费内网穿透服务,免费服务器支持绑定自定义域名
    • 管理内网服务器,内网web进行演示
    • 快速开发微信程序和第三方支付平台调试
    • 本地WEB外网访问、本地开发微信、TCP端口转发
    • 本站新增FRP服务器,基于 FRP 实现https、udp转发
    • 无需任何配置,下载客户端之后直接一条命令让外网访问您的内网不再是距离

目前博主使用的是花生壳 收费也就6块钱 给了两个SSL的域名速度还可以

  • https://hsk.oray.com/ 花生壳🥜 so easy to happy的东西
  • 无需依赖公网IP、无需配置路由器,花生壳支持在客户端上
  • 添加端口映射,快速将内网服务发布到外网

image-20230614235701735

开启内网穿透代理地址到本地 127.0.0.1:9080

修改 wxpay.properties 当中 wxpay.notify-domain 参数为你的内网穿透地址

image-20230615000333650

支付通知

通知报文

支付结果通知是以POST 方法访问商户设置的通知url,通知的数据以JSON 格式通过请求主体(BODY)传输。通知的数据包括了加密的支付结果详情。
(注:由于涉及到回调加密和解密,商户必须先设置好apiv3秘钥后才能解密回调通知,apiv3秘钥设置文档指引详见APIv3秘钥设置指引)

上面的为商户APIV3的密钥之前我们已经设置好了还未设置的请参考开篇->获取APIv3秘钥(后续都是使用这个秘钥)

通知签名

加密不能保证通知请求来自微信。微信会对发送给商户的通知进行签名,并将签名值放在通知的HTTP头Wechatpay-Signature。商户应当验证签名,以确认请求来自微信,而不是其他的第三方。签名验证的算法请参考 《微信支付API v3签名验证》。

官方话语我就不说了感兴趣的去看文档详细的

总结一下回调需要干的事情

1.签名验证


处理签名验证

构造验签名串

首先,商户先从应答中获取以下信息。

  • HTTP头Wechatpay-Timestamp 中的应答时间戳。
  • HTTP头Wechatpay-Nonce 中的应答随机串。
  • 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。

然后,请按照以下规则构造应答的验签名串。签名串共有三行,行尾以\n 结束,包括最后一行。\n为换行符(ASCII编码值为0x0A)。若应答报文主体为空(如HTTP状态码为204 No Content),最后一行仅为一个\n换行符。

应答时间戳\n
应答随机串\n
应答报文主体\n

我们可以看微信它是咋验证的我们就根据文档的要求改造一下子就行idea 按两下 shift 搜索 WechatPay2Validator

引用地址: com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator

image-20230618212514056

好像啊 直接Copy 新增wechat文件夹复制到该文件夹当中 命名为 WechatPay2ValidatorForRequest

模仿微信验证签名,自定义支付通知API验证签名,针对通知请求的签名验证

改造构造函数

// 回调报文
protected final String body;
// 回调唯一ID 没啥用反正原来存在我们就放在这呗
protected final String requestId;/*** 微信验证器** @param verifier  验证器* @param requestId 请求id* @param body      微信回调的body*/
public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {this.verifier = verifier;this.requestId = requestId;this.body = body;
}

改造验证方法

/*** 验证** @param request 请求* @return boolean 是否成功* @throws IOException ioexception*/
public final boolean validate(HttpServletRequest request) throws IOException {try {// 调用验证回调参数validateParameters(request);// 验签字符串String message = buildMessage(request);String serial = request.getHeader(WECHAT_PAY_SERIAL);// 签名String signature = request.getHeader(WECHAT_PAY_SIGNATURE);// 进行验证if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",serial, message, signature, request.getHeader(REQUEST_ID));}} catch (IllegalArgumentException e) {log.warn(e.getMessage());return false;}return true;
}/*** 构建验证签名消息* 参考文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml">参考文档</a>* <p>* 构造验签名串* 首先,商户先从应答中获取以下信息。* <p>* HTTP头Wechatpay-Timestamp 中的应答时间戳。* HTTP头Wechatpay-Nonce 中的应答随机串。* 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。* 然后,请按照以下规则构造应答的验签名串。签名串共有三行,行尾以\n 结束,包括最后一行。\n为换行符(ASCII编码值为0x0A)。* 若应答报文主体为空(如HTTP状态码为204 No Content),最后一行仅为一个\n换行符。* <p>************************************* 应答时间戳\n* 应答随机串\n* 应答报文主体\n************************************* <p>** @param request 请求* @return {@link String}* @throws IOException ioexception*/protected final String buildMessage(HttpServletRequest request) throws IOException {String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);String nonce = request.getHeader(WECHAT_PAY_NONCE);return timestamp + "\n"+ nonce + "\n"+ body + "\n";}

改造验证回调参数

/*** 验证参数** @param request 请求*/
protected final void validateParameters(HttpServletRequest request) {// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at lastString[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};// 这些头必须存在否则直接是伪造String header = null;for (String headerName : headers) {header = request.getHeader(headerName);if (header == null) {throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);}}// 循环完毕直接默认被赋值是时间戳String timestampStr = header;try {// 验证过期应答Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));// 拒绝过期应答if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);}} catch (DateTimeException | NumberFormatException e) {throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);}
}

完整代码 自定义验证签名器

package com.yby6.wechat;import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;/*** 模仿微信验证签名,自定义支付通知API验证签名,针对通知请求的签名验证** @author Yang Shuai* Create By 2023/06/18*/
public class WechatPay2ValidatorForRequest {protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);/*** 应答超时时间,单位为分钟*/protected static final long RESPONSE_EXPIRED_MINUTES = 5;protected final Verifier verifier;protected final String body;protected final String requestId;/*** 微信验证器** @param verifier  验证器* @param requestId 请求id* @param body      微信回调的body*/public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {this.verifier = verifier;this.requestId = requestId;this.body = body;}/*** 参数错误** @param message 消息* @return {@link IllegalArgumentException}*/protected static IllegalArgumentException parameterError(String message, Object... args) {message = String.format(message, args);return new IllegalArgumentException("parameter error: " + message);}/*** 验证失败** @param message 消息* @return {@link IllegalArgumentException}*/protected static IllegalArgumentException verifyFail(String message, Object... args) {message = String.format(message, args);return new IllegalArgumentException("signature verify fail: " + message);}/*** 验证** @param request 请求* @return boolean 是否成功* @throws IOException ioexception*/public final boolean validate(HttpServletRequest request) throws IOException {try {// 调用验证回调参数validateParameters(request);// 验签字符串String message = buildMessage(request);String serial = request.getHeader(WECHAT_PAY_SERIAL);// 签名String signature = request.getHeader(WECHAT_PAY_SIGNATURE);// 进行验证if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",serial, message, signature, request.getHeader(REQUEST_ID));}} catch (IllegalArgumentException e) {log.warn(e.getMessage());return false;}return true;}/*** 验证参数** @param request 请求*/protected final void validateParameters(HttpServletRequest request) {// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at lastString[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};// 这些头必须存在否则直接是伪造String header = null;for (String headerName : headers) {header = request.getHeader(headerName);if (header == null) {throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);}}// 循环完毕直接默认被赋值是时间戳String timestampStr = header;try {// 验证过期应答Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));// 拒绝过期应答if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);}} catch (DateTimeException | NumberFormatException e) {throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);}}/*** 构建验证签名消息* 参考文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml">参考文档</a>* <p>* 构造验签名串* 首先,商户先从应答中获取以下信息。* <p>* HTTP头Wechatpay-Timestamp 中的应答时间戳。* HTTP头Wechatpay-Nonce 中的应答随机串。* 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。* 然后,请按照以下规则构造应答的验签名串。签名串共有三行,行尾以\n 结束,包括最后一行。\n为换行符(ASCII编码值为0x0A)。* 若应答报文主体为空(如HTTP状态码为204 No Content),最后一行仅为一个\n换行符。* <p>************************************* 应答时间戳\n* 应答随机串\n* 应答报文主体\n************************************* <p>** @param request 请求* @return {@link String}* @throws IOException ioexception*/protected final String buildMessage(HttpServletRequest request) throws IOException {String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);String nonce = request.getHeader(WECHAT_PAY_NONCE);return timestamp + "\n"+ nonce + "\n"+ body + "\n";}
}

处理报文解密

2.验证成功后解密加密的报文

参数解密

下面详细描述对通知数据进行解密的流程:

  1. 1、用商户平台上设置的APIv3密钥【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】,记为key;
  2. 2、针对resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),取得对应的参数nonce和associated_data;
  3. 3、使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象;

注: AEAD_AES_256_GCM算法的接口细节,请参考rfc5116。微信支付使用的密钥key长度为32个字节,随机串nonce长度12个字节,associated_data长度小于16个字节并可能为空字符串。

证书和回调报文解密

为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。本章节详细介绍了加密报文的格式,以及如何进行解密。

微信返回来的加密报文格式

AES-GCM是一种NIST标准的认证加密算法, 是一种能够同时保证数据的保密性、 完整性和真实性的一种加密模式。它最广泛的应用是在TLS中。

证书和回调报文使用的加密密钥为APIv3密钥。

对于加密的数据,我们使用了一个独立的JSON对象来表示。为了方便阅读,示例做了Pretty格式化,并加入了注释。

{"original_type": "transaction", // 加密前的对象类型"algorithm": "AEAD_AES_256_GCM", // 加密算法// Base64编码后的密文"ciphertext": "...",// 加密使用的随机串初始化向量)"nonce": "...",// 附加数据包(可能为空)"associated_data": ""
}

⚠️ 加密的随机串,跟签名时使用的随机串没有任何关系,是不一样的。

解密

算法接口的细节,可以参考RFC 5116。

大部分编程语言(较新版本)都支持了AEAD_AES_256_GCM 。开发者可以参考下列的示例,了解如何使用您的编程语言实现解密。

我们引入的SDK已经有工具类直接用 com.wechat.pay.contrib.apache.httpclient.util.AesUtil

创建处理返回的 \n 的报文转json

package com.yby6.wechat;import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletRequest;import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;/*** 用于解析微信支付回调的数据** @author Yang Shuai* Create By 2023/06/14*/
public class HttpUtils {/*** 分析数据** @param request 请求* @return {@link Map}<{@link String}, {@link Object}>*/public static Map<String, Object> analysisData(HttpServletRequest request) {String body = HttpUtils.readData(request);return JSONUtil.toBean(body, HashMap.class);}/*** 将通知参数转化为字符串*/public 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();}}}}
}

知识点就这些了 实现一手

创建 nativeNotify 映射方法

/*** 支付通知->微信支付通过支付通知接口将用户支付成功消息通知给商户* 参考:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml">...</a>*/
@PostMapping("/notify")public Map<String, String> nativeNotify(HttpServletRequest request, HttpServletResponse response) {log.info("接收到微信服务回调......");try {//处理通知参数String body = HttpUtils.readData(request);Map<String, Object> bodyMap = JSONUtil.toBean(body, HashMap.class);String requestId = (String) bodyMap.get("id");log.info("支付通知的id ===> {}", requestId);// 签名的验证WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, requestId, body);if (!wechatPay2ValidatorForRequest.validate(request)) {log.error("通知验签失败");//失败应答response.setStatus(500);return WechatRep.fail();}log.info("通知验签成功:{}", bodyMap);log.info("回调业务处理完毕");// 成功应答response.setStatus(200);return WechatRep.ok();} catch (Exception e) {log.error("处理微信回调失败:", e);// 失败应答response.setStatus(500);return WechatRep.fail();}}

启动项目测试流程

开启内网穿透 映射你启动项目的端口 自己访问一下是否通

启动程序 请求下单接口 /api/wx-pay/native/native/{productId}

{productId} 查看商品表数据的ID

复制返回的微信二维码地址

进入 https://cli.im/url 生成扫描二维码 使用微信扫描

等待微信回调

image-20230618222248672

ok我们可以正常的接收到微信的回调我们需要根据回调的数据来处理自己系统的业务

修改回调方法 新增 processOrder 业务传递报文

log.info("通知验签成功:{}", bodyMap);
// 通知回调 -> 更新订单状态逻辑
wxPayService.processOrder(bodyMap);
log.info("回调业务处理完毕");

修改 WxPayService 服务类

可以搞redsi分布式锁根据实际业务来我们只是个demo就不要那么严谨

/**
* 一个可重入互斥 锁
*/
private final ReentrantLock lock = new ReentrantLock();/*** 通知回调-> 更新订单状态逻辑*/
@Transactional(rollbackFor = Exception.class)
@Override
public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException, InterruptedException {log.info("处理订单");//解密报文String plainText = decryptFromResource(bodyMap);// 将明文转换成mapMap<String, Object> plainTextMap = JSONUtil.toBean(plainText, Map.class);String orderNo = (String) plainTextMap.get("out_trade_no");// 微信特别提醒:// 在对业务数据进行状态检查和处理之前,// 要采用数据锁进行并发控制,以避免函数重入造成的数据混乱.// 尝试获取锁:// 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放.if (lock.tryLock()) {try {// 处理重复的通知// 接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。OrderInfo orderInfo = orderInfoService.lambdaQuery().eq(OrderInfo::getOrderNo, (orderNo)).one();if (null != orderInfo && !OrderStatus.NOTPAY.getType().equals(orderInfo.getOrderStatus())) {log.info("重复的通知,已经支付成功啦");return;}// 模拟通知并发//TimeUnit.SECONDS.sleep(5);// 更新订单状态orderInfoService.lambdaUpdate().eq(OrderInfo::getOrderNo, orderNo).set(OrderInfo::getOrderStatus, OrderStatus.SUCCESS.getType()).update();log.info("更新订单状态,订单号:{},订单状态:{}", orderNo, OrderStatus.SUCCESS);// 记录支付日志paymentInfoService.createPaymentInfo(plainText);} finally {// 要主动释放锁lock.unlock();}}
}

参数解密

/*** 对称解密*/
private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {log.info("密文解密");//通知数据拿到 resource 节点Map<String, String> resourceMap = (Map) bodyMap.get("resource");//数据密文String ciphertext = resourceMap.get("ciphertext");//随机串String nonce = resourceMap.get("nonce");//附加数据String associatedData = resourceMap.get("associated_data");log.info("密文 ===> {}", ciphertext);AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));// 使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);log.info("明文 ===> {}", plainText);return plainText;
}

记录支付日志

引入

/*** 支付日志*/
private final PaymentInfoService paymentInfoService;

修改 PaymentInfoService

package com.yby6.service;import cn.hutool.core.map.MapUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yby6.domain.PaymentInfo;
import com.yby6.enums.PayType;
import com.yby6.mapper.PaymentInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.Map;@Slf4j
@Service
public class PaymentInfoService extends ServiceImpl<PaymentInfoMapper, PaymentInfo> {/*** 创建付款信息** @param plainText 纯文本*/@Transactionalpublic void createPaymentInfo(String plainText) {log.info("记录支付日志: {}", plainText);Map<String, Object> plainTextMap = JSONUtil.toBean(plainText, Map.class);//订单号String orderNo = (String) plainTextMap.get("out_trade_no");//业务编号String transactionId = (String) plainTextMap.get("transaction_id");//支付类型String tradeType = (String) plainTextMap.get("trade_type");//交易状态String tradeState = (String) plainTextMap.get("trade_state");//用户实际支付金额Map<String, Object> amount = (Map<String, Object>) plainTextMap.get("amount");Integer payerTotal = MapUtil.getInt(amount, "payer_total");PaymentInfo paymentInfo = new PaymentInfo();paymentInfo.setOrderNo(orderNo);paymentInfo.setPaymentType(PayType.WXPAY.getType());paymentInfo.setTransactionId(transactionId);paymentInfo.setTradeType(tradeType);paymentInfo.setTradeState(tradeState);paymentInfo.setPayerTotal(payerTotal);paymentInfo.setContent(plainText);baseMapper.insert(paymentInfo);}
}

启动项目测试流程

开启内网穿透 映射你启动项目的端口 自己访问一下是否通

启动程序 请求下单接口 /api/wx-pay/native/native/{productId}

{productId} 查看商品表数据的ID

复制返回的微信二维码地址

进入 https://cli.im/url 生成扫描二维码 使用微信扫描

等待微信回调处理系统业务订单状态更改

image-20230618230135867

这篇关于从零玩转系列之微信支付实战PC端支付微信回调接口搭建的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/348531

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

W外链微信推广短连接怎么做?

制作微信推广链接的难点分析 一、内容创作难度 制作微信推广链接时,首先需要创作有吸引力的内容。这不仅要求内容本身有趣、有价值,还要能够激起人们的分享欲望。对于许多企业和个人来说,尤其是那些缺乏创意和写作能力的人来说,这是制作微信推广链接的一大难点。 二、精准定位难度 微信用户群体庞大,不同用户的需求和兴趣各异。因此,制作推广链接时需要精准定位目标受众,以便更有效地吸引他们点击并分享链接

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

搭建Kafka+zookeeper集群调度

前言 硬件环境 172.18.0.5        kafkazk1        Kafka+zookeeper                Kafka Broker集群 172.18.0.6        kafkazk2        Kafka+zookeeper                Kafka Broker集群 172.18.0.7        kafkazk3

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6+Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站,手机流量可以访问IPV6网络的服务,为什么不在电脑搭建Home Assistant(hass),来控制你的设备呢?@智能家居 @万物互联

滚雪球学Java(87):Java事务处理:JDBC的ACID属性与实战技巧!真有两下子!

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~ 🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!! 环境说明:Windows 10