vivo 互联网自研代码评审 VCR 落地实践

作者:vivo 互联网效能平台团队- Chi Wei

本文介绍了vivo工程效能团队基于 Gitlab、Gerrit等开源工具搭建的VCR平台,代码评审idea插件开发及开发过程中遇到的挑战、困难,并分享了相应的应对策略和优化方案。

代码评审是软件质量保证一种活动,由一个或者多个人对一个程序的部分或者全部源代码进阅读理解。一般来说分为作者和评审者两种角色,作者方提供代码逻辑的介绍和代码,评审者则对提供的代码基于设计,功能性和非功能性等方面认知进行阅读并提出问题。常见的评审组织形式是有同行评审(Peer Review)和小组检查 (Team Inspection)两种方式。

在代码评审中,评审的目的在通过代码的评审发现潜在的问题,同时分享和表达是代码评审的重要收获,我们知道人相同在不同的文化下生产力是不同的,代码评审是一个工具,工具受文化的影响的同时也影响着文化,最终朝着我们希望的责任共担、持续改进的方向发展。

一、代码评审演进

图片

随着互联网的发展,开发人员也越来越重视代码评审带来的代码的代码质量提高以及代码评审间接带来的分享及人员备份效果,已经不满足于只是简单的发现当前问题解决问题记录问题,需要满足从评审基本跟进、评论管理、评审报告以及评审方式多样化、评审与研发流程相结合等需求。

① 代码评审检查表:手工定义要检查项,检查完进行打卡标记结果。

② 插件快速评审导入导出:快速在插件上进行评论,并将评论结果导出给被评审人,被评审人导入评审结果查看,评审表不可复用,一旦代码变更则无法准确定位、也无法再次跟踪评审修改结果。

③ 在线代码评审:在线插件或网页评审,提供提交前提交后评审,可多人评审策略管控、代码评审与需求/缺陷关联管理。

④ 自动化代码评审:结合现有的Sonar扫描、安全扫描进行对提交的代码进行自动化检查,使代码在人工评审之前已经经历一轮自动评审,代码评审通过之后可自动触发构建、部署等。

⑤ 智能化代码评审:根据AI大模型,可对提交代码进行综合评价(编码标准、可用性、可读性、可维护性、安全性、高性能、异常控制、设计原则、可扩展性、代码复杂度等等)并给出相关测试建议等,未来大模型对代码评审还有更大的空间。

二、代码评审解决的需求和痛点是什么?

vivo当前已经有EasyCR评审工具,那为什么我们还需要继续开发调研代码评审工具呢?

我们先看看下面通过内部调研获取的信息,看看用户希望的代码评审工具需求和痛点是什么?

图片

针对当前vivo代码评审工具我们继续升级补充场景:

  1. 增加评审方式:对原自由评审方式(主要是提交后进行代码评审)增加评审控制方式(提交代码至仓库前进行代码评审、合并时提交代码评审)。

  2. 支持网页/插件:增加网页端评审功能,满足不同角色进行评审及用户体验上的优化,增强插件版评审功能。

  3. 支持研发流程控制:上线过程中可作为人工卡点一项检查项(可通过代码是否评审、代码评分、代码问题解决情况等进行判断),通过线上管理,提高上线质量。

  4. 支持自动化检查:代码提交前,提交后可进行代码自动化检查,对代码进行自动评审。

  5. 增加用户定制化需求:如评审权限、评审通知方式、评审策略多人评审管理、评审报告订阅等。

当前市场上有很多优秀的代码评审工具,但是很少有评审工具能满足所有的场景,角色不同,需要的能力不同,同一个角色不同团队使用的方式不同,我们需要一款解决用户痛痒爽的代码评审工具。

三、vivo代码评审系统架构

图片

四、vivo代码评审工具使用流程

在代码评审中,CR可以是一次Commit,也可以是一次MergeCommit,那么针对一次CR我们可以随时对已经提交的commit进行评审,也可以在CRpush至代码库之前拦截,同时也可以在一次合并之前进行代码评审。

代码评审模式:

1. 提交前评审(Pre-push Code Review)

2. 提交后评审(Post-push Code Review)

    ① 合并评审

    ② 自由评审

图片

提交前评审:VCR基于VCR在提交push至Gitlab代码仓库之前,对代码进行拦截,并进行评审,支持一次评审请求作为一次评审,可对一次一次评审请求查看所有变更记录并进行评审追踪。利用开源工具Gerrit,将评审请求推送至Gerrit中,评审通过后,将代码从Gerrit同步至Gitlab仓库

