从支付或退款之回调处理的设计,看一看抽象类的使用场景

一、背景

抽象类,包含抽象方法和实例方法,抽象方法待继承类去实例化,正是利用该特性,以满足不同支付渠道的差异化需求。
我们在做多渠道支付的时候,接收支付或退款的回调报文,然后去处理。这就意味着,我们往往会定义多组回调接口,把微信官方、支付宝官方、杭州银行等区分开来。
同时,他们之间又存在着许多共性,比如都需要验签,对比回调金额和本地金额是否一致,以及更新本地支付记录的状态等。

本文先会梳理,处理回调的一般逻辑,配合代码设计,尝试让你体会到在编程中,使用抽象类的魅力所在。

二、系统设计

在这里插入图片描述
我们针对不同的支付渠道,定义不同的回调接口,以区分报文的差异。

这里,以微信官方、支付宝官方和杭州银行三个渠道为示例。其实,我们实际对接的支付渠道比这多得多。

三、回调处理流程

在这里插入图片描述

四、抽象类的设计

在这里插入图片描述

  • 源码截图见下:

在这里插入图片描述

五、支付渠道的实现

支付回调处理和退款回调处理,不同的支付渠道会有不同的处理逻辑。有些支付渠道返回的报文,可能需要先进行解密。

0、验签

入参必须有account,然后我们会根据account取出所需要的密钥等信息,去对回调报文进行计算签名。 计算出来的签名和回调报文中的签名,如果不一致,则说明验签失败。

1、杭州银行

  • 支付回调处理
private static final String ERROR_CODE = "comm error";
private static final String SUCCESS_CODE = "got it";String requestResultJson = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());if (log.isInfoEnabled()) {log.info("杭州银行支付回调通知, 回调报文内容是:{}", requestResultJson);}if (StringUtils.isEmpty(requestResultJson)) {return ERROR_CODE;}Map<String, Object> resultMap = JSON.parseObject(requestResultJson, HashMap.class);if (HzBankSignUtil.SUCC_CODE.equalsIgnoreCase((String) resultMap.get(HzBankSignUtil.RESP_CODE))) {return lockPayNotify(resultMap);}return ERROR_CODE;
  • 退款回调处理

因为杭州银行的退款是同步的,所以这里没有对应实现。

  • 验签
    @Overrideprotected boolean doSign(Map<String, Object> resultMap, String account) {return HzBankSignUtil.dataVerifyByAccount(resultMap, CharsetUtil.UTF_8, account);}
  • 其他实例方法
    @Overrideprotected boolean enableSign() {return true;}@Overrideprotected String payChannelName() {return "HzBank";}/*** 获取平台支付流水号** @param resultMap* @return*/@Overrideprotected String getChannelTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("txnOrderId");}/*** 获取第三方支付流水号** @param resultMap* @return*/@Overrideprotected String getOutTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("respTxnSsn");}@Overrideprotected String getRefundTradeNo(Map<String, Object> resultMap) {return null;}@Overrideprotected String getOutRefundNo(Map<String, Object> resultMap) {return null;}/*** 获取支付金额** @param resultMap* @return*/@Overrideprotected Integer getPayAmt(Map<String, Object> resultMap) {return Integer.parseInt((String) resultMap.get("settleAmt"));}@Overrideprotected String getRefundAmt(Map<String, Object> resultMap) {return null;}@Overrideprotected Date getPayOkDate(Map<String, Object> resultMap) {return DateUtils.getDate(String.valueOf(resultMap.get("respTxnTime")), DateUtils.DATE_FORMAT_1);}@Overrideprotected String getRefundStatus(Map<String, Object> resultMap) {return null;}@Overrideprotected Date getRefundOkDate(Map<String, Object> resultMap) {return null;}

2、微信官方

