ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Spring Boot的实验室会议预约系统设计与避坑

基于Spring Boot的实验室会议预约系统设计与避坑

简介:这份 JavaWeb 研究生实验室管理系统源码包,面向高校实验室导师与研究生,围绕周报汇报、会议组织、代码共享、项目管理和请假审批等日常协作场景,提供一套可参考的综合实践实现。压缩包共 246 个文件,约 30.63MB,包含 62 个 Java 源文件、62 个 class 文件、24 个 JSP 页面、54 个 jar 依赖及 16 个 XML 配置等,覆盖后端业务层、前端页面、数据库映射与运行依赖,便于梳理分层结构与部署调试。已有 589 人学习下载。系统区分教师端与学生端:教师可查看学生历次汇报与代码、回复消息、创建会议并跟踪讨论;学生可查看公开汇报和代码、参与进行中的会议、创建项目、上传周报及查看回复。读者可据此理解角色权限、会议讨论、代码可见性与消息通知等模块设计,适合作为 JavaWeb 综合实践与二次开发底稿。

1. 别把它当成一个“增删改查练习”:先看清这个系统在实验室里到底卡在哪

研究生实验室里最磨人的事,往往不是跑不出实验数据,而是每周例会预约会议室靠群里接龙、设备借用靠口头约定、新人来了不知道谁能开哪台仪器。这套 JavaWeb 研究生实验室管理系统,核心解决的正是这种“人和事都在表格里、但流程全在微信群里”的失序状态。它把成员管理、会议室预约、会议纪要归档串成一条线,适合两类人:一类是想用现成骨架交课程设计或毕业设计的同学,另一类是实验室里实际在管日常事务、需要一套能跑在实验室内网的小系统的人。技术栈不花哨,Spring Boot 加 MyBatis 再加 MySQL,前端用 Thymeleaf 或者静态页都行,关键在会议预约和角色权限这两块能不能扛住真实使用的并发与越权。如果是第一次从源码包开始接触,建议先别急着改业务,把表结构和预约逻辑跑通,后面的坑会少一大半。

2. 工程骨架与依赖:为什么我砍掉了全家桶,只留 Spring Boot + MyBatis

2.1 选型取舍:研究生课题周期等不起“微服务化”

翻过不少同类毕设源码,十个里有八个上来就是 Spring Cloud 加 Nacos 加网关,结果本地起服务先要把注册中心拉起来,调一个预约接口要跨三个模块。这个系统的定位是单机部署、内网使用,实验室常驻用户几十人,峰值并发不会超过两位数,用微服务没有收益,反而把调试链路拉长了。我最终保留的组合是 Spring Boot 2.7 加 MyBatis 加 MySQL 8.0,前端模板引擎用 Thymeleaf 做服务端渲染,好处是部署只有一个 jar 包,数据库连接配好就能跑,不需要额外维护 Node 环境。

不引入 Spring Security 的理由也类似:研究生课题周期通常只有几个月,Spring Security 的过滤器链和配置类对新手来说调试成本偏高,出问题容易卡在“为什么这个请求 403”上。这里的做法是用一个轻量拦截器加 Session 做登录态和角色校验,权限粒度控制在“管理员 / 导师 / 学生”三级,够用而且容易改。如果后期真要接 OAuth 或者做前后端分离,再往上叠也不迟。

2.2 最小可运行依赖与启动配置

依赖部分只列关键项,版本按 Spring Boot 父工程统一管理,避免手写版本号打架。

<!-- pom.xml 关键依赖,其余由 parent 管理 --> <dependencies> <!-- Web 容器与 MVC,提供 REST 接口和模板渲染 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Thymeleaf 模板,用于服务端渲染的管理页面 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- MyBatis 启动器,简化 SqlSessionFactory 配置 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <!-- MySQL 驱动,注意 8.x 驱动类名带 cj --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>

