SpringBoot + Thymeleaf 实战:手把手教你从零撸一个婚纱租赁网站(附完整源码)
2026/5/7 14:19:59 网站建设 项目流程

SpringBoot + Thymeleaf 实战:从零构建婚纱租赁平台全流程指南

最近在技术社区看到不少关于SpringBoot实战项目的讨论,其中婚纱租赁系统这个选题引起了我的注意。作为一个典型的电商类应用,它既包含了商品管理、订单处理等通用功能,又涉及租赁业务特有的状态流转逻辑,非常适合用来练手。今天我就带大家用SpringBoot和Thymeleaf这对黄金组合,从零开始搭建一个完整的婚纱租赁平台。

这个项目特别适合已经掌握Java基础,想通过实战提升SpringBoot应用能力的中级开发者。我们会采用经典的MVC架构,前端使用Thymeleaf模板引擎实现服务端渲染,后端则基于SpringBoot快速搭建RESTful API。过程中我会重点分享那些官方文档里不会告诉你的实战技巧,比如如何优雅处理文件上传、设计租赁状态机,以及避免Thymeleaf的常见配置陷阱。

1. 项目初始化与环境搭建

开始编码前,我们需要准备好开发环境。推荐使用IntelliJ IDEA作为IDE,它对于SpringBoot项目的支持非常完善。通过Spring Initializr创建项目时,记得勾选以下关键依赖:

<dependencies> <!-- Web基础 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 模板引擎 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- 持久层 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- 开发工具 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> </dependencies>

application.properties中配置Thymeleaf模板引擎时,有几个关键参数需要注意:

# Thymeleaf配置 spring.thymeleaf.cache=false # 开发时关闭缓存 spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html spring.thymeleaf.mode=HTML spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.servlet.content-type=text/html

提示:在团队协作开发时,建议使用spring.thymeleaf.prefix=file:src/main/resources/templates/配置,这样修改模板文件后无需重启应用就能看到变化。

2. 数据库设计与实体建模

婚纱租赁系统的核心是商品和订单管理,我们需要设计合理的数据库结构。以下是主要实体关系:

实体主要字段关联关系
Userid, username, password, phone一对多Order
Dressid, name, price, stock, images一对多OrderItem
Orderid, status, totalAmount, address多对一User
OrderItemid, quantity, rentalDays多对一Order, 多对一Dress

对应的JPA实体类定义示例:

@Entity public class Dress { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private BigDecimal price; private Integer stock; @ElementCollection @CollectionTable(name = "dress_images", joinColumns = @JoinColumn(name = "dress_id")) @Column(name = "image_url") private List<String> images = new ArrayList<>(); // getters and setters } @Entity public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Enumerated(EnumType.STRING) private OrderStatus status; @ManyToOne private User user; @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) private List<OrderItem> items = new ArrayList<>(); public enum OrderStatus { PENDING, PAID, DELIVERED, RETURNED, CANCELLED } // 计算总金额的业务方法 public BigDecimal getTotalAmount() { return items.stream() .map(item -> item.getDress().getPrice() .multiply(BigDecimal.valueOf(item.getRentalDays()))) .reduce(BigDecimal.ZERO, BigDecimal::add); } }

注意:租赁业务需要特别关注库存扣减逻辑。建议使用乐观锁机制防止超租:

@Transactional public boolean rentDress(Long dressId, int quantity) { Dress dress = dressRepository.findById(dressId) .orElseThrow(() -> new RuntimeException("Dress not found")); if (dress.getStock() < quantity) { return false; } dress.setStock(dress.getStock() - quantity); dressRepository.save(dress); return true; }

3. 前端页面与Thymeleaf整合实战

Thymeleaf最大的优势在于它支持自然模板——即使没有后端服务,HTML文件也能正常在浏览器中打开。我们来看一个商品列表页的实现:

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>婚纱列表</title> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> </head> <body> <div class="container mt-4"> <div th:replace="~{fragments/header :: header}"></div> <div class="row"> <div class="col-md-3"> <div th:replace="~{fragments/sidebar :: categories}"></div> </div> <div class="col-md-9"> <div class="row"> <div th:each="dress : ${dresses}" class="col-md-4 mb-4"> <div class="card h-100"> <img th:src="@{${dress.images[0]}}" class="card-img-top" alt="婚纱图片"> <div class="card-body"> <h5 class="card-title" th:text="${dress.name}"></h5> <p class="card-text"> 价格: <span th:text="${#numbers.formatDecimal(dress.price, 1, 2)}"></span>元/天 </p> <a th:href="@{/dress/} + ${dress.id}" class="btn btn-primary">查看详情</a> </div> </div> </div> </div> </div> </div> </div> </body> </html>

几个Thymeleaf使用技巧:

