news 2026/9/9 9:33:11

校园商铺管理系统:SpringBoot2+Vue3+MyBatis-Plus全栈实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
校园商铺管理系统:SpringBoot2+Vue3+MyBatis-Plus全栈实战解析

搞校园商铺这种前后端分离的管理系统,其实最怕的不是功能多复杂,而是技术栈选得“看起来新、用起来坑”。我见过太多人一上来就追SpringBoot 3.x + JDK 17,结果第三方依赖一堆不兼容,折腾两天还没跑起来。这套项目反手选了SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0的组合,乍一看可能觉得“不够最新”,但真正上手跑通的人会明白,这套组合是目前国内中小型管理系统、课程设计、毕业设计里最稳的梯队之一。本文会把这套项目从数据库设计、后端鉴权、分页插件、文件上传、前端路由守卫到联调部署整个链路拆开讲,适合正在做毕设的在校生、想快速搭一套管理后台的初级工程师,以及所有想搞明白这几个技术点怎么协作的读者。

1. 为什么是这套技术栈:校园商铺项目的选型逻辑与版本抉择

很多同学拿到一个“校园商铺管理系统”的需求,第一反应是去搜最新的框架版本,但真正写业务代码的时候才发现,框架的新旧远没有生态的成熟度重要。这里把我为什么坚持用SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0这套组合的考虑写清楚,你看完再决定要不要照搬。

1.1 SpringBoot2与SpringBoot3:不是越新越好用

SpringBoot3在2022年底正式发布,确实带来了AOT编译、Jakarta EE命名空间、GraalVM原生镜像这些新特性,但它最大的代价是必须JDK17起步。如果你的电脑装的是JDK8,或者你需要在Windows Server 2012这种老环境上部署,那SpringBoot2仍然是更实际的选择。

SpringBoot2.x的最后一个稳定小版本是2.7.x,它已经积累了大量生产环境的踩坑修复,网上随便一搜就能找到各类异常的解决方案。更重要的是,SpringCloud Alibaba、ShardingSphere、XXL-Job这些国内常用组件对SpringBoot2的兼容性测试最多,你用起来遇到问题,基本都有人趟过路。

在这个项目里,SpringBoot2.7.x + JDK8的组合完全可以撑起校园商铺的并发量——校园商铺的定位本身就是学生和教职工群体,峰值在线人数撑死几百人,订单量一天几千笔,这个量级连数据库连接池的极限都没摸到。

提示:选型从来不是选“最新的”,而是选“最适合当前场景且周围资料最丰富的”。工作后你会发现,很多老项目的生产环境还停在SpringBoot1.5,但项目一样跑得稳稳的。

1.2 MyBatis-Plus凭什么替代传统MyBatis

传统MyBatis写SQL很灵活,但单表CRUD你得写一堆重复的XML mapper,比如insertupdateByIdselectListdeleteById,百分之八十的单表操作代码长得一模一样,纯粹是体力劳动。

MyBatis-Plus相当于在MyBatis之上叠了一层通用Mapper和通用Service。你定义一个BaseMapper<T>接口,继承它之后,单表的CRUD方法全都自动有了,你只需要关注那些真正复杂的多表关联查询和统计报表SQL。

在校园商铺项目里,商品表的增删改查、用户表的分页列表、订单表的条件查询,这些全是单表操作,MyBatis-Plus的selectPageselectList配合LambdaQueryWrapper能干掉大概一半的Mapper代码量。你要是用原生的MyBatis写,这些代码会淹没你真正需要写的核心业务逻辑。

1.3 Vue3组合式API与Vue2选项式的实际体验差异

Vue3发布已经好几年了,Vite的启动速度和热更新体验对比Webpack时代简直是降维打击。用Vue3写后台管理系统,最大的变化是逻辑复用从Mixin转向了组合式函数。

Vue2时代做登录态管理,你得在created生命周期里写一堆数据初始化逻辑,换个页面还得重新复制一遍。Vue3里用setup语法糖加refreactivecomputed,一个useUserStore组合函数就能把用户信息获取、权限校验、退出登录全部封装起来,哪个组件需要就直接调用。

Element Plus是Element UI的Vue3版本,坑的地方在于部分组件API改动比较大,比如el-dialogvisible.sync变成了v-modelel-tableslot-scope变成了#default="scope"。但这些问题你只要先跑通一个最小Demo基本都知道怎么回事了。

2. 校园商铺核心数据模型设计:从需求到建表的完整映射

