来源:酒店-餐厅双租户拆分 时间:2026-09-30
酒店点餐模块拆分为两个租户:
顾客通过二维码扫码下单,订单归属酒店租户(tenant_id=8)。但后续订单操作(接单、备餐、出餐、配送、完成、退单等)可能由餐厅员工在餐厅租户下执行。
MyBatis-Plus 的 TenantLineInnerInterceptor 会根据当前登录上下文自动追加 tenant_id = 当前租户ID:
WHERE ... AND tenant_id = 当前上下文WHERE ... AND tenant_id = 当前上下文WHERE ... AND tenant_id = 当前上下文传参数不能覆盖拦截器行为!
// ❌ 错误:以为传了 tenantId=8 就能查 tenant_id=8 的数据
orderMapper.selectKitchenOrderList(8, queryDate);
// 实际生成的 SQL:
SELECT ... FROM hotel_order
WHERE tenant_id = 8 -- XML 显式条件(参数传入)
AND tenant_id = 9 -- 拦截器自动追加(当前登录上下文)
-- 两个条件矛盾 → 查不到数据!
用于查询/操作另一个租户的数据。
// ✅ 正确:用 executeWithTenant 切换上下文
final Long hotelTenantId = 8L; // 从映射表解析
TenantContextHolder.executeWithTenant(hotelTenantId, new Runnable() {
@Override
public void run() {
// 拦截器现在追加 tenant_id = 8
orderMapper.selectKitchenOrderList(hotelTenantId, queryDate);
orderMapper.updateById(order); // WHERE tenant_id = 8
}
});
关键点:
executeWithTenant 切换的是整个上下文,拦截器和 XML 都会使用新值Supplier<T> 重载Runnable 重载用于系统级配置表(如映射表),所有租户都需要访问。
// ✅ 正确:映射表是系统级配置,用 executeIgnore 绕过
default HotelTenantMapping selectByRestaurantTenantId(Long restaurantTenantId) {
return TenantContextHolder.executeIgnore(() -> _selectByRestaurantTenantId(restaurantTenantId));
}
关键点:
tenant_id 应设为 1(系统级)tenant_id = ? 条件default 方法 + executeIgnore 包裹实际查询方法CREATE TABLE hotel_tenant_mapping (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 1 COMMENT '系统级配置,固定为1',
hotel_tenant_id BIGINT NOT NULL COMMENT '酒店租户ID',
restaurant_tenant_id BIGINT NOT NULL COMMENT '餐厅租户ID',
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
...
);
// Mapper 接口:用 default 方法 + executeIgnore
default Long selectRestaurantTenantId(Long hotelTenantId) {
return TenantContextHolder.executeIgnore(() -> _selectRestaurantTenantId(hotelTenantId));
}
// XML:不写 tenant_id 条件
<select id="_selectRestaurantTenantId" resultType="java.lang.Long">
SELECT restaurant_tenant_id
FROM hotel_tenant_mapping
WHERE hotel_tenant_id = #{hotelTenantId}
AND status = 'ACTIVE'
AND del_flag = 0
LIMIT 1
</select>
private Long resolveHotelTenantForAdmin() {
Long currentTenantId = resolveTenantId();
// 尝试从映射表反查:restaurant_tenant_id -> hotel_tenant_id
HotelTenantMapping mapping = tenantMappingMapper.selectByRestaurantTenantId(currentTenantId);
if (mapping != null && mapping.getHotelTenantId() != null) {
return mapping.getHotelTenantId();
}
// 未找到映射,说明当前就是酒店租户或未配置双租户
return currentTenantId;
}
private Long resolveRestaurantTenantId(Long hotelTenantId) {
Long restaurantTenantId = tenantMappingMapper.selectRestaurantTenantId(hotelTenantId);
return restaurantTenantId != null ? restaurantTenantId : hotelTenantId;
}
@Override
public List<HotelOrderVO> kitchenOrderList(final Date queryDate) {
final Long hotelTenantId = resolveHotelTenantForAdmin();
return TenantContextHolder.executeWithTenant(hotelTenantId, new java.util.function.Supplier<List<HotelOrderVO>>() {
@Override
public List<HotelOrderVO> get() {
List<HotelOrderVO> list = orderMapper.selectKitchenOrderList(hotelTenantId, queryDate);
for (HotelOrderVO vo : list) {
fillOrderItems(vo);
}
return list;
}
});
}
@Override
@Transactional(rollbackFor = Exception.class)
public void orderAccept(final Long id) {
final Long hotelTenantId = resolveHotelTenantForAdmin();
TenantContextHolder.executeWithTenant(hotelTenantId, new Runnable() {
@Override
public void run() {
HotelOrder order = requireOrder(id);
if (order.getStatus() != HotelOrderConstants.STATUS_PLACED) {
throw new BusinessException("只有待接单的订单才能接单");
}
order.setStatus(HotelOrderConstants.STATUS_ACCEPTED);
order.setAcceptTime(new Date());
orderMapper.updateById(order); // WHERE tenant_id = hotelTenantId
}
});
}
@Override
public DashboardVO dashboard() {
final Long hotelTenantId = resolveHotelTenantForAdmin();
final DashboardVO vo = new DashboardVO();
// 订单查询:切换到酒店租户
TenantContextHolder.executeWithTenant(hotelTenantId, new Runnable() {
@Override
public void run() {
// 订单相关查询...
}
});
// 营业时段:用当前上下文(餐厅租户)
List<HotelBusinessHours> hoursList = businessHoursMapper.selectEnabled(resolveTenantId());
// 餐段订单统计:切回酒店租户
TenantContextHolder.executeWithTenant(hotelTenantId, new Runnable() {
@Override
public void run() {
// 订单统计查询...
}
});
return vo;
}
// orderComplete 在 executeWithTenant(8, ...) 内调用 incrementDishSales
public void orderComplete(Long id) {
executeWithTenant(8, () -> {
// ... 订单操作 ...
incrementDishSales(id); // ❌ 内部 resolveTenantId() 返回 8(酒店租户)
});
}
private void incrementDishSales(Long orderId) {
// resolveTenantId() 返回 8(因为外层已切换到酒店租户)
Long restaurantTenantId = resolveRestaurantTenantId(resolveTenantId());
// resolveRestaurantTenantId(8) → 查映射表 → 返回 9 ✅ 碰巧正确
// 但如果映射表查询失败,回退返回 8 → executeWithTenant(8) → 查不到菜品数据 ❌
}
// 在切换租户前保存原始餐厅租户ID
public void orderComplete(final Long id) {
final Long hotelTenantId = resolveHotelTenantForAdmin();
final Long originalRestaurantTenantId = resolveRestaurantTenantId(resolveTenantId());
TenantContextHolder.executeWithTenant(hotelTenantId, new Runnable() {
@Override
public void run() {
// ... 订单操作 ...
incrementDishSales(id, originalRestaurantTenantId); // ✅ 显式传递
}
});
}
private void incrementDishSales(Long orderId, Long restaurantTenantId) {
List<HotelOrderItem> items = orderItemMapper.selectByOrderId(orderId);
TenantContextHolder.executeWithTenant(restaurantTenantId, new Runnable() {
@Override
public void run() {
for (HotelOrderItem item : items) {
dishMapper.incrementSales(item.getDishId(), item.getQuantity());
}
}
});
}
UPDATE hotel_order
SET status = 2, ..., tenant_id = 8 -- 实体内存中的值(加载时是 8,未改动)
WHERE id = ? AND tenant_id = 8 -- 拦截器追加
结论:tenant_id 保持原值,不会变。
UPDATE hotel_order
SET status = 7, reject_reason = ? -- 只 SET 显式指定的字段
WHERE id = ? AND tenant_id = 8 -- 拦截器追加
结论:没有 .set("tenant_id", ...) → tenant_id 不会被修改。
UPDATE hotel_order SET del_flag = id WHERE id = ? AND tenant_id = 8
结论:逻辑删除只改 del_flag,tenant_id 不变。
resolveHotelTenantForAdmin() 查映射表 → 未找到(8 是 hotel_tenant_id,不是 restaurant_tenant_id)→ 回退返回 8executeWithTenant(8, ...) → 当前上下文已经是 8 → no-op,无变化resolveHotelTenantForAdmin() 查映射表 → 找到 hotel_tenant_id = 8 → 返回 8executeWithTenant(8, ...) → 从 9 切换到 8 → 订单相关操作正确执行菜品管理、菜品分类、规格管理、营业时段、房间管理、入住管理、二维码管理、支付回调、对账管理等模块完全不涉及跨租户,不受影响。
executeWithTenant 切换上下文executeIgnore:映射表等跨租户共享数据,tenant_id = 1requireOrder + updateById 必须在同一个 executeWithTenant 内executeWithTenant(当前租户, ...) 是 no-op,不会改变现有行为updateById 的 SET 来自实体内存值,UpdateWrapper 只 SET 显式字段hotel_tenant_mapping(Flyway V1.0.144)HotelTenantMappingMapper.java / HotelTenantMappingMapper.xmlHotelOrderServiceImpl.java(所有订单操作方法)TenantContextHolder.executeWithTenant() / executeIgnore()