它是一个xml格式的报文,我们使用到了一个三方jar包, com.github.binarywang 下的一个工具包weixin-java-pay。

  • 支付回调处理

    • 1.打印回调报文
    • 2.判断返回状态码
    • 3.统一转换为Map<String,Object>类型
  • 退款回调处理

    • 1.打印回调报文
    • 2.判断返回状态码
    • 3.根据返回报文中的mch_id查询出对应的商户
    • 4.根据上一步的商户密钥,将xml转换为bean对象
    • 5.如果退款成功,则锁定该退款记录,准备处理
    • 6.统一转换为Map<String,Object>类型
  • 验签

    @Overrideprotected boolean doSign(Map<String, Object> paramMap, String account) {//根据account查询商户api密钥ChannelAccount channelAccount = channelAccountService.findByAccount(account);if (Objects.isNull(channelAccount)) {log.error("微信支付回调处理, 交易记录中的账户未配置账户的支付信息, [channelAccount={}]", account);return false;}return SignStrengthenUtils.checkSign(WxPayOrderNotifyResult.fromXML((String) paramMap.get("xmlString")), "MD5", channelAccount.getMchApiSecret());}
  • 其他实例方法
    @Overrideprotected boolean enableSign() {return wxPayConfiguration.isSignEnabled();}@Overrideprotected String payChannelName() {return "WX";}@Overrideprotected String getChannelTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("out_trade_no");}@Overrideprotected String getOutTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("transaction_id");}@Overrideprotected String getRefundTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("outRefundNo");}@Overrideprotected String getOutRefundNo(Map<String, Object> resultMap) {return (String) resultMap.get("refundId");}@Overrideprotected Integer getPayAmt(Map<String, Object> resultMap) {return Integer.parseInt((String) resultMap.get("total_fee"));}@Overrideprotected String getRefundAmt(Map<String, Object> resultMap) {return String.valueOf(resultMap.get("refundFee"));}@Overrideprotected Date getPayOkDate(Map<String, Object> resultMap) {return DateUtils.getDate(String.valueOf(resultMap.get("time_end")), DateUtils.DATE_FORMAT_1);}@Overrideprotected String getRefundStatus(Map<String, Object> resultMap) {return (String) resultMap.get("refundStatus");}@Overrideprotected Date getRefundOkDate(Map<String, Object> resultMap) {return DateUtils.getDate(String.valueOf(resultMap.get("successTime")), DateUtils.DATE_FORMAT_2);}

3、农行

  • 支付回调处理