  • 使用th:replace实现页面片段复用
  • th:each处理集合数据渲染
  • #numbers工具类格式化数字显示
  • 使用@{}语法生成正确的URL路径

对于表单处理,Thymeleaf也能很好地与Spring MVC绑定:

<form th:action="@{/order/create}" method="post" th:object="${orderForm}"> <div class="form-group"> <label>租赁天数</label> <input type="number" class="form-control" th:field="*{rentalDays}" min="1" max="30"> </div> <div class="form-group"> <label>收货地址</label> <textarea class="form-control" th:field="*{shippingAddress}" rows="3"></textarea> </div> <button type="submit" class="btn btn-primary">提交订单</button> </form>

对应的控制器代码:

@Controller @RequestMapping("/order") public class OrderController { @PostMapping("/create") public String createOrder(@Valid OrderForm form, BindingResult result, @AuthenticationPrincipal User user) { if (result.hasErrors()) { return "order/create"; } Order order = orderService.createOrder(user, form); return "redirect:/order/" + order.getId(); } }

4. 核心业务逻辑实现

4.1 购物车系统设计

购物车是电商系统的核心组件,我们采用Session存储临时购物车数据:

@Component @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) public class ShoppingCart { private final Map<Long, CartItem> items = new ConcurrentHashMap<>(); public void addItem(Dress dress, int rentalDays) { items.compute(dress.getId(), (k, v) -> { if (v == null) { return new CartItem(dress, rentalDays); } v.setRentalDays(v.getRentalDays() + rentalDays); return v; }); } public List<CartItem> getItems() { return new ArrayList<>(items.values()); } public BigDecimal getTotal() { return items.values().stream() .map(item -> item.getDress().getPrice() .multiply(BigDecimal.valueOf(item.getRentalDays()))) .reduce(BigDecimal.ZERO, BigDecimal::add); } @Data public static class CartItem { private final Dress dress; private int rentalDays; } }

在控制器中注入购物车:

@Controller @RequestMapping("/cart") public class CartController { private final ShoppingCart cart; @PostMapping("/add") public String addToCart(@RequestParam Long dressId, @RequestParam int rentalDays) { Dress dress = dressService.findById(dressId); cart.addItem(dress, rentalDays); return "redirect:/cart"; } @GetMapping public String viewCart(Model model) { model.addAttribute("cartItems", cart.getItems()); model.addAttribute("total", cart.getTotal()); return "cart/view"; } }

4.2 订单状态机实现

租赁业务比普通电商多了归还流程,我们需要设计严谨的状态流转:

@Service @Transactional public class OrderService { private final OrderRepository orderRepo; public Order createOrder(User user, OrderForm form) { Order order = new Order(); order.setUser(user); order.setStatus(OrderStatus.PENDING); // 转换购物车项为订单项 form.getItems().forEach(item -> { OrderItem orderItem = new OrderItem(); orderItem.setDress(item.getDress()); orderItem.setRentalDays(item.getRentalDays()); order.addItem(orderItem); }); return orderRepo.save(order); } public void processPayment(Long orderId) { Order order = orderRepo.findById(orderId) .orElseThrow(() -> new OrderNotFoundException(orderId)); if (order.getStatus() != OrderStatus.PENDING) { throw new IllegalStateException("订单状态异常"); } order.setStatus(OrderStatus.PAID); orderRepo.save(order); } public void markAsDelivered(Long orderId) { Order order = orderRepo.findById(orderId) .orElseThrow(() -> new OrderNotFoundException(orderId)); if (order.getStatus() != OrderStatus.PAID) { throw new IllegalStateException("订单状态异常"); } order.setStatus(OrderStatus.DELIVERED); orderRepo.save(order); } public void processReturn(Long orderId) { Order order = orderRepo.findById(orderId) .orElseThrow(() -> new OrderNotFoundException(orderId)); if (order.getStatus() != OrderStatus.DELIVERED) { throw new IllegalStateException("订单状态异常"); } order.setStatus(OrderStatus.RETURNED); // 恢复库存 order.getItems().forEach(item -> { Dress dress = item.getDress(); dress.setStock(dress.getStock() + 1); }); orderRepo.save(order); } }

4.3 文件上传与婚纱图片管理

婚纱展示需要高质量的图片,我们实现多图上传功能:

@Controller @RequestMapping("/admin/dress") public class AdminDressController { @Value("${upload.path}") private String uploadPath; @PostMapping("/{id}/images") public String uploadImages(@PathVariable Long id, @RequestParam("files") MultipartFile[] files) { Dress dress = dressService.findById(id); Arrays.stream(files).forEach(file -> { if (!file.isEmpty()) { try { String filename = UUID.randomUUID() + "." + StringUtils.getFilenameExtension(file.getOriginalFilename()); Path path = Paths.get(uploadPath, filename); Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING); dress.getImages().add("/uploads/" + filename); } catch (IOException e) { throw new RuntimeException("文件上传失败", e); } } }); dressService.save(dress); return "redirect:/admin/dress/" + id; } }

对应的配置文件:

# 文件上传配置 spring.servlet.multipart.max-file-size=5MB spring.servlet.multipart.max-request-size=10MB upload.path=./uploads

记得创建上传目录并配置资源映射:

@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${upload.path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**") .addResourceLocations("file:" + uploadPath + "/"); } }

5. 项目部署与性能优化

5.1 生产环境配置

部署到生产环境时,需要调整以下配置:

# 生产环境配置示例 spring.profiles.active=prod spring.thymeleaf.cache=true spring.datasource.url=jdbc:mysql://prod-db:3306/wedding_rental spring.datasource.username=app_user spring.datasource.password=${DB_PASSWORD} spring.jpa.hibernate.ddl-auto=validate

5.2 性能优化建议

  1. Thymeleaf模板缓存:生产环境务必开启模板缓存
  2. 静态资源处理
    • 启用Gzip压缩
    • 配置CDN加速静态资源
  3. 数据库优化
    • 为常用查询字段添加索引
    • 考虑使用二级缓存(如Ehcache)
  4. 会话管理
    • 将会话数据迁移到Redis
    • 配置合理的会话超时时间

示例缓存配置:

@Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { return new ConcurrentMapCacheManager("dresses", "categories"); } } @Service public class DressService { @Cacheable("dresses") public Dress findById(Long id) { return dressRepository.findById(id) .orElseThrow(() -> new DressNotFoundException(id)); } }

6. 项目扩展方向

完成基础功能后,可以考虑以下扩展:

  1. 预约试穿功能:添加日历预约系统
  2. 智能推荐:基于用户浏览历史推荐相关婚纱
  3. 会员积分系统:租赁累积积分可兑换优惠
  4. 多店铺支持:扩展为婚纱租赁平台
  5. 微信小程序端:开发移动端应用

以预约系统为例,可以这样实现:

@Entity public class Appointment { @Id @GeneratedValue private Long id; @ManyToOne private User user; @ManyToOne private Dress dress; private LocalDateTime startTime; private LocalDateTime endTime; @Enumerated(EnumType.STRING) private AppointmentStatus status; } public interface AppointmentRepository extends JpaRepository<Appointment, Long> { @Query("SELECT a FROM Appointment a " + "WHERE a.dress = :dress " + "AND a.status = 'CONFIRMED' " + "AND ((a.startTime BETWEEN :start AND :end) " + "OR (a.endTime BETWEEN :start AND :end))") List<Appointment> findConflicts(@Param("dress") Dress dress, @Param("start") LocalDateTime start, @Param("end") LocalDateTime end); }

在实现这个项目的过程中,最大的挑战是租赁业务的状态流转设计。特别是处理并发租赁请求时,如何保证库存数据的准确性需要特别注意。我最终采用了乐观锁结合数据库事务的方案,在实际测试中表现良好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询