Skip to content

多端接入架构指南

📋 文档说明

本文档详细说明了 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 TokenSession/CookieAdmin Token
接口路径/api/app/**/api/web/**/api/admin/**
分页大小10-2020-5050-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/admin

Step 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/admin

Step 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);
    }
}

📚 相关文档


📝 总结

关键要点

  1. 业务逻辑统一: 所有端共享 Domain 层和 App 层
  2. 接口差异化: 不同端有不同的 Controller 和 DTO
  3. 权限分离: 权限控制在 Trigger 层,不影响业务逻辑
  4. 独立部署: 支持按端独立部署和扩缩容
  5. 渐进式迁移: 可以逐步从单端迁移到多端

实施建议

  1. 先重构现有代码: 按照 CQRS 原则重构现有的 App 端代码
  2. 再添加新端: 在重构完成后,再添加 Web 端和 Admin 端
  3. 逐步迁移: 不要一次性迁移所有接口,可以逐个模块迁移
  4. 保持兼容: 在迁移过程中保持旧接口的兼容性

文档版本: v1.0
最后更新: 2025-11-14
维护者: Tour Mate Platform Team

Powered by VitePress