1. 项目背景与核心需求
民宿行业近年来呈现爆发式增长,但传统预订方式存在诸多痛点:电话预订容易出现沟通误差、第三方平台抽成过高(通常15%-20%)、房态更新不及时导致超订等。我们团队在实际调研中发现,超过60%的民宿业主希望建立自有预订渠道,而微信小程序凭借其10亿+的用户基础和即用即走的特性,成为最理想的解决方案载体。
这个基于SpringBoot的民宿预订小程序系统,主要解决三个核心问题:
- 实时房态同步:通过数据库事务控制,确保在多用户并发访问时不会出现"超卖"
- 多角色权限管理:设计了游客、会员、民宿主、平台管理员四级权限体系
- 轻量化交互体验:针对移动端特别优化了图片懒加载和表单验证逻辑
2. 技术架构设计
2.1 整体技术栈选型
mermaid复制graph TD
A[微信小程序] --> B[SpringBoot 2.7.x]
B --> C[MySQL 8.0]
B --> D[Redis 6.2]
C --> E[MyBatis-Plus 3.5.x]
D --> F[分布式锁]
选择这套技术栈主要基于以下考量:
- SpringBoot:快速构建RESTful API,内置Tomcat简化部署
- MyBatis-Plus:相比原生MyBatis减少约40%的SQL编写量
- Redis:处理秒杀类场景的库存缓存,实测QPS可达5000+
2.2 数据库设计要点
民宿系统的核心表关系如下:
sql复制CREATE TABLE `homestay` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) COLLATE utf8mb4_bin NOT NULL COMMENT '民宿名称',
`cover_img` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '封面图URL',
`price` decimal(10,2) NOT NULL COMMENT '基础价格',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '0下架 1可预订',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `room` (
`id` bigint NOT NULL AUTO_INCREMENT,
`homestay_id` bigint NOT NULL,
`type_name` varchar(50) COLLATE utf8mb4_bin NOT NULL COMMENT '房型名称',
`inventory` int NOT NULL COMMENT '库存量',
PRIMARY KEY (`id`),
KEY `idx_homestay` (`homestay_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
特别注意的点:
- 价格字段使用DECIMAL(10,2)避免浮点精度问题
- 为关联查询字段建立索引
- 使用utf8mb4字符集支持emoji表情
3. 核心功能实现
3.1 微信登录集成
小程序端调用wx.login获取code,后端通过AuthService处理:
java复制@Service
public class AuthServiceImpl implements AuthService {
@Value("${wx.appid}")
private String appid;
@Value("${wx.secret}")
private String secret;
@Override
public WxAuthResponse wxLogin(String code) {
// 1. 调用微信接口获取session_key
String url = String.format(
"https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
appid, secret, code);
// 2. 使用RestTemplate发起HTTP请求
WxAuthResponse response = restTemplate.getForObject(url, WxAuthResponse.class);
// 3. 生成自定义token
String token = JwtUtils.generateToken(response.getOpenid());
// 4. 缓存用户会话信息
redisTemplate.opsForValue().set(
"user:session:" + response.getOpenid(),
response.getSession_key(),
30, TimeUnit.MINUTES);
return response.setToken(token);
}
}
关键点:session_key需要缓存但不应返回给前端,token有效期建议设置为7天
3.2 高并发库存控制
采用Redis分布式锁+数据库乐观锁双重保障:
java复制public boolean bookRoom(Long roomId, LocalDate checkInDate, int nights) {
// 分布式锁key
String lockKey = "lock:room:" + roomId + ":" + checkInDate;
// 使用UUID作为锁值,防止误删
String lockValue = UUID.randomUUID().toString();
try {
// 尝试获取锁,设置10秒过期
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(locked)) {
throw new BusinessException("当前预订人数过多,请稍后再试");
}
// 查询房态
Room room = roomMapper.selectById(roomId);
if (room.getInventory() <= 0) {
throw new BusinessException("该房型已售罄");
}
// 更新库存(带版本号乐观锁)
int updated = roomMapper.updateInventoryWithVersion(
roomId,
room.getVersion());
if (updated == 0) {
throw new ConcurrentUpdateException("库存变更冲突");
}
// 创建订单记录
return createOrder(roomId, checkInDate, nights);
} finally {
// 释放锁时要验证lockValue
String currentValue = redisTemplate.opsForValue().get(lockKey);
if (lockValue.equals(currentValue)) {
redisTemplate.delete(lockKey);
}
}
}
4. 性能优化实践
4.1 图片加载优化
采用三级缓存策略:
- 微信小程序本地缓存(优先)
- CDN静态资源加速
- 后端动态压缩(使用Thumbnailator库)
配置示例:
properties复制# application.properties
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**
4.2 SQL查询优化
针对高频查询使用覆盖索引:
sql复制ALTER TABLE `order` ADD INDEX `idx_user_status` (`user_id`, `status`);
复杂分页查询优化:
java复制public Page<OrderVO> queryUserOrders(Long userId, int page, int size) {
// 先查ID(避免回表)
Page<Long> idPage = new Page<>(page, size);
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>()
.select(Order::getId)
.eq(Order::getUserId, userId)
.orderByDesc(Order::getCreateTime);
orderMapper.selectPage(idPage, queryWrapper);
// 批量查询明细
if (!idPage.getRecords().isEmpty()) {
List<OrderVO> details = orderMapper.batchSelectDetail(idPage.getRecords());
return idPage.convert(id -> details.stream()
.filter(d -> d.getId().equals(id))
.findFirst()
.orElse(null));
}
return new Page<>();
}
5. 安全防护措施
5.1 接口防刷策略
使用Guava RateLimiter做API限流:
java复制@Aspect
@Component
public class RateLimitAspect {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String key = request.getRemoteAddr();
RateLimiter limiter = limiters.computeIfAbsent(key,
k -> RateLimiter.create(rateLimit.value()));
if (!limiter.tryAcquire()) {
throw new BusinessException("操作过于频繁,请稍后再试");
}
return pjp.proceed();
}
}
5.2 XSS防护方案
采用Jsoup做HTML过滤:
java复制public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
XssHttpServletRequestWrapper wrappedRequest =
new XssHttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(wrappedRequest, response);
}
}
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return StringUtils.isBlank(value) ? value : Jsoup.clean(value, Whitelist.basic());
}
}
6. 部署与监控
6.1 容器化部署
Docker Compose配置示例:
yaml复制version: '3'
services:
app:
image: homestay:1.0
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2-alpine
ports:
- "6379:6379"
volumes:
mysql_data:
6.2 监控方案
集成Spring Boot Actuator + Prometheus:
xml复制<!-- pom.xml -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
配置项:
properties复制management.endpoints.web.exposure.include=health,metrics,prometheus
management.metrics.export.prometheus.enabled=true
7. 开发经验总结
在实际开发中,我们遇到了几个典型问题及解决方案:
-
微信支付回调处理:
- 问题:微信支付异步通知可能因网络问题重复发送
- 解决:使用redis记录处理状态,关键代码:
java复制String payNotifyKey = "pay:notify:" + outTradeNo; if (redisTemplate.opsForValue().setIfAbsent(payNotifyKey, "1", 24, TimeUnit.HOURS)) { // 处理业务逻辑 } -
地理位置搜索优化:
- 问题:传统LIKE查询无法满足周边搜索需求
- 解决:采用MySQL空间索引:
sql复制ALTER TABLE homestay ADD SPATIAL INDEX `idx_location` (`location`); SELECT id, ST_Distance_Sphere(location, POINT(116.404, 39.915)) AS distance FROM homestay WHERE ST_Distance_Sphere(location, POINT(116.404, 39.915)) < 5000 ORDER BY distance; -
小程序包体积控制:
- 将静态资源转移到CDN
- 使用分包加载策略
- 启用微信云开发存储非核心资源
这个项目从技术选型到最终上线共耗时3个月,期间最大的收获是认识到分布式环境下数据一致性的重要性。建议后续开发者重点关注:
- 事务消息机制的应用
- 灰度发布方案的设计
- 全链路压测的实施