当前位置: 首页 > news >正文

乐企数电发票分布式发票号码生成重复的问题修复思路分享

文章目录

  • 1.前言
  • 2.解决思路
    • 2.1错误姿势
    • 2.2歪打正着
    • 2.3正确姿势
  • 3.总结

1.前言

    由于之前接了乐企数电开票,服务上线之后,使用的公司少没有啥问题,后面切换了两家日开票量大的公司上线之后,就发现发票号码生成重复了,后面下班紧急修复了,修复到凌晨3点发了一个版本上去,之后才没有重复的,这次其实是歪打正着的搞好了,后面我思考分析了下之后重新优化了之后上了几个版本后观察,使用数据库表的方式还是会有一定的概率会重复,之前歪打正着是因为使用了@Transactional注解,事务传播级别设置为READ_COMMITTED,后面揭晓为啥是歪打正着。在这个项目中使用了biz-ratelimiter-redissonlock-manualctrltrans-spring-boot-start启动器,在使用它的时候有一些注意事项,下面分享是啥注意事项。在那两家公司切换上线之后发票号码生成重复了之后,导致开票数据有问题,后面处理修复了好几天的数据,也是搞的蛋疼的很,所以才分享一下这个问题的解决思路。

2.解决思路

参考文章

https://mp.weixin.qq.com/s/-1Xh_g3b58hA765uLqDs6A
https://www.cnblogs.com/qwg-/p/18080431

2.1错误姿势

    使用了这个

    @BizIdempotentManualCtrlTransLimiterAnno(isOpenManualCtrlTrans = true, isOpenRedissonLock = true)

    一般情况下在一个方法中单用分布式锁即可,不用手动去控制事务的提交,各个表的数据操作错了就错了,只要不影响业务就行,一个微服务的操作多张表不用做到数据的特别的强一致性,在多个微服务之间都没有使用seata这种分布式事务框架的,都是使用最终一致性的MQ来达到最终一致性,更何况这种一个微服务的多表数据之间就不需要这种强的数据一致性操作了,如果这种搞一个大方法来手动提交事务就会是一个大事务提交,这种处理实际上也是比较坑的,所以不推荐这种使用。

    @BizIdempotentManualCtrlTransLimiterAnno(isOpenManualCtrlTrans = true)

    如果在一个方法中使用了分布式锁+手动控制事务,这个方法的切面中涉及到多张表的操作,这种情况下使用姿势就有很大的问题,一是:分布式锁的粒度太大,二是:在一个大的方法中,涉及操作多张表的增删改查的时候,不用去手动控制事务的,使用默认的机制就可以了,springBoot默认会自动取提交事务的,事务的提交也是一个异步的的过程,如果手动控制一个事务的提交,在一个大方法执行完最后提交的这个事务会是一个大事务,也就是说,这个事务里面操作的表数据太多了对应一次需要执行的sql也会有很多,所以使用分布式锁 + 手动控制事务 + mybatisPlus的乐观锁 这种情况下使用数据了的表来存储乐企下载的赋码段之后在来计算一个自增的发票号码,在高并发的情况下就会生成重复,之前使用的AtomicLong或LongAdder(这两个的性能差不多),使用这两个来自增Long类型,只使用于单节点的服务上,如果在多节点上,就会重复,假设两个请求都读取到库中表里当前发票号加入内存还是上一个事务没有提交之后的值,这种加入计算就会重复了,所以正确的Long的原子自增需要使用RedisTemplate的increment来自增一个Long,因为RedisTemplate的increment是原子的。

    使用这种方法的弊端在哪里?

    1.分布式锁粒度大,操作了一个大事务

    2.默认springBoot的mysql的事务隔离基本是REPEATABLE_READ可重复读,所以每个请求线程操作的数据都是相互隔离不可见的,只有当事务提交之后读取出来的才是最新的值,这里就存在一个空隙,操作不是原子的,事务提交是异步的,每次读取的值都有可能是上次(或是上上次,不晓得是哪一次的)的久的值,这种就导致本次计算重复。

    3.使用了AtomicLong或LongAdder来内存中原子加,是适用于单节点,如果在分布式环境下,多节点每个节点上的内存数据都有可能是久的值,这种就会导致计算之后发票号产生大量的重复。