提交后评审

①合并评审:VCR基于Gitlab 在一次MR的基础上进行代码评审。

②自由评审:针对用户当前代码库当前分支信息或历史commit进行评审。

五、vivo代码评审工具实施

5.1 确认技术架构

提交仓库前进行代码评审,我们使用当前成熟的代码评审Gerrit,实施过程中最大的问题是用户如何低成本切换及简单评审的问题,对于当前Gerrit评审工具遇到的问题如何解决呢?

1) 我们知道Gerrit评审工具需要提供给用户Gerrit代码库地址,并进行下载使用,当前用户使用的代码库习惯不能更改,也是不愿意修改的,那么我们如何解决呢?

给插件加持,提供用户黑盒切换至评审代码库,或执行一键下载代码库功能,底层使用Gerrit与代码托管库同步机制解决代码一致性问题,用户在使用代码库时同原使用方式一致。

图片

2) Git代码提交,CR为最小单位,CR可作为一次评审,但还有很多用户使用的习惯是一次push作为一次评审,如何解决用户一次push为一次评审呢?

a)需要对代码关系链需要进行整理,识别出一次push作为一次评审记录,用户多次追加提交记录至评审请求,需要重新识别出关系链作为原push请求的评审记录,Git原生对代码变更的情况比较多,我们对一些场景进行分析再特殊处理,不穷举。

图片

b)可对最小粒度CR的评审,也同时提供一次push请求内容进行评审,更方便快捷。

用户不管是提交前评审、合并时评审,都可能会产生一次push,多次commit,用户需要对最小粒度CR评审,也需要对最新变更所有内容进行评审。

5.2 插件改造实施

根据我们对用户的调研过程中,用户对代码评审插件网页同时兼容的要求比较高,针对idea插件我们如何改造代码评审,这里我们着重对Gerrit插件改造展开说明。

步骤1:了解插件框架、配置、打包、运行

1)插件框架整体介绍

图片

(图片来源于网络)

  • 开发方式:在官网的描述中,创建IDEA插件工程的方式有两种分别是使用DevKit(IntelliJ Platform Plugin 模版创建)和Gradle构建方式,这两种方式在构建项目和打包发布上有所区别,同时官方提供了将Devkit迁移至Gradle的方式。参考:Developing a Plugin | IntelliJ Platform Plugin SDK

  • 框架入口:一个 IDEA 插件开发完,要考虑把它嵌入到哪,比如是从 IDEA 窗体的 Edit、Tools 等进入配置还是把窗体嵌入到左、右工具条还是IDEA窗体下的对话框。

  • UI:思考的是窗体需要用到什么语言开发,没错,用的就是 Swing、Awt 的技术能力。

  • API:在 IDEA 插件开发中,一般都是围绕工程进行的,那么基本要从通过 IDEA 插件 JDK 开发能力中获取到工程信息、类信息、文件信息等。

  • 外部功能:这一个是用于把插件能力与外部系统结合,比如你是需要把拿到的接口上传到服务器,还是从远程下载文件等等。

 2)Gradle创建

新版通过 New-> Project->IDE Plugin进行创建,旧版通过New Project->Gradle->IntelliJ Platform Plugin进行创建。

项目结构如下:

图片

3)配置介绍

  • plugin.xml

