个人博客系统项目测试报告
项目背景介绍
背景:当在学习一项技能的时候,我们总会习惯通过博客来记录所学的知识点,方便后期遗忘时随时查看和快速复习。本次开发的Web网站程序便是为了更加轻量和方便地记录自己的学习笔记
概述:一个Web网站程序,可以随时随地查看自己和其他用户发布的博客,也可以自己注册登录后发布属于自己的博客
相关技术栈:SpringBoot SpringMVC MyBatis MySQL Redis HTML CSS JavaScript
博客登录页:

博客注册页:

博客主页:

个人博客列表页:

博客详情页:

博客编辑页:

草稿列表:

写博客页:

测试计划
1. 手工测试
1.1 编写测试用例






1.2 执行测试用例(部分)
由于篇幅及时间受限,仅针对部分用例进行了测试
测试环境
操作系统:
版本 Windows 10 家庭中文版
版本号 22H2
安装日期 2021/6/19
操作系统内部版本 19045.2728
体验 Windows Feature Experience Pack 120.2212.4190.0浏览器:
Google Chrome 版本 111.0.5563.65(正式版本) (64 位)
网络:
协议: Wi-Fi 5 (802.11ac)
网络频带: 5 GHz
博客登录页:界面能否正常加载,输入正确或错误的账号、密码及验证码是否能得到预期的响应
a)界面

b)输入正确的账号、密码错误的验证码
预期结果:弹窗提示验证码错误,并自动刷新验证码
实际结果如下:

c)输入正确的验证码、错误的账号或密码
预期结果:提示用户名或密码错误。
实际结果如下:

d)输入正确的账号、密码及验证码:
预期结果:跳转到个人博客列表页。
实际结果如下:

e)输入正确的账号和密码,同一用户重复登录:
预期结果:提示用户重复登录,然后返回该用户博客列表页。
实际结果如下:


f)同一用户多次输入错误的密码及正确的验证码:
预期结果:提示用户账户被冻结,一段时间内禁止该用户登录。
实际结果如下:

博客列表页:检测界面是否符合预期,点击“查看全文”按钮是否能跳转到对应的博客详情页,点击注销是否能退出登录
a)界面

b)点击查看详情
预期结果:进入到对应的博客详情页,且能够正确加载文章内容。
实际结果如下:

c)点击删除
预期结果:删除对应文章,弹窗提示删除成功,作者文章计数更新,并自动刷新页面。
实际结果如下:



通过观察我们发现此处作者的文章计数器并未更新,不符合预期结果。由于这里采用了Redis缓存用户信息,而此处采用的是缓存过期自动更新的设置,没有进行主动的数据同步,推测是由于缓存更新不及时导致的结果。

此处我们手动删除缓存后再刷新测试,得到结果如下:

可以确定导致问题的原因就是缓存更新不及时导致
d)点击修改
预期结果:获取对应文章内容,进入对应文章修改页面。
实际结果如下:

e)退出登录
预期结果:删除对应用户token,并返回登录页
实际结果如下:

2. 使用Selenium进行Web端UI自动化测试
2.1 为什么需要自动化测试
由于随着项目版本的迭代,功能的逐渐增多,各种功能回归测试的需要,单纯的手工测试已经难以满足我们对于测试效率的要求,于是我们引入了自动化测试。它能够将人工从重复机械的测试过程中解放,使得人力资源能够投入到更加关键的测试中。
2.2 测试环境
测试环境
操作系统:
版本 Windows 10 家庭中文版
版本号 22H2
安装日期 2021/6/19
操作系统内部版本 19045.2728
体验 Windows Feature Experience Pack 120.2212.4190.0浏览器:
Microsoft Edge版本 111.0.1661.44 (正式版本) (64 位)
网络:
协议: Wi-Fi 5 (802.11ac)
网络频带: 5 GHzSelenium:
版本:4.0.0
驱动版本 :111.0.1661.44(x64)
2.3 编写测试用例
2.4 自动化测试代码
2.4.1 实现工具类,增加代码复用
package com.example.blogautotest.common;import org.apache.commons.io.FileUtils;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.edge.EdgeDriver;import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;public class AutotestUtils {public static EdgeDriver driver;//创建驱动对象public static EdgeDriver createDriver(){//单例模式if(driver==null){driver=new EdgeDriver();//创建隐式等待driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));}return driver;}//获取当前时间将截图按照时间保存public List<String> getTime(){//文件夹以天保存//截图以毫秒时间戳保存SimpleDateFormat sim1=new SimpleDateFormat("yyyyMMdd-HHmmssSS");SimpleDateFormat sim2 = new SimpleDateFormat("yyyyMMdd");String filename=sim1.format(System.currentTimeMillis());String dirname=sim2.format(System.currentTimeMillis());List<String> list = new ArrayList<>();list.add(dirname);list.add(filename);return list;}//获取屏幕截图,把所有的用例执行的结果保存下来public void getScreenShot(String str) throws IOException {List<String> list=getTime();// ./指的是当前的项目路径下,也就是BlogAutoTest下String filename="./src/test/java/com/blogautotest/"+list.get(0)+"/"+str+"_"+list.get(1)+".png";File srcfile=driver.getScreenshotAs(OutputType.FILE);//把屏幕截图生成的文件放到指定的路径FileUtils.copyFile(srcfile,new File(filename));}
}
2.4.2 登陆页面
package com.example.blogautotest.Tests;import com.example.blogautotest.common.AutotestUtils;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.openqa.selenium.By;
import org.openqa.selenium.edge.EdgeDriver;import java.io.IOException;//设置优先级
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BlogLoginTest extends AutotestUtils {public static final String UNIVERSAL_KAPTCHA_CODE="c8fd27d19b2aa9fa24affd2a4726778c";public static EdgeDriver driver=createDriver();@BeforeAllpublic static void baseControl(){driver.get("http://127.0.0.1:8080/login.html");}/*检查登录页面打开是否正确检查点:登录标题 用户名是否存在*/@Test@Order(1)public void loginPageLoadRight() throws IOException {//检验页面是否加载正确(两个检查点)driver.findElement(By.cssSelector("body > div.login-container > div > h3"));driver.findElement(By.cssSelector("body > div.login-container > div > div:nth-child(2) > span"));getScreenShot(getClass().getName());}@ParameterizedTest@CsvSource({"zhangsan,123","admin,admin"})@Order(2)public void loginSuc(String name , String password) throws InterruptedException, IOException{driver.findElement(By.cssSelector("#username")).clear();driver.findElement(By.cssSelector("#password")).clear();driver.findElement(By.cssSelector("#rightCode")).clear();driver.findElement(By.cssSelector("#username")).sendKeys(name);driver.findElement(By.cssSelector("#password")).sendKeys(password);driver.findElement(By.cssSelector("#rightCode")).sendKeys(UNIVERSAL_KAPTCHA_CODE);driver.findElement(By.cssSelector("#submit")).click();//处理弹窗Thread.sleep(300);driver.switchTo().alert().accept();//对登录结果进行检测,存在草稿页元素代表登录成功driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)"));//getScreenShot(getClass().getName());driver.navigate().back();}@ParameterizedTest@CsvSource({"admin,123","zhangsan,666"})@Order(3)public void loginFail(String name,String password) throws IOException, InterruptedException {driver.findElement(By.cssSelector("#username")).clear();driver.findElement(By.cssSelector("#password")).clear();driver.findElement(By.cssSelector("#rightCode")).clear();driver.findElement(By.cssSelector("#username")).sendKeys(name);driver.findElement(By.cssSelector("#password")).sendKeys(password);driver.findElement(By.cssSelector("#rightCode")).sendKeys(UNIVERSAL_KAPTCHA_CODE);driver.findElement(By.cssSelector("#submit")).click();//处理弹窗Thread.sleep(300);//获取弹窗内容String text=driver.switchTo().alert().getText();String except="登陆成功!";driver.switchTo().alert().accept();Assertions.assertNotEquals(except,text);//获取当前页面截屏//getScreenShot(getClass().getName());}
}
2.4.3 博客主页
package com.example.blogautotest.Tests;import com.example.blogautotest.common.AutotestUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.edge.EdgeDriver;import java.io.IOException;public class BlogListTest extends AutotestUtils {public static EdgeDriver driver=createDriver();@BeforeAllpublic static void baseControl(){driver.get("http://127.0.0.1:8080/blog_list.html");}@Testpublic void listPageLoadRight(){//检查博客列表加载是否正常driver.findElement(By.cssSelector("body > div.container > div > div.blog-pagnation-wrapper > button:nth-child(1)"));driver.findElement(By.cssSelector("body > div.container > div > div.blog-pagnation-wrapper > button:nth-child(4)"));}@Testpublic void jumpTest() throws InterruptedException {//测试分页能否正常跳转Thread.sleep(500);driver.findElement(By.cssSelector("body > div.container > div > div.blog-pagnation-wrapper > button:nth-child(3)")).click();Assertions.assertEquals("http://127.0.0.1:8080/blog_list.html?pindex=2&psize=3",driver.getCurrentUrl());Thread.sleep(500);driver.findElement(By.cssSelector("body > div.container > div > div.blog-pagnation-wrapper > button:nth-child(2)")).click();Assertions.assertEquals("http://127.0.0.1:8080/blog_list.html?pindex=1&psize=3",driver.getCurrentUrl());Thread.sleep(500);driver.navigate().back();}
}
2.4.4 博客详情页
package com.example.blogautotest.Tests;import com.example.blogautotest.common.AutotestUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.edge.EdgeDriver;import java.io.IOException;public class BlogDetailTest extends AutotestUtils {public static EdgeDriver driver=createDriver();@BeforeAllpublic static void baseControl(){driver.get("http://127.0.0.1:8080/blog_content.html?id=12869974016131072");}@Testpublic void blogDeailLoadRight() throws IOException{driver.findElement(By.cssSelector("#data"));driver.findElement(By.cssSelector("#title"));}
}
2.4.5 博客编辑页
package com.example.blogautotest.Tests;import com.example.blogautotest.common.AutotestUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.edge.EdgeDriver;import java.io.IOException;public class BlogEditTest extends AutotestUtils {public static EdgeDriver driver=createDriver();@BeforeAllpublic static void baseControl(){driver.get("http://127.0.0.1:8080/blog_edit.html");}@Testpublic void editAndSubimitBlog() throws IOException, InterruptedException {driver.findElement(By.cssSelector("#title")).sendKeys("自动化测试");//博客系统使用到的编辑是第三方软件,所以不能直接使用sendKeys向编辑模块发送文本driver.findElement(By.cssSelector("#editorDiv > div.editormd-toolbar > div > ul > li:nth-child(30)")).click();driver.findElement(By.cssSelector("body > div.blog-edit-container > div.title > button:nth-child(2)")).click();Thread.sleep(300);String actual=driver.switchTo().alert().getText();driver.switchTo().alert().accept();String expect = "恭喜:添加成功!";Assertions.assertEquals(expect,actual);}
}
2.4.6 个人列表页
package com.example.blogautotest.Tests;import com.example.blogautotest.common.AutotestUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.edge.EdgeDriver;public class MyBlogListTest extends AutotestUtils {public static EdgeDriver driver=createDriver();@BeforeAllpublic static void baseControl(){driver.get("http://127.0.0.1:8080/myblog_list.html");}@Testpublic void myListPageLoadRight(){//检查博客列表加载是否正常driver.findElement(By.cssSelector("body > div.nav > a:nth-child(7)"));driver.findElement(By.cssSelector("body > div.container > div.container-left > div > a"));driver.findElement(By.cssSelector("body > div.container > div.container-right > div.blog-pagnation-wrapper > button:nth-child(1)"));driver.findElement(By.cssSelector("body > div.container > div.container-right > div.blog-pagnation-wrapper > button:nth-child(3)"));}@Testpublic void jumpTest(){//测试导航栏能否正常跳转driver.findElement(By.cssSelector("body > div.nav > a:nth-child(4)")).click();Assertions.assertEquals("http://127.0.0.1:8080/blog_list.html",driver.getCurrentUrl());driver.navigate().back();driver.findElement(By.cssSelector("body > div.nav > a:nth-child(6)")).click();Assertions.assertEquals("http://127.0.0.1:8080/blog_edit.html",driver.getCurrentUrl());driver.navigate().back();}
}
2.4.7 退出驱动
package com.example.blogautotest.Tests;import com.example.blogautotest.common.AutotestUtils;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.edge.EdgeDriver;//用于最后关闭驱动
public class driverQuitTest extends AutotestUtils {public static EdgeDriver driver=createDriver();@Testpublic void driverQuit(){driver.quit();}
}
2.4.8 测试结果

可以看到所有测试用例均通过
2.4.9 自动化测试亮点
1.通过使用junit5中的注解,避免生成过多的测试对象,减少资源和时间上的浪费,提高了自动化执行的效率
2.只创建一次驱动,避免每个用例重复创建驱动造成时间和资源的浪费。
3.使用参数化,保持用例的整洁,提高代码的可读性。
4.使用隐式等待,提高了自动化运行效率,提高了自动化的稳定性。
5.使用屏幕截图,方便问题的溯源以及解决。
2.4.10 完整代码
BlogAutoTest: 个人博客系统UI自动化测试
相关文章:
个人博客系统项目测试报告
项目背景介绍 背景:当在学习一项技能的时候,我们总会习惯通过博客来记录所学的知识点,方便后期遗忘时随时查看和快速复习。本次开发的Web网站程序便是为了更加轻量和方便地记录自己的学习笔记 概述:一个Web网站程序,…...
flutter安装自用笔记
参照文章: 开发环境搭建 Flutter环境配置步骤: 1.系统配置要求 2.Java环境 3.Flutter SDK 4.Android 开发环境一、系统配置要求 操作系统:Windows 7 SP1 或更高的版本(基于 x86-64 的 64 位操作系统) 磁盘空间&…...
tomcat线程池以及在SpringBoot中的启动过程
tomcat两大组件:连接器Connector,容器Container tomcat线程池 Tomcat线程池扩展了ThreadPoolExecutor,行为稍有不同 重写了ThreadPoolExecutor的execute方法 如果总线程数达到maximumPoolSize,不会立刻抛RejectedExecutionExcept…...
第十四届中国大学生创新创业大赛
文章目录比赛官网比赛题目含金量非常高建议参加的学生推荐几个我感兴趣的题目联系比赛官网 官网地址:http://www.fwwb.org.cn/ 实际叫做:中国大学生创新创业大赛 比赛题目 题目公布查看地址:http://www.fwwb.org.cn/topic/index 题目有…...
LeetCode:322. 零钱兑换——动态规划从案例入门
🍎道阻且长,行则将至。🍓 🌻算法,不如说它是一种思考方式🍀算法专栏: 👉🏻123 一、🌱322. 零钱兑换 题目描述:给你一个整数数组coins,…...
【lwIP(第四章)】网络接口
目录一、lwIP网络接口简介二、lwIP的netif结构三、lwIP的netif相关函数1. lwIP网络接口的全局变量2. netif_add()函数3. netif_remove()函数4. netif_set_default()函数一、lwIP网络接口简介 lwIP协议栈支持多种不同的网络接口(网卡),由于网卡…...
Vue3 pinia入门篇(一)
系列文章目录 主要为了记录如何使用Pinia在Vue3中的使用方式(下面会介绍为什么使用Vue3选型) 文章目录系列文章目录不用Vue2使用Pinia举例子?1.笔者的个人看法:2.总结一、Pinia是什么1.状态管理工具(类比Vuexÿ…...
python面向对象编程解释
python是一个面向对象的编程语言 面向过程的开发语言有C,面向对象除了python还有java等语言 具体来讲: 面向过程 :举个例子,比如说,把大象装进冰箱总共分几步,第一步,把冰箱门打开,…...
ARM(IMX6U)嵌入式软件裸机开发之环境搭建与配置
目录 前沿 Ubuntu 和 Windows 文件互传 Ubuntu 下 NFS 和 SSH 服务开启 Ubuntu 交叉编译工具链安装 Source Insight 软件安装和使用 Visual Studio Code 软件的安装和使用 前沿 为什么我们要学习裸机开发呢? 1、裸机开发是了解所使用的 CPU 最直接、最简单的方…...
Java文件复制多种方法
1、InputStream与OutputStream 创建两个文件 - 源和目标。然后我们从源创建InputStream并使用OutputStream将其写入目标文件进行 java 复制文件操作。 private static void copyFileUsingStream(File source, File dest) throws IOException {InputStream is null;OutputStr…...
Java语言-----封装、继承、抽象、多态、接口
目录 前言 一.封装 1.1封装的定义 1.2访问修饰符的使用 二.继承 2.1继承的定义 2.2继承的方法 2.3继承使用注意点 三.多态 3,1多态的定义 3.2动态绑定 3.3方法重写 3.4向上(向下)转型 四.抽象 4.1抽象的概述和定义 4.2抽象的使用 五…...
基于深度学习的瓶子检测软件(UI界面+YOLOv5+训练数据集)
摘要:基于深度学习的瓶子检测软件用于自动化瓶子检测与识别,对于各种场景下的塑料瓶、玻璃瓶等进行检测并计数,辅助计算机瓶子生产回收等工序。本文详细介绍深度学习的瓶子检测软件,在介绍算法原理的同时,给出Python的…...
仿网易云小程序(一)
目录 一、项目准备 二、项目初始化 1.新建项目 2.封装service请求 三、底部导航栏的设计 四、MV页面的设计 1.将获取到的数据进行渲染 2.播放量数据进行处理转换 3.时长数据进行处理转换 五、MV组件的抽离封装 六、请求的抽离video 七、下拉重新请求新的数据 八、跳转到…...
【C++】vector模拟实现及其应用
文章目录vector的介绍vector的使用及其实现vector的定义vector iterator 的使用vector空间增长问题vector的增删查改vector的介绍 vector是表示可变大小数组的序列容器。就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素…...
JS看这一篇就够啦,JS基础大全,可用于快速回顾知识,面试首选
1 JS简介 更多JS内容可以看MDN:点击传送 浏览器分成两部分:渲染引擎和 JS 引擎 渲染引擎:用来解析HTML与CSS,俗称内核,比如 chrome 浏览器的 blink ,老版本的 webkitJS 引擎:也称为 JS 解释器…...
武汉凯迪正大GB4208外壳防护等级试具
一、IP1X 试验探棒 产品概述: 符合IEC61032图1试具A、GB16842试具A、GB4208IP1、IEC60529IP1、IEC60065 等标准要求。用于防止手背触及的防护检验。 技术参数: 1、探球直径:50mm 2、挡板直径:45mm 3、挡板厚度:…...
Cent OS 从零部署ruoyi-cloud教程
1、java环境安装 https://blog.csdn.net/m0_61035257/article/details/125705400 Java_home设置 https://blog.csdn.net/m0_51104427/article/details/123924893 2、mysql安装 https://blog.csdn.net/ShockChen7/article/details/126965940 若安装的是Mysql8,建议…...
ChatGPT相关核心算法
ChatGPT 的卓越表现得益于其背后多项核心算法的支持和配合。本文将分别介绍作为其实现基础的 Transformer 模型、激发出其所蕴含知识的Prompt/Instruction Tuning 算法、其涌现出的思维链能力、以及确保其与人类意图对齐的基于人类反馈的强化学习算法。 1.基于Transformer的预…...
Python导入模块,Python import用法(超级详细)
使用 Python 进行编程时,有些功能没必须自己实现,可以借助 Python 现有的标准库或者其他人提供的第三方库。比如说,在前面章节中,我们使用了一些数学函数,例如余弦函数 cos()、绝对值函数 fabs() 等,它们位…...
大量产品“GPT 化”,开源大模型 AI 应用开发框架发布
大型语言模型(LLM)的出现,让我们看到了 AI 在自然语言处理方面的潜力,它涌现出来的创造力和思维能力令人叹为观止,并在新一代人机交互领域释放了大量的想象空间。 目前,决策者、产品负责人和开发者都在抢滩…...
大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...
【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...
2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
STM32---外部32.768K晶振(LSE)无法起振问题
晶振是否起振主要就检查两个1、晶振与MCU是否兼容;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容(CL)与匹配电容(CL1、CL2)的关系 2. 如何选择 CL1 和 CL…...
Ubuntu Cursor升级成v1.0
0. 当前版本低 使用当前 Cursor v0.50时 GitHub Copilot Chat 打不开,快捷键也不好用,当看到 Cursor 升级后,还是蛮高兴的 1. 下载 Cursor 下载地址:https://www.cursor.com/cn/downloads 点击下载 Linux (x64) ,…...
ubuntu系统文件误删(/lib/x86_64-linux-gnu/libc.so.6)修复方案 [成功解决]
报错信息:libc.so.6: cannot open shared object file: No such file or directory: #ls, ln, sudo...命令都不能用 error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory重启后报错信息&…...
Sklearn 机器学习 缺失值处理 获取填充失值的统计值
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 使用 Scikit-learn 处理缺失值并提取填充统计信息的完整指南 在机器学习项目中,数据清…...
【FTP】ftp文件传输会丢包吗?批量几百个文件传输,有一些文件没有传输完整,如何解决?
FTP(File Transfer Protocol)本身是一个基于 TCP 的协议,理论上不会丢包。但 FTP 文件传输过程中仍可能出现文件不完整、丢失或损坏的情况,主要原因包括: ✅ 一、FTP传输可能“丢包”或文件不完整的原因 原因描述网络…...
人工智能 - 在Dify、Coze、n8n、FastGPT和RAGFlow之间做出技术选型
在Dify、Coze、n8n、FastGPT和RAGFlow之间做出技术选型。这些平台各有侧重,适用场景差异显著。下面我将从核心功能定位、典型应用场景、真实体验痛点、选型决策关键点进行拆解,并提供具体场景下的推荐方案。 一、核心功能定位速览 平台核心定位技术栈亮…...