2.2歪打正着

    在最最最最最最外层的public方法上加了@Transactional注解,事务传播级别设置为READ_COMMITTED和将Long的自增操作由AtomicLong改为了RedisTemplate的increment这个,还有就是使用了:

    @BizIdempotentManualCtrlTransLimiterAnno(isOpenManualCtrlTrans = true, isOpenRedissonLock = true)

    只使用分布式锁,不手动控制事务避免了大事务的提交,这个是可以解决,但是如果内部方法有任何的异常抛出,最终抛出返回的异常是事务只读的一个异常,所以这种方式会有这么一个问题,这个问题是由于@Transactional注解的切面执行顺序比@BizIdempotentManualCtrlTransLimiterAnno的注解切面顺序优先级高,就相当于@BizIdempotentManualCtrlTransLimiterAnno的注解切面抛出的任何异常最后都会被@Transactional注解的切面捕捉到,最终抛出了一个事务只读的一个异常,上面参考文章第一个链接的那篇文章上说的是自定义分布式锁注解+@Transactional注解会有问题,跟我遇到的这个大致相似,但是我这个歪打正着是在最最最最最最外层的public方法上加了@Transactional注解,事务传播级别设置为READ_COMMITTED(这种搞暂时是没有重复了,是因为可以读已经提交的事务数据,也就是线程一计算最新的发票号码事务自动提交之后,其它线程可以读到最新当前发票号码提交的事务的数据),这种搞了之后,虽然歪打正着的解决了,但是运行时间较短,并发不高,很不会在重复,如果在并发较高的情况下,这个事务自动提交是异步的,等待提交的事务数据库那边还处理不过来导致事务堆积,还有就是读已提交只是对于单个节点的服务可见。这种两个情况下都是会有问题的,只不过并发小不会出现重复。

2.3正确姿势

    使用redis来存在从乐企下载的发票赋码段相关信息(起始号码、终止号码、当前号码,触发下载号码值:终止号码减 50)、发票赋码下载结果(下载status、ywlsh业务流水号)这个需要存redis、还有一个就是发票计算需要使用RedisTemplate的increment来计算,所以这里使用了三个缓存加分布式锁才解决了这个发票重复的问题,redis中的发票当前发票号码必须以RedisTemplate的increment中计算存储的为准,如果以redis中存储那个下载实体对象来的话就会导致重复,因为这个发票赋码段的存储redis更新会有一个延迟,所以跟数据库那种方式一样会有一个更新延迟,所以只能以RedisTemplate的increment计算存储redis中的为准,就不会重复了。

3.总结

    这个问题也是搞的非常的蛋疼,经过了生产的实践之后的经验总结,如果上面使用存redis的方式还不能解决,估计怕是要写lua脚本来搞了,但是最终使用存redis的方式来计算解决了这个问题,不然就更加的麻烦了,希望我的分享对你有所启发和帮助,请一键三连,么么么哒!

相关文章:

乐企数电发票分布式发票号码生成重复的问题修复思路分享

文章目录 1.前言2.解决思路2.1错误姿势2.2歪打正着2.3正确姿势 3.总结 1.前言 由于之前接了乐企数电开票,服务上线之后,使用的公司少没有啥问题,后面切换了两家日开票量大的公司上线之后,就发现发票号码生成重复了,后面…...

多级缓存架构设计与实践经验

多级缓存架构设计与实践经验 在互联网大厂Java求职者的面试中,经常会被问到关于多级缓存的架构设计和实践经验。本文通过一个故事场景来展示这些问题的实际解决方案。 第一轮提问 面试官:马架构,欢迎来到我们公司的面试现场。请问您对多级…...

LCD1602液晶显示屏详解(STM32)

目录 一、介绍 二、传感器原理 1.原理图​编辑 2.接口说明 三、程序设计 main文件 lcd1602.h文件 lcd1602.c文件 四、实验效果 五、资料获取 项目分享 一、介绍 LCD1602A字符型液晶显示模块是专门用于显示字母、数字元、符号等的点阵型液晶显示模块。分4位和8位数据…...

Golang | 集合求交

文章目录 bitmap求交集2个有序链表多个有序链表跳表 bitmap求交集 2个有序链表 多个有序链表 为什么非最大的所有都要往后移动呢?因为现在已经知道交集即使有,也最小都是这个目前最大的了,其他不是最大的不可能是交集,所有除了最大…...

