
简介本资源是一份面向计算机专业本科生与Java初学者的课程设计/毕业设计参考文档聚焦公交站牌广告灯箱这一城市公共设施的信息化管理痛点提供完整的设计思路与技术实现方案。文档采用B/S架构与MVC三层模式涵盖系统用户管理、网站资讯发布、广告任务全流程申请-审核-合同、留言反馈及安全登录等五大核心模块并详细说明了Java后端开发、MySQL数据库建模、Eclipse开发环境配置等关键技术要点。资源为单个1.13MB的Word文档.docx内容结构完整含中英文摘要、绪论、主流技术选型分析JSP、MySQL、系统功能设计与实现说明、目录及参考文献可直接用于课程报告撰写或项目复现参考。目前已有41人学习下载适合需要理解Web系统设计逻辑、掌握MVC落地实践及获取规范文档模板的学习者。1. 公交站牌广告灯箱管理系统为什么一个“不起眼”的Java Web项目成了城运系统里最常被低估的稳定性压舱石你可能在面试时被问过“用Java写个增删改查”也可能在课程设计里做过“图书管理系统”——但真正跑在城市毛细血管里的Java系统往往不是电商后台或金融中台而是像公交站牌广告灯箱管理系统这样没有高并发不拼TPS却天天扛着户外断电重启、4G模块掉线、LED屏花屏、广告主临时换图、城管部门突击检查台账的多重压力。它不是炫技的Spring Cloud微服务而是一套基于MVC分层、MySQL持久化、Eclipse开发、Tomcat部署的“稳字诀”工程前端要适配老旧触摸屏扫码支付后端要对接LED控制器协议如RS485 Modbus-RTU数据库得存下十年广告排期与电费结算单。这不是玩具项目是真实交付中甲方验收时必看“设备在线率报表”和“广告投放日志导出功能”的硬需求。如果你正面临Java课程设计、中小软件公司外包交付、或智慧交通类国企内部系统升级这个系统就是你把MVC理论焊进肌肉记忆、把MySQL事务踩进业务逻辑、把Eclipse调试技巧练成条件反射的黄金靶场——它小但五脏俱全它旧但边界清晰它不刷存在感却天天在真实世界里跑着。2. 从零搭起MVC骨架用Eclipse Tomcat MySQL构建可调试的最小闭环2.1 为什么选原生Servlet/JSP而非Spring Boot——给真实交付留出“可控性余量”很多新手看到“Java Web”第一反应是Spring Boot但在公交站牌这类项目里我坚持用原生Servlet JSP JDBC构建MVC三层。原因很现实部署环境受限大量老城区公交站亭只配了Java 8 Tomcat 7Spring Boot 2.7要求Java 11强行升级会导致LED控制器驱动兼容性崩盘运维可追溯性当某块灯箱屏突然不刷新广告时运维人员需要直接SSH进服务器tail -f catalina.out看到的是“AdControllerServlet: deviceIDSH-0234 timeout”而不是一堆Spring AOP代理栈学习穿透力更强手写web.xml配置servlet-mapping、手动管理Connection生命周期、在JSP里用c:forEach遍历广告列表——这些“笨功夫”逼你理解HTTP请求如何穿过容器、Session如何绑定用户、事务隔离级别怎么影响排期冲突。提示本方案使用Eclipse IDE for Enterprise Java Developers2021-09版配套Tomcat 7.0.109非最新版因需兼容JDK 1.8、MySQL 5.7.36。版本锁死不是守旧而是为避免java.lang.ClassNotFoundException: javax.servlet.http.HttpServletRequest这类血泪翻车。2.2 Eclipse工程结构拒绝“src下堆满.java”的野路子在Eclipse新建Dynamic Web Project后必须按以下结构组织右键项目 → Properties → Project Facets → 勾选Dynamic Web Module 3.0BusAdSystem/ ├── src/ # Java源码 │ ├── controller/ # Servlet入口如AdScheduleServlet.java │ ├── model/ # POJO DAO如AdPlan.java, AdPlanDAO.java │ └── util/ # 工具类DBUtil.java, DateUtil.java ├── WebContent/ # Web根目录等价于WebApp │ ├── WEB-INF/ │ │ ├── web.xml # 核心配置servlet映射、filter、context-param │ │ └── lib/ # 手动放入mysql-connector-java-5.1.47.jar │ ├── index.jsp # 登录页含验证码生成逻辑 │ ├── admin/ # 后台页面ad_list.jsp, device_manage.jsp │ └── public/ # 静态资源css/, js/, images/ └── build/ # Eclipse自动生成勿手动修改关键点web.xml中必须声明welcome-file-list指向index.jsp否则访问http://localhost:8080/BusAdSystem/会404lib/下jar包必须右键 → Build Path → Add to Build Path否则编译通过但运行时报NoClassDefFoundErrorJSP中禁止写Java脚本% %所有业务逻辑必须在Servlet中处理JSP只负责展示——这是MVC的底线不是教条。2.3 MySQL建库建表用“业务字段思维”代替“技术字段思维”别一上来就建user、admin、role三张表。公交广告系统的核心实体是广告计划AdPlan、灯箱设备Device、广告素材AdMaterial。建表语句必须带业务注释-- 创建数据库字符集强制utf8mb4避免广告标题含emoji时报错 CREATE DATABASE bus_ad_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bus_ad_system; -- 灯箱设备表记录每块屏的物理属性与通信状态 CREATE TABLE device ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, device_code VARCHAR(20) NOT NULL UNIQUE COMMENT 设备唯一编码如SH-0234, location VARCHAR(100) NOT NULL COMMENT 安装位置XX路与YY路交叉口, status TINYINT DEFAULT 1 COMMENT 设备状态1-在线0-离线2-故障, last_online_time DATETIME COMMENT 最后心跳时间, power_consumption DECIMAL(8,2) DEFAULT 0.00 COMMENT 本月累计耗电量kWh, created_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT灯箱设备主表; -- 广告计划表一条计划一块屏一个时段一张图 CREATE TABLE ad_plan ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id BIGINT NOT NULL COMMENT 关联device.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 0 COMMENT 同屏多计划时的播放顺序0-9, status ENUM(pending,running,finished,canceled) DEFAULT pending COMMENT 计划状态, created_by VARCHAR(20) NOT NULL COMMENT 创建人账号, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (device_id) REFERENCES device(id) ON DELETE CASCADE, FOREIGN KEY (material_id) REFERENCES ad_material(id) ON DELETE RESTRICT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意ON DELETE CASCADE用于设备报废时自动清理其历史计划但ON DELETE RESTRICT防止误删广告素材导致计划失效status用ENUM而非INT避免代码里出现if(status 1)这种玄学判断直接if(running.equals(plan.getStatus()))power_consumption字段看似多余但实际验收时城管部门要求提供“单屏月度能耗报表”提前埋点省去后期加字段的联调成本。3. MVC分层落地让Controller管路由、Model管数据、View管呈现3.1 Controller层Servlet不只是跳转更是业务门面以“查询某设备所有广告计划”为例AdPlanServlet.java不能只做request.getRequestDispatcher().forward()必须承担三件事参数校验deviceCode不能为空且符合SH-\\d{4}格式权限拦截普通广告员只能查自己创建的计划管理员可查全部异常兜底数据库连接失败时返回友好提示而非500白页。// src/controller/AdPlanServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String deviceCode request.getParameter(deviceCode); // 1. 参数校验 if (StringUtils.isBlank(deviceCode) || !deviceCode.matches(SH-\\d{4})) { request.setAttribute(errorMsg, 设备编码格式错误例SH-0234); request.getRequestDispatcher(/admin/error.jsp).forward(request, response); return; } // 2. 权限控制简化版从session取角色 String role (String) request.getSession().getAttribute(userRole); ListAdPlan planList; try { if (admin.equals(role)) { planList new AdPlanDAO().findAllByDeviceCode(deviceCode); } else { String username (String) request.getSession().getAttribute(username); planList new AdPlanDAO().findByDeviceCodeAndCreator(deviceCode, username); } } catch (SQLException e) { // 3. 数据库异常统一处理 log.error(查询设备[{}]广告计划失败, deviceCode, e); request.setAttribute(errorMsg, 系统繁忙请稍后再试); request.getRequestDispatcher(/admin/error.jsp).forward(request, response); return; } request.setAttribute(planList, planList); request.getRequestDispatcher(/admin/ad_list.jsp).forward(request, response); }逻辑说明StringUtils.isBlank()来自Apache Commons Lang比 null || .equals()更安全log.error()必须用SLF4Jslf4j-api-1.7.32.jarslf4j-simple-1.7.32.jar避免System.out.println()污染生产日志forward()前必须return否则后续代码仍会执行导致IllegalStateException: getWriter() has already been called。3.2 Model层DAO不是CRUD搬运工而是业务规则的守门人AdPlanDAO.java的insert()方法必须包含业务约束同一设备同一时段不能有重叠计划防广告漏播计划结束时间不能早于开始时间新计划不能与已存在的running状态计划冲突。// src/model/AdPlanDAO.java public boolean insert(AdPlan plan) throws SQLException { // 业务校验1时间合理性 if (plan.getStartTime().after(plan.getEndTime())) { throw new IllegalArgumentException(结束时间不能早于开始时间); } // 业务校验2时段冲突检测核心 String conflictSql SELECT COUNT(*) FROM ad_plan WHERE device_id ? AND status running AND ((? BETWEEN start_time AND end_time) OR (? BETWEEN start_time AND end_time) OR (start_time BETWEEN ? AND ?) OR (end_time BETWEEN ? AND ?)); int conflictCount queryForInt(conflictSql, plan.getDeviceId(), plan.getStartTime(), plan.getEndTime(), plan.getStartTime(), plan.getEndTime(), plan.getStartTime(), plan.getEndTime()); if (conflictCount 0) { throw new BusinessException(该时段已被其他广告占用请调整播放时间); } // 执行插入 String sql INSERT INTO ad_plan (device_id, material_id, start_time, end_time, play_order, status, created_by) VALUES (?, ?, ?, ?, ?, ?, ?); return update(sql, plan.getDeviceId(), plan.getMaterialId(), plan.getStartTime(), plan.getEndTime(), plan.getPlayOrder(), plan.getStatus(), plan.getCreatedBy()) 0; }参数说明queryForInt()是自封装的JDBC工具方法避免重复写PreparedStatement冲突检测SQL用四重BETWEEN覆盖所有时间重叠场景A在B内、B在A内、A开头在B内、A结尾在B内BusinessException是自定义异常上层Servlet捕获后转为用户可见提示而非暴露SQL细节。3.3 View层JSP不是HTML而是MVC的最终契约ad_list.jsp必须严格遵循“只读取request属性不操作数据库”原则。用JSTL替代Scriptlet% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % !DOCTYPE html html headtitle广告计划列表/title/head body h2设备[${param.deviceCode}]广告计划/h2 c:if test${not empty errorMsg} div stylecolor:red${errorMsg}/div /c:if table border1 tr th序号/th th素材名称/th th播放时段/th th状态/th th操作/th /tr c:forEach items${planList} varplan varStatusstatus tr td${status.count}/td td${plan.materialName}/td td fmt:formatDate value${plan.startTime} patternyyyy-MM-dd HH:mm/ ~ fmt:formatDate value${plan.endTime} patternyyyy-MM-dd HH:mm/ /td td c:choose c:when test${plan.status running}span stylecolor:green播放中/span/c:when c:when test${plan.status pending}待生效/c:when c:otherwise${plan.status}/c:otherwise /c:choose /td td a hrefAdPlanServlet?actiondeleteid${plan.id}删除/a /td /tr /c:forEach /table /body /html关键点fmt:formatDate确保时间显示格式统一避免new SimpleDateFormat().format()在多线程下的NonThreadSafe问题${plan.materialName}来自AdPlan对象的getter要求DAO查询时已关联查询ad_material.name用LEFT JOIN实现删除链接带actiondelete由同一Servlet的doPost()处理体现RESTful思想雏形。4. 避坑指南那些让项目卡在验收前夜的“幽灵问题”4.1 现象Eclipse启动Tomcat后浏览器访问http://localhost:8080/BusAdSystem/显示404原因Eclipse未将项目发布到Tomcat或web.xml中welcome-file-list未正确配置。常见误操作是右键项目 → Run As → Run on Server但未勾选“Add project to the deployment assembly”。解决右键项目 → Properties → Deployment Assembly → 点击Add → Archive File → 选择WebContent/目录 → Finish再确认web.xml中有welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list4.2 现象登录成功后跳转到admin/index.jsp但页面报错javax.servlet.ServletException: java.lang.NoClassDefFoundError: org/apache/commons/dbcp/BasicDataSource原因DBUtil.java中使用了Apache Commons DBCP连接池但commons-dbcp-1.4.jar未放入WebContent/WEB-INF/lib/或放入后未Add to Build Path。解决下载commons-dbcp-1.4.jar和依赖commons-pool-1.6.jar复制到lib/目录 → 右键jar → Build Path → Add to Build Path检查DBUtil中是否漏写import org.apache.commons.dbcp.BasicDataSource;。4.3 现象广告计划插入时MySQL报错Data truncation: Incorrect datetime value: 0000-00-00 00:00:00原因MySQL 5.7默认开启STRICT_TRANS_TABLES模式禁止插入零日期。而某些老旧JDBC驱动如mysql-connector-java-5.1.23在ResultSet.getDate()遇到NULL时返回0000-00-00。解决在JDBC URL后追加参数# db.properties jdbc.urljdbc:mysql://localhost:3306/bus_ad_system?useSSLfalseserverTimezoneAsia/ShanghaizeroDateTimeBehaviorconvertToNullzeroDateTimeBehaviorconvertToNull将零日期转为NULLJava端用rs.getTimestamp()接收即可。4.4 现象LED控制器发送心跳包Java后台收到但last_online_time字段始终为NULL原因DeviceServlet中解析JSON时前端用{deviceCode:SH-0234,timestamp:2023-10-01T12:00:00}而后端用SimpleDateFormat(yyyy-MM-dd HH:mm:ss)解析ISO格式失败。解决改用Instant.parse()Java 8String timestampStr json.getString(timestamp); // 2023-10-01T12:00:00 Instant instant Instant.parse(timestampStr); // 自动识别ISO格式 device.setLastOnlineTime(Timestamp.from(instant));4.5 现象广告素材上传后图片路径存入数据库为/images/20231001120000.jpg但JSP中img src${plan.imageUrl}显示404原因/images/是相对路径而Tomcat默认不提供静态资源服务需在web.xml中配置默认Servletservlet-mapping servlet-namedefault/servlet-name url-pattern/images/*/url-pattern /servlet-mapping或更推荐将图片存入WebContent/images/并在上传时用getServletContext().getRealPath(/images/)获取绝对路径保存文件。5. 设备通信层集成用Java Socket打通灯箱控制器的“最后一公里”5.1 为什么不用HTTP API——户外设备的通信现实你以为灯箱屏会提供RESTful接口现实是90%的国产LED控制器只支持串口透传TCP长连接。它们内置ESP32模组通过4G向中心服务器发送心跳每30秒一次并监听TCP端口接收指令如CMD:UPDATE_IMAGE|SH-0234|http://cdn.example.com/ads/20231001.jpg。这意味着你的Java系统必须开启一个TCP ServerSocket监听端口如9001维护设备连接池MapString, Socket用设备编码作key实现心跳保活收到心跳包立即回ACK超时3次标记设备离线将Web端下发的广告计划转换为控制器能识别的ASCII指令。5.2 TCP服务端骨架轻量级、可监控、不阻塞主线程// src/util/DeviceTcpServer.java作为ServletContextListener启动 public class DeviceTcpServer implements ServletContextListener { private static final Logger log LoggerFactory.getLogger(DeviceTcpServer.class); private ServerSocket serverSocket; private final MapString, Socket deviceSockets new ConcurrentHashMap(); Override public void contextInitialized(ServletContextEvent sce) { try { serverSocket new ServerSocket(9001); log.info(设备TCP服务启动监听端口9001); // 启动独立线程处理连接避免阻塞Web容器 new Thread(() - acceptConnections()).start(); } catch (IOException e) { log.error(TCP服务启动失败, e); } } private void acceptConnections() { while (!Thread.currentThread().isInterrupted()) { try { Socket socket serverSocket.accept(); // 为每个设备分配独立线程处理 new Thread(new DeviceHandler(socket)).start(); } catch (IOException e) { if (!serverSocket.isClosed()) { log.error(接受连接异常, e); } } } } // 外部调用向指定设备下发指令 public void sendCommand(String deviceCode, String command) { Socket socket deviceSockets.get(deviceCode); if (socket ! null socket.isConnected() !socket.isClosed()) { try (PrintWriter out new PrintWriter(socket.getOutputStream(), true)) { out.println(command); // 指令以\n结尾 } catch (IOException e) { log.warn(向设备[{}]发送指令失败: {}, deviceCode, command, e); deviceSockets.remove(deviceCode); // 清理失效连接 } } } }逻辑说明ConcurrentHashMap保证多线程安全避免HashMap在高并发下死循环PrintWriter自动flush无需手动调用out.flush()sendCommand()方法供AdPlanServlet调用当计划状态变更为running时触发sendCommand(deviceCode, CMD:PLAY| materialUrl)。5.3 设备心跳与状态同步让数据库实时反映物理世界DeviceHandler线程需完成三件事解析设备首次连接时发送的注册包含deviceCode、firmwareVersion每30秒读取一次心跳包更新device.last_online_time断连时更新device.status0。// DeviceHandler.java private class DeviceHandler implements Runnable { private final Socket socket; private final BufferedReader in; private String deviceCode; public DeviceHandler(Socket socket) throws IOException { this.socket socket; this.in new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); } Override public void run() { try { // 步骤1等待注册包格式REG|SH-0234|v2.1.0 String regLine in.readLine(); if (regLine ! null regLine.startsWith(REG|)) { String[] parts regLine.split(\\|); if (parts.length 2) { deviceCode parts[1]; deviceSockets.put(deviceCode, socket); log.info(设备[{}]注册成功, deviceCode); updateDeviceStatus(deviceCode, 1); // 更新数据库状态为在线 } } // 步骤2持续监听心跳 while (socket.isConnected() !socket.isClosed()) { String heartbeat in.readLine(); if (heartbeat ! null heartbeat.startsWith(HEARTBEAT|)) { updateDeviceStatus(deviceCode, 1); } else if (heartbeat null) { break; // 连接关闭 } } } catch (IOException e) { log.warn(设备[{}]连接异常: {}, deviceCode, e.getMessage()); } finally { if (deviceCode ! null) { deviceSockets.remove(deviceCode); updateDeviceStatus(deviceCode, 0); // 标记离线 } closeQuietly(socket, in); } } private void updateDeviceStatus(String code, int status) { try { String sql UPDATE device SET status ?, last_online_time NOW() WHERE device_code ?; DBUtil.update(sql, status, code); } catch (SQLException e) { log.error(更新设备[{}]状态失败, code, e); } } }参数说明closeQuietly()是自封装方法捕获IOException不抛出避免finally块污染日志updateDeviceStatus()直接执行SQL不走DAO层因为这是基础设施层操作与业务无关心跳包解析用startsWith(HEARTBEAT|)而非正则减少GC压力——户外设备CPU弱能省则省。6. 验收级技巧用“三张表一个脚本”搞定甲方最关心的审计报表甲方验收时不会问你用了什么设计模式但一定会盯着三份报表广告投放合规性报表证明每块屏的广告内容、时长、频次符合《户外广告管理条例》设备在线率日报要求“月度在线率≥99.5%”低于此值扣减运维费电费结算单按每块屏实际耗电量kWh与广告主分摊费用。6.1 用MySQL视图固化报表逻辑杜绝手工Excel造假在MySQL中创建三个视图让报表查询变成一句SELECT * FROM v_ad_compliance_report-- 广告合规性视图统计每块屏每日播放总时长、素材数量、是否超时 CREATE VIEW v_ad_compliance_report AS SELECT d.device_code, d.location, DATE(ap.start_time) as report_date, COUNT(*) as ad_count, SEC_TO_TIME(SUM(TIMESTAMPDIFF(SECOND, ap.start_time, ap.end_time))) as total_play_time, CASE WHEN SUM(TIMESTAMPDIFF(SECOND, ap.start_time, ap.end_time)) 86400 THEN 超时 ELSE 正常 END as status_flag FROM ad_plan ap JOIN device d ON ap.device_id d.id WHERE ap.status finished AND ap.start_time DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY d.device_code, d.location, DATE(ap.start_time); -- 设备在线率视图按天计算每块屏在线时长占比 CREATE VIEW v_device_uptime_daily AS SELECT device_code, DATE(last_online_time) as report_date, ROUND( (COUNT(*) * 30) / (24 * 60 * 2), -- 假设心跳30秒一次全天应有2880次 4 ) as uptime_rate FROM device WHERE last_online_time DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY device_code, DATE(last_online_time);注意SEC_TO_TIME()将秒数转为HH:MM:SS格式比在Java层转换更高效ROUND(..., 4)保留4位小数满足财务精度要求。6.2 用Java定时任务生成PDF报表替代人工导出利用iText 7生成带Logo、页眉页脚、表格边框的PDF每天凌晨2点自动生成昨日报表// src/util/DailyReportGenerator.java public class DailyReportGenerator { public static void generateYesterdayReport() throws Exception { LocalDate yesterday LocalDate.now().minusDays(1); String filename ad_report_ yesterday .pdf; PdfWriter writer new PdfWriter(WebContent/reports/ filename); PdfDocument pdf new PdfDocument(writer); Document document new Document(pdf); // 添加页眉 document.add(new Paragraph(公交站牌广告管理系统 - 日报) .setTextAlignment(TextAlignment.CENTER) .setFontSize(16) .setMarginBottom(20)); // 查询合规性报表数据 ListMapString, Object data queryComplianceReport(yesterday); // 构建表格列设备编码、位置、广告数、播放时长、状态 Table table new Table(UnitValue.createPercentArray(new float[]{2, 3, 1, 2, 1})) .setWidthPercent(100).setMarginTop(10); table.addHeaderCell(设备编码); table.addHeaderCell(安装位置); table.addHeaderCell(广告数); table.addHeaderCell(播放时长); table.addHeaderCell(状态); for (MapString, Object row : data) { table.addCell(new Cell().add(new Paragraph((String) row.get(device_code)))); table.addCell(new Cell().add(new Paragraph((String) row.get(location)))); table.addCell(new Cell().add(new Paragraph(row.get(ad_count).toString()))); table.addCell(new Cell().add(new Paragraph((String) row.get(total_play_time)))); table.addCell(new Cell().add(new Paragraph((String) row.get(status_flag)))); } document.add(table); document.close(); log.info(昨日报表生成完成: {}, filename); } }关键点PdfWriter路径指向WebContent/reports/确保浏览器可通过http://localhost:8080/BusAdSystem/reports/ad_report_2023-10-01.pdf直接下载表格列宽用百分比数组{2,3,1,2,1}避免文字撑破页面queryComplianceReport()方法复用DAO层SQL保证数据一致性。6.3 最后一道防线用MySQL事件调度器自动归档历史数据避免ad_plan表膨胀到百万级导致查询变慢创建每月1号自动归档上月数据的事件-- 开启事件调度器 SET GLOBAL event_scheduler ON; -- 创建归档存储过程 DELIMITER $$ CREATE PROCEDURE archive_ad_plan() BEGIN DECLARE last_month_start DATE DEFAULT DATE_SUB(LAST_DAY(NOW()), INTERVAL DAY(LAST_DAY(NOW())) DAY); DECLARE last_month_end DATE DEFAULT LAST_DAY(DATE_SUB(NOW(), INTERVAL 1 MONTH)); INSERT INTO ad_plan_archive SELECT *, NOW() FROM ad_plan WHERE start_time last_month_start AND start_time last_month_end; DELETE FROM ad_plan WHERE start_time last_month_start AND start_time last_month_end; END$$ DELIMITER ; -- 创建每月执行事件 CREATE EVENT monthly_ad_plan_archive ON SCHEDULE EVERY 1 MONTH DO CALL archive_ad_plan();我的血泪经验是不要等甲方提“系统变慢”才想起归档。我在第三个交付项目里因未设归档ad_plan表达200万行SELECT COUNT(*)要8秒被甲方指着说“这叫稳定系统”。现在所有新项目建库第一天就敲完这三行命令——它不炫酷但能让验收汇报PPT里那张“系统响应时间200ms”的图表永远真实。希望帮到你。本文还有配套的精品资源点击获取