private String doAbcPayRes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {String requestMsg = request.getParameter(AbcBankConfig.MSG);if (log.isInfoEnabled()) {log.info("农业银行支付回调通知, 回调报文内容是, 解密前:{}", requestMsg);}if (StringUtils.isEmpty(requestMsg)) {return JSON.toJSONString(IcbcNotifyResponseDTO.error("回调报文不能为空"));}final String decodeMessage = Base64Code.Decode64(requestMsg);if (log.isInfoEnabled()) {log.info("农业银行支付回调通知, 回调报文内容是, 解密后:{}", decodeMessage);}Map<String, Object> resultMap = XmlUtil.xmlToMap(decodeMessage);Map<String, Object> messageMap = (Map<String, Object>) resultMap.get(AbcBankConfig.MESSAGE);Map<String, Object> trxResponseMap = (Map<String, Object>) messageMap.get(AbcBankConfig.TRX_RESPONSE);//将原文透传下去,供校验签名trxResponseMap.put(AbcBankConfig.MSG, decodeMessage);if (AbcBankConfig.RC_SUCCESS.equalsIgnoreCase((String) trxResponseMap.get(AbcBankConfig.RETURN_CODE))) {return lockPayNotify(trxResponseMap);}return JSON.toJSONString(IcbcNotifyResponseDTO.error("支付回调处理失败"));}
  • 退款回调处理

农行的退款是同步的,不是采用异步通知的方式。

  • 验签
@Overrideprotected boolean doSign(Map<String, Object> trxResponseMap, String account) {String msg = (String) trxResponseMap.get(AbcBankConfig.MSG);return AbcBankSignUtil.verifySignByAccount(new XMLDocument(msg), account);}
  • 其他实例方法
    @Overrideprotected boolean enableSign() {return true;}@Overrideprotected String payChannelName() {return "ABC";}/*** 获取平台支付流水号** @param resultMap* @return*/@Overrideprotected String getChannelTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("OrderNo");}/*** 获取第三方支付流水号.* <p>* <p>upay流水号</p>** @param resultMap* @return*/@Overrideprotected String getOutTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("iRspRef");}@Overrideprotected String getRefundTradeNo(Map<String, Object> resultMap) {return null;}@Overrideprotected String getOutRefundNo(Map<String, Object> resultMap) {return null;}/*** 获取支付金额** @param resultMap* @return*/@Overrideprotected Integer getPayAmt(Map<String, Object> resultMap) {String amount = (String) resultMap.get("Amount");Precondition.notEmpty(amount, "支付回调金额不能为空");return AmountUtils.changeY2F(amount);}@Overrideprotected String getRefundAmt(Map<String, Object> resultMap) {return null;}@Overrideprotected Date getPayOkDate(Map<String, Object> resultMap) {//格式: YYYY/MM/DDString txDate = String.valueOf(resultMap.get("HostDate")).replaceAll("/", "-");//格式:HH:MM:SSString txTime = String.valueOf(resultMap.get("HostTime"));return DateUtil.parseDateTime(txDate + " " + txTime);}@Overrideprotected String getRefundStatus(Map<String, Object> resultMap) {return null;}@Overrideprotected Date getRefundOkDate(Map<String, Object> resultMap) {return null;}

4、工行

  • 支付回调处理

注意,工行的回调参数,是在query参数里,不是在requestBody。虽然是post接口,但是接口的Content-Type是application/x-www-form-urlencoded。这一点和其他支付方式的回调有较大差异。

    /*** 支付回调的参数*/public final static String APIGW_RSPDATA = "apigw_rspdata";/*** 支付回调的签名*/public final static String APIGW_SIGN = "apigw_sign";/*** 支付回调的证书ID*/public final static String APIGW_CERTID = "apigw_certid";private String doIcbcPayRes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {// Content-Type:application/x-www-form-urlencodedString sign = request.getParameter(IcbcConfig.APIGW_SIGN);String certId = request.getParameter(IcbcConfig.APIGW_CERTID);String rspData = request.getParameter(IcbcConfig.APIGW_RSPDATA);if (log.isInfoEnabled()) {log.info("工商银行支付回调通知, 回调报文内容是:[rspData={}, sign={}, certId={}]", rspData, sign, certId);}if (StringUtils.isEmpty(sign) || StringUtils.isEmpty(certId) || StringUtils.isEmpty(rspData)) {return JSON.toJSONString(IcbcNotifyResponseDTO.error("回调报文不能为空"));}Map<String, Object> resultMap = JSON.parseObject(rspData, HashMap.class);resultMap.put(IcbcConfig.APIGW_SIGN, sign);resultMap.put(IcbcConfig.APIGW_CERTID, certId);resultMap.put(IcbcConfig.APIGW_RSPDATA, rspData);if (IcbcConfig.SUCC_CODE.equalsIgnoreCase((String) resultMap.get(IcbcConfig.RESULT_CODE))) {return lockPayNotify(resultMap);}return JSON.toJSONString(IcbcNotifyResponseDTO.error("支付回调处理失败"));}
  • 退款回调处理

  • 验签
    protected boolean doSign(Map<String, Object> resultMap, String account) {// 校验签名ApiClient apiClient = IcbcBankApiClientCache.getApiClientByAccount(account);try {return apiClient.doVerifyWithExit((String) resultMap.get(IcbcConfig.APIGW_RSPDATA),(String) resultMap.get(IcbcConfig.APIGW_CERTID),(String) resultMap.get(IcbcConfig.APIGW_SIGN),"UTF-8");} catch (Exception e) {log.error("{}签名出现异常,[resultMap={}, certId={}]", payChannelName(),JSON.toJSONString(resultMap), resultMap.get(IcbcConfig.APIGW_CERTID), e);return false;}}
  • 其他实例方法
@Overrideprotected boolean enableSign() {return true;}@Overrideprotected String payChannelName() {return "ICBC";}/*** 获取平台支付流水号** @param resultMap* @return*/@Overrideprotected String getChannelTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("orderNo");}/*** 获取第三方支付流水号.* <p>* <p>upay流水号</p>** @param resultMap* @return*/@Overrideprotected String getOutTradeNo(Map<String, Object> resultMap) {return (String) resultMap.get("serialNo");}@Overrideprotected String getRefundTradeNo(Map<String, Object> resultMap) {return null;}@Overrideprotected String getOutRefundNo(Map<String, Object> resultMap) {return null;}/*** 获取支付金额** @param resultMap* @return*/@Overrideprotected Integer getPayAmt(Map<String, Object> resultMap) {return AmountUtils.changeY2F((String) resultMap.get("totalAmount"));}@Overrideprotected String getRefundAmt(Map<String, Object> resultMap) {return null;}@Overrideprotected Date getPayOkDate(Map<String, Object> resultMap) {String txDate = String.valueOf(resultMap.get("txDate"));String txTime = String.valueOf(resultMap.get("txTime"));return DateUtils.getDate(txDate + txTime, DateUtils.DATE_FORMAT_1);}@Overrideprotected String getRefundStatus(Map<String, Object> resultMap) {return null;}@Overrideprotected Date getRefundOkDate(Map<String, Object> resultMap) {return null;}

六、处理支付/退款记录

上文列举了杭州银行、微信官方、农行、工行等四种支付渠道的实例,相信你后续接入其他支付渠道也是轻轻松松。

下面,我们将介绍公共的处理实现,因为退款逻辑和支付逻辑大同小异,所以我这里只说下支付的实现。

1、抽象回调的返回报文

    /*** 封装回调响应失败的报文** @param msg* @return*/protected abstract String assemblerResponseErrorMsg(String msg);/*** 封装回调响应成功的报文** @param msg* @return*/protected abstract String assemblerResponseSuccessMsg(String msg);

2、非空校验

public String lockPayNotify(Map<String, Object> paramMap) {// 平台订单号String channelTradeNo = getChannelTradeNo(paramMap);String outTradeNo = getOutTradeNo(paramMap);Integer payAmt = getPayAmt(paramMap);if (StringUtils.isEmpty(channelTradeNo) || StringUtils.isEmpty(outTradeNo) || payAmt <= 0) {log.error("{}支付回调通知失败, 平台支付流水号/第三方支付流水号/回调金额均不能为空![channelTradeNo={},outTradeNo={},payAmt={}]",payChannelName(), channelTradeNo, outTradeNo, payAmt);return assemblerResponseErrorMsg("outTradeNo is null Or channelTradeNo is null Or settleAmt is null");}try {return handlePayResult(paramMap, channelTradeNo);} catch (Exception e) {log.error("{}支付回调通知, 处理出现异常,详细错误:", payChannelName(), e);return assemblerResponseErrorMsg(e.getMessage());}}

3、分布式锁

在这里插入图片描述

4、核心逻辑

try {String outTradeNo = getOutTradeNo(paramMap);Integer payAmt = getPayAmt(paramMap);// 支付成功时间Date notifyPayOkDate = getPayOkDate(paramMap);//查找支付订单和判断支付状态PayTrade payTrade = checkPayTradeIsExist(channelTradeNo);if (null == payTrade) {return assemblerResponseErrorMsg("channelTradeNo:[" + channelTradeNo + "] not exist");}if (PayConstants.TRADESTATUS.SUCCESS == payTrade.getStatus()) {return assemblerResponseErrorMsg("channelTradeNo:[" + channelTradeNo + "] has paid, please do not repeat invoke");}//校验签名if (!checkSign(paramMap, payTrade, outTradeNo)) {return assemblerResponseErrorMsg("channelTradeNo:[" + channelTradeNo + "] sign error");}//校验金额if (!checkAmountEqual(payAmt, payTrade, outTradeNo)) {return assemblerResponseErrorMsg("channelTradeNo:[" + channelTradeNo + "] amount not equal");}// 处理订单if (payTradeAppService.handlePayStatus(payTrade, channelTradeNo, outTradeNo, notifyPayOkDate)) {return assemblerResponseSuccessMsg("deal payNotify Success");}return assemblerResponseErrorMsg("channelTradeNo:[" + channelTradeNo + "] update payTrade fail");} catch (Exception e) {if (log.isWarnEnabled()) {log.warn("处理支付回调出现异常", e);}throw new IllegalArgumentException("处理支付回调出现异常", e);}

七、总结

本文以支付和退款回调的实际业务为例,在使用抽象类的情况下,程序代码变得更加易懂,且大大提升了程序的拓展性。
每次接入新的支付渠道,对程序改动的影响和风险降低不少,比如你要接入连连支付,只需要新定义一个连连支付的实现类,并不会改动到其他原有支付的代码逻辑。

其实,我们在实现对账逻辑的时候,也会使用大量的设计模式。(有空梳理下对账逻辑的程序实现)
换言之,抽象类的使用,正是设计模式的一个基石。

我们使用了抽象类,却搞不清是使用了什么设计模式。这倒没什么,怕的是,你没想去减少代码的冗余。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/31429.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【python 深度学习】解决遇到的问题

目录 一、RuntimeError: module compiled against API version 0xc but this version of numpy is 0xb 二、AttributeError: module ‘tensorflow’ has no attribute ‘flags’ 三、conda 更新 Please update conda by running 四、to search for alternate channels that…

Kubernetes 调度 约束

调度约束 Kubernetes 是通过 List-Watch 的机制进行每个组件的协作&#xff0c;保持数据同步的&#xff0c;每个组件之间的设计实现了解耦。 用户是通过 kubectl 根据配置文件&#xff0c;向 APIServer 发送命令&#xff0c;在 Node 节点上面建立 Pod 和 Container。 APIServer…

腾讯云轻量应用服务器和云服务器有什么区别?

腾讯云轻量服务器和云服务器有什么区别&#xff1f;为什么轻量应用服务器价格便宜&#xff1f;是因为轻量服务器CPU内存性能比云服务器CVM性能差吗&#xff1f;轻量应用服务器适合中小企业或个人开发者搭建企业官网、博客论坛、微信小程序或开发测试环境&#xff0c;云服务器CV…

开源数据库Mysql_DBA运维实战 (DDL语句)

DDL DDL语句 数据库定义语言&#xff1a;数据库、表、视图、索引、存储过程. 例如:CREATE DROP ALTER DDL库 定义库{ 创建业务数据库&#xff1a;CREAATE DATABASE ___数据库名___ ; 数据库名要求{ a.区分大小写 b.唯一性 c.不能使用关键字如 create select d.不能单独使用…

unable to write symref for HEAD: Permission denied

今天从gitee上面克隆项目到本地时报错如下 warning: unable to unlink ‘D:/IDEAcode/ruiji1.0/.git/HEAD.lock’: Invalid argument error: unable to write symref for HEAD: Permission denied 解决方法&#xff1a;将要存放项目的文件夹权限修改为完全控制 原先权限&…

W5100S-EVB-PICO 做TCP Server进行回环测试(六)

前言 上一章我们用W5100S-EVB-PICO开发板做TCP 客户端连接服务器进行数据回环测试&#xff0c;那么本章将用开发板做TCP服务器来进行数据回环测试。 TCP是什么&#xff1f;什么是TCP Server&#xff1f;能干什么&#xff1f; TCP (Transmission Control Protocol) 是一种面向连…

十一、结合数字孪生与时间技术进行多维分析设计与实施

大数据可视化中心以主题为分析对象,选择业务分类下的某个主题,可以在数据面板中展示其二维图表,在地图中标记其空间分布,并叠加其相应的二维或三维图层。 1、界面设计 其主界面设计详上图,各部分功能介绍如下: 1.1、主题与图层面板,从上到下,从左到右分别是: ①折…

【1++的数据结构】之二叉搜索树

&#x1f44d;作者主页&#xff1a;进击的1 &#x1f929; 专栏链接&#xff1a;【1的数据结构】 文章目录 一&#xff0c;什么是二叉搜索树二&#xff0c;二叉搜索树的操作及其实现2.1 插入操作及其实现2.2 查找操作及其实现2.3 删除操作及其实现 三&#xff0c;构造及其析构四…

分布式链路追踪概述

分布式链路追踪概述 文章目录 分布式链路追踪概述1.分布式链路追踪概述1.1.什么是 Tracing1.2.为什么需要Distributed Tracing 2.Google Dapper2.1.Dapper的分布式跟踪2.1.1.跟踪树和span2.1.2.Annotation2.1.3.采样率 3.OpenTracing3.1.发展历史3.2.数据模型 4.java探针技术-j…

TOMCAT部署及优化(Tomcat配置文件参数优化,Java虚拟机(JVM)调优)

TOMCAT tomcat &#xff1a;是一个开放源代码的web应用服务器&#xff0c;基于java代码开发的。也可以理解为tomacat就是处理动态请求和基于java代码的页面开发。可以在html当中写入java代码&#xff0c;tomcat可以解析html页面当中的java&#xff0c;执行动态请求&#xff0c;…

Java算法_ LRU 缓存(LeetCode_Hot100)

题目描述&#xff1a;请你设计并实现一个满足 LRU &#xff08;最近最少使用&#xff09; 缓存 约束的数据结构。 获得更多&#xff1f;算法思路:代码文档&#xff0c;算法解析的私得。 运行效果 完整代码 import java.util.HashMap; import java.util.Map;/*** 2 * Author: L…

makefile include 使用介绍

文章目录 前言一、include 关键字1. 语法介绍2. 处理方式示例&#xff1a; 二、- include 操作总结 前言 一、include 关键字 1. 语法介绍 在 Makefile 中&#xff0c;include 指令&#xff1a; 类似于 C 语言中的 include 。将其他文件的内容原封不动的搬入当前文件。 当 …

打破音频语言障碍,英语音频翻译成文字软件助你畅快对话

要理解外语歌曲对我来说难如登天。不过&#xff0c;这种痛苦没有持续太久&#xff0c;我发现了一种音频翻译技术&#xff0c;它像一个语言转换器&#xff0c;可以即时将外语歌曲翻译成我听得懂的语言&#xff01;我惊喜地试用后&#xff0c;终于可以在听歌的同时看到翻译的歌词…

QT压缩解压文件

文章目录 前言一、下载Quazip二、编译Quazip1.使用vs2019打开quazip.sln2.使用Qt VS Tools打开外层的.pro工程3.编译 三、工程使用1.配置头文件路径2.配置静态库lib目录3.添加库4.动态库dll放到.exe同级目录下5.使用 前言 Qt工程中需要用到zip压缩解压功能&#xff0c;网上搜索…

【Tool】win to go 制作随身硬盘

前言 话说我一冲动买了512G固态硬盘&#xff0c;原本是装个ubuntu系统的&#xff0c;这个好装&#xff0c;但是用处太少&#xff0c;就像改成win10的 经历一堆坑之后&#xff0c;终于使用WTG安装好了 步骤 1.下载个WTG辅助工具 Windows To Go 辅助工具|WTG辅助工具 v5.6.1…

leetcode - 75. 颜色分类(java)

颜色分类 leetcode - 75. 颜色分类题目描述双指针代码演示 双指针算法专题 leetcode - 75. 颜色分类 难度 - 中等 原题链接 - 颜色分类 题目描述 给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums &#xff0c;原地对它们进行排序&#xff0c;使得相同颜色的元素相邻&…

【安装部署】Mysql下载及其安装的详细步骤

1.下载压缩包 官网地址&#xff1a;www.mysql.com 2.环境配置 1.先解压压缩包 2.配置环境变量 添加环境变量&#xff1a;我的电脑--->属性-->高级-->环境变量-->系统变量-->path 3.在mysql安装目录下新建my.ini文件并&#xff0c;编辑my.ini文件 编辑内容如…

Centos7.9安装lrzsz进行文件传输---Linux工作笔记059

这里咱们lrzsz命令,需要用来进行文件传输,因为如果不安装这个命令的话,那么 传输安装包什么的就不方便因为只有少数传输工具,才支持,直接拖拽的.没有的时候就可以用这个工具,用命令来传输 直接就是: sz 文件名 就可以把文件下载下来 rz 选择一个文件, 就可以把文件上传到当…

FISCO BCOS V3.0 Air建链体验——对比V2.9建链差别

前提 好久不见&#xff0c;最近因为毕业的手续等问题&#xff0c;一直都没有更新&#xff0c;FISCO BCOS第二季task挑战赛如期展开啦&#xff0c;因为毕业的问题&#xff0c;也是非常遗憾的错过了上一期的task挑战赛&#xff0c;这一期一定双倍挑战&#xff0c;hhhhhh Air版本…

面试热题(单词搜索)

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的单元格内的字母构成&#xff0c;其中“相邻”单元格是那些水平相邻或垂直相…