
最近社交平台上“大衣男回归出发巴黎时装周”的话题热度不低很多读者私信问我能不能出一篇技术向的拆解文章。时装周相关的业务场景其实非常适合用来练手 Web 开发它包含内容展示、活动排期、预约报名、款式管理和后台维护等典型模块几乎就是一个迷你版的企业级内容管理系统。本文就以“巴黎时装周”为主题背景带你从零搭建一套完整的时装周内容展示与预约系统顺便聊清楚 Spring Boot Thymeleaf MySQL 这套组合在实际项目里的配置思路、代码组织和排错方法。这套教程不仅适合刚学完 Java Web 的初学者也适合需要快速交付内容型管理系统的开发者。跟着文章走你能掌握项目结构设计、数据表建模、业务接口开发、页面渲染、表单校验和常见异常排查最后还能用一套可复制的方案去迁移到其他类似场景比如展会预约、新品发布、线下活动报名等。1. 背景与核心概念1.1 时装周内容展示系统是什么抛开娱乐新闻的热度不谈单看业务需求“巴黎时装周”背后其实是一个典型的内容运营场景主办方需要展示活动日程、设计师信息、参展品牌、单品款式还要支持媒体或买手在线预约后台则需要一套管理界面来维护这些内容。这类系统通常叫“活动内容管理系统”或“展会预约平台”和电商系统最大的区别在于它的核心不是交易闭环而是“内容发布 用户预约 后台维护”三段式结构。理解这个边界很重要因为很多初学者容易把这类项目做成大杂烩结果前后台业务边界混乱代码难以维护。本文要实现的系统定位就是内容展示加预约不涉及支付、库存、物流等电商复杂逻辑这让我们可以把更多精力放在架构清晰度和代码规范上。1.2 为什么选择 Spring Boot Thymeleaf MySQL技术选型不追求最热门而追求适合业务规模和团队能力。这里选用 Spring Boot是因为它解决了 Spring 生态里最头疼的配置问题内置 Tomcat打 jar 包就能跑非常适合快速交付这类内容型项目。Thymeleaf 是服务端渲染模板引擎它允许我们在 HTML 页面中直接使用${}、th:each这类语法渲染数据对 SEO 友好首屏加载也快。由于系统规模不大没必要拆前后端分离架构用服务端渲染可以让代码量和部署成本都降下来。MySQL 负责数据持久化存储活动、款式、预约记录等结构化信息。三者的组合是 Java Web 开发中最经典、资料最丰富、排错最容易的一套搭配。1.3 核心功能拆解为了让技术点集中也方便读者理解我们把系统拆成五个模块活动日程展示巴黎时装周会有多场次发布活动每场活动有标题、时间、地点、设计师简介等字段。款式图库管理维护每场活动下的代表单品包括服装名称、风格标签、衣长描述、材质、展示图地址等。预约报名用户选择到场日期和场次填写姓名和联系方式系统写入预约记录。后台登录校验使用简单的 Spring Boot 拦截器做权限校验避免未登录人员直接访问后台管理接口。后台维护支持添加新活动、维护款式、查看预约列表。“大衣男回归”这个点可以理解成一种单品风格回归——即大衣类男装重新成为本季时装周的重点。我们正好在款式模块里加入“风格标签”字段用大衣、复古、极简等标签演示数据筛选场景。2. 环境准备与版本说明2.1 开发环境本文示例以常见的 Java 开发环境为例具体版本可以结合你本机的实际环境微调软件建议版本用途JDK11 或 17编译运行 Spring Boot 项目Maven3.6依赖管理和项目构建Spring Boot2.7.x 或 3.x应用开发框架MySQL5.7 及以上数据存储IDEIntelliJ IDEA开发和调试Thymeleaf跟随 Spring Boot 版本服务端页面渲染如果你使用的是 Spring Boot 3.x需要注意 JDK 必须 17 及以上而且部分配置写法会有调整。文章中的代码以 Spring Boot 2.7 为主依赖坐标基本都是通用的升级到 3.x 时把javax.servlet换成jakarta.servlet即可。2.2 初始化项目最简单的初始化方式是通过 Spring Initializr。在 start.spring.io 页面选择 Maven、Java 11然后添加 Web、Thymeleaf、Spring Data JPA、MySQL Driver、Validation、Lombok 这几个依赖生成 zip 包后导入 IDE。如果你更习惯手动创建也可以直接新建 Maven 项目在pom.xml里引入依赖后面章节会给出完整的 pom 配置。2.3 数据库准备先在 MySQL 中创建库CREATE DATABASE IF NOT EXISTS paris_fashion DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE paris_fashion;使用utf8mb4是为了让数据库能正确存储中文和一些特殊字符比如品牌名中的é或 emoji 风格标签。3. 核心设计数据模型与后台架构3.1 数据表设计我们设计三张核心业务表fashion_show、outfit、appointment另外加一张admin_user用于后台登录。表名用途关键字段fashion_show时装周活动场次title, show_time, location, designer, description, statusoutfit款式单品show_id, name, style_tag, material, cover_url, descriptionappointment现场预约show_id, visitor_name, phone, visit_date, statusadmin_user后台用户username, password_hashoutfit表通过show_id关联到fashion_show这样每场活动都能维护多个代表款式。appointment也通过show_id关联活动避免用户预约时还需要手动输入活动名称。3.2 为什么把样式字段单独建表很多初学者习惯于把服装款式字段直接写在活动表里用逗号分隔多个风格标签。这样做在前期看起来省事但后续一旦要做“按风格筛选款式”或“统计大衣类单品数量”SQL 会变得非常难写而且数据容易脏。独立建表后我们可以用一条普通的多表关联查询实现筛选还能在 Java 层用实体关系直观映射。这其实是在强调一个通用原则一对多关系不要用逗号拼字符串单独建表才是规范做法。3.3 项目整体结构为了让代码分层清晰采用常见的包结构com.paris.fashion ├── FashionApplication.java ├── controller │ ├── PageController.java │ └── AdminController.java ├── entity │ ├── FashionShow.java │ ├── Outfit.java │ ├── Appointment.java │ └── AdminUser.java ├── repository │ ├── FashionShowRepository.java │ ├── OutfitRepository.java │ ├── AppointmentRepository.java │ └── AdminUserRepository.java ├── service │ ├── FashionShowService.java │ ├── OutfitService.java │ └── AppointmentService.java ├── interceptor │ └── AdminInterceptor.java └── web └── WebConfig.javaController 只负责接收请求和返回视图业务逻辑放到 Service数据访问放到 Repository。这种分层的价值在项目变大后会体现得非常明显每个类职责单一出问题时可以快速定位。4. 完整实战搭建时装周预约系统4.1 编写 pom.xml 依赖创建项目后先检查pom.xml。下面是一份可用的核心依赖配置?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.paris/groupId artifactIdparis-fashion/artifactId version1.0.0/version nameparis-fashion/name description巴黎时装周内容展示与预约系统/description properties java.version11/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration excludes exclude groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /exclude /excludes /configuration /plugin /plugins /build /projectMySQL 驱动在 Spring Boot 2.7 中坐标是mysql:mysql-connector-java到了 Spring Boot 3.x 推荐用com.mysql:mysql-connector-j这一点升级时要注意。4.2 配置 application.yml在src/main/resources/application.yml中写入如下配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/paris_fashion?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你自己的密码 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true thymeleaf: cache: false mode: HTML encoding: UTF-8ddl-auto: update表示 Hibernate 会根据实体类自动更新表结构开发阶段比较方便。但生产环境不建议使用这个配置最好由 DBA 维护 SQL 脚本避免实体字段调整时自动改表引发事故。thymeleaf.cache: false可以在开发调试时让页面修改即时生效不用重启应用。4.3 编写实体类活动实体文件路径src/main/java/com/paris/fashion/entity/FashionShow.javapackage com.paris.fashion.entity; import lombok.Data; import javax.persistence.*; import javax.validation.constraints.NotBlank; import java.time.LocalDateTime; Data Entity Table(name fashion_show) public class FashionShow { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; NotBlank(message 活动标题不能为空) private String title; private LocalDateTime showTime; private String location; private String designer; Column(columnDefinition TEXT) private String description; private String status; }这里用JsonFormat可以做而是把 LocalDateTime 交给前端格式化的思路更常见。模板渲染时用 Thymeleaf 的#temporals.format输出即可。款式实体文件路径src/main/java/com/paris/fashion/entity/Outfit.javapackage com.paris.fashion.entity; import lombok.Data; import javax.persistence.*; Data Entity Table(name outfit) public class Outfit { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private Long showId; private String name; private String styleTag; private String material; private String coverUrl; Column(columnDefinition TEXT) private String description; }styleTag字段用来存储“大衣”“复古”“极简”这类标签后期可以在页面上做筛选查询。预约实体文件路径src/main/java/com/paris/fashion/entity/Appointment.javapackage com.paris.fashion.entity; import lombok.Data; import javax.persistence.*; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; import java.time.LocalDateTime; Data Entity Table(name appointment) public class Appointment { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private Long showId; NotBlank(message 姓名不能为空) private String visitorName; NotBlank(message 手机号不能为空) Pattern(regexp ^1[3-9]\\d{9}$, message 手机号格式不正确) private String phone; private LocalDateTime visitDate; private String status; Column(updatable false) private LocalDateTime createTime; }手机号校验使用正则表达式前端和后端都可以复用。这里提醒一下Pattern只能做格式校验不能保证号码真实有效实际项目中应该配合短信验证码服务。后台用户实体文件路径src/main/java/com/paris/fashion/entity/AdminUser.javapackage com.paris.fashion.entity; import lombok.Data; import javax.persistence.*; Data Entity Table(name admin_user) public class AdminUser { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(unique true, nullable false) private String username; Column(nullable false) private String passwordHash; }4.4 编写 RepositorySpring Data JPA 可以通过方法名自动生成查询不需要手写 SQL适合这个体量的项目。文件路径src/main/java/com/paris/fashion/repository/FashionShowRepository.javapackage com.paris.fashion.repository; import com.paris.fashion.entity.FashionShow; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface FashionShowRepository extends JpaRepositoryFashionShow, Long { ListFashionShow findByStatusOrderByShowTimeDesc(String status); }文件路径src/main/java/com/paris/fashion/repository/OutfitRepository.javapackage com.paris.fashion.repository; import com.paris.fashion.entity.Outfit; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface OutfitRepository extends JpaRepositoryOutfit, Long { ListOutfit findByShowId(Long showId); ListOutfit findByStyleTagContaining(String styleTag); }文件路径src/main/java/com/paris/fashion/repository/AppointmentRepository.javapackage com.paris.fashion.repository; import com.paris.fashion.entity.Appointment; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface AppointmentRepository extends JpaRepositoryAppointment, Long { ListAppointment findByShowIdOrderByCreateTimeDesc(Long showId); }文件路径src/main/java/com/paris/fashion/repository/AdminUserRepository.javapackage com.paris.fashion.repository; import com.paris.fashion.entity.AdminUser; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface AdminUserRepository extends JpaRepositoryAdminUser, Long { OptionalAdminUser findByUsername(String username); }4.5 编写 Service 层Service 层做两件事业务校验和事务管理。先看活动与款式服务。文件路径src/main/java/com/paris/fashion/service/FashionShowService.javapackage com.paris.fashion.service; import com.paris.fashion.entity.FashionShow; import com.paris.fashion.repository.FashionShowRepository; import org.springframework.stereotype.Service; import java.util.List; Service public class FashionShowService { private final FashionShowRepository fashionShowRepository; public FashionShowService(FashionShowRepository fashionShowRepository) { this.fashionShowRepository fashionShowRepository; } public ListFashionShow listPublishedShows() { return fashionShowRepository.findByStatusOrderByShowTimeDesc(published); } public FashionShow getShow(Long id) { return fashionShowRepository.findById(id) .orElseThrow(() - new IllegalArgumentException(活动不存在)); } public FashionShow saveShow(FashionShow show) { if (show.getStatus() null) { show.setStatus(published); } return fashionShowRepository.save(show); } }文件路径src/main/java/com/paris/fashion/service/AppointmentService.javapackage com.paris.fashion.service; import com.paris.fashion.entity.Appointment; import com.paris.fashion.repository.AppointmentRepository; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; Service public class AppointmentService { private final AppointmentRepository appointmentRepository; public AppointmentService(AppointmentRepository appointmentRepository) { this.appointmentRepository appointmentRepository; } public Appointment createAppointment(Appointment appointment) { appointment.setStatus(pending); appointment.setCreateTime(LocalDateTime.now()); return appointmentRepository.save(appointment); } public ListAppointment listAppointments() { return appointmentRepository.findAll(); } }构造函数注入是 Spring 官方推荐的注入方式以后写单元测试时可以直接传 mock 对象不需要依赖反射。4.6 编写 Controller页面控制器文件路径src/main/java/com/paris/fashion/controller/PageController.javapackage com.paris.fashion.controller; import com.paris.fashion.entity.Appointment; import com.paris.fashion.entity.FashionShow; import com.paris.fashion.entity.Outfit; import com.paris.fashion.service.AppointmentService; import com.paris.fashion.service.FashionShowService; import com.paris.fashion.service.OutfitService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; Controller public class PageController { private final FashionShowService fashionShowService; private final OutfitService outfitService; private final AppointmentService appointmentService; public PageController(FashionShowService fashionShowService, OutfitService outfitService, AppointmentService appointmentService) { this.fashionShowService fashionShowService; this.outfitService outfitService; this.appointmentService appointmentService; } GetMapping(/) public String index(Model model) { model.addAttribute(shows, fashionShowService.listPublishedShows()); return index; } GetMapping(/show/{id}) public String showDetail(PathVariable Long id, Model model) { FashionShow show fashionShowService.getShow(id); model.addAttribute(show, show); model.addAttribute(outfits, outfitService.getOutfitsByShow(id)); model.addAttribute(appointment, new Appointment()); return detail; } PostMapping(/appointment) public String submitAppointment(Valid ModelAttribute Appointment appointment, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { FashionShow show fashionShowService.getShow(appointment.getShowId()); model.addAttribute(show, show); model.addAttribute(outfits, outfitService.getOutfitsByShow(appointment.getShowId())); return detail; } appointmentService.createAppointment(appointment); return redirect:/show/ appointment.getShowId() ?bookedtrue; } GetMapping(/style) public String styleFilter(RequestParam(value tag, required false) String tag, Model model) { model.addAttribute(shows, fashionShowService.listPublishedShows()); model.addAttribute(outfits, outfitService.filterByTag(tag)); model.addAttribute(currentTag, tag); return style; } }这里有几个值得注意的地方预约提交失败时不能直接刷新页面而是通过 Model 回传错误信息再渲染同一个详情页。成功预约后使用redirect:重定向避免用户刷新浏览器导致表单重复提交。Valid和BindingResult必须成对出现否则校验失败时会直接抛出异常。后台管理控制器文件路径src/main/java/com/paris/fashion/controller/AdminController.javapackage com.paris.fashion.controller; import com.paris.fashion.entity.FashionShow; import com.paris.fashion.service.AppointmentService; import com.paris.fashion.service.FashionShowService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; Controller RequestMapping(/admin) public class AdminController { private final FashionShowService fashionShowService; private final AppointmentService appointmentService; public AdminController(FashionShowService fashionShowService, AppointmentService appointmentService) { this.fashionShowService fashionShowService; this.appointmentService appointmentService; } GetMapping(/login) public String loginPage() { return admin/login; } GetMapping(/shows) public String listShows(Model model) { model.addAttribute(shows, fashionShowService.listPublishedShows()); model.addAttribute(newShow, new FashionShow()); return admin/shows; } PostMapping(/shows) public String createShow(ModelAttribute FashionShow show) { fashionShowService.saveShow(show); return redirect:/admin/shows; } GetMapping(/appointments) public String listAppointments(Model model) { model.addAttribute(appointments, appointmentService.listAppointments()); return admin/appointments; } }考虑到文章篇幅后台登录部分的完整 Session 代码可以参照前面提到的拦截器思路自行扩展下文会用一段代码演示核心拦截逻辑。4.7 编写 Thymeleaf 页面首页模板文件路径src/main/resources/templates/index.html!DOCTYPE html html langzh xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title巴黎时装周专场/title link relstylesheet href/webjars/bootstrap/5.1.3/css/bootstrap.min.css /head body div classcontainer mt-4 h1巴黎时装周 · 本季重点回归/h1 p classtext-muted大衣男风格回归出发巴黎时装周第一时间掌握活动场次与代表款式。/p h2 classmt-4活动日程/h2 div classrow div classcol-md-4 mt-3 th:eachshow : ${shows} div classcard div classcard-body h5 classcard-title th:text${show.title}活动标题/h5 p classcard-text th:text${#temporals.format(show.showTime, yyyy-MM-dd HH:mm)}/p p classcard-text th:text${show.location}地点/p a classbtn btn-primary th:href{/show/{id}(id${show.id})}查看详情/a /div /div /div /div /div /body /htmlth:each是 Thymeleaf 循环渲染的语法#temporals.format用来格式化时间这两个是服务端渲染中最高频的用法。活动详情与预约页文件路径src/main/resources/templates/detail.html!DOCTYPE html html langzh xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title th:text${show.title}活动详情/title /head body div classcontainer mt-4 a href/ classbtn btn-outline-secondary mb-3返回首页/a h1 th:text${show.title}活动标题/h1 p th:text设计师 ${show.designer}/p p th:text${show.description}/p h2 classmt-4代表款式/h2 div classrow div classcol-md-3 mt-3 th:eachoutfit : ${outfits} div classcard img th:src${outfit.coverUrl} classcard-img-top alt款式图片 div classcard-body h5 classcard-title th:text${outfit.name}款式名称/h5 p classcard-text th:text风格标签 ${outfit.styleTag}/p p classcard-text th:text材质 ${outfit.material}/p /div /div /div /div h2 classmt-4我要预约/h2 form th:action{/appointment} methodpost th:object${appointment} input typehidden th:field*{showId} / div classmb-3 label姓名/label input typetext classform-control th:field*{visitorName} / span th:if${#fields.hasErrors(visitorName)} th:errors*{visitorName} classtext-danger/span /div div classmb-3 label手机号/label input typetext classform-control th:field*{phone} / span th:if${#fields.hasErrors(phone)} th:errors*{phone} classtext-danger/span /div div classmb-3 label到访时间/label input typedatetime-local classform-control th:field*{visitDate} / /div button typesubmit classbtn btn-success提交预约/button /form /div /body /html使用th:field*{showId}时实体类中必须有对应字段渲染时会自动把当前活动 ID 写入隐藏域。这里要注意datetime-local输入的值格式是yyyy-MM-ddTHH:mm需要自行转换或使用DateTimeFormat注解做格式化实际项目中更推荐传时间戳减少前端格式耦合。4.8 添加测试数据用以下 SQL 插入两条演示数据数据内容尽量贴近“大衣男回归”和“巴黎时装周”主题USE paris_fashion; INSERT INTO fashion_show (title, show_time, location, designer, description, status) VALUES (2025 巴黎男装秋冬发布大衣回归专场, 2025-01-15 10:00:00, 巴黎大皇宫, Jean Moreau, 本季聚焦大衣类男装重现经典轮廓与当代面料工艺的结合。, published), (新锐设计师联合秀, 2025-01-16 14:00:00, 巴黎玛黑区艺术中心, Chen Wei, 多位亚洲设计师联合展示极简风格与街头元素的碰撞。, published); INSERT INTO outfit (show_id, name, style_tag, material, cover_url, description) VALUES (1, 双排扣羊毛大衣, 大衣, 复古, 羊毛混纺, https://example.com/coat1.jpg, 经典双排扣剪裁宽松廓形设计。), (1, 驼色系带大衣, 大衣, 极简, 羊绒, https://example.com/coat2.jpg, 高支羊绒面料适合秋冬通勤场景。), (1, 黑色连帽派克大衣, 大衣, 街头, 棉质帆布, https://example.com/coat3.jpg, 兼具街头感与实用性的轻量级大衣。), (2, 解构西装外套, 极简, 解构, 亚麻混纺, https://example.com/suit1.jpg, 不对称剪裁体现面料的自然肌理。);这里有两个字段值得说明show_time使用DATETIME类型把时间语义存清楚。cover_url在真实项目中应该存可访问的图片地址开发环境可直接放测试图片链接。4.9 运行与验证在项目根目录执行mvn spring-boot:run看到类似Tomcat started on port(s): 8080的日志后访问http://localhost:8080/预期效果首页展示两条活动卡片按照发布时间倒序显示。点击“查看详情”进入活动页可以看到 3 到 4 个款式卡片。填写姓名和手机号约谈后提交返回详情页并提示预约成功。管理员访问/admin/shows可以看到活动维护入口访问/admin/appointments查看预约列表。打开后台预约列表时因为目前没有登录校验理论上任何人都能访问。下一节补上拦截器做权限控制。4.10 补一个最小登录拦截器文件路径src/main/java/com/paris/fashion/interceptor/AdminInterceptor.javapackage com.paris.fashion.interceptor; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object adminUser session.getAttribute(adminUser); if (adminUser null) { response.sendRedirect(/admin/login); return false; } return true; } }文件路径src/main/java/com/paris/fashion/web/WebConfig.javapackage com.paris.fashion.web; import com.paris.fashion.interceptor.AdminInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AdminInterceptor()) .addPathPatterns(/admin/**) .excludePathPatterns(/admin/login); } }登录接口本身不复杂核心思路是接收表单用户名和密码查库比对哈希值比对通过后把用户对象存入session。完整实现建议参考 Spring Security 或 BCrypt 工具类不要明文存储密码。5. 常见问题与排查思路在实际开发中下面几个问题出现频率最高整理成表格方便收藏查阅。问题现象常见原因解决思路启动报“Unable to open JDBC Connection”数据库未启动或密码错误检查 MySQL 服务状态、连接 URL、账号密码页面显示中文乱码Thymeleaf 编码或数据库字符集错误确认application.yml中 UTF-8 配置库表使用 utf8mb4表单校验不生效缺少Valid或 BindingResult 位置错误校验注解必须在实体字段上Controller 参数中Valid后紧跟BindingResult预约提交刷新后重复预约表单直接返回视图而非重定向Post/Redirect/Get 模式提交成功后redirect:后台页面未登录可直接访问缺少拦截器或拦截器路径配置错误配置拦截器.addPathPatterns(/admin/**)升级 Spring Boot 3.x 后编译失败javax.servlet已被jakarta.servlet替换全量替换为jakarta包名即可showTime前端显示不正确LocalDateTime 前端直接输出序列化问题使用 Thymeleaf 的#temporals.format或用字符串参数每条问题背后都对应一个开发阶段容易踩中的点。像乱码问题很多人只在页面设置了 UTF-8却忽略了数据库连接串里的characterEncodingutf8这一点值得格外注意。6. 最佳实践与工程建议6.1 数据层面不要在生产用 ddl-autospring.jpa.hibernate.ddl-autoupdate只适合开发环境快速迭代。正式部署时表结构变更应该走数据库迁移脚本推荐引入 Flyway 或 Liquibase。这类工具能把每次变更记录下来团队协作时不会出现“我本地能跑你本地报错”的尴尬。对于预约系统还要给appointment表的phone字段和show_id字段建立索引SQL 如下CREATE INDEX idx_appointment_phone ON appointment(phone); CREATE INDEX idx_appointment_show ON appointment(show_id);索引不是越多越好但高频查询字段值得建立尤其是按手机号检索预约记录这种典型场景。6.2 安全层面密码必须哈希存储后台用户的密码绝对不允许明文存储。推荐使用 BCrypt。Spring Security 自带的BCryptPasswordEncoder是一个稳妥选择你也可以在工具类中引入spring-security-crypto依赖。实际项目中登录逻辑加入失败次数限制连续错误后锁定账号一段时间能有效避免暴力破解。6.3 业务层面前端校验和后端校验都要做Thymeleaf 页面中可以直接用 HTML 的required属性做一些前端提示但后端一定要再次校验。原因很简单攻击者可以绕过浏览器直接向后端接口提交数据如果后端不校验脏数据就会进入数据库。本项目中Valid和BindingResult就是最低限度的后端防线。6.4 代码层面分层职责要清晰Controller 不要出现业务代码比如不要直接在 Controller 里查数据库、拼接字符串。这种代码在小项目里很省事但一旦项目超过十个接口维护成本会急剧上升。建议所有 SQL 查询只出现在 Repository业务规则只出现在 ServiceController 只负责参数接收和视图跳转。6.5 体验层面走出“比赛项目感”很多初学者做完系统后会发现“功能都有但就是不像正式项目”。差距通常出在三个细节错误提示文案不够具体。页面空数据时没有友好占位。时间日期没有统一格式化。这些看起来不是核心技术问题却直接影响系统可用性。建议在模板中处理空集合div th:if${#lists.isEmpty(outfits)} p classtext-muted该场次暂未上传代表款式请稍后再来。/p /div6.6 部署建议生产环境建议用java -jar配合 Systemd 或容器化部署同时配置 JVM 参数java -Xms512m -Xmx1024m -jar paris-fashion-1.0.0.jar --spring.profiles.activeprod不同环境用不同application-{profile}.yml文件数据库连接、日志级别、缓存开关都放进对应配置文件避免改代码。7. 总结与学习路线本文围绕“大衣男回归出发巴黎时装周”这个话题完成了一套可以运行的时装周内容展示与预约系统。你学到了 Spring Boot 的项目结构、JPA 实体和 Repository 的编写方式、Thymeleaf 服务端渲染、表单校验、重定向避免重复提交、以及一个最小的后台登录拦截器。这套代码里的很多模式可以直接迁移到其他活动报名类项目中。接下来如果你要继续深入推荐按以下顺序学习用 Spring Security 替换自写拦截器理解认证与授权的关系。引入 Flyway 管理数据库脚本理解迁移工具的价值。把图片存储改为云存储体验文件访问鉴权和 CDN 加速。给预约接口设计分页查询学习 Spring Data JPA 分页用法。尝试用 Redis 做验证码存储和限流理解高并发场景的应对思路。如果你按照文章把项目跑起来相信会对 Spring Boot 后端开发的完整链路有更直观的认识。代码能跑只是第一步把结构写清晰、把边界想清楚才是真正提升的过程。收藏这篇文章下次写同类系统时直接对照操作就好。