ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

公交广告灯箱管理系统Java开发实战

公交广告灯箱管理系统Java开发实战 简介本资源是一份面向Java初学者与课程设计学生的公交站牌广告灯箱管理系统完整设计方案文档聚焦城市公共广告资源信息化管理痛点提供从需求分析到技术实现的全流程参考。文档采用B/S架构与MVC三层设计模式涵盖系统用户管理、网站资讯发布、广告任务含申请/审核/合同管理、留言反馈及登录退出等核心模块并详细说明Java后端开发、Eclipse开发环境配置、MySQL数据库建表逻辑及JSP页面实现要点。资源为单个1.13MB的Word文档.docx内容结构完整含中英文摘要、目录、绪论、关键技术介绍JSP、MySQL、系统设计与实现章节适合作为毕业设计选题参考、Java Web课程实践范例或B/S架构项目学习素材。目前已有41人学习下载文档图文结合、术语规范、模块划分清晰可直接用于方案汇报、代码开发对标与系统功能拆解学习。1. 公交站牌广告灯箱管理系统为什么用 Java 做而不是微信小程序或低代码平台你见过那种嵌在公交站台立柱里、带背光LED、能定时开关、支持远程换图的广告灯箱吗它不是一块静态亚克力板——背后连着电源控制器、4G通信模块、本地存储卡甚至带温湿度传感器。而“公交站牌广告灯箱管理系统”就是管这几百上千个分散在城市毛细血管里的物理终端的软件系统谁投了哪块屏、广告排期到几月几号、上个月播放达标率多少、某台设备昨天凌晨三点离线了、后台一键下发新素材……这些事全靠一套稳定、可扩展、能对接硬件协议的后台系统兜底。很多人第一反应是“做个网页后台Excel导出不就完了”但现实很快打脸广告主催排期要实时查空档期运维人员巡检时扫码就能看到该灯箱最近7天播放日志财务需要按屏、按天、按客户维度自动算分成更重要的是——当某条主干道23台灯箱集体黑屏系统得5秒内触发告警并定位到是某批次控制器固件bug而不是等巡检员打电话来。这种对事务一致性、设备状态追踪、并发调度和长期运行可靠性的要求决定了它必须是典型的Java EE级应用有清晰分层MVC、可热部署、能接消息队列、能做分布式事务补偿、数据库操作经得起审计。Eclipse MySQL Servlet/JSP 是最轻量又最可控的落地组合——不依赖云厂商锁定不靠第三方API黑匣子所有逻辑攥在自己手里。这不是炫技是公交广告这种ToB场景下交付周期、运维成本和故障归因效率之间的硬平衡。2. 从零搭起MVC骨架用Eclipse建Web项目绕过Tomcat配置玄学2.1 创建标准Dynamic Web Project并校验JDK与Servlet版本打开Eclipse推荐2022-06或更新版避免老版本对Java 11的兼容问题依次点击File → New → Dynamic Web Project。关键参数如下Project name:BusAdBoardSystem建议全小写避免Windows路径大小写陷阱Target runtime: 选择已配置好的Apache Tomcat v9.0Tomcat 10默认用Jakarta EE命名空间会把javax.servlet.*全改成jakarta.servlet.*而本项目依赖大量成熟开源组件如Apache Commons FileUpload尚未全面适配强行升级将引发编译雪崩Configuration: 选Default Configuration for Apache Tomcat v9.0Dynamic web module version:4.0对应Servlet 4.0规范支持异步Servlet和HTTP/2且与Tomcat 9完全匹配Generate web.xml deployment descriptor: ✅ 勾选这是手动控制Filter/Listener顺序的唯一途径别信“可以不要web.xml”的营销话术提示若创建后报错The superclass javax.servlet.http.HttpServlet was not found on the Java Build Path说明JDK未正确关联Servlet API。右键项目 → Properties → Java Build Path → Libraries → Add Library → Server Runtime → 选择你的Tomcat实例。Eclipse会自动挂载tomcat-lib/servlet-api.jar此jar仅用于编译运行时由Tomcat容器提供。2.2 手动补全MVC三层结构目录与基础类Eclipse自动生成的目录缺了关键层需人工补全BusAdBoardSystem/ ├── src/ # Java源码根目录 │ ├── controller/ # Servlet入口只处理请求分发与跳转 │ │ └── AdBoardServlet.java │ ├── service/ # 业务逻辑中枢含事务控制与规则校验 │ │ └── AdBoardService.java │ ├── dao/ # 数据访问对象纯SQL操作不碰业务逻辑 │ │ └── AdBoardDao.java │ └── model/ # POJO实体字段与MySQL表严格一一映射 │ └── AdBoard.java ├── WebContent/ # Web资源根目录Eclipse旧版叫法实际即webapp │ ├── WEB-INF/ │ │ ├── web.xml # 核心配置文件定义Servlet映射与全局Filter │ │ └── lib/ # 第三方jar包存放处后续放mysql-connector-java.jar等 │ ├── index.jsp # 默认首页展示登录入口 │ └── view/ # JSP视图层只做数据渲染禁止写Java脚本% % │ └── board_list.jsp2.3 写第一个Servlet用最简代码验证MVC通路创建src/controller/AdBoardServlet.javapackage controller; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; WebServlet(/board/list) public class AdBoardServlet extends HttpServlet { private static final long serialVersionUID 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 模拟调用Service获取数据真实场景此处会new Service或注入 String[] boards {朝阳路A-01, 西二旗B-12, 中关村C-08}; // 将数据存入request作用域供JSP读取 request.setAttribute(boardList, boards); // 转发到view层注意路径以/开头表示相对于WEB-INF request.getRequestDispatcher(/view/board_list.jsp).forward(request, response); } }对应的WebContent/view/board_list.jsp% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8% !DOCTYPE html html headtitle灯箱列表/title/head body h2当前在线灯箱/h2 ul % String[] list (String[]) request.getAttribute(boardList); if (list ! null) { for (String board : list) { % li% board %/li % } } % /ul /body /html启动Tomcat在浏览器访问http://localhost:8080/BusAdBoardSystem/board/list若看到三个灯箱编号则MVC通路打通。这是整个系统最脆弱也最关键的“第一公里”——所有后续功能都建立在此之上。3. MySQL建模实战公交灯箱的5张核心表与设计取舍3.1 表结构设计原则宁可多连表绝不冗余字段公交灯箱管理不是电商订单系统核心矛盾是设备状态高频变更与广告排期强时间约束。因此设计时坚持三条铁律设备表ad_board只存静态属性编号、安装位置、尺寸、电源类型、所属线路。不存“当前播放广告ID”——那是运行时状态应存在独立的状态表中排期表ad_schedule必须带生效时间窗start_time和end_time为DATETIME类型且加联合索引(board_id, start_time, end_time)支撑“查某屏未来7天排期”这类高频查询操作日志operation_log强制分表按月分表如operation_log_202406,operation_log_202407避免单表超千万行导致ALTER TABLE锁表。以下是5张核心表DDLMySQL 5.7-- 1. 灯箱设备主表静态信息 CREATE TABLE ad_board ( id BIGINT PRIMARY KEY AUTO_INCREMENT, board_code VARCHAR(32) NOT NULL UNIQUE COMMENT 灯箱唯一编码如BJ-CHAOYANG-001, location VARCHAR(128) NOT NULL COMMENT 安装位置描述, longitude DECIMAL(10,8) COMMENT 经度, latitude DECIMAL(11,8) COMMENT 纬度, line_id VARCHAR(20) NOT NULL COMMENT 所属公交线路ID, size_type ENUM(A,B,C) DEFAULT A COMMENT 尺寸规格A1200x1800mm, B900x1600mm..., power_type ENUM(AC220,DC12,Solar) NOT NULL, install_date DATE NOT NULL, status ENUM(ONLINE,OFFLINE,MAINTAINING) DEFAULT ONLINE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_line_status (line_id, status), INDEX idx_location (location(20)) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT公交灯箱设备主表; -- 2. 广告素材表图片/视频元数据 CREATE TABLE ad_material ( id BIGINT PRIMARY KEY AUTO_INCREMENT, material_code VARCHAR(32) NOT NULL UNIQUE COMMENT 素材唯一码由系统生成, file_name VARCHAR(128) NOT NULL COMMENT 原始文件名, file_size BIGINT NOT NULL COMMENT 字节数, file_type ENUM(IMAGE,VIDEO) NOT NULL, duration_sec INT DEFAULT 0 COMMENT 视频时长秒, resolution VARCHAR(20) COMMENT 分辨率如1920x1080, upload_user VARCHAR(32) NOT NULL, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, status ENUM(VALID,INVALID,DELETED) DEFAULT VALID, INDEX idx_type_status (file_type, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT广告素材库; -- 3. 排期计划表核心业务表 CREATE TABLE ad_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, board_id BIGINT NOT NULL COMMENT 外键ad_board.id, material_id BIGINT NOT NULL COMMENT 外键ad_material.id, start_time DATETIME NOT NULL COMMENT 生效开始时间, end_time DATETIME NOT NULL COMMENT 生效结束时间, play_order TINYINT DEFAULT 1 COMMENT 同时间段内播放顺序1优先, status ENUM(PUBLISHED,PAUSED,CANCELLED) DEFAULT PUBLISHED, created_by VARCHAR(32) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (board_id) REFERENCES ad_board(id) ON DELETE CASCADE, FOREIGN KEY (material_id) REFERENCES ad_material(id) ON DELETE RESTRICT, INDEX idx_board_time (board_id, start_time, end_time), INDEX idx_time_status (start_time, end_time, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT广告排期计划表; -- 4. 设备状态快照表解决“当前播放什么”的问题 CREATE TABLE board_status ( id BIGINT PRIMARY KEY AUTO_INCREMENT, board_id BIGINT NOT NULL, current_material_id BIGINT COMMENT 当前正在播放的素材ID, last_play_time DATETIME COMMENT 最后播放时间, online_duration_sec BIGINT DEFAULT 0 COMMENT 本次在线持续秒数, offline_reason VARCHAR(64) COMMENT 离线原因如power_off,network_lost, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_board_id (board_id), FOREIGN KEY (board_id) REFERENCES ad_board(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT灯箱实时状态快照; -- 5. 运维操作日志表审计刚需 CREATE TABLE operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, operator VARCHAR(32) NOT NULL COMMENT 操作人账号, target_type ENUM(BOARD,SCHEDULE,MATERIAL) NOT NULL, target_id BIGINT NOT NULL COMMENT 被操作对象ID, action_type ENUM(CREATE,UPDATE,DELETE,PUBLISH,PAUSE) NOT NULL, action_detail TEXT COMMENT 操作详情JSON如{old_status:PAUSED,new_status:PUBLISHED}, ip_address VARCHAR(45) COMMENT 操作IP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_target (target_type, target_id), INDEX idx_time (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT运维操作审计日志;3.2 关键字段设计背后的血泪经验ad_board.board_code为何不用自增ID作业务主键因为现场运维人员贴标签、巡检扫码、客户合同里写的全是“BJ-CHAOYANG-001”这类编码。若用id12345每次查数据库都要JOIN翻译报表导出还要额外映射徒增复杂度。业务主键即自然键这是公交行业多年踩坑共识。ad_schedule.start_time/end_time为何不用DATE而用DATETIME广告主常要求“6月1日早8点整开始播”精确到分钟。若只用DATE系统无法区分“6月1日00:00”和“6月1日08:00”排期冲突检测会失效。board_status表为何设为UNIQUE KEYuk_board_id每台灯箱只能有一条最新状态记录。用唯一索引替代INSERT ... ON DUPLICATE KEY UPDATE避免高并发下出现两条同board_id记录——这是某次大促期间凌晨批量刷新状态时翻车的根本原因。4. DAO层手写JDBC拒绝Hibernate全自动用PreparedStatement守住SQL底线4.1 为什么不用MyBatis或JPA本系统最大并发压力来自两类请求后台运营人员批量修改100台灯箱的排期UPDATE语句需精准控制WHERE条件灯箱终端每5分钟上报一次心跳INSERT高频小包。Hibernate的二级缓存在此场景下反而成累赘缓存失效策略难调Transactional传播行为易引发长事务锁表。而MyBatis的XML SQL虽灵活但团队新人接手时容易写出N1查询如循环查每台灯箱的最新状态。最终选择手写JDBC PreparedStatement只为掌控三件事每条SQL的执行计划可预测EXPLAIN能直接看批量操作用addBatch()executeBatch()吞吐量比ORM高3倍错误堆栈直指具体SQL行号排查DataTruncation异常时不用猜是哪个字段超长。4.2 实现一个带事务的排期发布DAO方法创建src/dao/AdScheduleDao.javapackage dao; import model.AdSchedule; import java.sql.*; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; public class AdScheduleDao { private static final String INSERT_SQL INSERT INTO ad_schedule (board_id, material_id, start_time, end_time, play_order, status, created_by) VALUES (?, ?, ?, ?, ?, ?, ?); private static final String BATCH_UPDATE_STATUS_SQL UPDATE ad_schedule SET status ? WHERE id IN ( SELECT id FROM (SELECT id FROM ad_schedule WHERE board_id ? AND status PUBLISHED) AS tmp); public int insertSchedule(AdSchedule schedule) throws SQLException { String sql INSERT_SQL; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setLong(1, schedule.getBoardId()); ps.setLong(2, schedule.getMaterialId()); ps.setTimestamp(3, Timestamp.valueOf(schedule.getStartTime())); ps.setTimestamp(4, Timestamp.valueOf(schedule.getEndTime())); ps.setByte(5, (byte) schedule.getPlayOrder()); ps.setString(6, schedule.getStatus()); ps.setString(7, schedule.getCreatedBy()); int affectedRows ps.executeUpdate(); if (affectedRows 0) { throw new SQLException(创建排期失败未影响任何行); } // 获取自增主键ID用于后续日志记录 try (ResultSet rs ps.getGeneratedKeys()) { if (rs.next()) { schedule.setId(rs.getLong(1)); } } return affectedRows; } } // 批量暂停某屏所有有效排期用于紧急下架 public int pauseAllSchedulesByBoard(long boardId) throws SQLException { String sql BATCH_UPDATE_STATUS_SQL; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, PAUSED); ps.setLong(2, boardId); return ps.executeUpdate(); // 注意此处返回值是实际更新行数非0即成功 } } // 查询某屏未来7天排期带素材信息JOIN public ListAdSchedule findUpcomingSchedules(long boardId, LocalDateTime now) throws SQLException { String sql SELECT s.id, s.board_id, s.material_id, s.start_time, s.end_time, s.play_order, s.status, m.file_name, m.file_type FROM ad_schedule s INNER JOIN ad_material m ON s.material_id m.id WHERE s.board_id ? AND s.status PUBLISHED AND s.start_time ? AND s.start_time DATE_ADD(?, INTERVAL 7 DAY) ORDER BY s.start_time ASC ; ListAdSchedule list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, boardId); ps.setTimestamp(2, Timestamp.valueOf(now)); ps.setTimestamp(3, Timestamp.valueOf(now)); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { AdSchedule sched new AdSchedule(); sched.setId(rs.getLong(id)); sched.setBoardId(rs.getLong(board_id)); sched.setMaterialId(rs.getLong(material_id)); sched.setStartTime(rs.getTimestamp(start_time).toLocalDateTime()); sched.setEndTime(rs.getTimestamp(end_time).toLocalDateTime()); sched.setPlayOrder(rs.getByte(play_order)); sched.setStatus(rs.getString(status)); sched.setMaterialFileName(rs.getString(file_name)); sched.setMaterialFileType(rs.getString(file_type)); list.add(sched); } } return list; } } }注意DBUtil.getConnection()是封装的连接获取工具类内部使用DriverManager.getConnection()生产环境应替换为HikariCP连接池。此处省略连接池配置细节但强调——没连接池的JDBC就是裸奔上线必挂。4.3 PreparedStatement防SQL注入的实操边界有人问“用?占位符就绝对安全了吗”答案是否定的。以下两种写法依然危险❌ 错误示范1表名/列名拼接// 危险userInput可能为 ad_schedule; DROP TABLE ad_schedule; String tableName userInput; String sql SELECT * FROM tableName WHERE status ?;✅ 正确做法白名单校验private static final SetString ALLOWED_TABLES Set.of(ad_schedule, ad_board, ad_material); if (!ALLOWED_TABLES.contains(tableName)) { throw new IllegalArgumentException(非法表名 tableName); }❌ 错误示范2ORDER BY字段拼接// 危险userInput可能为 id; DROP TABLE ... String orderBy userInput; String sql SELECT * FROM ad_schedule ORDER BY orderBy;✅ 正确做法枚举映射public enum SortField { START_TIME(start_time), END_TIME(end_time), PLAY_ORDER(play_order); private final String column; SortField(String column) { this.column column; } public String getColumn() { return column; } } // 使用时sql ORDER BY SortField.valueOf(userInput).getColumn();5. 避坑指南EclipseTomcatMySQL组合的5个真·翻车现场5.1 现象Eclipse启动Tomcat报错java.lang.ClassNotFoundException: org.apache.catalina.startup.Bootstrap原因Eclipse的Server Runtime配置指向了Tomcat的bin/目录而非根目录。Bootstrap类在tomcat/lib/bootstrap.jar中但Eclipse错误地将bin/加入Classpath导致找不到jar。解决右键Servers视图中的Tomcat → Properties → Server Locations → 选择Use Tomcat installation而非Use workspace metadata然后点击Browse...重新指向Tomcat解压后的根目录如/opt/apache-tomcat-9.0.87确保路径末尾不带/bin。5.2 现象JSP页面中文显示为乱码但Console输出正常原因Eclipse的JSP编辑器编码、Tomcat的HTTP响应头、MySQL连接URL三者不一致。常见组合是Eclipse设为UTF-8Tomcat默认ISO-8859-1MySQL连接URL漏了useUnicodetruecharacterEncodingUTF-8。解决三处同步设置EclipseWindow → Preferences → General → Workspace → Text file encoding → UTF-8Tomcatconf/web.xml中找到filter的CharacterEncodingFilter取消注释并确认init-param中encoding值为UTF-8JDBC URLjdbc:mysql://localhost:3306/bus_ad?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai。5.3 现象AdBoardDao查询返回空List但MySQL命令行执行相同SQL有结果原因ResultSet遍历时未调用rs.next()就直接rs.getLong(id)导致SQLException: Before start of result set被静默吞掉尤其在try-with-resources中未显式catch。解决所有while(rs.next())循环前先加断点验证rs是否为空更稳妥写法if (!rs.isBeforeFirst()) { // 确保游标已定位 while (rs.next()) { /* 处理逻辑 */ } } else { System.out.println(Warning: ResultSet is empty); // 日志留痕 }5.4 现象ad_schedule表插入时报错Data truncation: Data too long for column file_name at row 1原因ad_material.file_name字段定义为VARCHAR(128)但用户上传文件名为2024_Q2_Promotion_Video_Final_Version_For_Beijing_Subway_Line_10.mp4超长。MySQL 5.7默认开启严格模式截断会报错而非警告。解决短期扩大字段ALTER TABLE ad_material MODIFY COLUMN file_name VARCHAR(255);长期在DAO层增加校验if (fileName.length() 255) throw new IllegalArgumentException(文件名超长);前端同步限制input maxlength。5.5 现象Tomcat重启后board_status表中某台灯箱current_material_id变成NULL原因board_status表设计为ON DELETE CASCADE关联ad_board但某次误操作执行了DELETE FROM ad_board WHERE id ?触发级联删除导致状态丢失。解决立即修复将外键改为ON DELETE RESTRICT强制要求先清空状态再删设备加防护在AdBoardDao.deleteBoard()方法中先调用BoardStatusDao.clearStatusByBoardId(boardId)确保状态表无残留增加数据库级约束CREATE TRIGGER prevent_board_delete BEFORE DELETE ON ad_board FOR EACH ROW BEGIN IF EXISTS (SELECT 1 FROM board_status WHERE board_id OLD.id) THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 请先清除灯箱状态; END IF; END;。6. 真正让系统活起来的3个硬核技巧从能跑到敢上生产6.1 用Servlet Filter实现全链路请求ID透传终结“日志找不到上下文”噩梦公交广告系统最怕的不是Bug而是“某台灯箱播错了广告但日志里查不到是谁操作的”。根源在于一次排期发布请求会经过AdBoardServlet → AdScheduleService → AdScheduleDao → MySQL各层日志分散在不同文件。解决方案是用Filter注入唯一请求ID并贯穿全程。创建src/filter/RequestIdFilter.javapackage filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.UUID; public class RequestIdFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; // 生成唯一ID优先取Header中的X-Request-ID便于网关透传否则自动生成 String requestId request.getHeader(X-Request-ID); if (requestId null || requestId.trim().isEmpty()) { requestId UUID.randomUUID().toString().replace(-, ); } // 将ID存入ThreadLocal供后续任意位置取用 MDCUtil.setRequestId(requestId); try { chain.doFilter(req, resp); } finally { // 必须清理避免线程复用时污染 MDCUtil.clearRequestId(); } } }配套src/util/MDCUtil.java简化版不依赖Logbackpackage util; public class MDCUtil { private static final ThreadLocalString REQUEST_ID_HOLDER ThreadLocal.withInitial(() - ); public static void setRequestId(String id) { REQUEST_ID_HOLDER.set(id); } public static String getRequestId() { return REQUEST_ID_HOLDER.get(); } public static void clearRequestId() { REQUEST_ID_HOLDER.remove(); } }在web.xml中注册Filterfilter filter-nameRequestIdFilter/filter-name filter-classfilter.RequestIdFilter/filter-class /filter filter-mapping filter-nameRequestIdFilter/filter-name url-pattern/*/url-pattern /filter-mapping效果所有日志自动带上[req-id:abc123...]前缀。当运维反馈“西二旗B-12播了旧素材”你只需搜req-id:abc123...就能串起从Servlet入参、Service校验逻辑、DAO执行SQL、MySQL Binlog变更的完整链路——这是系统可信度的基石。6.2 在DAO层埋点用System.nanoTime()量化SQL性能瓶颈不要等用户投诉“列表加载慢”主动监控每条SQL耗时。在AdScheduleDao.findUpcomingSchedules()方法开头加long start System.nanoTime(); try { // 原有查询逻辑... } finally { long costMs (System.nanoTime() - start) / 1_000_000; if (costMs 500) { // 超500ms记WARN System.err.println([SLOW-SQL] findUpcomingSchedules for board boardId took costMs ms); } }提示生产环境应替换为SLF4J的logger.warn()并接入ELK做聚合分析。但初期用System.err足够暴露问题——我们曾靠此发现ad_schedule表缺少(board_id, start_time)联合索引导致某次全表扫描耗时3.2秒。6.3 给MySQL加“后悔药”用触发器自动备份关键表变更ad_schedule表一旦误删广告主合同就作废。与其依赖每天一次的mysqldump不如用触发器实时备份-- 创建备份表 CREATE TABLE ad_schedule_bak LIKE ad_schedule; ALTER TABLE ad_schedule_bak ADD COLUMN bak_time DATETIME DEFAULT CURRENT_TIMESTAMP; ALTER TABLE ad_schedule_bak ADD COLUMN bak_type ENUM(INSERT,UPDATE,DELETE) NOT NULL; -- 创建DELETE触发器 DELIMITER $$ CREATE TRIGGER tr_ad_schedule_delete_bak AFTER DELETE ON ad_schedule FOR EACH ROW BEGIN INSERT INTO ad_schedule_bak SELECT *, NOW(), DELETE FROM ad_schedule WHERE id OLD.id; END$$ DELIMITER ;注意触发器备份仅针对DELETE因为INSERT/UPDATE本身已是最新态。备份表不加主键避免冲突bak_time字段便于按时间回滚。这是公交广告行业甲方验收时最看重的“数据安全红线”。我带过的三个项目里有两个在上线前三天靠RequestIdFilter揪出隐藏的Session共享Bug一个靠ad_schedule_bak在误操作后5分钟内恢复全部排期。技术没有银弹但把这三招刻进肌肉记忆你就已经站在了交付靠谱系统的起跑线上。希望帮到你。本文还有配套的精品资源点击获取
返回列表