这里要说明两点。第一,mysql-connector-j是 MySQL 8 之后的新坐标,老项目里常见的mysql-connector-java在部分仓库里也能拉到,但混用容易出现驱动类加载不到的问题,统一用新坐标更省心。第二,MyBatis 启动器版本不要随手写 latest,和 Spring Boot 版本存在兼容区间,2.3.x 配 Boot 2.7 是稳妥组合。

application.yml里真正容易翻车的是时区和连接池参数,下面这份是我在联调阶段反复改过的版本:

server: port: 8080 servlet: session: timeout: 30m # 实验室用户经常开着页面去吃饭,30 分钟比较合适 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/lab_manage?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: lab_user password: change_me hikari: maximum-pool-size: 10 # 内网小系统 10 个连接足够,开大反而抢数据库资源 minimum-idle: 2 connection-timeout: 3000 # 3 秒拿不到连接就报错,别让页面一直转圈 thymeleaf: cache: false # 开发期关缓存,改模板不用重启 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 数据库下划线字段自动映射 Java 驼峰属性

serverTimezone必须显式写成Asia/Shanghai,否则 MySQL 8 的驱动在某些环境下会按 UTC 解析datetime,导致预约时间整体偏移 8 小时。这个坑后面排错章节还会展开,先记住别省略。allowPublicKeyRetrieval=true是在本地用密码认证插件时避免连接被拒的兜底参数,生产内网如果做了账号权限收敛,可以按实际情况去掉。

2.3 跑通第一个页面:从登录拦截器到首页

骨架搭好后,先做一个最小登录流程验证链路是否通:静态登录页提交账号密码,后端查lab_member表比对密码哈希,成功则写 Session。下面只截取拦截器的核心逻辑,因为它决定了后面会议模块的权限边界。

// LoginInterceptor.java public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); // 未登录一律打回登录页,避免直接拼 URL 访问内部接口 if (session == null || session.getAttribute("loginUser") == null) { response.sendRedirect("/login"); return false; } return true; } }