手机充电进入“秒充“时代:泡面刚下锅,电量已满格

现代人的生活节奏越来越快,手机充电技术也在飞速发展。从最初的"充电一整晚"到如今的"秒充"时代,充电效率的提升正在悄然改变着我们的生活习惯。最新数据显示,目前最快的手机充电技术仅需4分30秒就能充满一部手机的电量&…...

网站字体文件过大 导致字体从默认变成指定字体的时间过长

1.选择字体中只用到的字符集较小的包 只用到了数字,所以使用了 xx-sans.ttf的版本(86kb) 2.转换ttf格式为woff2 转换后26kb 3.使用字体 // 定义字体 font-face {font-family: "myFont";src: url(/assets/fonts/myFont.woff2) format(woff2);font-weigh…...

WPF常用技巧汇总 - Part 2

WPF常用技巧汇总-CSDN博客 主要用于记录工作中发现的一些问题和常见的解决方法。 目录 WPF常用技巧汇总-CSDN博客 1. DataGrid Tooltip - Multiple 2. DataGrid Tooltip - Cell值和ToolTip值一样 3. DataGrid Tooltip - Cell值和ToolTip值不一样 4. DataGrid - Ctrl A /…...

C++中析构函数

析构函数 析构函数(Destructor)是类的一种特殊成员函数,用于在对象的生命周期结束时执行清理操作,他的主要作用是释放对象占用资源,例如动态分配的内存,文件句柄或网络连接等。 特点 名称与类名称相同 单…...

树莓派超全系列教程文档--(44)如何在树莓派上编译树莓派内核

如何在树莓派上编译树莓派内核 构建内核下载内核源代码 本地构建内核构建配置使用 LOCALVERSION 自定义内核版本构建安装内核 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 构建内核 操作系统预装的默认编译器和链接器被配置为构建在该操作系统…...

flask返回文件的同时返回其他参数

参考:flask实现上传文件与下载文件_flask 文件上传和下载-CSDN博客 在 Flask 中,返回文件的同时附加额外参数(如处理时间)可以通过 自定义 HTTP 响应头 或 返回 JSON 数据与文件结合 的方式实现。以下是具体方法和示例: 方法 1:通过 HTTP 响应头 附加参数(推荐) 将参…...

C++23 std::move_only_function:一种仅可移动的可调用包装器 (P0288R9)

文章目录 一、定义与基本概念1.1 定义1.2 基本概念 二、特点2.1 仅可移动性2.2 支持多种限定符2.3 无target_type和target访问器2.4 强前置条件 三、使用场景3.1 处理不可复制的可调用对象3.2 性能优化3.3 资源管理 四、与其他可调用包装器的对比4.1 与std::function的对比4.2 …...

Zookeeper实现分布式锁实战应用

Zookeeper实现分布式锁实战应用示例 1. 分布式锁概述 在分布式系统中,当多个进程或服务需要互斥地访问共享资源时,就需要分布式锁来协调。Zookeeper因其强一致性和临时节点特性,非常适合实现分布式锁。 2. Zookeeper实现分布式锁的核心原理…...

使用 Playwright 构建高效爬虫:原理、实战与最佳实践

随着网站前端技术日益复杂,传统的基于请求解析(如 requests、BeautifulSoup)的爬虫在处理 JavaScript 渲染的网站时变得力不从心。Playwright,作为微软推出的一款强大的自动化浏览器控制框架,不仅适用于自动化测试,也成为了处理现代网站爬取任务的利器。 本篇文章将带你…...

ComfyUI for Windwos与 Stable Diffusion WebUI 模型共享修复

#工作记录 虽然在安装ComfyUI for Windwos时已经配置过extra_model_paths.yaml 文件,但升级ComfyUI for Windwos到最新版本后发现原先的模型配置失效了,排查后发现,原来是 extra_model_paths.yaml 文件在新版本中被移动到了C盘目录下&#x…...

【RabbitMQ消息队列】详解(一)

初识RabbitMQ RabbitMQ 是一个开源的消息代理软件,也被称为消息队列中间件,它遵循 AMQP(高级消息队列协议),并且支持多种其他消息协议。 核心概念 生产者(Producer):创建消息并将其…...

【MySQL数据库入门到精通-08 约束】