<!DOCTYPE idea-plugin PUBLIC "Plugin/DTD" "http://xxxx">
<idea-plugin><!-- 插件唯一id,不能和其他插件项目重复,所以推荐使用包名+插件名com.xxx.xxx的格式插件不同版本之间不能更改,若没有指定,则与name相同 --><id> com.your.company.unique.plugin.id </id>
<!-- 插件名称,别人在官方插件库搜索你的插件时使用的名称 --><name> Plugin display name here </name>
<!-- 插件版本,格式:BRANCH.BUILD.FIX (MAJOR.MINOR.FIX) -->vs<version>1.0.0</version>
<!-- 供应商主页和email(不能使用默认值,必须修改成自己的)--><vendor email="support@yourcompany.com" url="https://www.yourcompany.com">YourCompany</vendor><!-- 插件的描述 (不能使用默认值,必须修改成自己的。并且需要大于40个字符)--><description><![CDATA[Enter short description for your plugin here.<br><em>most HTML tags may be used</em>
]]></description> <!-- 插件版本变更信息,使用<![CDATA[  ]]> 来支持HTML格式;将展示在 settings | Plugins 对话框和插件仓库的Web页面 --><change-notes><![CDATA[<p><li>1.0.0</li><ul><li>1.新增xxx功能 <br/>2.优化xxx功能 <br/></li></ul></p>]]></change-notes><!-- please see http://confluence.jetbrains.net/display/IDEADEV/Build+Number+Ranges for description --><!-- 插件兼容构建的IDE版本, until-build可以不写,默认到最新版 --><idea-version since-build="203.4818.26" until-build="211"/><!-- please see http://confluence.jetbrains.net/display/IDEADEV/Plugin+Compatibility+with+IntelliJ+Platform+Productson how to target different products --><!-- 插件依赖,可以依赖模块或插件 --><depends>com.intellij.modules.lang</depends><depends>Git4Idea</depends><depends optional="true" config-file="plugin-maven.xml">org.jetbrains.idea.maven</depends><!—idea第一次打开, 实际上就是订阅了应用程序打开的事件--><application-components><component><implementation-class>xxxxx</implementation-class></component></application-components>
<!—打开项目 --><project-components><component><implementation-class>xxxxx</implementation-class></component></project-components>
<!-- 插件定义的扩展点,以供其他插件扩展该插件,类似Java的抽象类的功能 如何在https://plugins.jetbrains.com/docs/intellij/plugin-extensions.html --><extensionPoints></extensionPoints><!-- 声明该插件对IDEA core或其他插件的扩展,Ns是NameSpace的缩写 --><extensions defaultExtensionNs="com.intellij"><toolWindow id="代码评审" icon="/icons/xx_13x13.png" anchor="bottom" factoryClass="xxx" /></extensions><!-- 编写插件动作 https://plugins.jetbrains.com/docs/intellij/plugin-actions.html--><actions><action id="com.xx.xx.AddCommentAction"class="com.xx.xx.actions.AddCommentAction"text="添加评论"description="为选中的代码添加评论意见"icon="AllIcons.Actions.StartDebugger"> <!—编辑器右键弹出菜单--!><add-to-group group-id="EditorPopupMenu" anchor="first"/><!--快捷方式--!><keyboard-shortcut first-keystroke="alt X" keymap="$default"/> </action></action></actions>
</idea-plugin>

4)插件运行调试打包安装

Gradle构建方式进行调试打包安装

  • 运行/调试:runIde 可以选择Debug模式或者是Run模式

图片

  • 打包

图片

  • 安装:可以将打的包发布市场(本地idea配置插件仓库),从Marketplace搜索插件或者是直接从Settings->plugins->Install->Install Plugin from Disk安装

图片

步骤2:研究Gerrit插件源码,搞清楚整理开发流程和模块

图片

步骤3:基于Gerrit插件规划VCR插件模块,增加clone、branch、mergeRequest、VCR模块,并对各组件增强

图片

步骤4:定制原有流程模块push,自动化关联工作项

图片

图片

在使用Git依赖插件之前,先了解一下插件的扩展以及扩展点(Extensions、Extension Points)。

Intellij 平台提供了允许一个插件与其他插件或者 IDE 交互的 extensions 以及 extension points 的概念。

  • Extension Points:如果你想要你的插件可以被其他插件使用,那么你必须在你的插件内声明一个或多个扩展点(extension points)。每个扩展点定义了允许访问这个点的类或者接口。

  • Extensions:如果你想要你的插件扩展其他插件或者 Intellij 平台,你必须声明一个或多个 extensions。

可以在 plugin.xml 中的和块中定义 extensions 以及 extension points。

图片

  • plugin.xml

<!--依赖插件包--!>
<depends>Git4Idea</depends>
<!—idea第一次打开, 实际上就是订阅了应用程序打开的事件-->
<application-components>
<component>
<implementation-class>com.demo.intellij.plugin.vcr.push.VcrPushExtension$Proxy</implementation-class>
</component>
</application-components>

上述我们看到依赖的Git4Idea 包,如果我们想修改原生的的Git,先看下push依赖包中如何实现的。

  • Git4Idea(plugin.xml)

<extensions defaultExtensionNs="com.intellij"><pushSupport implementation="git4idea.push.GitPushSupport"/>
...
</extensions>
  • intellij-dvcs.jar(plugin.xml)
<extensionPoints><extensionPoint name="pushSupport"interface="com.intellij.dvcs.push.PushSupport"area="IDEA_PROJECT"dynamic="true"/>
....
</extensionPoints>

