简介:本资源是一份面向Java初学者与课程设计学生的公交站牌广告灯箱管理系统毕业设计文档,聚焦城市公共广告资源信息化管理痛点,提供从需求分析到技术实现的完整方案。文档采用标准学术论文结构,含摘要、英文摘要、目录及五章正文,详细阐述B/S架构下基于MVC模式的系统设计逻辑,涵盖用户管理、资讯发布、广告任务调度、留言反馈等核心模块,并说明Eclipse开发环境、MySQL数据库选型及JSP技术应用依据。资源为单个1.13MB的Word文档(.docx),内容完整覆盖系统功能设计、技术选型论证与实现效果分析,适合作为Java Web课程设计参考、毕业论文写作范本或B/S架构实践学习材料。目前已有41人下载学习,文档中包含可直接复用的模块划分思路、数据库表设计要点及MVC分层实现说明,有助于快速理解企业级Web系统开发全流程。
1. 这不是又一个“学生毕设Demo”:一个能真正在区级广告运营公司跑通的Java公交站牌灯箱管理系统
你可能已经点开过不下五个标着“公交站牌管理系统”的GitHub仓库,点进去一看:首页是黑底白字的Welcome to BusAdSystem,后台登录页连验证码都没有,数据库脚本里user表只有id、username、password三列——这种系统,连测试环境都算不上,更别说部署到真实广告运营现场。但这份《基于Java的公交站牌广告灯箱管理系统设计与实现.docx》不一样。它不是纸上谈兵的课程设计说明书,而是一份完整覆盖“广告位申请→合同录入→灯箱排期→客户留言→数据导出”全业务链路的落地文档,且所有模块均基于真实管理痛点设计:比如“任务信息管理”模块不只存个标题和日期,而是明确区分了「广告主类型(本地商户/连锁品牌/政府宣传)」「投放周期(单日/周频/季度包段)」「灯箱物理位置编码(如ZB-087-A3)」;“网站资讯管理”支持富文本发布政策通知,并自动关联到对应区域的灯箱点位地图;“留言管理”字段包含「来电号码、是否需回电、紧急程度(普通/加急/投诉)」三级分类。它用最朴素的JSP+Servlet+MySQL组合,在没上Spring Boot、没接Redis、没搞微服务的前提下,靠扎实的MVC分层和严谨的SQL事务控制,把一套年管理200+灯箱点位、月处理80+广告订单的轻量级运营系统稳稳托住了。如果你正被甲方催着交一个“能马上填进Excel台账、能导出PDF合同、能查到昨天谁在哪个站台贴了小广告”的系统,而不是“能跑通HelloWorld”的教学案例——这份文档就是你该立刻下载、拆解、复现的起点。
2. 为什么选JSP+Servlet+MySQL这套“老组合”?不是技术落后,而是对现场运维的尊重
2.1 JSP不是过时的代名词:它是公交广告公司IT岗的真实技术水位线
很多人看到文档里写“采用JSP语言”,第一反应是皱眉:“都2024年了还用JSP?”。但现实是:全国90%以上的区县级公交广告运营公司,其IT支持人员要么是刚毕业的计算机专科生,要么是兼任网管的行政文员。他们没有K8s集群运维经验,但能熟练操作Navicat连接MySQL、用Eclipse调试JSP页面、在Tomcat日志里grep报错关键词。JSP的核心价值在于零客户端依赖——客户经理用一台借来的Windows笔记本,打开IE浏览器输入http://192.168.1.100:8080/admanager,就能完成广告位预约;财务人员在老旧的XP系统上,不用装任何插件就能导出带公章的PDF结算单。这背后是JSP的硬核能力:HTML混编逻辑的极致简化。比如用户注册页的邮箱校验,不是调用某个前端UI库的复杂validator,而是直接在JSP里嵌入:
<% String email = request.getParameter("email"); boolean isValidEmail = email != null && email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); if (!isValidEmail) { out.println("<span style='color:red'>邮箱格式错误,请重新输入</span>"); } %>这段代码没有引入任何框架,不依赖Node.js环境,甚至不需要Webpack打包——它就安静地躺在.jsp文件里,Tomcat启动时自动编译成Servlet。对一线运维来说,这意味着:当客户说“昨天还能注册,今天点提交就白屏”,你只需要SSH登录服务器,tail -f /usr/local/tomcat/logs/catalina.out,5秒内定位到是NullPointerException还是SQL语法错误,改完.jsp文件touch一下,Tomcat热加载即生效。这种“所见即所得”的调试路径,是React/Vue项目在生产环境里永远无法提供的确定性。
2.2 MySQL 5.7是公交广告系统的“黄金版本”:稳定压倒一切
文档中反复强调使用MySQL而非Oracle或PostgreSQL,这不是技术保守,而是成本与风险的精密计算。公交广告公司的数据库,核心诉求只有三个:数据不丢、查询够快、备份简单。MySQL 5.7完美匹配:
- 数据不丢:
innodb_flush_log_at_trx_commit=1+sync_binlog=1双保险,确保每笔广告合同录入后断电也不丢数据; - 查询够快:广告位查询高频走
location_code索引,SELECT * FROM ad_task WHERE location_code LIKE 'ZB-087%' AND start_date >= '2024-06-01',在20万条记录下毫秒级响应; - 备份简单:
mysqldump --single-transaction --routines --triggers ad_system > backup_$(date +%Y%m%d).sql一条命令搞定全库热备,恢复时mysql ad_system < backup_20240601.sql即可,无需DBA值守。
提示:文档里提到的“MySQLServer 2008”实为笔误,应为MySQL 5.7(2015年GA版)。当前主流Linux发行版(CentOS 7/Ubuntu 20.04)默认源均提供5.7.x,安装命令
sudo apt install mysql-server-5.7(Ubuntu)或sudo yum install mysql-community-server-5.7(CentOS),比折腾MySQL 8.0的SSL配置或字符集兼容问题省心十倍。
2.3 Eclipse不是IDE选择,而是团队协作的契约
为什么坚持用Eclipse而非IntelliJ IDEA?文档没明说,但第5章“系统实现”里的细节暴露了真相:所有JSP文件都严格遵循/WebContent/WEB-INF/jsp/xxx.jsp路径规范,所有Servlet类都放在src/com/bussystem/servlet/包下,且每个Servlet都显式声明@WebServlet("/admin/taskAdd")。这种结构化,是Eclipse的WTP(Web Tools Platform)插件强制约束的结果。当你在Eclipse里右键New → Servlet,它自动生成的代码骨架就包含:
@WebServlet("/admin/taskAdd") public class TaskAddServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub String locationCode = request.getParameter("locationCode"); String advertiser = request.getParameter("advertiser"); // ... 业务逻辑 request.getRequestDispatcher("/success.jsp").forward(request, response); } }这个看似“笨拙”的模板,实则是团队协作的隐形护栏:新来的实习生只要按F3跳转到TaskAddServlet,就能瞬间理解请求路径、参数名、跳转逻辑;运维同事看web.xml里<servlet-mapping>配置,就能确认URL路由是否被Nginx反向代理正确转发。而IntelliJ的自由度太高,容易导致不同开发者写出/api/v1/task/add、/task/create、/addTask三种风格的路径,最终让前端联调变成噩梦。Eclipse的“不自由”,恰恰是公交广告这类小团队项目的最大自由。
3. MVC三层不是教科书概念,而是把“广告合同录入”这个动作拆解成可审计的原子操作
3.1 Model层:用POJO+DAO封装业务语义,拒绝裸SQL污染业务逻辑
文档第4.3节“数据库设计”给出的ad_task表结构,是理解Model层设计的关键:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT PK | 主键 |
| location_code | VARCHAR(20) | 灯箱物理编码(ZB-087-A3) |
| advertiser_name | VARCHAR(100) | 广告主全称 |
| contract_no | VARCHAR(50) | 合同编号(AD202406001) |
| start_date | DATE | 投放起始日 |
| end_date | DATE | 投放结束日 |
| status | TINYINT | 状态(0-待审核,1-已签约,2-已排期,3-已下刊) |
Model层不是简单地把字段映射成Java Bean。以AdTask.java为例,它的设计直指业务痛点:
public class AdTask { private Long id; private String locationCode; // 物理编码,非ID!这是业务关键标识 private String advertiserName; private String contractNo; private Date startDate; private Date endDate; private Integer status; // 业务方法:计算投放天数(排除节假日) public int getActualDays() { // 调用工具类HolidayUtil.getWorkdays(startDate, endDate) return HolidayUtil.getWorkdays(startDate, endDate); } // 业务方法:生成合同PDF的文件名 public String getContractFileName() { return "CONTRACT_" + contractNo + "_" + locationCode + ".pdf"; } }DAO层则用纯JDBC封装CRUD,避免Hibernate的“黑匣子”式SQL生成:
public class AdTaskDao { public List<AdTask> findTasksByLocation(String locationCode) throws SQLException { String sql = "SELECT * FROM ad_task WHERE location_code = ? AND status IN (1,2)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, locationCode); ResultSet rs = ps.executeQuery(); return parseResultSet(rs); // 将ResultSet转为AdTask列表 } } // 关键:所有更新操作都显式开启事务 public void updateStatus(Long taskId, Integer newStatus) throws SQLException { String sql = "UPDATE ad_task SET status = ?, update_time = NOW() WHERE id = ?"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); // 手动控制事务 try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, newStatus); ps.setLong(2, taskId); ps.executeUpdate(); } conn.commit(); // 成功则提交 } catch (SQLException e) { conn.rollback(); // 失败则回滚 throw e; } } }注意:
DBUtil.getConnection()必须从连接池(如HikariCP)获取,文档虽未提,但这是性能底线。在context.xml中配置:<Resource name="jdbc/AdSystemDB" auth="Container" type="javax.sql.DataSource" maxTotal="20" minIdle="5" initialSize="5" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/ad_system?useSSL=false&serverTimezone=Asia/Shanghai" username="aduser" password="adpass"/>
3.2 View层:JSP不是写HTML,而是构建可复用的业务组件
View层常被误解为“美工活”,但在本系统中,它是业务规则的可视化出口。以“任务信息列表页”(/admin/taskList.jsp)为例,它不是简单循环输出表格,而是内嵌业务判断:
<%@ page import="java.util.List" %> <%@ page import="com.bussystem.model.AdTask" %> <%@ page import="com.bussystem.dao.AdTaskDao" %> <% List<AdTask> taskList = new AdTaskDao().findAllTasks(); %> <table class="table"> <thead> <tr> <th>灯箱位置</th> <th>广告主</th> <th>合同号</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <% for (AdTask task : taskList) { %> <tr class="<%= task.getStatus() == 2 ? 'bg-success' : '' %>"> <td><%= task.getLocationCode() %></td> <td><%= task.getAdvertiserName() %></td> <td><%= task.getContractNo() %></td> <td> <% switch(task.getStatus()) { case 0: out.print("待审核"); break; case 1: out.print("已签约"); break; case 2: out.print("<strong>已排期</strong>"); break; case 3: out.print("已下刊"); break; } %> </td> <td> <!-- 状态机驱动的操作按钮 --> <% if (task.getStatus() == 0) { %> <a href="taskApprove.jsp?id=<%= task.getId() %>" class="btn btn-sm btn-primary">审核</a> <% } else if (task.getStatus() == 1) { %> <a href="taskSchedule.jsp?id=<%= task.getId() %>" class="btn btn-sm btn-info">排期</a> <% } else if (task.getStatus() == 2) { %> <a href="taskDownload.jsp?id=<%= task.getId() %>" class="btn btn-sm btn-warning">导出PDF</a> <% } %> </td> </tr> <% } %> </tbody> </table>这段JSP的价值在于:将业务状态机(待审核→已签约→已排期→已下刊)完全外化到视图层。前端无需理解RESTful API的状态码,只需根据<td>里的文字决定显示哪个按钮;后端Servlet也无需返回冗余的isApprovable布尔值,View层自己通过task.getStatus()做判断。这种“状态即视图”的设计,让系统行为变得极其透明——当客户问“为什么这个合同不能导出PDF?”,你直接打开taskList.jsp,指着<% if (task.getStatus() == 2) { %>这行代码,他就懂了。
3.3 Controller层:Servlet是业务流程的守门人,不是HTTP路由器
Controller层在文档中体现为LoginServlet、TaskAddServlet等类,其核心职责不是“接收参数”,而是执行业务前置校验与后置审计。以TaskAddServlet的doPost方法为例:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 【前置校验】业务规则拦截 String locationCode = request.getParameter("locationCode"); if (!locationCode.matches("^ZB-\\d{3}-[A-Z]\\d$")) { // 强制物理编码格式 request.setAttribute("error", "灯箱编码格式错误,应为ZB-XXX-A#"); request.getRequestDispatcher("/admin/taskAdd.jsp").forward(request, response); return; } String startDateStr = request.getParameter("startDate"); String endDateStr = request.getParameter("endDate"); if (DateUtil.parseDate(startDateStr).after(DateUtil.parseDate(endDateStr))) { request.setAttribute("error", "开始日期不能晚于结束日期"); request.getRequestDispatcher("/admin/taskAdd.jsp").forward(request, response); return; } // 【业务执行】调用DAO保存 AdTask task = new AdTask(); task.setLocationCode(locationCode); task.setStartDate(DateUtil.parseDate(startDateStr)); task.setEndDate(DateUtil.parseDate(endDateStr)); task.setStatus(0); // 新建状态为待审核 new AdTaskDao().insertTask(task); // 【后置审计】记录操作日志(关键!) String operator = (String) request.getSession().getAttribute("username"); AuditLog.log("TASK_ADD", operator, "新增广告任务:" + locationCode, task.getId()); // 【结果导向】跳转到成功页,非JSON响应 request.setAttribute("message", "任务已提交,等待审核"); request.getRequestDispatcher("/admin/taskSuccess.jsp").forward(request, response); }这里没有@RequestBody注解,没有DTO转换,但有三个不可替代的业务锚点:
- 格式强校验:
locationCode必须符合ZB-XXX-A#模式,这是物理世界与数字系统的唯一映射,不容妥协; - 时间逻辑校验:
startDate必须早于endDate,这是广告投放的基本常识,由Controller在入口处掐死; - 操作留痕:
AuditLog.log()写入独立日志表,记录谁、何时、对哪个任务做了什么——当客户质疑“我的合同为什么没排期?”,运维可直接查audit_log表,精准定位到操作人和时间点。
这种“校验-执行-留痕”的三段式Controller,才是MVC在真实业务中的血肉。
4. 避坑:那些让公交广告公司IT岗深夜崩溃的5个真实陷阱
4.1 现象:登录成功后跳转到空白页,浏览器地址栏显示/admin/index.jsp,但页面无任何内容
原因:index.jsp中引用了<%@ include file="/header.jsp" %>,而/header.jsp里有一段<% session.getAttribute("username") %>,当用户未登录时session为空,getAttribute返回null,JSP引擎尝试将null转为字符串时抛出NullPointerException,但错误被Tomcat的error-page配置静默吞掉。
解决:在所有被include的公共JSP(如header.jsp、menu.jsp)顶部添加空值防护:
<% String username = (String) session.getAttribute("username"); if (username == null) { response.sendRedirect("/login.jsp"); return; } %>4.2 现象:管理员修改广告任务状态后,数据库status字段更新成功,但列表页仍显示旧状态
原因:AdTaskDao.updateStatus()方法中,conn.setAutoCommit(false)后未正确关闭PreparedStatement,导致事务未提交,连接池归还连接时自动回滚。
解决:严格使用try-with-resources,确保PreparedStatement和Connection在finally块中释放:
public void updateStatus(Long taskId, Integer newStatus) throws SQLException { String sql = "UPDATE ad_task SET status = ? WHERE id = ?"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, newStatus); ps.setLong(2, taskId); ps.executeUpdate(); } conn.commit(); // 显式提交 } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; } }4.3 现象:导出PDF合同失败,日志报错java.lang.NoClassDefFoundError: com/lowagie/text/Document
原因:iText 2.1.7(文档中隐含使用的版本)的jar包未放入WEB-INF/lib/目录,或放入后Tomcat未重启导致类加载器未识别。
解决:
- 下载
itext-2.1.7.jar(注意:不是iText 5/7,因文档中PdfWriter.getInstance()调用方式仅兼容2.x); - 将jar复制到
/WebContent/WEB-INF/lib/; - 在Eclipse中右键项目 →
Properties → Deployment Assembly,确认lib/目录已加入部署路径; - 强制重启Tomcat:
sudo systemctl restart tomcat,而非仅刷新应用。
4.4 现象:在MySQL中执行SELECT * FROM ad_task WHERE location_code = 'ZB-087-A3'很快,但JSP页面查询超时
原因:JDBC连接URL缺少useUnicode=true&characterEncoding=UTF-8参数,导致中文字段(如advertiser_name)在传输中乱码,MySQL被迫放弃索引走全表扫描。
解决:修改DBUtil.getConnection()中的URL:
private static final String URL = "jdbc:mysql://localhost:3306/ad_system?" + "useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false";4.5 现象:用户注册时输入密码123456,数据库user表中存储的是明文,而非加密字符串
原因:UserDao.insertUser()方法中,直接执行INSERT INTO user (password) VALUES (?),未调用密码加密逻辑。
解决:在UserServlet的doPost中,注册前必须加密:
String rawPassword = request.getParameter("password"); String encryptedPassword = DigestUtils.md5Hex(rawPassword + "AD_SALT_2024"); // 加盐MD5 user.setPassword(encryptedPassword); new UserDao().insertUser(user);并在login.jsp的登录验证中,用相同盐值加密用户输入再比对。
5. 把“导出PDF合同”做成可审计的闭环:从JSP页面到盖章文件的完整链路
5.1 PDF生成不是炫技,而是满足广告行业刚需的合规动作
公交广告行业的特殊性在于:每一份灯箱投放都对应一份具有法律效力的纸质合同。客户经理拿着平板电脑给商户签完电子协议,回到办公室必须立刻打印、盖章、扫描归档。系统里的“导出PDF”按钮,本质是打通“数字审批流”与“物理盖章流”的最后一公里。文档第5.4.2节提到“任务信息管理模块的实现”,但没展开PDF生成细节。这里补全真实可用的方案:用iText 2.1.7(兼容JDK 1.6+,无需额外依赖)生成带公司LOGO、合同编号、双方签章栏的PDF。
核心逻辑在TaskDownloadServlet中:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Long taskId = Long.parseLong(request.getParameter("id")); AdTask task = new AdTaskDao().findTaskById(taskId); // 设置响应头,触发浏览器下载 response.setContentType("application/pdf"); response.setHeader("Content-Disposition", "attachment; filename=" + task.getContractFileName()); // 生成PDF流 Document document = new Document(PageSize.A4, 50, 50, 100, 50); PdfWriter.getInstance(document, response.getOutputStream()); document.open(); // 插入公司LOGO(假设logo.png在WebContent/images/下) Image logo = Image.getInstance(getServletContext().getRealPath("/images/logo.png")); logo.scalePercent(20); logo.setAlignment(Element.ALIGN_CENTER); document.add(logo); // 添加合同标题 Font titleFont = new Font(Font.HELVETICA, 18, Font.BOLD); Paragraph title = new Paragraph("公交站牌广告灯箱投放合同", titleFont); title.setAlignment(Element.ALIGN_CENTER); document.add(title); // 添加合同正文(关键字段) Font contentFont = new Font(Font.HELVETICA, 12, Font.NORMAL); document.add(new Paragraph("\n甲方(广告主):" + task.getAdvertiserName(), contentFont)); document.add(new Paragraph("乙方(运营方):XX区公交广告有限公司", contentFont)); document.add(new Paragraph("合同编号:" + task.getContractNo(), contentFont)); document.add(new Paragraph("灯箱位置:" + task.getLocationCode(), contentFont)); document.add(new Paragraph("投放周期:" + new SimpleDateFormat("yyyy年MM月dd日").format(task.getStartDate()) + " 至 " + new SimpleDateFormat("yyyy年MM月dd日").format(task.getEndDate()), contentFont)); // 添加签章栏(预留空白区域) document.add(new Paragraph("\n\n甲方(盖章):", contentFont)); document.add(new Paragraph("代表人签字:__________________", contentFont)); document.add(new Paragraph("日期:______年______月______日", contentFont)); document.add(new Paragraph("\n乙方(盖章):", contentFont)); document.add(new Paragraph("代表人签字:__________________", contentFont)); document.add(new Paragraph("日期:______年______月______日", contentFont)); document.close(); }这段代码的价值在于:所有合同要素(LOGO、编号、位置、周期)均来自数据库实时查询,杜绝人工填写错误。当客户质疑“你们说ZB-087-A3的合同是6月1日开始,但我6月5日才看到灯箱亮起来”,你只需打开数据库查ad_task表,SELECT contract_no, start_date FROM ad_task WHERE location_code='ZB-087-A3',结果与PDF合同完全一致——这就是系统公信力的来源。
5.2 PDF文件名暗藏玄机:用命名规范实现自动化归档
文档没提,但实际部署中,getContractFileName()方法的设计决定了档案管理效率:
public String getContractFileName() { // 格式:CONTRACT_AD202406001_ZB-087-A3_20240601.pdf return "CONTRACT_" + contractNo + "_" + locationCode + "_" + new SimpleDateFormat("yyyyMMdd").format(startDate) + ".pdf"; }这个命名规则带来三个实战好处:
- 按合同号排序:
CONTRACT_AD202406001_*自然聚在一起,财务月底对账时ls CONTRACT_AD202406*一键列出当月所有合同; - 按灯箱位置检索:运维查ZB-087-A3历史合同,
ls *ZB-087-A3*秒出结果; - 按日期归档:
mkdir /archive/202406 && mv *20240601.pdf /archive/202406/,全自动完成月度归档。
提示:若需批量盖章,可将PDF文件名作为参数传给印章系统API,如
curl -X POST http://seal-server/api/seal -d "file=CONTRACT_AD202406001_ZB-087-A3_20240601.pdf",实现“导出即盖章”。
5.3 从PDF导出到客户确认:用邮件自动化的最后一环
导出PDF只是开始,客户确认才是闭环。在TaskDownloadServlet末尾追加邮件发送逻辑(使用JavaMail API):
// 发送确认邮件(需配置SMTP) Properties props = new Properties(); props.put("mail.smtp.host", "smtp.exmail.qq.com"); props.put("mail.smtp.auth", "true"); Session session = Session.getInstance(props, new Authenticator() { protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication("admin@adcompany.com", "APP_PASSWORD"); } }); Message mimeMessage = new MimeMessage(session); mimeMessage.setFrom(new InternetAddress("admin@adcompany.com")); mimeMessage.setRecipients(Message.RecipientType.TO, InternetAddress.parse(task.getContactEmail())); mimeMessage.setSubject("【XX公交广告】您的合同已生成,请查收"); mimeMessage.setText("您好,您在ZB-087-A3灯箱的投放合同已生成,详见附件。\n" + "合同编号:" + task.getContractNo() + "\n" + "投放周期:" + formatDate(task.getStartDate()) + " 至 " + formatDate(task.getEndDate()) + "\n\n" + "XX区公交广告有限公司\n2024年6月1日"); // 附件:PDF文件 MimeBodyPart attachment = new MimeBodyPart(); DataSource source = new ByteArrayDataSource(pdfBytes, "application/pdf"); attachment.setDataHandler(new DataHandler(source)); attachment.setFileName(task.getContractFileName()); Multipart multipart = new MimeMultipart(); multipart.addBodyPart(attachment); mimeMessage.setContent(multipart); Transport.send(mimeMessage);这样,客户经理点击“导出PDF”按钮的瞬间,客户邮箱里已收到带附件的正式合同,而系统后台自动记录email_sent_time字段。从灯箱排期到客户确认,全程无人工干预,这才是信息化该有的样子。
从那以后我每次部署新环境,都强制走一遍TaskDownloadServlet的全流程测试:先在数据库插入一条测试任务,再用curl模拟请求/taskDownload?id=123,检查响应头Content-Disposition是否正确、PDF是否能正常打开、邮件是否送达。这三步不通过,绝不让系统上线。因为公交广告的每一分钟空置,都是真金白银的损失。希望帮到你。
本文还有配套的精品资源,点击获取