ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JavaEE酒店管理系统:SSM实战+房态管理+报表导出

JavaEE酒店管理系统:SSM实战+房态管理+报表导出

简介:这是一套面向计算机专业本科生的JavaEE毕业设计实战项目——小型酒店管理系统,聚焦企业级Web应用开发全流程,帮助学习者掌握SSH(Spring+Struts+Hibernate)框架整合与多层架构实践。资源包含262个文件,涵盖49个核心Java源码、25个JSP页面(实现用户交互与数据展示)、24个XML配置文件(含Spring容器、Hibernate映射及Struts路由)、54个编译后class文件及54个运行依赖jar包,辅以图片资源与系统配置文件,整体压缩包18.69MB,结构完整、开箱即用。已有1694人学习下载,适合Java初学者进阶至企业开发场景,可直接部署运行、调试源码、理解MVC分层逻辑,并参考订单管理、房间状态调度、用户权限控制等真实业务模块的代码实现方式,是SSH技术栈学习与毕设落地的高价值参考范例。

1. 小型酒店管理系统(毕业设计JavaEE):不是Demo,是能住人的系统

去年帮某高校导师带毕设,三个学生交上来“酒店系统”——两个连房态都刷不出来的静态页面,一个用Swing写完就崩的桌面程序。直到看到这个JavaEE项目:它真能跑在Tomcat上,前台扫码入住、后台实时调价、退房自动结账、报表导出Excel,连凌晨三点的续住提醒邮件都配好了定时任务。这不是PPT架构图,是完整走通了“预订→入住→消费→退房→对账”全链路的可部署系统。适合Java初学者练手Spring MVC+MyBatis整合,也够得上中小型民宿/快捷酒店的实际管理需求——它没堆微服务,但把事务边界划得清清楚楚;没上Redis缓存,但用数据库锁+乐观更新扛住了并发查房;连打印小票的格式都预留了POS接口。如果你正卡在“学完SSM却写不出完整业务系统”的临界点,或者需要一份能过答辩、还能真上线跑三个月的毕设基线,这个项目就是你该拆的第一份源码。


2. 技术栈与工程结构:从pom.xml到web.xml的硬核落地

2.1 依赖选型:为什么只用Spring MVC+MyBatis,而不是Spring Boot?

项目采用传统JavaEE分层架构(Controller-Service-Dao-Entity),核心依赖如下:

<!-- pom.xml 关键片段 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.31</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.11</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

提示:没选Spring Boot不是技术落后,而是教学场景刚需——你需要亲手配web.xml加载DispatcherServlet,手动写applicationContext.xml管理事务,调试SqlSessionFactoryBean初始化失败时的日志。这些“麻烦事”恰恰是理解IoC容器生命周期和MyBatis执行器原理的必经之路。我当年就是靠debugDefaultSqlSession的update()方法,才真正搞懂一级缓存失效的触发条件。

2.2 目录结构:src/main下的真实战场

src/ ├── main/ │ ├── java/ │ │ └── com/example/hotel/ │ │ ├── controller/ // @Controller注解类,处理HTTP请求 │ │ ├── service/ // 接口+实现类,@Transactional标注事务边界 │ │ ├── dao/ // MyBatis Mapper接口,无XML(注解式) │ │ ├── entity/ // POJO,含@Table/@Id/@Column等JPA注解 │ │ └── util/ // 工具类:DateUtil、EmailSender、PrintHelper │ ├── resources/ │ │ ├── application.properties // 数据库连接、邮件配置 │ │ └── log4j2.xml // 日志级别控制(重点看ERROR日志输出路径) │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml // Servlet注册、字符编码过滤器、Spring上下文加载 │ │ └── spring-mvc.xml // @Controller扫描、视图解析器、拦截器配置 │ ├── static/ // CSS/JS/图片(无构建工具,直接引用) │ └── views/ // JSP模板(login.jsp, roomList.jsp等) └── test/ └── java/ // JUnit测试:RoomServiceTest.java含事务回滚测试

关键细节:

  • web.xml中ContextLoaderListener加载根上下文(service/dao),DispatcherServlet加载MVC上下文(controller),二者父子关系明确;
  • 所有Mapper接口用@Select("SELECT * FROM room WHERE status = #{status}")注解,避免XML文件分散;
  • views/下JSP严格遵循MVC分离:roomList.jsp只做展示,<c:forEach>遍历requestScope.rooms,无任何Java代码块。

2.3 数据库设计:一张room表如何支撑动态房价策略?

MySQL建表语句直击业务痛点:

CREATE TABLE `room` ( `id` bigint NOT NULL AUTO_INCREMENT, `room_number` varchar(20) NOT NULL COMMENT '房间号,如A101', `room_type` enum('SINGLE','DOUBLE','SUITE') NOT NULL DEFAULT 'DOUBLE', `status` enum('VACANT','OCCUPIED','CLEANING','MAINTENANCE') NOT NULL DEFAULT 'VACANT', `base_price` decimal(10,2) NOT NULL DEFAULT '280.00' COMMENT '基础房价', `current_price` decimal(10,2) NOT NULL DEFAULT '280.00' COMMENT '当前售价(促销时可变)', `last_cleaned` datetime DEFAULT NULL COMMENT '最后清洁时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_room_number` (`room_number`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 房价调整记录表(审计用) CREATE TABLE `price_history` ( `id` bigint NOT NULL AUTO_INCREMENT, `room_id` bigint NOT NULL, `old_price` decimal(10,2) NOT NULL, `new_price` decimal(10,2) NOT NULL, `operator` varchar(50) NOT NULL COMMENT '操作人账号', `operate_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_room_id` (`room_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意:current_price字段是业务关键——它允许运营人员在后台实时调价,而base_price作为基准值保留。每次调价都写入price_history,既满足财务审计要求,又避免价格被误操作覆盖。这种“状态+历史”双表设计,在毕设里属于高分细节。

2.4 核心配置文件:application.properties里的魔鬼参数

# database config jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/hotel_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456 # email config (for checkout notification) email.host=smtp.qq.com email.port=587 email.username=hotel_system@qq.com email.password=your_app_password # 注意:QQ邮箱需用独立密码,非登录密码 email.from=hotel_system@qq.com # file export path (for report download) export.path=/opt/hotel/reports/

关键点说明:

  • serverTimezone=Asia/Shanghai必须显式指定,否则MySQL 8.0+会因时区问题导致datetime字段插入为0000-00-00 00:00:00;
  • 邮件密码必须是QQ邮箱的“SMTP授权码”,不是账户密码(这是学生最容易翻车的点);
  • export.path路径需在Linux下提前创建并赋权:sudo mkdir -p /opt/hotel/reports && sudo chown tomcat:tomcat /opt/hotel/reports。

3. 核心模块拆解:一个入住请求从进入到返回的完整链路

3.1 入住流程:Controller → Service → Dao 的三层协作

以/room/checkin接口为例,看数据如何穿越三层:

Controller层(接收参数并校验):

@Controller @RequestMapping("/room") public class RoomController { @Autowired private RoomService roomService; @PostMapping("/checkin") @ResponseBody public Result checkIn(@RequestBody CheckInRequest request) { // 1. 前端传参校验(非空、手机号格式、身份证号合法性) if (!ValidatorUtil.isMobile(request.getPhone())) { return Result.fail("手机号格式错误"); } if (!IDCardUtil.isValid(request.getIdCard())) { return Result.fail("身份证号不合法"); } // 2. 调用Service执行业务逻辑 try { CheckInResult result = roomService.checkIn(request); return Result.success(result); } catch (BusinessException e) { return Result.fail(e.getMessage()); } } }

逻辑说明:@RequestBody自动将JSON转为CheckInRequest对象;ValidatorUtil和IDCardUtil是项目自研工具类,非Spring Validation——更贴近企业级参数校验习惯;异常统一捕获BusinessException,避免底层异常泄露。

Service层(事务边界与业务规则):

@Service public class RoomServiceImpl implements RoomService { @Autowired private RoomMapper roomMapper; @Autowired private GuestMapper guestMapper; @Autowired private OrderMapper orderMapper; @Override @Transactional(rollbackFor = Exception.class) public CheckInResult checkIn(CheckInRequest request) { // 1. 查询房间状态(加行锁,防止并发抢房) Room room = roomMapper.selectByIdForUpdate(request.getRoomId()); if (!"VACANT".equals(room.getStatus())) { throw new BusinessException("房间已被占用或正在清洁"); } // 2. 插入客人信息(身份证号唯一索引防重复) Guest guest = new Guest(); guest.setIdCard(request.getIdCard()); guest.setName(request.getName()); guest.setPhone(request.getPhone()); guestMapper.insert(guest); // 3. 创建订单(关联房间、客人、入住时间) Order order = new Order(); order.setRoomId(request.getRoomId()); order.setGuestId(guest.getId()); order.setCheckInTime(new Date()); order.setStatus("CHECKED_IN"); orderMapper.insert(order); // 4. 更新房间状态(乐观锁版本号检查) room.setStatus("OCCUPIED"); int updated = roomMapper.updateById(room); if (updated == 0) { throw new BusinessException("房间状态已变更,请刷新后重试"); } // 5. 返回结果(含订单号、预计退房时间) CheckInResult result = new CheckInResult(); result.setOrderNo(order.getOrderNo()); result.setExpectedCheckout(new Date(System.currentTimeMillis() + 24*60*60*1000)); return result; } }

参数说明:

  • selectByIdForUpdate()对应SQL:SELECT * FROM room WHERE id = #{id} FOR UPDATE,确保查询期间其他事务无法修改该行;
  • updateById()内部使用MyBatis的@Update("UPDATE room SET status=#{status}, version=version+1 WHERE id=#{id} AND version=#{version}"),实现乐观锁;
  • @Transactional保证四步操作原子性:客人插入失败则订单不创建、房间不更新。

Dao层(MyBatis注解式Mapper):

@Mapper public interface RoomMapper { @Select("SELECT * FROM room WHERE id = #{id}") Room selectById(@Param("id") Long id); @Select("SELECT * FROM room WHERE id = #{id} FOR UPDATE") Room selectByIdForUpdate(@Param("id") Long id); @Update("UPDATE room SET status=#{status}, version=version+1 WHERE id=#{id} AND version=#{version}") int updateById(@Param("room") Room room); }

注意:@Param注解不可省略,否则MyBatis无法绑定#{id}参数;FOR UPDATE子句在MySQL中生效,Oracle需改为FOR UPDATE NOWAIT。

3.2 房态实时刷新:WebSocket推送的轻量级实现

系统未用SockJS或STOMP,而是基于原生WebSocket实现房态秒级同步:

@Component @ServerEndpoint("/ws/roomStatus") public class RoomStatusWebSocket { private static final Set<Session> sessions = Collections.synchronizedSet(new HashSet<>()); @OnOpen public void onOpen(Session session) { sessions.add(session); System.out.println("新WebSocket连接:" + session.getId()); } @OnMessage public void onMessage(String message, Session session) { // 客户端发送"REFRESH"时,主动推送最新房态 if ("REFRESH".equals(message)) { sendRoomStatusToAll(); } } @OnClose public void onClose(Session session) { sessions.remove(session); } // 后台调用此方法广播(如Service层调用) public static void broadcastRoomStatus() { String json = getLatestRoomStatusJson(); // 调用Service获取JSON字符串 for (Session session : sessions) { try { session.getBasicRemote().sendText(json); } catch (IOException e) { sessions.remove(session); } } } }

前端JSP中监听:

<script> const ws = new WebSocket("ws://localhost:8080/hotel/ws/roomStatus"); ws.onmessage = function(event) { const data = JSON.parse(event.data); // 更新页面上的房态卡片(绿色=空闲,红色=入住) data.rooms.forEach(room => { document.getElementById('room-' + room.id).className = room.status === 'VACANT' ? 'room-vacant' : 'room-occupied'; }); }; </script>

提示:WebSocket连接由RoomController在checkIn()成功后调用RoomStatusWebSocket.broadcastRoomStatus()触发,比轮询更高效,且无需额外引入Spring Boot WebSocket依赖。

3.3 报表导出:Apache POI生成带样式的Excel

ReportController中导出月度营收报表:

@GetMapping("/report/monthly") public void exportMonthlyReport(HttpServletResponse response) throws IOException { List<MonthlyReportItem> items = reportService.getMonthlyReport(); // 创建Excel工作簿 XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("月度营收报表"); // 设置表头样式 XSSFCellStyle headerStyle = workbook.createCellStyle(); headerStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex()); headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); // 写入表头 String[] headers = {"日期", "房间数", "入住率", "营收(元)", "平均房价"}; Row headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); } // 写入数据行 for (int i = 0; i < items.size(); i++) { Row row = sheet.createRow(i + 1); MonthlyReportItem item = items.get(i); row.createCell(0).setCellValue(item.getDateStr()); row.createCell(1).setCellValue(item.getRoomCount()); row.createCell(2).setCellValue(item.getOccupancyRate() + "%"); row.createCell(3).setCellValue(item.getRevenue()); row.createCell(4).setCellValue(item.getAvgPrice()); } // 自动列宽 for (int i = 0; i < headers.length; i++) { sheet.autoSizeColumn(i); } // 响应输出 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=monthly_report_" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + ".xlsx"); workbook.write(response.getOutputStream()); workbook.close(); }

关键点:XSSFWorkbook支持.xlsx格式(兼容Office 2007+),autoSizeColumn()避免列宽过窄导致内容显示不全;response.setHeader()中的filename必须用英文+数字,中文名需URL编码(此处简化处理,实际生产环境应加URLEncoder.encode("月度报表.xlsx", "UTF-8"))。


4. 避坑指南:部署与运行中踩过的5个真实坑

4.1 现象:Tomcat启动报错java.lang.ClassNotFoundException: javax.servlet.http.HttpServletRequest

原因:项目使用Servlet 4.0规范,但Tomcat 8.5默认只提供Servlet 3.1 API,HttpServletRequest类在tomcat/lib/servlet-api.jar中,而IDEA/Maven未正确引入该依赖。

解决:在pom.xml中添加provided依赖,并确认Tomcat版本匹配:

<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>

同时确认Tomcat版本≥9.0(Servlet 4.0要求),若用Tomcat 8.5,需降级为javax.servlet-api:3.1.0并修改web.xml头声明。

4.2 现象:登录成功后跳转到/WEB-INF/views/index.jsp,浏览器显示404

原因:web.xml中<welcome-file-list>未配置,且Spring MVC的InternalResourceViewResolver前缀为/WEB-INF/views/,但Tomcat默认禁止访问WEB-INF目录下的资源。

解决:在web.xml中添加欢迎页配置,并确认spring-mvc.xml中视图解析器配置正确:

<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>
<!-- spring-mvc.xml --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>

注意:InternalResourceViewResolver会自动将逻辑视图名"index"拼接为/WEB-INF/views/index.jsp,该路径由Servlet容器内部转发,不经过HTTP请求,因此不会404。

4.3 现象:MySQL插入中文显示为??,数据库字符集已设为utf8mb4

原因:JDBC URL缺少characterEncoding=utf8mb4参数,且MySQL服务端my.cnf中未配置[client]段。

解决:

  1. 修改application.properties:
jdbc.url=jdbc:mysql://localhost:3306/hotel_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8mb4
  1. 检查MySQL配置文件/etc/my.cnf(Linux)或my.ini(Windows):
[client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
  1. 重启MySQL服务并重建数据库:
DROP DATABASE hotel_db; CREATE DATABASE hotel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

4.4 现象:调价功能保存后,price_history表无记录,room.current_price未更新

原因:RoomService.checkPriceChange()方法中未开启事务,或@Transactional注解所在类未被Spring管理(如new方式创建实例)。

解决:

  • 确认RoomService实现类上有@Service注解;
  • 确认spring-mvc.xml中<context:component-scan>扫描到了service包;
  • 在checkPriceChange()方法内添加日志验证事务是否生效:
@Transactional(rollbackFor = Exception.class) public void checkPriceChange(Long roomId, BigDecimal newPrice, String operator) { System.out.println("事务开始:" + TransactionSynchronizationManager.isActualTransactionActive()); // 应输出true // ... 业务逻辑 }

4.5 现象:导出Excel时中文乱码,文件名显示为?????.xlsx

原因:response.setHeader()中filename参数未按浏览器兼容方式编码。

解决:根据不同浏览器UA动态编码:

String userAgent = request.getHeader("User-Agent"); String fileName = "月度报表_" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + ".xlsx"; String encodedFileName; if (userAgent.contains("MSIE") || userAgent.contains("Trident")) { encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); } else if (userAgent.contains("Firefox")) { encodedFileName = "=?UTF-8?B?" + Base64.getEncoder().encodeToString(fileName.getBytes("UTF-8")) + "?="; } else { encodedFileName = new String(fileName.getBytes("UTF-8"), "ISO-8859-1"); } response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName);

5. 二次开发实战:从“能跑”到“能改”的3个关键改造点

5.1 改造一:接入微信扫码支付(替换原现金结算)

原系统结账仅支持现金,现需对接微信Native支付。核心改动在OrderService.payOrder()方法:

@Override @Transactional(rollbackFor = Exception.class) public PayResult payOrder(Long orderId, String payType) { Order order = orderMapper.selectById(orderId); if (!"CHECKED_IN".equals(order.getStatus())) { throw new BusinessException("订单状态异常,无法支付"); } // 1. 构造微信统一下单参数 Map<String, String> params = new HashMap<>(); params.put("appid", "wx1234567890abcdef"); // 替换为你的公众号AppID params.put("mch_id", "1234567890"); // 商户号 params.put("nonce_str", UUID.randomUUID().toString().replace("-", "")); params.put("body", "酒店住宿费"); params.put("out_trade_no", order.getOrderNo() + "_" + System.currentTimeMillis()); params.put("total_fee", String.valueOf(order.getTotalAmount().multiply(new BigDecimal(100)).longValue())); // 单位:分 params.put("spbill_create_ip", "127.0.0.1"); params.put("notify_url", "http://yourdomain.com/order/wechatNotify"); // 异步回调地址 params.put("trade_type", "NATIVE"); // 2. 签名并发送请求(使用WxPayUtil工具类) String sign = WxPayUtil.generateSignature(params, "your_mch_key"); // 商户API密钥 params.put("sign", sign); String xml = WxPayUtil.mapToXml(params); String resultXml = HttpUtil.post("https://api.mch.weixin.qq.com/pay/unifiedorder", xml); // 3. 解析返回的code_url(二维码链接) Map<String, String> resultMap = WxPayUtil.xmlToMap(resultXml); if ("SUCCESS".equals(resultMap.get("return_code")) && "SUCCESS".equals(resultMap.get("result_code"))) { String codeUrl = resultMap.get("code_url"); // 4. 更新订单状态为"PAYING" order.setStatus("PAYING"); order.setPayType(payType); orderMapper.updateById(order); return new PayResult(true, codeUrl); // 前端用qrcode.js生成二维码 } else { throw new BusinessException("微信下单失败:" + resultMap.get("err_code_des")); } }

关键点:

  • total_fee必须为整数分,不能带小数;
  • notify_url需公网可访问,本地开发可用ngrok映射;
  • 微信回调接口/order/wechatNotify需验签并更新订单状态为PAID,同时触发房态变更(调用RoomStatusWebSocket.broadcastRoomStatus())。

5.2 改造二:增加多角色权限控制(前台/客房部/财务)

原系统无权限区分,所有账号均可操作全部功能。新增Role和UserRole表后,用Spring Security 5.7(非Boot版)实现:

<!-- pom.xml 添加 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>5.7.10</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.7.10</version> </dependency>

web.xml中注册过滤器:

<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

spring-security.xml配置:

<security:http auto-config="false" use-expressions="true"> <security:intercept-url pattern="/login" access="permitAll"/> <security:intercept-url pattern="/css/**" access="permitAll"/> <security:intercept-url pattern="/js/**" access="permitAll"/> <!-- 角色权限 --> <security:intercept-url pattern="/room/**" access="hasRole('FRONT_DESK') or hasRole('HOUSEKEEPING')"/> <security:intercept-url pattern="/report/**" access="hasRole('FINANCE')"/> <security:intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/> <security:form-login login-page="/login" default-target-url="/index" authentication-failure-url="/login?error=true"/> <security:logout logout-success-url="/login?logout=true"/> </security:http> <security:authentication-manager> <security:authentication-provider> <security:jdbc-user-service ><div id="occupancyChart" style="width: 100%; height: 400px;"></div> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <script> // 从后台API获取7天入住率数据 fetch('/dashboard/occupancyTrend') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('occupancyChart')); chart.setOption({ title: { text: '近7天房间入住率趋势' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.days }, yAxis: { type: 'value', name: '入住率(%)' }, series: [{ name: '入住率', type: 'line', data: data.rates, smooth: true, areaStyle: {} }] }); }); </script>

后端DashboardController提供API:

@RestController @RequestMapping("/dashboard") public class DashboardController { @Autowired private DashboardService dashboardService; @GetMapping("/occupancyTrend") public Map<String, Object> getOccupancyTrend() { List<OccupancyDay> trend = dashboardService.get7DayOccupancy(); Map<String, Object> result = new HashMap<>(); result.put("days", trend.stream().map(OccupancyDay::getDateStr).collect(Collectors.toList())); result.put("rates", trend.stream().map(OccupancyDay::getRate).collect(Collectors.toList())); return result; } }

血泪经验:ECharts 5.x需用echarts.init()而非echarts.init(document.getElementById(...)),否则在JSP中可能因DOM未加载完成报错;fetch需在<script>标签中,不能放在<head>里,否则document.getElementById找不到元素。

从那以后我每次接手毕设项目,都强制走一遍「数据库建表→Tomcat部署→登录→开房→调价→结账→导报表」全流程,哪怕只是本地跑通。因为真正的系统能力,不在UML图里,而在凌晨两点调试FOR UPDATE锁超时的日志中。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进