从上述可看到,Git4Idea 的GitPushSupport扩展实现push的功能点,接下来我们主要对GitPushSupport进行javassist字节码修改以达到扩展git push组件能力。

扩展使用GitPushSupport之前,需要将需要的类进行装载至GitPlugin中,然后再对GitPushSupport进行字节码改造,至此对git Push原生插件页进行改造。

图片

步骤5:使用树状列表模式,展示一次push请求VCR提交内容及多个CR情况

主要是实现JTreeTable,对VCR与CR进行管理。

一次评审请求VCR包含所有CR的提交变更记录,可针对该变更记录进行代码评审,单个CR也可以进行评审。

图片

步骤6:展示变更文件视图及定制评论展示模块,精准定位代码

代码评审主要根据编辑器获取代码行及位置,评论可精准定位到代码行。

图片

1)changeBrowser变更视图展示VCR变更文件信息

图片

2)双击文件,diff视图展示inline和side-by-side两种代码差异

声明扩展,针对扩展类进行定制化改造。

  • plugin.xml

<diff.DiffTool implementation="com.demo.intellij.plugin.vcr.ui.diff.VcrCommentsDiffTool$Proxy"/>

图片

3)添加代码块评论,定位代码块

  • AddCommentAction.java

public class AddCommentAction extends AnAction implements DumbAware {public AddCommentAction(String label,Icon icon,CommentsDiffTool commentsDiffTool,                 Editor editor, List<CommentInfo> fileComments....) {super(label, null, icon);
}
private CommentInput createComment() {
//获取用户选择代码位置位置//行的情况下,默认是开头和行结束  得到光标的位置caretModel.getOffset();
/*取到插字光标模式对象 CaretModel caretModel = editor.getCaretModel();
得到光标的位置int caretOffset = caretModel.getOffset();
//得到一行开始和结束的地方
int lineNum = document.getLineNumber(caretOffset);
int lineStartOffset = document.getLineStartOffset(lineNum);
int lineEndOffset = document.getLineEndOffset(lineNum);
获取一行内容String lineContent = document.getText(new TextRange(lineStartOffset, lineEndOffset));
*/
Document document = editor.getDocument();
int lineNum  = document.getLineNumber(editor.getCaretModel().getOffset()) ;
int lineStartOffset = document.getLineStartOffset(lineNum);
int lineEndOffset = document.getLineEndOffset(lineNum);
String lineContent = document.getText(new TextRange(lineStartOffset, lineEndOffset));
.....
}
}

所有评论展示列表如何精准定位代码

  • SafeHtmlHistoryComments.java

public class SafeHtmlHistoryComments extends JPanel {private Iterable<CommentInfo> fileComments;private List<CommentInfo> commentInfos = new ArrayList<>();private CommentInfo currentCommentInfo;private SelectedComment selectedComment;private SelectedComment operatorSelectedComment;private Editor editor;public SafeHtmlHistoryComments(Editor editor,Iterable<CommentInfo> fileComments, Comment selectedComment) {super(new BorderLayout());....HistoryCommentListPanel historyCommentListPanel = new HistoryCommentListPanel(fileComments);//双击table某行触发代码定位historyCommentListPanel.addTableMouseDoubleHit(new Consumer<CommentInfo>() {@Overridepublic void consume(CommentInfo commentInfo) { codeTextHit(editor,commentInfo);}});}/*** 定位代码* @param editor* @param commentInfo*/private static void codeTextHit(Editor editor, CommentInfo commentInfo) {SelectionModel selectionModel = editor.getSelectionModel();// 优化:如果文件修改过了,则不进行选中操作,换为提示if (null != commentInfo.startIndex && null != commentInfo.endIndex && commentInfo.startIndex != 0 && commentInfo.endIndex != 0) {editor.getCaretModel().moveToOffset(commentInfo.endIndex);selectionModel.setSelection(commentInfo.startIndex, commentInfo.endIndex);} else if (null != commentInfo.line && commentInfo.line != 0) {int lineNum = commentInfo.line - 1;editor.getCaretModel().moveToOffset(lineNum);CharSequence charsSequence = editor.getMarkupModel().getDocument().getCharsSequence();if(null!=commentInfo.range) {RangeUtils.Offset offset = RangeUtils.rangeToTextOffset(charsSequence, commentInfo.range);selectionModel.setSelection(offset.start, offset.end);}else{Document document = editor.getDocument();int lineStartOffset = document.getLineStartOffset(lineNum);int lineEndOffset = document.getLineEndOffset(lineNum);selectionModel.setSelection(lineStartOffset, lineEndOffset);}}editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);}
....
}

六、未来展望

6.1 自动化代码评审