拦截器注册时要注意放行登录页和静态资源,否则会出现“登录页自己也被拦截”的死循环。我一般把/login、/css/**、/js/**加进 excludePathPatterns,其余路径全部拦截。参数上,request.getSession(false)表示不存在会话时不新建,避免为每个未登录请求都创建一个空 Session 拖内存。

这个阶段验证通过的标准很简单:浏览器访问/index能重定向到登录页,登录成功后跳回首页并能看到当前用户名。别小看这一步,后面会议模块的越权问题,根源往往就是拦截器只做了“有没有登录”,没做“这个人能不能看这条数据”。

3. 表结构设计:会议预约的时间字段选错,后面全在还债

3.1 核心表关系:会议室、预约单、参会人三张表就够

这个系统的会议模块不需要建模成日历系统,三张表可以覆盖绝大多数实验室场景:meeting_room存会议室基础信息,meeting_booking存预约单(含开始时间、结束时间、发起人、主题),meeting_attendee存参会人与预约单的关联。成员表lab_member单独存在,通过外键或者逻辑关联挂到预约单上。

建表时最容易埋雷的是时间字段类型。我见过不少源码包把start_time设计成varchar(20),理由是“前端传什么就存什么”,结果做冲突检测时 SQL 里要写字符串比较,跨月跨年直接排序错乱。更稳妥的选择是datetime,精度到秒即可,不需要datetime(3)这种毫秒级精度,实验室预约不会精确到毫秒。

下面这段初始化 SQL 是经过调整的版本,注意索引和字符集:

-- 实验室成员表,role 用枚举字符串而非数字,可读性优先 CREATE TABLE lab_member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(64) NOT NULL COMMENT 'SHA-256 加盐后的哈希', real_name VARCHAR(32) NOT NULL, role VARCHAR(16) NOT NULL DEFAULT 'STUDENT' COMMENT 'ADMIN/MENTOR/STUDENT', lab_group VARCHAR(32) DEFAULT NULL COMMENT '课题组,用于数据隔离', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -- 会议室表,capacity 用于前端提示人数是否超限 CREATE TABLE meeting_room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_name VARCHAR(64) NOT NULL, location VARCHAR(128) DEFAULT NULL, capacity INT NOT NULL DEFAULT 10, status TINYINT NOT NULL DEFAULT 1 COMMENT '1 可用 0 停用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 预约单,状态用字符串,便于排查时直接看懂 CREATE TABLE meeting_booking ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL, title VARCHAR(128) NOT NULL COMMENT '会议主题', start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, booker_id BIGINT NOT NULL COMMENT '发起人 member.id', status VARCHAR(16) NOT NULL DEFAULT 'BOOKED' COMMENT 'BOOKED/CANCELLED/FINISHED', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_room_time (room_id, start_time, end_time), KEY idx_booker (booker_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 参会人关联表,主键用联合唯一防止重复插入 CREATE TABLE meeting_attendee ( id BIGINT PRIMARY KEY AUTO_INCREMENT, booking_id BIGINT NOT NULL, member_id BIGINT NOT NULL, attend_status VARCHAR(16) NOT NULL DEFAULT 'INVITED' COMMENT 'INVITED/ACCEPTED/ABSENT', UNIQUE KEY uk_booking_member (booking_id, member_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

idx_room_time这个联合索引是给冲突检测查询用的,查询条件按room_id过滤后再按时间段比对,索引顺序要和查询条件顺序对齐。字符集统一utf8mb4,别用utf8,后者在 MySQL 里不是真正的四字节 UTF-8,存个特殊符号就报错。

3.2 时间字段的类型对比与选择依据

把几种常见做法摊开看,选择逻辑会更清楚:

字段类型优点缺点适用场景
varchar(20)前端传什么存什么,无格式转换无法直接比较大小,排序按字典序,冲突检测要额外解析临时演示,不建议
timestamp带时区转换,存储空间小受数据库time_zone影响,跨时区迁移容易偏移,范围到 2038 年需要严格时区语义的系统
datetime不带时区转换,存什么读什么,范围大需要应用层统一时区内网单时区系统,本项目的选择
bigint存毫秒比较方便,无格式问题可读性差,SQL 里排查要手动转高并发日志类场景

这个系统部署在实验室局域网,所有用户在同一时区,用datetime最省心:数据库里存的就是北京时间,前端展示也是北京时间,中间不需要任何转换。代价是如果将来要做多时区,得在应用层统一处理。选型没有绝对对错,关键是把代价放在明面上。

3.3 别急着写业务代码:先把测试数据灌进去

表建好后,建议先手动插几条数据验证查询,而不是直接写 Controller。下面这段 SQL 用来造一条会议室记录和两条时间相邻的预约,方便后面测试冲突检测:

INSERT INTO meeting_room (room_name, location, capacity) VALUES ('A301 研讨室', '实验楼 A 座 301', 12); INSERT INTO lab_member (username, password, real_name, role, lab_group) VALUES ('stu01', 'hashed_pwd_1', 'A同学', 'STUDENT', '视觉组'), ('mentor01', 'hashed_pwd_2', '某导师', 'MENTOR', '视觉组'); -- 第一条预约:周一 14:00 到 15:00 INSERT INTO meeting_booking (room_id, title, start_time, end_time, booker_id, status) VALUES (1, '视觉组周会', '2025-06-02 14:00:00', '2025-06-02 15:00:00', 1, 'BOOKED'); -- 第二条预约:周一 14:30 到 15:30,与上一条重叠,用于验证冲突逻辑 INSERT INTO meeting_booking (room_id, title, start_time, end_time, booker_id, status) VALUES (1, '设备评审', '2025-06-02 14:30:00', '2025-06-02 15:30:00', 2, 'BOOKED');

灌完数据后手动执行冲突查询,确认能查出重叠记录,再把这个查询搬进代码。很多翻车案例是 SQL 在客户端跑得通,进了 MyBatis 因为参数类型或时区配置对不上,查出来永远为空,白白浪费半天。

4. 会议模块核心逻辑:冲突检测、并发锁和参会人状态

4.1 预约冲突检测:时间重叠公式别写反

判断两个时间段是否重叠,标准写法是已有开始 < 新结束 AND 已有结束 > 新开始,四个比较符号少一个都会漏判。下面这段是 MyBatis Mapper 里的查询,只统计未取消的预约:

<!-- MeetingBookingMapper.xml --> <select id="countConflict" resultType="int"> SELECT COUNT(*) FROM meeting_booking WHERE room_id = #{roomId} AND status != 'CANCELLED' AND start_time &lt; #{endTime} AND end_time &gt; #{startTime} </select>

SQL 里<和>必须转义成&lt;和&gt;,否则 XML 解析直接报错,这是新手最常撞的墙之一。参数说明:roomId是目标会议室,startTime和endTime是本次预约的起止时间,类型用java.time.LocalDateTime,MyBatis 会按 JDBC 的TIMESTAMP类型传入,和数据库datetime字段对应。

对应的 Service 层逻辑要保持“先查后插”的顺序,并且把状态判断放在查询里而不是查出来再过滤,减少无效数据传输:

// MeetingBookingService.java @Transactional(rollbackFor = Exception.class) public void createBooking(MeetingBooking booking) { // 基础校验:结束时间必须晚于开始时间,防止前端传错 if (!booking.getEndTime().isAfter(booking.getStartTime())) { throw new IllegalArgumentException("结束时间必须晚于开始时间"); } // 冲突检测:同一会议室、同一时间段内已有未取消预约则拒绝 int conflict = bookingMapper.countConflict( booking.getRoomId(), booking.getStartTime(), booking.getEndTime()); if (conflict > 0) { throw new IllegalStateException("该时间段已被预约"); } booking.setStatus("BOOKED"); bookingMapper.insert(booking); }

@Transactional的rollbackFor显式写成Exception.class,是为了让受检异常也能触发回滚,默认只回滚运行时异常。这个方法在单线程下没问题,但两个请求同时进来,都查到冲突为 0,然后都插入成功,就出现了时间重叠的两条记录。下一节处理这个问题。

4.2 并发预约:用行锁把“查-插”包成原子操作

实验室场景并发不高,但“两个人同时点预约”完全可能发生,尤其是组会前几分钟。最省事的做法是在事务里先对会议室行加悲观锁,把同一会议室的预约请求串行化:

<!-- MeetingRoomMapper.xml --> <select id="selectForUpdate" resultType="MeetingRoom"> SELECT id, room_name, capacity, status FROM meeting_room WHERE id = #{roomId} FOR UPDATE </select>

Service 里调整顺序,先锁会议室再查冲突:

@Transactional(rollbackFor = Exception.class) public void createBooking(MeetingBooking booking) { if (!booking.getEndTime().isAfter(booking.getStartTime())) { throw new IllegalArgumentException("结束时间必须晚于开始时间"); } // 对会议室行加锁,同一会议室的并发请求在这里排队 MeetingRoom room = roomMapper.selectForUpdate(booking.getRoomId()); if (room == null || room.getStatus() != 1) { throw new IllegalStateException("会议室不存在或已停用"); } int conflict = bookingMapper.countConflict( booking.getRoomId(), booking.getStartTime(), booking.getEndTime()); if (conflict > 0) { throw new IllegalStateException("该时间段已被预约"); } booking.setStatus("BOOKED"); bookingMapper.insert(booking); }

FOR UPDATE锁的是meeting_room里id对应的行,直到事务提交或回滚才释放。参数上注意:锁的粒度是会议室级别,不是全表,不同会议室的预约互不影响。代价是如果某个事务里做了耗时操作(比如调外部接口),会把同一会议室的请求堵住,所以事务方法里只放数据库操作,别塞文件上传或远程调用。

如果不想用悲观锁,另一个思路是给预约表加唯一约束,但时间段重叠没法用普通唯一索引表达,只能靠应用层或者数据库触发器,复杂度更高。对这个体量的系统,行锁是性价比最高的方案。

4.3 参会人批量入库与状态流转

预约创建成功后,发起人通常会勾选一批参会人。批量插入时用 MyBatis 的foreach拼一条多值 INSERT,比循环单条插入少很多网络往返:

<insert id="batchInsert"> INSERT INTO meeting_attendee (booking_id, member_id, attend_status) VALUES <foreach collection="list" item="item" separator=","> (#{item.bookingId}, #{item.memberId}, #{item.attendStatus}) </foreach> </insert>

foreach的separator写成逗号,拼出来是VALUES (...),(...),(...)的形式。注意booking_id要等预约主表插入拿到自增 ID 之后再赋值,所以批量插入要放在createBooking的主插入之后、事务提交之前,保证要么全成功要么全回滚。

状态流转这块,实验室里最常见的诉求是“会议结束后标记谁没来”。我不会做复杂的审批流,只保留三个状态:INVITED表示已邀请,ACCEPTED表示确认参加,ABSENT表示缺席。更新语句按联合唯一键定位,避免重复更新:

// 按 bookingId + memberId 精确更新参会状态 attendeeMapper.updateStatus(bookingId, memberId, "ABSENT");

更新时加一个前置判断“当前状态不是 ABSENT 才更新”,避免重复点击导致的状态回退。这个细节在演示时不明显,实际用起来能省掉很多脏数据。

5. 避坑排查:五个我真实撞过的翻车现场

5.1 附件上传到容器里“消失”了

现象:本地开发时会议纪要附件能正常预览,打成 jar 包部署到实验室服务器后,上传成功但重启服务文件全没了。

原因:代码里用了相对路径upload/,Spring Boot 打成可执行 jar 后,工作目录和开发时 IDE 的工作目录不一致,文件被写到了临时解压目录,服务重启即清空。

解决:在application.yml里配置绝对路径,并在启动时检查目录是否存在:

lab: upload-dir: /data/lab-manage/upload
// 启动时创建目录,避免第一次上传因目录不存在而失败 @PostConstruct public void initUploadDir() throws IOException { Path path = Paths.get(uploadDir); if (!Files.exists(path)) { Files.createDirectories(path); } }

部署时把这个目录挂到宿主机持久化卷上,别放在容器可写层里。

5.2 预约时间整体偏移 8 小时

现象:数据库里查出来是14:00,页面上显示22:00,怎么改前端格式化都没用。

原因:JDBC URL 里没写serverTimezone,或者写成了UTC,驱动按 UTC 解析datetime,而 JVM 默认时区是东八区,来回转换就偏了 8 小时。

解决:连接串统一加serverTimezone=Asia/Shanghai,同时确认服务器系统时区不是 UTC。如果已经产生脏数据,用 SQL 批量修正:

-- 仅当确认数据整体偏移 8 小时时执行,执行前先备份 UPDATE meeting_booking SET start_time = DATE_ADD(start_time, INTERVAL 8 HOUR), end_time = DATE_ADD(end_time, INTERVAL 8 HOUR) WHERE status = 'BOOKED';

这条语句不能无脑跑,必须先抽样确认偏移方向,否则会把正确数据改错。

5.3 两个请求同时预约同一时段,都成功了

现象:压测时用两个并发请求预约同一会议室同一时间段,两条记录都插入成功。

原因:countConflict和insert之间没有锁,两个事务都读到冲突为 0。

解决:按 4.2 节的方式,在事务里先SELECT ... FOR UPDATE锁会议室行。改完后并发请求只有一个能成功,另一个抛“该时间段已被预约”。注意锁的查询必须在同一个事务里,且事务隔离级别用默认的REPEATABLE READ即可。

5.4 A 组学生能看到 B 组的会议纪要

现象:学生登录后手动改 URL 里的bookingId,能打开其他课题组的会议详情页。

原因:Controller 只校验了登录态,没校验数据归属,查询条件里缺lab_group过滤。

解决:在 Service 层按当前登录用户的labGroup过滤,管理员和导师可以跨组查看,学生只能看本组:

// 查询详情时附加数据边界,currentUser 从 Session 取 public MeetingBooking getDetail(Long bookingId, LabMember currentUser) { MeetingBooking booking = bookingMapper.selectById(bookingId); if (booking == null) { throw new IllegalArgumentException("预约不存在"); } // 学生只能看本组发起的会议,防止越权 if ("STUDENT".equals(currentUser.getRole()) && !currentUser.getLabGroup().equals(booking.getLabGroup())) { throw new SecurityException("无权查看该会议"); } return booking; }

这个过滤条件要写在服务层而不是前端隐藏按钮,前端隐藏只能防误操作,防不了手动拼 URL。

5.5 中文会议主题存进数据库变成问号

现象:提交带中文的会议主题,数据库里显示???,页面回显也乱码。

原因:建库时用了utf8而不是utf8mb4,或者 JDBC 连接串没带characterEncoding=utf8mb4。

解决:建库语句统一DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci,连接串加useUnicode=true&characterEncoding=utf8mb4。已经建好的库用ALTER DATABASE lab_manage CHARACTER SET utf8mb4;修正,表也要逐张改。改完后重启应用,旧数据如果已经损坏只能重新录入。

6. 部署联调:用 Docker Compose 把 MySQL 和应用一起拉起来

本地开发时我习惯直接跑mvn spring-boot:run,但交付给实验室实际使用,最省事的还是 Docker Compose 一把梭。下面这份编排把 MySQL 和应用放在同一个网络里,应用通过服务名连数据库,避免写死 IP:

# docker-compose.yml version: "3.8" services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root_change_me MYSQL_DATABASE: lab_manage TZ: Asia/Shanghai volumes: - ./mysql-data:/var/lib/mysql # 数据持久化,别用匿名卷 - ./init.sql:/docker-entrypoint-initdb.d/init.sql # 首次启动自动建表 ports: - "3306:3306" command: --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci app: build: . depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/lab_manage?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root_change_me TZ: Asia/Shanghai ports: - "8080:8080" volumes: - ./upload:/data/lab-manage/upload # 附件目录挂出来,容器重建不丢文件

几个参数值得单独说。TZ: Asia/Shanghai在 MySQL 容器和应用容器里都要设,前者影响数据库会话时区,后者影响 JVM 默认时区,两头对齐才不会出现“应用算出来的时间和数据库存的对不上”。depends_on只保证启动顺序,不保证 MySQL 已经可连接,所以应用侧最好加一个重试或者健康检查,否则容器第一次启动时容易因为连不上库直接退出。挂载init.sql到初始化目录只在数据卷为空时执行一次,后面改表结构要靠手动迁移脚本,别指望改文件重启就生效。

交付前我会固定跑一遍验证清单:用两个账号并发预约同一会议室,确认只有一个成功;用学生账号手动拼其他组的bookingId,确认返回无权查看;上传一个中文名附件,重启容器后确认文件还在;把系统时间调到整点前后各预约一次,确认边界时间不重叠。这几个动作花不了十分钟,但能挡住后面绝大部分返工。

血泪教训是早期我图省事把上传目录写在容器内部,演示当天服务重启,所有会议纪要附件归零,现场只能重新上传。从那以后我每次部署前都先确认三件事:数据卷挂了吗、时区对齐了吗、并发锁真的在事务里吗。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表