作为一名长期从事企业级应用开发的Java工程师,我最近完成了一个基于SpringBoot的家政服务管理系统项目。这个系统源于我朋友经营的一家本地家政公司的实际需求——他们迫切需要从传统的纸质记录和电话预约转型为数字化管理。
家政行业在近五年保持着年均18%的增长率,但大多数中小型家政公司仍在使用Excel甚至纸质笔记本来管理订单。这种管理方式存在三个致命问题:一是服务人员调度完全依赖经验,经常出现时间冲突;二是客户无法实时查看服务进度;三是历史数据难以统计分析。这正是我们构建这个系统的核心驱动力。
选择SpringBoot作为基础框架经过了多重考量。首先,它的自动配置特性让我们能快速搭建起包含安全认证、数据库连接等企业级功能的系统。其次,内嵌Tomcat简化了部署流程,这对技术储备有限的家政公司尤为重要。最后,Spring生态完善的扩展性为未来可能的微服务拆分预留了空间。
经过对三个主流技术方案的对比测试,我们最终确定了以下技术组合:
后端核心:
数据层:
前端方案:
辅助工具:
技术选型心得:MyBatis-Plus相比JPA在复杂查询场景下更符合国内开发习惯,其Wrapper条件构造器能大幅减少SQL编写。Redis不仅用于缓存,其分布式锁特性在解决订单并发问题时表现出色。
系统采用经典的三层架构,但针对家政业务特点做了特殊优化:
code复制┌─────────────────────────────────────┐
│ 表现层 (Presentation) │
│ ┌─────────┐ ┌──────────┐ │
│ │ Web │ │ Mobile │ │
│ │(Vue.js) │ │(小程序) │ │
│ └─────────┘ └──────────┘ │
└───────────────┬─────────────────────┘
│ HTTP/JSON
┌───────────────▼─────────────────────┐
│ 业务层 (Service) │
│ ┌──────────────────────────────┐ │
│ │ 订单服务 │ 支付服务 │ 调度服务 │ │
│ └──────────────────────────────┘ │
└───────────────┬─────────────────────┘
│ JDBC/MyBatis
┌───────────────▼─────────────────────┐
│ 持久层 (Persistence) │
│ ┌──────────────┬────────────────┐ │
│ │ MySQL │ Redis │ │
│ │ (业务数据) │ (缓存/锁) │ │
│ └──────────────┴────────────────┘ │
└─────────────────────────────────────┘
关键设计决策:
家政系统涉及三类角色:客户、服务人员和管理员。我们采用RBAC模型实现权限控制,但在数据模型上做了优化:
java复制@Entity
@Table(name = "sys_user")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@JsonIgnore
private String password;
@Enumerated(EnumType.STRING)
private UserType type; // CUSTOMER, WORKER, ADMIN
// 关联角色组
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "sys_user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
// 员工专属字段
private String skillLevel;
private String idCardNo;
private LocalDate joinDate;
}
安全配置的关键代码:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/orders/**").hasAnyRole("CUSTOMER", "WORKER")
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtFilter jwtFilter() {
return new JwtFilter();
}
}
踩坑记录:最初使用LAZY加载角色导致权限校验失败,必须设为EAGER。后来通过JWT Claims携带角色信息优化了性能。
订单状态机设计是核心难点,我们采用枚举实现状态流转:
java复制public enum OrderStatus {
PENDING {
public boolean canTransferTo(CONFIRMED) { return true; }
},
CONFIRMED {
public boolean canTransferTo(IN_PROGRESS) { return true; }
},
IN_PROGRESS {
public boolean canTransferTo(COMPLETED) { return true; }
},
COMPLETED,
CANCELLED;
public boolean canTransferTo(OrderStatus next) {
return false;
}
}
订单创建时的并发控制方案:
java复制@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final RedisTemplate<String, String> redisTemplate;
private final OrderMapper orderMapper;
@Transactional
public Order createOrder(OrderDTO dto) {
String lockKey = "order:lock:" + dto.getWorkerId() + ":" + dto.getServiceTime();
try {
// 获取分布式锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(locked)) {
throw new BusinessException("操作过于频繁,请稍后重试");
}
// 检查时间冲突
if (orderMapper.existsConflict(dto.getWorkerId(), dto.getServiceTime())) {
throw new BusinessException("该时段已被预约");
}
Order order = OrderMapper.INSTANCE.toEntity(dto);
order.setStatus(OrderStatus.PENDING);
orderMapper.insert(order);
return order;
} finally {
redisTemplate.delete(lockKey);
}
}
}
我们开发了基于规则的初级调度算法:
java复制public class SchedulingService {
public Worker assignWorker(Order order) {
// 规则1:优先选择技能匹配的员工
List<Worker> candidates = workerDao.findBySkill(order.getRequiredSkill());
// 规则2:优先选择当前任务最少的员工
candidates.sort(Comparator.comparingInt(w ->
orderDao.countTodayOrdersByWorker(w.getId())));
// 规则3:优先选择距离客户最近的员工(需要GIS数据)
if (!candidates.isEmpty()) {
return candidates.get(0);
}
throw new NoAvailableWorkerException();
}
}
为解决"虚假打卡"问题,我们设计了三级验证机制:
实现代码示例:
java复制public class VerificationService {
public boolean verifyServiceCompletion(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
// 检查GPS轨迹
GpsTrack track = gpsService.getTrack(order.getWorker().getId(),
order.getServiceTime());
if (!geoService.isInRange(track, order.getCustomer().getAddress())) {
return false;
}
// 检查人脸识别记录
if (!faceCheckService.hasValidCheck(order.getWorker().getId(),
order.getServiceTime())) {
return false;
}
return true;
}
}
支付模块最易出现数据不一致,我们的解决方案:
java复制@Transactional
public void processPaymentCallback(PaymentCallback callback) {
Payment payment = paymentDao.findByOrderId(callback.getOrderId());
if (payment == null) {
throw new PaymentException("订单不存在");
}
if (payment.getStatus() == PaymentStatus.SUCCESS) {
log.warn("重复回调: {}", callback);
return;
}
payment.setStatus(callback.isSuccess() ? SUCCESS : FAILED);
payment.setCallbackTime(LocalDateTime.now());
paymentDao.update(payment);
if (callback.isSuccess()) {
orderService.updateOrderStatus(payment.getOrderId(), CONFIRMED);
}
}
我们采用多级缓存方案:
配置示例:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000));
return manager;
}
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
发现订单查询接口存在N+1问题:
sql复制-- 优化前
SELECT * FROM orders WHERE user_id = ?;
-- 对每个订单执行:
SELECT * FROM services WHERE id = ?;
优化方案:
java复制@Mapper
public interface OrderMapper {
@Select("SELECT o.*, s.name as service_name, s.price as service_price " +
"FROM orders o LEFT JOIN services s ON o.service_id = s.id " +
"WHERE o.user_id = #{userId}")
@Results({
@Result(property = "service.id", column = "service_id"),
@Result(property = "service.name", column = "service_name"),
@Result(property = "service.price", column = "service_price")
})
List<Order> findByUserIdWithService(Long userId);
}
Docker Compose编排方案:
yaml复制version: '3.8'
services:
app:
image: homemgmt:1.0
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=homemgmt
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana
ports:
- "3000:3000"
volumes:
mysql_data:
SpringBoot Actuator配置:
properties复制management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.metrics.export.prometheus.enabled=true
management.metrics.tags.application=homemgmt
关键监控指标:
经过三个月的开发和优化,系统已在试点门店稳定运行,主要带来以下改进:
后续演进方向:
在开发过程中,最大的收获是认识到业务复杂性往往超过技术难度。例如,家政服务中的"服务完成"判定就需要考虑多种异常场景。这提醒我们在设计阶段就要深入业务现场,避免技术方案与实际情况脱节。