校园商铺系统表面上是个“电商系统”,但它的业务边界比淘宝京东小得多,核心就四个角色:管理员、商户、普通用户、订单。把这四块的数据模型理清楚,后端代码和前端页面才能有据可依。

2.1 商户、商品、订单、用户四张主表如何落库

我习惯先画出一张“业务数据流”:用户注册登录→浏览商户列表→查看某个商户的商品→下单→商户处理订单。这张图决定了你的主表至少有四条:usermerchantproductorder

用户表字段不算多,但要注意区分普通用户和管理员,我是用一个role字段(1-普通用户,2-商户管理员,3-超级管理员)来区分的,别拆成两张表,因为登录逻辑和密码字段完全一致。

商品表要关联商户ID,价格字段用DECIMAL(10,2)存,千万别用FLOAT,浮点数算金额会出现0.1+0.2=0.30000000000000004这种问题。库存字段用INT,同时加一个status字段控制上下架状态。

订单表是核心,字段比较杂:订单编号、用户ID、商户ID、商品ID、数量、总金额、收货地址、支付方式、订单状态。这里有一个设计细节:订单编号不要用数据库自增ID,业务上需要展示给用户看的单号,我建议用时间戳加随机数生成,比如yyyyMMddHHmmss加四位随机数,避免用户猜出订单总量。

2.2 状态字段与枚举映射:避免魔法数字满天飞

很多新手写订单状态喜欢直接用数字0、1、2硬编码,写到后面自己都忘了1代表什么。我会在项目里建一个OrderStatusEnum,把状态定义成可读性强的枚举,并在MyBatis-Plus的实体字段上写好对应的注释。

public enum OrderStatusEnum { UNPAID(0, "待付款"), PAID(1, "已付款"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"); private final int code; private final String desc; OrderStatusEnum(int code, String desc) { this.code = code; this.desc = desc; } }

实体类映射上,用@EnumValue注解让MyBatis-Plus自动做枚举和数据库数字之间的转换,代码里写的是OrderStatusEnum.PAID,数据库里存的是1,既保证可读性又保证存储效率。

MySQL8.0相比5.7有几点需要注意:一是字符集默认是utf8mb4而不是utf8,建表时用utf8mb4_0900_ai_ci排序规则,不然emojis存不进去;二是WITH子句(CTE)这回真的得到了良好支持,写复杂统计SQL能省很多事。

3. SpringBoot2后端骨架搭建:认证、分页、上传一个都不能少

数据模型设计好后,就该搭后端骨架了。一套成熟的后端项目核心就三块:登录鉴权怎么做、列表分页怎么查最爽、文件上传怎么存怎么取。这三块搞定,其他都是堆接口。

3.1 JWT登录校验的拦截器实现与白名单设计

前后端分离项目的登录态管理,主流方案是JWT。用户登录成功后后端签发一个Token返回给前端,前端每次请求都把它塞进请求头的Authorization里,后端拦截器统一校验。这个方案的好处是服务端无状态,换服务器不用同步Session,适合部署在轻量级云服务器环境。

这套项目拦截器的核心逻辑是:先判断请求路径是否在白名单里(比如/api/user/login/api/user/register),如果在就直接放行。如果不在白名单,解析请求头中的Token,校验签名和有效期,解析失败就返回401提示重新登录。

@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { // Token无效,返回401 } } response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); return false; }

这里有一个必须注意的坑:跨域请求时浏览器会先发一个OPTIONS预检请求,这个请求不会携带自定义请求头,如果你在拦截器里直接拿Authorization去解析,一定会报空指针。所以拦截器里第一步必须先放行OPTIONS方法,否则前端所有请求都会死在CORS阶段。

3.2 MyBatis-Plus分页插件配置:从写SQL到一条selectPage

MyBatis-Plus的分页和传统MyBatis最大的区别:不用写LIMIT ? , ?,不用写<select>查询总条数,你只需要配置一个分页拦截器,然后调selectPage方法。

在配置类里注入MybatisPlusInterceptor,并添加PaginationInnerInterceptor。这里配置的数据库类型要写成DbType.MYSQL,因为Oracle、PostgreSQL的分页语法各不相同,MyBatis-Plus需要知道你用的哪种数据库才能生成正确的分页SQL。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

然后在Service层调用:

Page<Product> page = new Page<>(current, size); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Product::getMerchantId, merchantId); wrapper.orderByDesc(Product::getCreateTime); productMapper.selectPage(page, wrapper);

注意Page对象当传入currentsize后,MyBatis-Plus会自动查询总数并计算总页数,你不用自己再写一个count(*)查询。返回值里getRecords()拿当前页数据,getTotal()拿总记录数,前端分页组件直接绑定这两个字段即可。

3.3 文件上传与静态资源映射:商铺图片的存取方案

商户入驻后要传店铺Logo、商品要传展示图,这属于典型的文件上传场景。本地存储方案是最适合这一项目的——直接存在服务器某个目录下,然后通过SpringBoot的静态资源映射暴露给前端。

application.yml里配置一个自定义上传路径,比如D:/upload/或者Linux下的/data/shop/upload/,然后写一个通用文件上传接口:

@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("上传文件不能为空"); } String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); // 重命名文件,防止重名覆盖 String newFileName = UUID.randomUUID().toString().replace("-", "") + suffix; File dest = new File(uploadPath + newFileName); try { FileUtils.copyInputStreamToFile(file.getInputStream(), dest); return Result.success("/files/" + newFileName); } catch (IOException e) { log.error("文件上传失败", e); return Result.error("文件上传失败"); } }

