多端接入架构指南
📋 文档说明
本文档详细说明了 Tour Mate Platform 如何支持多个客户端(App、Web、Admin)接入,以及如何在保持业务逻辑统一的前提下,为不同端提供差异化的服务。
文档版本: v1.0
最后更新: 2025-11-14
适用场景: App 端、Web 端、后台管理系统
🎯 设计目标
1. 业务逻辑统一
- ✅ 所有端共享同一套 Domain 层和 App 层
- ✅ 避免重复开发和维护成本
- ✅ 保证业务规则的一致性
2. 接口差异化
- ✅ 不同端可以有不同的接口定义
- ✅ 不同端可以有不同的权限控制
- ✅ 不同端可以有不同的数据展示
3. 独立部署
- ✅ 支持按端独立部署
- ✅ 支持按端独立扩缩容
- ✅ 支持按端独立版本管理
🏗️ 整体架构
┌─────────────────────────────────────────────────────────────┐
│ 客户端层 │
├─────────────────────────────────────────────────────────────┤
│ App 客户端 │ Web 客户端 │ Admin 客户端 │
│ (iOS/Android) │ (浏览器) │ (管理后台) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Trigger 层(接口层) │
├─────────────────────────────────────────────────────────────┤
│ App Controller │ Web Controller │ Admin Controller │
│ /api/app/** │ /api/web/** │ /api/admin/** │
│ │ │ │
│ - 协议转换 │ - 协议转换 │ - 协议转换 │
│ - 参数校验 │ - 参数校验 │ - 参数校验 │
│ - 权限控制 │ - 权限控制 │ - 权限控制 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ App 层(应用服务层) │
│ 【所有端共享】 │
├─────────────────────────────────────────────────────────────┤
│ 命令服务 │ 查询服务 │
│ - ActivityApplicationService│ - ActivityQueryApplicationService│
│ - UserApplicationService │ - UserQueryApplicationService │
│ - ChatApplicationService │ - ChatQueryApplicationService │
│ │ │
│ 【业务逻辑统一,所有端复用】 │ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Domain 层(领域层) │
│ Infrastructure 层(基础设施层) │
│ 【所有端共享】 │
└─────────────────────────────────────────────────────────────┘📁 推荐的目录结构
方案 1: 按客户端类型划分(推荐)
tour-mate-platform/
│
├── tour-mate-platform-trigger/ # Trigger 层
│ └── src/main/java/com/alisunxin/api/trigger/
│ ├── app/ # App 端触发器
│ │ ├── ActivityController.java
│ │ ├── UserController.java
│ │ ├── ChatController.java
│ │ └── interceptor/
│ │ └── AppAuthInterceptor.java
│ │
│ ├── web/ # Web 端触发器
│ │ ├── WebActivityController.java
│ │ ├── WebUserController.java
│ │ ├── WebChatController.java
│ │ └── interceptor/
│ │ └── WebAuthInterceptor.java
│ │
│ └── admin/ # 管理端触发器
│ ├── AdminActivityController.java
│ ├── AdminUserController.java
│ ├── AdminStatisticsController.java
│ ├── AdminSystemController.java
│ └── interceptor/
│ └── AdminAuthInterceptor.java
│
├── tour-mate-platform-api/ # API 层(接口契约)
│ └── src/main/java/com/alisunxin/api/api/
│ ├── app/ # App 端 DTO
│ │ ├── request/
│ │ └── response/
│ │
│ ├── web/ # Web 端 DTO
│ │ ├── request/
│ │ └── response/
│ │
│ └── admin/ # 管理端 DTO
│ ├── request/
│ └── response/
│
├── tour-mate-platform-app/ # App 层(应用服务)
│ └── src/main/java/com/alisunxin/api/service/
│ ├── ActivityApplicationService.java # 所有端共享
│ ├── ActivityQueryApplicationService.java
│ ├── UserApplicationService.java
│ └── ...
│
├── tour-mate-platform-domain/ # Domain 层(所有端共享)
├── tour-mate-platform-infrastructure/ # Infrastructure 层(所有端共享)
└── tour-mate-platform-types/ # Types 层(所有端共享)方案 2: 单 Controller 多路径映射
tour-mate-platform-trigger/
└── src/main/java/com/alisunxin/api/trigger/http/
├── ActivityController.java # 统一的 Controller
├── UserController.java
└── ...在 Controller 中通过路径区分:
java
@RestController
@RequestMapping("/api")
public class ActivityController {
// App 端接口
@GetMapping("/app/activities/my/created")
public Response<List<AppActivityResponse>> getMyCreatedActivitiesForApp() {
// App 端逻辑
}
// Web 端接口
@GetMapping("/web/activities/my/created")
public Response<List<WebActivityResponse>> getMyCreatedActivitiesForWeb() {
// Web 端逻辑
}
// Admin 端接口
@GetMapping("/admin/activities/created")
public Response<List<AdminActivityResponse>> getCreatedActivitiesForAdmin() {
// Admin 端逻辑
}
}🎨 三端差异化设计
1. App 端(移动端)
特点
- 用户视角,关注个人数据
- 需要轻量级响应
- 需要推送通知支持
- 需要地理位置服务
接口示例
java
@RestController
@RequestMapping("/api/app/activity")
public class ActivityController {
@Resource
private ActivityApplicationService activityApplicationService;
@Resource
private ActivityQueryApplicationService activityQueryApplicationService;
/**
* App 端 - 查询我创建的活动
* 特点:只返回必要字段,减少流量消耗
*/
@GetMapping("/my/created")
public Response<List<AppActivityResponse>> getMyCreatedActivities() {
String userId = getCurrentUserId();
List<ActivityAggregate> aggregates = activityQueryApplicationService
.getUserCreatedActivities(userId);
// 转换为 App 端响应对象(轻量级)
List<AppActivityResponse> responses = aggregates.stream()
.map(this::convertToAppResponse)
.collect(Collectors.toList());
return Response.success(responses);
}
/**
* App 端 - 附近的活动
* 特点:基于地理位置查询
*/
@GetMapping("/nearby")
public Response<List<AppActivityResponse>> getNearbyActivities(
@RequestParam Double longitude,
@RequestParam Double latitude,
@RequestParam(defaultValue = "5") Integer radius) {
// 调用应用服务查询附近活动
List<ActivityAggregate> aggregates = activityQueryApplicationService
.getNearbyActivities(longitude, latitude, radius);
return Response.success(convertToAppResponses(aggregates));
}
/**
* 转换为 App 端响应对象(轻量级)
*/
private AppActivityResponse convertToAppResponse(ActivityAggregate aggregate) {
return AppActivityResponse.builder()
.activityId(aggregate.getActivityId().getValue())
.title(aggregate.getTitle())
.locationName(aggregate.getLocation().getName())
.startTime(aggregate.getStartTime())
.currentParticipants(aggregate.getCurrentParticipants())
.maxParticipants(aggregate.getMaxParticipants())
.status(aggregate.getStatus().name())
.coverImage(aggregate.getCoverImage()) // 缩略图
.build();
}
}App 端 DTO 示例
java
@Data
@Builder
public class AppActivityResponse {
private String activityId;
private String title;
private String locationName;
private LocalDateTime startTime;
private Integer currentParticipants;
private Integer maxParticipants;
private String status;
private String coverImage; // 只返回缩略图 URL
// 不包含详细描述、完整地址等大字段
}2. Web 端(PC 端)
特点
- 用户视角,但展示更丰富
- 可以返回更多数据
- 支持复杂筛选和排序
- 更好的用户体验
接口示例
java
@RestController
@RequestMapping("/api/web/activity")
public class WebActivityController {
@Resource
private ActivityQueryApplicationService activityQueryApplicationService;
/**
* Web 端 - 查询我创建的活动
* 特点:返回更丰富的数据,支持分页和排序
*/
@GetMapping("/my/created")
public Response<PageResult<WebActivityResponse>> getMyCreatedActivities(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize,
@RequestParam(required = false) String sortBy) {
String userId = getCurrentUserId();
// 调用应用服务(支持分页)
PageResult<ActivityAggregate> pageResult = activityQueryApplicationService
.getUserCreatedActivitiesWithPage(userId, page, pageSize, sortBy);
// 转换为 Web 端响应对象(更丰富)
List<WebActivityResponse> responses = pageResult.getData().stream()
.map(this::convertToWebResponse)
.collect(Collectors.toList());
return Response.success(PageResult.of(responses, pageResult.getTotal()));
}
/**
* Web 端 - 活动搜索
* 特点:支持复杂的搜索条件
*/
@PostMapping("/search")
public Response<PageResult<WebActivityResponse>> searchActivities(
@RequestBody WebActivitySearchRequest request) {
// 调用应用服务进行复杂查询
PageResult<ActivityAggregate> pageResult = activityQueryApplicationService
.searchActivities(request);
return Response.success(convertToWebPageResult(pageResult));
}
/**
* 转换为 Web 端响应对象(更丰富)
*/
private WebActivityResponse convertToWebResponse(ActivityAggregate aggregate) {
return WebActivityResponse.builder()
.activityId(aggregate.getActivityId().getValue())
.title(aggregate.getTitle())
.description(aggregate.getDescription()) // 完整描述
.location(convertToLocationVO(aggregate.getLocation())) // 完整地址
.startTime(aggregate.getStartTime())
.endTime(aggregate.getEndTime())
.currentParticipants(aggregate.getCurrentParticipants())
.maxParticipants(aggregate.getMaxParticipants())
.status(aggregate.getStatus().name())
.tags(aggregate.getTags())
.coverImage(aggregate.getCoverImage())
.images(aggregate.getImages()) // 多张图片
.creator(convertToCreatorVO(aggregate.getCreatorId()))
.createdTime(aggregate.getCreatedTime())
.build();
}
}Web 端 DTO 示例
java
@Data
@Builder
public class WebActivityResponse {
private String activityId;
private String title;
private String description; // 完整描述
private LocationVO location; // 完整地址信息
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer currentParticipants;
private Integer maxParticipants;
private String status;
private List<String> tags;
private String coverImage;
private List<String> images; // 多张图片
private CreatorVO creator; // 创建者信息
private LocalDateTime createdTime;
// 包含更丰富的数据
}3. Admin 端(管理后台)
特点
- 管理员视角,关注全局数据
- 需要更多的统计和分析功能
- 需要更强的权限控制
- 支持批量操作
接口示例
java
@RestController
@RequestMapping("/api/admin/activity")
public class AdminActivityController {
@Resource
private ActivityApplicationService activityApplicationService;
@Resource
private ActivityQueryApplicationService activityQueryApplicationService;
/**
* Admin 端 - 查询所有活动
* 特点:管理员可以查看所有活动,支持复杂筛选
*/
@PostMapping("/list")
public Response<PageResult<AdminActivityResponse>> listActivities(
@RequestBody AdminActivityQueryRequest request) {
// 调用应用服务查询(管理员权限)
PageResult<ActivityAggregate> pageResult = activityQueryApplicationService
.queryActivitiesForAdmin(request);
// 转换为 Admin 端响应对象(包含管理信息)
List<AdminActivityResponse> responses = pageResult.getData().stream()
.map(this::convertToAdminResponse)
.collect(Collectors.toList());
return Response.success(PageResult.of(responses, pageResult.getTotal()));
}
/**
* Admin 端 - 活动统计
* 特点:提供丰富的统计数据
*/
@GetMapping("/statistics")
public Response<ActivityStatisticsVO> getActivityStatistics(
@RequestParam(required = false) LocalDateTime startDate,
@RequestParam(required = false) LocalDateTime endDate) {
// 调用应用服务获取统计数据
ActivityStatisticsVO statistics = activityQueryApplicationService
.getActivityStatistics(startDate, endDate);
return Response.success(statistics);
}
/**
* Admin 端 - 强制取消活动
* 特点:管理员可以强制取消任何活动
*/
@PostMapping("/force-cancel")
public Response<Void> forceCancelActivity(
@RequestBody AdminCancelActivityRequest request) {
// 调用应用服务(管理员权限)
activityApplicationService.forceCancelActivity(
request.getActivityId(),
request.getReason(),
getCurrentAdminId()
);
return Response.success();
}
/**
* Admin 端 - 批量操作
* 特点:支持批量审核、批量下架等
*/
@PostMapping("/batch-operation")
public Response<BatchOperationResult> batchOperation(
@RequestBody AdminBatchOperationRequest request) {
// 调用应用服务进行批量操作
BatchOperationResult result = activityApplicationService
.batchOperation(request);
return Response.success(result);
}
/**
* 转换为 Admin 端响应对象(包含管理信息)
*/
private AdminActivityResponse convertToAdminResponse(ActivityAggregate aggregate) {
return AdminActivityResponse.builder()
.activityId(aggregate.getActivityId().getValue())
.title(aggregate.getTitle())
.description(aggregate.getDescription())
.location(convertToLocationVO(aggregate.getLocation()))
.startTime(aggregate.getStartTime())
.endTime(aggregate.getEndTime())
.currentParticipants(aggregate.getCurrentParticipants())
.maxParticipants(aggregate.getMaxParticipants())
.status(aggregate.getStatus().name())
.tags(aggregate.getTags())
.creator(convertToCreatorVO(aggregate.getCreatorId()))
.createdTime(aggregate.getCreatedTime())
.updatedTime(aggregate.getUpdatedTime())
// 管理信息
.reportCount(aggregate.getReportCount()) // 举报次数
.viewCount(aggregate.getViewCount()) // 浏览次数
.isReviewed(aggregate.isReviewed()) // 是否已审核
.reviewedBy(aggregate.getReviewedBy()) // 审核人
.reviewedTime(aggregate.getReviewedTime()) // 审核时间
.build();
}
}Admin 端 DTO 示例
java
@Data
@Builder
public class AdminActivityResponse {
// 基础信息
private String activityId;
private String title;
private String description;
private LocationVO location;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer currentParticipants;
private Integer maxParticipants;
private String status;
private List<String> tags;
private CreatorVO creator;
private LocalDateTime createdTime;
private LocalDateTime updatedTime;
// 管理信息(只有管理端需要)
private Integer reportCount; // 举报次数
private Integer viewCount; // 浏览次数
private Boolean isReviewed; // 是否已审核
private String reviewedBy; // 审核人
private LocalDateTime reviewedTime; // 审核时间
private String riskLevel; // 风险等级
private List<String> violations; // 违规记录
}🔐 权限控制
1. 基于拦截器的权限控制
java
// App 端拦截器
@Component
public class AppAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 验证 App 端用户 Token
String token = request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new UnauthorizedException("未登录");
}
// 验证 Token 并获取用户信息
UserInfo userInfo = jwtService.validateToken(token);
// 将用户信息放入上下文
UserContext.setCurrentUser(userInfo);
return true;
}
}
// Web 端拦截器
@Component
public class WebAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// Web 端可能使用 Session 或 Cookie
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
throw new UnauthorizedException("未登录");
}
UserInfo userInfo = (UserInfo) session.getAttribute("user");
UserContext.setCurrentUser(userInfo);
return true;
}
}
// Admin 端拦截器
@Component
public class AdminAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 验证管理员 Token
String token = request.getHeader("Admin-Token");
if (StringUtils.isEmpty(token)) {
throw new UnauthorizedException("未登录");
}
// 验证管理员权限
AdminInfo adminInfo = adminService.validateAdminToken(token);
// 检查管理员角色和权限
if (!adminInfo.hasPermission(getRequiredPermission(handler))) {
throw new ForbiddenException("无权限");
}
AdminContext.setCurrentAdmin(adminInfo);
return true;
}
}2. 配置拦截器
java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private AppAuthInterceptor appAuthInterceptor;
@Resource
private WebAuthInterceptor webAuthInterceptor;
@Resource
private AdminAuthInterceptor adminAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// App 端拦截器
registry.addInterceptor(appAuthInterceptor)
.addPathPatterns("/api/app/**")
.excludePathPatterns("/api/app/auth/login", "/api/app/auth/register");
// Web 端拦截器
registry.addInterceptor(webAuthInterceptor)
.addPathPatterns("/api/web/**")
.excludePathPatterns("/api/web/auth/login", "/api/web/auth/register");
// Admin 端拦截器
registry.addInterceptor(adminAuthInterceptor)
.addPathPatterns("/api/admin/**")
.excludePathPatterns("/api/admin/auth/login");
}
}📊 三端对比表
| 特性 | App 端 | Web 端 | Admin 端 |
|---|---|---|---|
| 用户视角 | 个人用户 | 个人用户 | 管理员 |
| 数据范围 | 个人数据 | 个人数据 | 全局数据 |
| 响应数据 | 轻量级 | 丰富 | 最丰富(含管理信息) |
| 权限控制 | 用户级 | 用户级 | 管理员级 |
| 认证方式 | JWT Token | Session/Cookie | Admin Token |
| 接口路径 | /api/app/** | /api/web/** | /api/admin/** |
| 分页大小 | 10-20 | 20-50 | 50-100 |
| 特殊功能 | 推送通知、地理位置 | 复杂搜索、高级筛选 | 批量操作、统计分析 |
🚀 实施步骤
阶段 1: 当前状态(单端)
tour-mate-platform-trigger/
└── http/
└── ActivityController.java # 当前只有 App 端阶段 2: 重构为多端支持
Step 1: 创建目录结构
bash
mkdir -p tour-mate-platform-trigger/src/main/java/com/alisunxin/api/trigger/app
mkdir -p tour-mate-platform-trigger/src/main/java/com/alisunxin/api/trigger/web
mkdir -p tour-mate-platform-trigger/src/main/java/com/alisunxin/api/trigger/adminStep 2: 移动现有 Controller
bash
# 将现有 Controller 移到 app 目录
mv ActivityController.java app/
mv UserController.java app/Step 3: 创建 Web 端 Controller
java
// 复制 App 端 Controller,修改路径和响应对象
@RestController
@RequestMapping("/api/web/activity")
public class WebActivityController {
// Web 端特定逻辑
}Step 4: 创建 Admin 端 Controller
java
// 创建 Admin 端 Controller,添加管理功能
@RestController
@RequestMapping("/api/admin/activity")
public class AdminActivityController {
// Admin 端特定逻辑
}Step 5: 创建各端的 DTO
bash
mkdir -p tour-mate-platform-api/src/main/java/com/alisunxin/api/api/app
mkdir -p tour-mate-platform-api/src/main/java/com/alisunxin/api/api/web
mkdir -p tour-mate-platform-api/src/main/java/com/alisunxin/api/api/adminStep 6: 配置拦截器
java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 配置各端拦截器
}
}✅ 最佳实践
1. 应用服务层完全复用
java
// ✅ 正确:所有端共享同一套应用服务
@RestController
@RequestMapping("/api/app/activity")
public class AppActivityController {
@Resource
private ActivityApplicationService activityApplicationService; // 共享
@Resource
private ActivityQueryApplicationService activityQueryApplicationService; // 共享
}
@RestController
@RequestMapping("/api/web/activity")
public class WebActivityController {
@Resource
private ActivityApplicationService activityApplicationService; // 共享
@Resource
private ActivityQueryApplicationService activityQueryApplicationService; // 共享
}2. DTO 按端分离
java
// App 端响应对象(轻量级)
public class AppActivityResponse {
private String activityId;
private String title;
private String locationName;
// 只包含必要字段
}
// Web 端响应对象(丰富)
public class WebActivityResponse {
private String activityId;
private String title;
private String description;
private LocationVO location;
// 包含更多字段
}
// Admin 端响应对象(最丰富)
public class AdminActivityResponse {
private String activityId;
private String title;
private String description;
private LocationVO location;
// 包含管理信息
private Integer reportCount;
private Boolean isReviewed;
}3. 权限在 Trigger 层处理
java
// ✅ 正确:权限控制在 Controller 或拦截器中
@RestController
@RequestMapping("/api/admin/activity")
public class AdminActivityController {
@PostMapping("/force-cancel")
@RequiresPermission("activity:force-cancel") // 权限注解
public Response<Void> forceCancelActivity(@RequestBody AdminCancelActivityRequest request) {
// 业务逻辑
}
}4. 业务逻辑不重复
java
// ❌ 错误:在不同端的 Controller 中重复业务逻辑
@RestController
@RequestMapping("/api/app/activity")
public class AppActivityController {
public Response<String> createActivity(@RequestBody AppCreateActivityRequest request) {
// 重复的业务逻辑
ActivityAggregate aggregate = ActivityAggregate.builder()
.title(request.getTitle())
// ...
.build();
activityRepository.save(aggregate);
}
}
// ✅ 正确:业务逻辑在应用服务中,Controller 只负责协议转换
@RestController
@RequestMapping("/api/app/activity")
public class AppActivityController {
@Resource
private ActivityApplicationService activityApplicationService;
public Response<String> createActivity(@RequestBody AppCreateActivityRequest request) {
// 调用应用服务
String activityId = activityApplicationService.createActivity(
userId, title, description, location,
startTime, endTime, maxParticipants, tags
);
return Response.success(activityId);
}
}📚 相关文档
📝 总结
关键要点
- ✅ 业务逻辑统一: 所有端共享 Domain 层和 App 层
- ✅ 接口差异化: 不同端有不同的 Controller 和 DTO
- ✅ 权限分离: 权限控制在 Trigger 层,不影响业务逻辑
- ✅ 独立部署: 支持按端独立部署和扩缩容
- ✅ 渐进式迁移: 可以逐步从单端迁移到多端
实施建议
- 先重构现有代码: 按照 CQRS 原则重构现有的 App 端代码
- 再添加新端: 在重构完成后,再添加 Web 端和 Admin 端
- 逐步迁移: 不要一次性迁移所有接口,可以逐个模块迁移
- 保持兼容: 在迁移过程中保持旧接口的兼容性
文档版本: v1.0
最后更新: 2025-11-14
维护者: Tour Mate Platform Team