  1. 代码提交评审或代码合并之前,先自动化检查(Sonar/安全扫描)快速发现并纠正潜在问题,检查成功后提交评审。

  2. 代码评审通过之后,结合流水线,自定义部署构建策略,实现快速迭代。

  3. 自动汇聚测试报告,根据评审问题类型进行分类,不断改进Sonar检查规则,从而形成良性循环。

6.2智能化代码评审

  1. 提交代码评审之后,通过AI大模型对代码进行综合评价,并给出建议。

  2. 通过智能代码评审,产生评审报告,并进行智能化分析。

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

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

相关文章

墨刀原型--多tab切换显示对应页面场景交互步骤

一般我们画原型页面&#xff0c;PC端或者APP端或小程序端&#xff0c;都会有页面会切换多个tab或状态&#xff0c;同时对应页面显示对应的页面数据。 设计思路如下&#xff1a; 以订单列表页面为例&#xff1a; 可以将订单列表页面分为3部分&#xff0c;固定的头部、状态栏、…

java和网络安全,哪个就业前景更大?

常年以来&#xff0c;Java一直占据着程序语言的前三名&#xff0c;因此也就成了许多进入IT行业的首选语言。但随着5G时代的兴起&#xff0c;网络安全也成了当今最火热的“风口行业”。导致很多年轻人不知如何选择&#xff0c;一直处于纠结徘徊的状态。下面盾叔就带大家了解一下…

Qt—贪吃蛇项目(由0到1实现贪吃蛇项目)

用Qt实现一个贪吃蛇项目 一、项目介绍二、游戏大厅界面实现2.1完成游戏大厅的背景图。2.2创建一个按钮&#xff0c;给它设置样式&#xff0c;并且可以跳转到别的页面 三、难度选择界面实现四、 游戏界面实现五、在文件中写入历史战绩5.1 从文件里提取分数5.2 把贪吃蛇的长度存入…

关于vue创建项目失败报错(镜像过期)的解决方案

在新建vue项目时出现以下错误&#xff1a; 原因&#xff1a; npm.taobao.org和registry.npm.taobao.org旧域名于2021年官方公告域名更换事件&#xff0c;已于2022年05月31日零时起停止服务&#xff0c;域名HTTPS证书于2024年1月22日正式到期&#xff0c;不可再用。 解决方案:…

【vue3】【vant】 移动端古诗词句子发布收藏项目

更多项目点击&#x1f446;&#x1f446;&#x1f446;完整项目成品专栏 【vue3】【vant】 移动端古诗词句子发布收藏项目 获取源码方式项目说明&#xff1a;其中功能包括素材包含&#xff1a;项目运行环境运行截图 获取源码方式 加Q群&#xff1a;632562109项目说明&#xf…

突破Web3红海,DePIN如何构建创新生态系统?

撰文&#xff1a;TinTinLand 本文来源香港Web3媒体Techub News专栏作者TinTinLand 2023 年 DePIN 赛道的火热成为 Web3 行业的重点关注方向&#xff0c;当前如何以可扩展、去中心化、安全方式推动 DePIN 赛道赋能下的 AI 版图建设&#xff0c;寻找更多 Web3 行业创新机遇成为…

JS(JavaScript)事件处理(事件绑定)趣味案例

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

创新前沿:Web3如何颠覆传统计算机模式

随着Web3技术的快速发展&#xff0c;传统的计算机模式正面临着前所未有的挑战和改变。本文将深入探讨Web3技术的定义、原理以及它如何颠覆传统计算机模式&#xff0c;以及对全球科技发展的潜在影响。 1. 引言&#xff1a;Web3技术的兴起与背景 Web3不仅仅是技术创新的一种&…

QT中的样式表.qss文件

一、前言 qt中样式表的改变有几种方法&#xff0c;第一种就是直接在ui界面对应的组件右键修改样式表&#xff0c;还有一种就是直接在程序里面修改样式表&#xff0c;我知道的还有一种就是qss文件&#xff0c;这个文件就是将在程序中写的修改样式表的语句写道qss文件中&#xff…

次世代霍尔电磁摇杆搭配磁悬浮马达,这款手柄手感超丝滑,谷粒金刚3Pro体验

燥热的天气里&#xff0c;周末在家打上几局游戏&#xff0c;确实更容易放松身心&#xff0c;玩游戏的时候&#xff0c;键鼠、手柄一类的游戏外设特别重要&#xff0c;对我们的游戏体验影响很大&#xff0c;所以挑选起来总是格外挑剔。现在国产的游戏手柄已经今非昔比了&#xf…

grpc学习golang版(八、双向流示例)

系列文章目录 第一章 grpc基本概念与安装 第二章 grpc入门示例 第三章 proto文件数据类型 第四章 多服务示例 第五章 多proto文件示例 第六章 服务器流式传输 第七章 客户端流式传输 第八章 双向流示例 文章目录 一、前言二、定义proto文件三、编写server服务端四、编写client客…

YouTube广告投放指南:如何投放 YouTube视频广告

在海外广告投放中&#xff0c;YOutube是重要的渠道之一。这篇文章Maskfog将为你介绍Youtube广告类型以及广告投放流程&#xff0c;继续看下去&#xff01; YouTube 视频广告的类型 1.信息流视频广告 信息流视频广告显示在 YouTube 主页、搜索结果页面上&#xff0c;并作为 Yo…

餐饮点餐系统

餐饮点餐系统是一款为餐厅和顾客提供便捷点餐服务的在线平台。 1.DDL CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID,username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名,password VARCHAR(255) NOT NULL COMMENT 密码,email VARCHAR(100) UNIQUE…

python爬虫之scrapy框架基本使用

python爬虫之scrapy框架基本使用 1、环境安装&#xff1a;pip install scrapy 2、创建一个工程&#xff1a;scrapy startproject xxxPro 3、cd xxxPro 4、在spiders子目录中创建一个爬虫文件&#xff1a;scrapy genspider spiderName www.xxx.com 5、执行工程&#xff1a;scra…

3DEXPERIENCE平台正在推动仿真技术的创新,旨在创造仿真设计的新境界

随着企业数字化转型的不断推进&#xff0c;3DEXPERIENCE 平台正以其前瞻性的技术和服务重塑仿真设计领域的新高度&#xff0c;助力企业实现仿真技术的再次飞跃。该平台不仅整合了先进的仿真工具与设计流程&#xff0c;还促进了跨部门的协作&#xff0c;降低分析仿真对硬件的要求…

远程桌面无法复制粘贴文件到本地怎么办?

远程桌面不能复制粘贴问题 Windows远程桌面为我们提供了随时随地访问文件和数据的便捷途径&#xff0c;大大提升了工作和生活的效率。然而&#xff0c;在使用过程中&#xff0c;我们也可能遇到一些问题。例如&#xff0c;在通过远程桌面传输文件时&#xff0c;常常会出现无法复…

突破SaaS产品运营困境:多渠道运营如何集中管理?

随着数字化时代的到来&#xff0c;SaaS&#xff08;软件即服务&#xff09;产品已成为企业日常运营不可或缺的工具。然而&#xff0c;在竞争激烈的市场环境下&#xff0c;SaaS产品运营越来越重视多渠道、多平台布局&#xff0c;以更广泛地触及潜在用户&#xff0c;然而&#xf…

Android Native 客户端属性配置系统使用说明

Android Native 客户端属性配置系统使用说明 背景和问题现代 android 开发基本都基于 gradle 属性设置来进行定制化编译,随着项目的迭代,工程结构越发复杂,配置属性越来越多,越来越多的配置使得上手难度越来越大。 解决方案设计一般而言,在 android 开发中,Gradle 属性系…

新国都:昙花一现or未来可期?

从波动起伏到强势爆发&#xff0c;这份业绩能否持续&#xff1f; 今天我们抽取一名铁杆粉丝想要咨询的公司来说一说——新国都。 一句话总结这家以第三方支付为主营业务的公司业绩&#xff1a;盈利能力突然爆发&#xff0c;23年净利润暴增近16倍&#xff0c;24年Q1净利润大增6…

mysql解压版本安装5.7

1. 官网下载好解压版本 我这边5.7版本 https://dev.mysql.com/downloads/file/?id523570 mysql官网 创建 my.ini文件 内容如下 [client] #客户端设置&#xff0c;即客户端默认的连接参数# socket /data/mysqldata/3306/mysql.sock #用于本地连接的socket套接字 # 默…