这个JAVA多商户家政服务平台的设计初衷,是解决传统家政服务行业存在的几个痛点:服务预约效率低、商户管理混乱、客户体验割裂。平台通过"预约抢单+自营商城"的双引擎模式,实现了从服务匹配到商品销售的全流程闭环。
我去年参与过一个类似项目的重构,当时客户最大的抱怨就是各个功能模块像孤岛一样互不相通。这次设计的亮点在于:
后端采用Spring Boot 2.7 + MyBatis Plus组合,这个选择基于三个考虑:
数据库使用MySQL 8.0,主要利用其:
系统拆分为四个微服务:
这种划分的边界清晰,但要注意分布式事务问题。我们最终采用Seata的AT模式解决跨服务订单创建问题。
抢单功能的核心代码逻辑:
java复制// 使用Redis的Sorted Set实现抢单队列
public boolean grabOrder(Long orderId, Long staffId) {
String key = "grab_queue:" + orderId;
// 判断是否已被抢
if (redisTemplate.opsForZSet().size(key) > 0) {
return false;
}
// 加入抢单队列,score为时间戳
return redisTemplate.opsForZSet().add(key, staffId, System.currentTimeMillis());
}
几个关键设计点:
商户入驻流程包含三个状态机:
我们采用Spring StateMachine实现,配置示例:
java复制@Configuration
@EnableStateMachine(name = "merchantStateMachine")
public class MerchantStateMachineConfig extends EnumStateMachineConfigurerAdapter<MerchantStates, MerchantEvents> {
@Override
public void configure(StateMachineStateConfigurer<MerchantStates, MerchantEvents> states) throws Exception {
states.withStates()
.initial(MerchantStates.PENDING)
.state(MerchantStates.APPROVED)
.state(MerchantStates.REJECTED);
}
}
商城商品表通过service_id字段与服务项目关联:
sql复制CREATE TABLE `mall_product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`service_id` bigint DEFAULT NULL COMMENT '关联的服务ID',
`name` varchar(100) NOT NULL,
`price` decimal(10,2) NOT NULL,
`merchant_id` bigint NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_service` (`service_id`),
KEY `idx_merchant` (`merchant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这种设计实现了:
基于用户行为的协同过滤算法:
java复制DataModel model = new FileDataModel(new File("behavior.csv"));
UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
UserNeighborhood neighborhood = new NearestNUserNeighborhood(20, similarity, model);
Recommender recommender = new GenericUserBasedRecommender(model, neighborhood, similarity);
采用多级缓存架构:
配置示例:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000));
return cacheManager;
}
}
订单表按照商户ID分片,采用ShardingSphere实现:
yaml复制spring:
shardingsphere:
datasource:
names: ds0,ds1
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..15}
table-strategy:
inline:
sharding-column: merchant_id
algorithm-expression: t_order_$->{merchant_id % 16}
采用四层防护:
支付流程关键代码:
java复制public PaymentResult pay(PaymentRequest request) {
// 1. 验证签名
if (!signatureService.verify(request)) {
throw new SecurityException("签名验证失败");
}
// 2. 执行风控规则
RiskEngine.check(request);
// 3. 调用支付渠道
return paymentChannelService.pay(request);
}
多商户数据隔离通过以下方式实现:
核心监控指标包括:
使用Prometheus + Grafana搭建看板,关键配置:
yaml复制- job_name: 'order_service'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['order-service:8080']
采用ELK栈处理日志:
日志查询优化技巧:
json复制{
"query": {
"bool": {
"must": [
{ "term": { "merchantId": "123" }},
{ "range": { "@timestamp": { "gte": "now-1d/d" }}}
]
}
}
}
现象:高峰期抢单响应慢
排查过程:
解决方案:
java复制@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory factory = new LettuceConnectionFactory();
factory.setShareNativeConnection(false);
factory.setPoolConfig(new GenericObjectPoolConfig<>());
return factory;
}
现象:A商户看到B商户的数据
原因:MyBatis拦截器没有正确解析商户上下文
修复方案:
java复制@Intercepts(@Signature(type= Executor.class, method="query",
args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class MerchantInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 从ThreadLocal获取当前商户
Long merchantId = MerchantContext.get();
// 修改参数对象
ParameterHandler ph = (ParameterHandler) invocation.getArgs()[1];
if (ph instanceof DefaultParameterHandler) {
((DefaultParameterHandler) ph).getParameterObject().setMerchantId(merchantId);
}
return invocation.proceed();
}
}
通过Spring的@Conditional实现功能插件化:
java复制public interface PaymentPlugin {
PaymentResult pay(PaymentRequest request);
}
@Configuration
@ConditionalOnProperty(name = "payment.alipay.enabled", havingValue = "true")
public class AlipayConfig {
@Bean
public PaymentPlugin alipayPlugin() {
return new AlipayPlugin();
}
}
使用Activiti实现可配置的服务流程:
java复制public void startServiceFlow(Long orderId) {
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
"serviceFlow",
Variables.putValue("orderId", orderId)
);
// 存储流程实例ID到订单
orderService.updateOrderProcessId(orderId, instance.getId());
}
Docker Compose编排文件示例:
yaml复制version: '3'
services:
user-service:
image: registry.example.com/user-service:1.0
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
redis:
image: redis:6
ports:
- "6379:6379"
采用Nginx + Lua实现:
nginx复制location /api {
access_by_lua_block {
local merchantId = ngx.var.arg_merchantId
if merchantId and string.sub(merchantId, -1) == "0" then
ngx.var.backend = "new_version"
else
ngx.var.backend = "old_version"
end
}
proxy_pass http://$backend;
}
采用Spotless插件统一代码风格:
gradle复制spotless {
java {
googleJavaFormat()
removeUnusedImports()
trimTrailingWhitespace()
}
}
使用Swagger + YApi的方案:
示例注解:
java复制@Operation(summary = "提交抢单")
@PostMapping("/grab")
public ResponseEntity<GrabResult> grabOrder(
@Parameter(description = "订单ID") @RequestParam Long orderId) {
// ...
}
使用JMeter模拟抢单场景:
测试关键指标:
重点测试以下场景:
测试用例示例:
java复制@Test
public void testIdempotentGrab() {
// 第一次抢单
grabOrder(1L, 100L);
// 第二次相同请求
GrabResult result = grabOrder(1L, 100L);
assertEquals("ALREADY_GRABBED", result.getCode());
}
在实际开发中,有几点特别值得注意:
商户入驻流程一定要做成分步式,我们最初设计的单页表单转化率只有30%,改成三步表单后提升到65%
抢单功能的超时时间要根据业务场景动态可调,我们遇到过因固定设置30秒导致农村地区商户抢单困难的问题
商城商品一定要与服务强关联,初期我们尝试过独立运营商城,转化率不到2%,改为服务关联推荐后达到15%
分布式事务要慎用,能通过最终一致性解决的场景就不要用强一致性,我们曾因过度使用Seata导致订单创建性能下降70%