文件重命名为UUID是必须的操作,不然用户上传两个同名文件会互相覆盖。然后配置WebMvcConfigurer把/files/**映射到本地磁盘目录:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceHandler("/files/**") .addResourceLocations("file:" + uploadPath); }

前端拿到返回的URL直接拼在图片地址上就能访问了。这里的坑点在于Windows和Linux的路径分隔符不同,写配置文件时最好用相对路径或者做了兼容处理,不然部署到服务器上图片全部加载不出来。

4. Vue3+Element Plus前端模块:登录态、动态路由与数据表格

后端骨架搭好后,前端这边的活儿也不少。Vue3的项目组织方式和Vue2有挺大区别,特别是路由守卫、状态管理、网络请求这三块,处理不好项目越写越乱。

4.1 Vite项目初始化与目录结构

创建一个Vue3项目,我用的是Vite,命令很简单:

npm create vite@latest shop-admin -- --template vue

不是非要用pnpm,但建议把依赖装完后,把目录按功能划分好,我习惯的结构是:

src/ ├── api/ // 接口请求定义 ├── assets/ // 静态资源 ├── components/ // 公共组件 ├── router/ // 路由配置 ├── stores/ // Pinia状态管理 ├── utils/ // 工具函数(axios封装在这里) ├── views/ // 页面组件 ├── App.vue └── main.js

很多新手喜欢把所有请求直接写在组件里,当时确实爽,但页面一多你就发现接口地址到处散落,有个接口字段要改得全局搜索。把接口按模块拆分放到api目录,每个页面对应一个JS文件,维护成本低得多。

4.2 axios实例封装与组合式函数调用

Axios封装的核心是统一处理请求头、统一拦截错误。我在utils/request.js里创建一个axios实例,设置baseURL、超时时间,并在请求拦截器里自动带Token:

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg || '请求失败') if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.msg)) } return res }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } )

注意baseURL我用的是/api,然后在Vite配置文件里配devServer的代理,把/api转发到后端的localhost:8080,这样前端开发时不会遇到跨域问题,部署时也能直接交给Nginx统一转发。

4.3 Element Plus表格+分页组件联动

后台管理系统用得最多的场景就是表格列表,Element Plus的el-table配合分页组件,其实有一套固定写法。核心思想是:所有查询条件都绑定到一个查询对象上,点击搜索或切换页码时调用同一个查询方法。

<template> <div> <el-form :inline="true"> <el-form-item label="商品名称"> <el-input v-model="queryForm.name" placeholder="请输入商品名称" clearable /> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> <el-button @click="handleReset">重置</el-button> </el-form-item> </el-form> <el-table :data="tableData" border stripe v-loading="loading"> <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="name" label="商品名称" /> <el-table-column prop="price" label="价格" /> <el-table-column prop="stock" label="库存" /> <el-table-column label="操作" width="200"> <template #default="scope"> <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="queryForm.current" v-model:page-size="queryForm.size" :total="total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="getList" @current-change="getList" /> </div> </template>

这里有个Vue3的版本差异:el-pagination用了v-model:current-pagev-model:page-size,这是Element Plus针对Vue3的写法,老写法current-page.sync在Vue3里已经不适用。

5. 联调与部署阶段的坑:MySQL8.0时区、跨域、驼峰映射

后端和前端都写完,真正痛苦的阶段才开始。联调和部署中遇到的环境问题,比业务代码本身更磨人。我把这套项目里最容易踩的三个坑提前列出来,你能避开就避开。

5.1 MySQL8.0驱动与serverTimezone的折腾

MySQL8.0的JDBC驱动从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver,连接URL里还强制要求加serverTimezone参数,不然会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized

spring: datasource: url: jdbc:mysql://localhost:3306/shop_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver

allowPublicKeyRetrieval=true这个参数是在MySQL8.0下用caching_sha2_password认证插件时必须的,不然你用Navicat连上了但Java连接时会报Public Key Retrieval is not allowed。密码加密方式最好在创建用户时指定成mysql_native_password,减少一堆莫名的认证问题。

另外一个容易忽略的点:MySQL8.0的utf8mb4是真正的四字节UTF-8,MySQL5.7里的utf8本质是utf8mb3,存不了emoji。如果你在MySQL8.0的建表语句里直接写CHARSET=utf8,虽然也能创建成功,但遇到特殊字符照样炸,统一用utf8mb4最稳。

5.2 跨域配置的三种写法与正确姿势

前端Vite默认端口5173,后端SpringBoot端口8080,两者开发环境必然要解决跨域。这里的跨域问题其实分两层:前端的代理转发和后端的CORS配置。

开发环境最省事的方案是在Vite配置里做代理,前端代码里所有请求都走相对路径/api,Vite把它转发到后端,这种情况下浏览器看到的请求是同源的,根本不会有跨域问题。

// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })

生产环境部署时,Nginx反向代理也是类似的配置逻辑,把/api转发给Java进程。如果某些特殊场景必须走前端直连后端,那你才需要后端加@CrossOrigin或CORS配置类。注意跨域配置类里allowedOriginPatterns要写成"*",别用allowedOrigins("*"),因为这方法在携带凭证的情况下会直接被浏览器拒绝。

5.3 逻辑删除与唯一索引冲突的连锁问题

MyBatis-Plus默认支持逻辑删除,在实体字段上加@TableLogic注解,删除时自动执行UPDATE SET deleted=1 WHERE id=?,SELECT时自动拼接AND deleted=0。这个机制本身没问题,但配合数据库唯一索引时会出现一个经典bug。

假设商品表给product_code字段加了唯一索引。用户删除了商品A,它的deleted变成了1,但记录还在表里。这时新增一个商品B,product_code又恰好和A一样,数据库直接报Duplicate entry。因为MySQL的唯一索引对逻辑删除的记录依然生效。

解决方案有三种:一是删除唯一索引,让代码层面自己去查重(不推荐);二是唯一索引改成联合字段(deleted, product_code)(MySQL不允许唯一索引里有两个NULL,但deleted默认值为0,不是NULL,所以可以);三是彻底物理删除。

最稳妥的办法其实是在业务设计上避免这种冲突:商品编码可以带上UUID或时间戳后缀,从源头保证唯一性。逻辑删除字段作为兜底,别指望它解决所有问题。

6. 这套源码怎么用起来:从导入到部署的完整动作清单

说再多理论,最后还是要落地上手。这里给一份从拿到源码到跑起来的完整操作顺序,每一步都验证过,照着做基本半小时内能启动。

6.1 安装MySQL8.0并初始化数据库脚本

Windows和Linux装MySQL8.0的流程不太一样。Windows下载msi安装包一路Next,安装时选Server only,编码选utf8mb4,密码设为root方便本地调试。Linux的Debian系可以用apt直接装:

sudo apt update sudo apt install mysql-server-8.0 sudo systemctl enable --now mysql sudo mysql_secure_installation

装好后用命令行或Navicat执行项目提供的shop_db.sql脚本,这个脚本里包含建库、建表、插入初始数据。执行前检查一下SQL脚本开头的CREATE DATABASE语句,确认编码是utf8mb4

6.2 导入后端Maven项目并修改配置

后端项目用IDEA打开,选择pom.xml作为Maven项目导入,等依赖下载完成。然后改application.yml里的数据库账号密码:

spring: datasource: username: root password: 你的密码

JDK版本确认是1.8,Maven镜像如果下载慢,在settings.xml里配置阿里云镜像。启动ShopApplication.java,看到控制台输出Started ShopApplication in 5.32 seconds就说明后端起来了。

6.3 启动前端Vue项目

前端项目目录下执行:

npm install npm run dev

如果npm install报错,大概率是Node版本太老或太新,Vite3要求Node14.18+或16+,Node18是最稳的选择。启动成功后浏览器访问http://localhost:5173,看到登录页说明前后端已经打通。

6.4 生产环境的轻量部署参考

开发环境跑通只是第一步,真正部署时可以只用一台轻量云服务器搞定。后端打成jar包,前端构建成静态文件,Nginx配置如下:

server { listen 80; server_name your.domain.com; # 前端静态文件 location / { root /opt/shop-frontend/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上传图片访问 location /files/ { alias /opt/shop-upload/; } }

这里try_files是给Vue的history路由用的,保证前端路由刷新时不会404。图片路径单独配置alias,别把上传目录暴露在静态资源根目录下,安全性和可维护性都更好。

7. 我实际用这套组合做项目时的几点体会

最后说点不一定写进文档、但确实影響开发效率的体会。

先说我踩过最大的坑:MyBatis-Plus的老版本和SpringBoot2.7的兼容性问题。如果你在pom里引入了某个2.x早期的MyBatis-Plus依赖版本,可能会出现启动实例化失败。这不是代码写错,是版本冲突。建议直接用mybatis-plus-boot-starter的3.5.3版本,这个版本适配SpringBoot2.7.x稳定,我跑下来基本没出过幺蛾子。

再说Vue3的开发体验。用习惯setup语法糖后确实回不去Vue2的Options API了,但要注意reactiveref的选择:表单对象我习惯用reactive,单独的基础类型变量用ref,混用容易在模板里多个.value写错。模板里ref会自动解包,reactive不会,你只需要记住这个原则就行。

还有一点关于JWT的时效设计。校园商铺里有“找回密码”“修改密码”这类操作,Token有效期设太短会影响体验,设太长又不安全。我通常会拆成两个参数:一个短期访问Token(2小时),一个长期刷新Token(7天)。但这个项目是单体应用,不搞微服务也要按这个思路去设计,谁也不想用户在逛商铺逛到一半突然被踢回登录页。

整套项目跑通后,后续可以扩展的方向其实不少:比如引入Redis做商铺缓存和Token黑名单、接入微信扫码支付、用WebSocket做订单状态实时推送。但那是后面的事了,先把这套地基打牢,往后想加什么功能都不慌。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 9:33:05

SpringBoot+Vue校园疫情防控管理系统:从需求拆解到部署答辩全解析

做了这么多年的 Java 后端&#xff0c;带过的学生、看过毕设源码不计其数。要说最适合拿来当课设或毕设的题目&#xff0c;SpringBootVue 校园疫情防控信息管理系统绝对算一个典型。它听起来有现实背景&#xff0c;做起来业务链路完整&#xff0c;技术栈又正好踩中现在企业里最…

作者头像 李华
网站建设 2026/9/9 9:32:56

电话号码的字母组合:回溯算法与DFS递归的经典入门题解

周末刷力扣的时候&#xff0c;碰到 57. 电话号码的字母组合 这道题。说实话&#xff0c;这道题在 力扣热题100 里算是一道非常经典的“回溯”入门题&#xff0c;但别看它难度标着Medium&#xff0c;其实核心思想并不复杂&#xff0c;很多刚接触 DFS回溯 的朋友容易被“映…

作者头像 李华
网站建设 2026/9/9 9:32:29

VS2019离线安装包处理全攻略:从rar解压到C++环境验证指南

简介&#xff1a;VS2019 C离线压缩包专为C开发者打造&#xff0c;是一个完整的Visual Studio 2019 C离线安装解决方案&#xff0c;可彻底摆脱网络限制&#xff0c;在断网环境中快速部署专业级C开发环境&#xff0c;尤其适合网络受限的企业内网、实验室或临时开发机场景。压缩包…

作者头像 李华
网站建设 2026/9/9 9:31:51

从magnitude读懂地震震级:能量、烈度与避险常识

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 9:31:18

药企AI研发赋能:监管合规与研发效率的协同落地路径

1. 这不是“AI画饼”&#xff0c;而是药企研发管线里正在跑的真实算力 最近跟三家做创新药的团队聊完&#xff0c;我桌上堆着六份不同格式的分子对接报告、四套刚跑完的ADMET预测结果截图&#xff0c;还有两台笔记本屏幕上并排开着AlphaFold2的结构比对窗口和自家化合物库的SMI…

作者头像 李华
网站建设 2026/9/9 9:28:40

Web自动化测试中Select下拉框操作全解:从原生select到自定义组件

刚开始写Web自动化测试用例的时候&#xff0c;我以为下拉框选择就是click一下再click一下&#xff0c;直到在某个后台系统里被“所属部门”这个Select下拉框卡了整整半天&#xff0c;才意识到这种常见的交互控件远比想象中复杂。这篇文章是Web自动化测试系列的第二篇&#xff0…

作者头像 李华