文章目录 4、约束4.1 概述4.2 约束演示1. 根据需求,完成表的创建2. SQL数据库3. 结果 4.3 外键约束4.3.1 介绍1. 根据需求,完成表的创建2. SQL数据库3. 结果4.3.2 外键约束建立1. 语法2. SQL语句3. 现象4.3.3 外键删除更新行为1. 知识点2.SQL3.结果 4、约…...

C++笔记-模板进阶和继承(上)

一.模板进阶 1.1非模板类型参数 那之前学过的stack举例,在这之前我们如果要用N,就要用宏来定义,但是宏毕竟有局限性: 如果我要用到两个stack,一个要求10个空间,另一个要求100空间呢? 这时候…...

云计算赋能质检LIMS的价值 质检LIMS系统在云计算企业的创新应用

在云计算技术高速发展的背景下,实验室信息化管理正经历深刻变革。质检LIMS(实验室信息管理系统)作为实验室数字化转型的核心工具,通过与云计算深度融合,为企业提供了高弹性、高安全性的解决方案。本文将探讨质检LIMS在…...

2025系统架构师---数据抽象(Data Abstraction)‌与‌面向对象架构风格

引言 在软件系统复杂度与规模不断攀升的今天,如何设计出可扩展、易维护且能快速响应需求变化的架构,是每一位系统架构师面临的挑战。‌数据抽象(Data Abstraction)‌与‌面向对象架构风格(Object-Oriented Architectu…...

[python] 基于WatchDog库实现文件系统监控

Watchdog库是Python中一个用于监控文件系统变化的第三方库。它能够实时监测文件或目录的创建、修改、删除等操作,并在这些事件发生时触发相应的处理逻辑,因此也被称为文件看门狗。 Watchdog库的官方仓库见:watchdog,Watchdog库的官…...

缺省处理、容错处理

布尔判定 假:false 0 null undefined NaN 可选符.?和?? let obj {name: jim,data: {money: 0,age: 18,fn(a){return a}} }1、如果左侧的值为null或者undefined,则使用右侧值。需要使用"??" obj?.data?.a…...

Taro on Harmony :助力业务高效开发纯血鸿蒙应用

背景 纯血鸿蒙逐渐成为全球第三大操作系统,业界也掀起了适配鸿蒙原生的浪潮,用户迁移趋势明显,京东作为国民应用,为鸿蒙用户提供完整的购物体验至关重要。   去年 9 月,京东 AP…...

Java基础——排序算法

排序算法不管是考试、面试、还是日常开发中都是一个特别高频的点。下面对八种排序算法做简单的介绍。 1. 冒泡排序(Bubble Sort) 原理:相邻元素比较,每一轮将最大元素“冒泡”到末尾。 示例数组:[5, 3, 8, 1, 2] pub…...

【操作系统原理07】输入/输出系统

文章目录 零.大纲一.I/O设备的概念和分类0.大纲1.什么是I/O设备2.I/O分类 二.I/O控制器0.大纲1.I/O设备的电子部件(I/O控制器)2.IO控制器组成3.内存映像I/O VS 寄存器独立编址 三.I/O控制方式0.大纲与总结1.程序直接控制方式(1) 操…...

IM云端搜索全面升级,独家能力拓展更多“社交连接”玩法

在这个数字时代,网络让信息传递前所未有的便捷,但同时,海量数据堆积也让内容检索变得像大海捞针。尤其是在我们日常工作生活中最常用的即时通信软件中,信息的快速查找和精准定位正变得越来越重要。 但传统的本地搜索功能受限于设…...

汽车产业链主表及类别表设计

(提前设计,备用) 一、汽车产业链类别表(industry_chain_category) 设计要点 1、核心字段:定义产业链分类(如零部件、整车制造、销售服务等) 2、主键约束:自增ID作为唯一标…...

有效的字母异位词

recorded&#xff1a;用于统计或抵消字符出现次数。 class Solution { public:bool isAnagram(string s, string t) {int record[26]{0};for(int i0;i<s.size();i){record[s[i]-a];}for(int i0;i<t.size();i){record[t[i]-a]--;}for(int i0;i<26;i){if(record[i]!0){…...

汽车网络安全 -- 理解暴露面、攻击面和攻击向量

1.暴露面是攻击面的子集 举个例子&#xff0c;房子都有门、窗户&#xff0c;这些窗户、门不管是否打开&#xff0c;都可能被小偷利用进入到房内&#xff0c;因此这些门窗可能是潜在的漏洞&#xff0c;所以称之为攻击面(Attack Surface)。 小偷经过长期观察&#xff0c;发现家…...

C++异步利器:全面理解 std::packaged_task

在现代 C&#xff08;C11及以后&#xff09;中&#xff0c;并发与异步编程是不可回避的重要技能。我们常常希望把某些计算任务扔给后台线程去处理&#xff0c;同时又能优雅地获取任务结果。 这时候&#xff0c;std::packaged_task 就是一个非常强大的工具。 本文将带你深入理解…...

Animate 中HTMLCanvas 画布下的鼠标事件列表(DOM 鼠标)

在 JavaScript 和 ‌Adobe Animate&#xff08;CreateJS&#xff09;‌ 中&#xff0c;常用的鼠标交互事件可分为两大类&#xff1a;‌基础 DOM 事件‌ 和 ‌CreateJS 扩展事件‌12。以下是完整分类&#xff1a; 一、基础 DOM 鼠标事件 事件名触发场景冒泡特性click鼠标左键单…...

RagFlow文档切块提升

1.RagFlow切块介绍 2.复现优化 2.1 General 通用分块 def parser_text(self, txt, blockSize512, overlapSize0, delimiter"\n!?;。&#xff1b;&#xff01;&#xff1f;"):文本分割sentences self.split_text_by_period_qh(txt, delimiter, blockSizeblockSize)…...

音频转base64

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>音频转Base64</title><style>.containe…...

蓝桥杯 11. 打印大X

打印大X 原题目链接 题目描述 小明希望用星号拼凑&#xff0c;打印出一个大 X&#xff0c;他要求能够控制笔画的宽度和整个字的高度。 为了便于比对空格&#xff0c;所有的空白位置都以句点符 . 来代替。 输入描述 输入两个整数 m 和 n&#xff0c;表示笔画的宽度和 X 的高…...

页面需要重加载才能显示的问题修改

1.问题描述&#xff1a;跳转页面后&#xff0c;只有点击重新加载后才会显示内容 经过测试后&#xff1a; / 跳转详情 const goToDetail (bookId) > { router.push({ path: /classic-detail, query: { book_id: bookId } }) } 执行完以上代码后&#xff0c;页面从classics…...

On the Biology of a Large Language Model——Claude团队的模型理解文章【论文阅读笔记】其二——数学计算部分

这篇内容的源博文是 On the Biology of a Large Language Model 这是Anthropic&#xff0c;也就是Claude的团队的一遍技术博客。他的主要内容是用一种改良版的稀疏编码器来解释LLM在inference过程中内部语义特征的激活模式。因为原文太长&#xff0c;我把原文分成了几份来写阅读…...

Python语言基础知识详解:标识符与变量

Python语言基础知识详解&#xff1a;标识符与变量 一、标识符&#xff08;Identifiers&#xff09; 定义 标识符是用于命名变量、函数、类、模块或其他对象的名称。它是代码中对实体的唯一标识。 1. 标识符的命名规则 Python的标识符需遵循以下规则&#xff1a; 允许的字符 由…...

google chrome 中 fcitx5 候选框不跟随光标

我的电脑&#xff1a;ubuntu22.04&#xff0c;窗口系统&#xff1a;wayland 2025/4/26 号更新的谷歌浏览器 今天打开浏览器发现输入法的候选框固定在左上角不动了&#xff0c;一番折腾&#xff0c;发现解决办法如下&#xff1a; 在搜索框中输入 about:flags搜索 wayland&#…...

深入浅出提示词工程(结合 DeepSeek)

提示词工程 Prompt 即提示、指令&#xff0c;所以提示工程也叫「指令工程」 用户输入的问题称为 Prompt&#xff0c;本文主要探讨 System Prompt&#xff08;我将其翻译成「系统预设」&#xff09; 使用 Prompt 的目的 直接提问 如「我该学 Vue 还是 React&#xff1f;」&…...

OpenVLA:大语言模型用于机器人操控的经典开源作品

TL;DR 2024 年斯坦福大学提出的 OpenVLA&#xff0c;基于大语言模型实现机器人操控&#xff0c;代码完全开源。 Paper Notes Name&#xff1a;OpenVLA: An Open-Source Vision-Language-Action ModelURL&#xff1a;https://openvla.github.io/作者&#xff1a;斯坦福&#…...

数值分析、数值代数之追赶法

数值分析、数值代数之追赶法 MATLAB 中&#xff0c;diag 函数用法追赶法推导过程代码运行过程 MATLAB 中&#xff0c;diag 函数用法 在 MATLAB 中&#xff0c;diag 函数用于处理矩阵的对角线元素或创建对角矩阵。以下是其常见的用法&#xff1a; 1.提取矩阵的对角线元素 2.创…...

深入浅出JVM - Java架构师面试实战

深入浅出JVM - Java架构师面试实战 本文通过模拟一位拥有十年Java研发经验的资深架构师马架构与面试官之间的对话&#xff0c;深入探讨了JVM的核心知识点。涵盖内存结构、垃圾回收算法、垃圾回收器、内存调优工具及参数配置等关键领域。 第一轮提问 面试官&#xff1a; 马架…...

Qt网络数据解析方法总结

在Qt中解析网络数据通常涉及接收原始字节流&#xff0c;并将其转换为有意义的应用层数据。以下是详细步骤和示例&#xff1a; 1. 网络数据接收 使用QTcpSocket或QUdpSocket接收数据&#xff0c;通过readyRead()信号触发读取&#xff1a; // 创建TCP Socket并连接信号 QTcpSo…...

[AHOI2001] 质数和分解

import java.util.*;public class Main {static int[] ss new int[201];public static void main(String[] args) {Scanner sc new Scanner(System.in);while (sc.hasNextInt()) { int n sc.nextInt();int num 0; // 记录质数个数int[] dp new int[201];dp[0] 1;for (in…...

说一下Drop与delete区别

在数据库操作里&#xff0c;DROP与DELETE是两个重要且功能不同的命令&#xff0c;以下为你详细介绍二者的区别&#xff1a; 功能层面 DROP&#xff1a;此命令用于删除数据库、表、视图、索引等数据库对象。一旦执行&#xff0c;数据库对象就会被彻底删除&#xff0c;其定义和…...

基于云原生架构的后端微服务治理实战指南

一、引言&#xff1a;为什么在云原生时代更需要微服务治理&#xff1f; 在单体应用时代&#xff0c;开发和部署虽然简单&#xff0c;但随着系统规模的扩大&#xff0c;单体架构的维护成本急剧上升&#xff0c;部署频率受限&#xff0c;模块之间相互影响&#xff0c;最终导致系…...

后端响应巨量数据,如何优化性能?

WebSocket流式传输 fetch虚拟滚动 &#xff08;渲染性能提升&#xff0c;一次性记载固定条数&#xff09;分片滚动 fetch流式传输 async function streamData(url) {unction streamOutput(msg) {// 发送 POST 请求fetch(url, {method:"POST",body:JSON.stringify({ …...

《代码整洁之道》第4章 注释 - 笔记

注释的恰当用法是弥补代码表达意图时遭遇的失败&#xff0c;良好的代码&#xff0c;让读者看代码就能明白含义。 代码在变动&#xff0c;在演化。注释并不总是随之变动。不准确的注释比没有注释要坏的多。注释算的上是一种没办法去除的恶。 注释不能美化代码 与其花时间编写…...

闭包与装饰器(python)

此 Python 代码借助闭包构建了计算对数的函数。闭包指的是一个函数与其所引用的外部变量共同构成的一个整体。借助闭包&#xff0c;我们能够创建具有特定行为的函数&#xff0c;并且这些函数可以记住其创建时的环境。 代码详细分析 导入模块 python import math 导入 math …...

学成在线网页

技术&#xff1a;h5css&#xff0c;静态页面 主页&#xff1a; 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0&quo…...

AI测试工具Testim——告别自动化测试维护难题

随着人工智能技术的快速发展&#xff0c;AI测试工具正在成为提升软件研发效能的关键。每款AI的特性各有差异&#xff0c;今天&#xff0c;我们就给大家介绍一款专注于Web和移动应用的端到端的AI测试工具--Testim。 Testim的简介 官网地址&#xff1a;https://www.testim.io/ 简…...