一套后端API驱动四端——织码在线教育系统多端统一学习体验设计

引言

“培训视频手机上能看吗?”“下班路上能用平板学吗?”“微信里能直接打开吗?”——这是企业培训管理者每天都在被问到的问题。对学员而言,期望在任何设备、任何时间都能无缝进入学习状态;对开发团队而言,同时维护 Web、App、小程序、H5 四套独立前端代码是沉重的负担,功能迭代要同步四次、Bug 要修四处、接口要对四遍。

织码在线教育系统通过一套后端 API 驱动四端的架构设计,实现了真正意义上的多端统一——Web 端、App 端、微信小程序、H5 页面,四端数据同源、进度同步、体验一致。后端只需维护一套 RESTful API,前端各端按场景选型、按需适配,既保证了学员的无缝体验,又将研发维护成本降低了 60% 以上。

本文将从架构设计和工程实践两个维度,深入拆解这套多端统一方案的技术细节,涵盖四端覆盖矩阵、多端数据兼容设计、学习进度跨端同步、多端会话管理、Web SSR 方案以及 App 多端编译策略。


一、四端覆盖矩阵与统一架构

1.1 四端技术选型

系统覆盖的四端及其技术方案如下:

技术方案适用场景核心特性
Web 学习端Nuxt 3 + Vue 3 SSRPC/移动浏览器SSR 首屏直出、SEO 友好、秒级加载
App 移动端UniApp(H5/小程序/Android/iOS)主力移动学习入口原生体验、推送通知、断点续学
微信小程序UniApp 小程序编译即用即走场景无需安装、微信生态内传播
H5 页面UniApp H5 编译分享链接直接访问零安装门槛、浏览器直接打开

四端共享同一套后端 RESTful API,差异仅体现在前端渲染层和特定能力(如推送通知、本地缓存策略)。核心原则是:业务逻辑在后端收口,前端只负责展示和交互。

1.2 统一 API 网关设计

所有端的请求统一经过 API 网关,网关负责鉴权、限流、日志和请求路由,后端微服务对各端透明:

# Spring Cloud Gateway 路由配置(application.yml)
spring:
  cloud:
    gateway:
      routes:
        - id: course-service
          uri: lb://course-service
          predicates:
            - Path=/api/course/**
          filters:
            - name: RequestRateLimiter  # 限流:每端独立计数
              args:
                redis-rate-limiter.replenishRate: 50
                redis-rate-limiter.burstCapacity: 100
            - name: JwtAuthFilter         # 统一 JWT 鉴权

网关层通过 X-Client-Type 请求头识别来源端(WEB/APP/MINIPROGRAM/H5),后端服务可据此做差异化处理(如小程序端返回精简字段),但绝大多数接口逻辑对各端完全一致。

在这里插入图片描述

二、多端数据兼容设计

2.1 标准化数据结构

不同端对数据的消费方式存在差异:Web 端和 App 端的视频播放器 SDK 不同,所需播放凭证格式不同;小程序端受限于包体积,图片需要按需加载缩略图;分页场景下,小程序下拉加载更多和 PC 端分页器的交互模式也不同。

解法: 后端 API 返回标准化数据结构,各端按需取用字段,避免后端针对不同端出多套接口。视频播放凭证由各端客户端在播放时按需请求,不在列表接口中预取。

// 统一课程详情响应结构
public class CourseDetailVO {
    private Long courseId;
    private String title;
    private String coverUrl;          // 封面图(各端自行按需缩放)
    private String description;
    private List<ChapterVO> chapters; // 章节列表(各端按需展开)
    private CourseStatVO stats;       // 统计数据
    // 不在此处返回视频播放凭证,由播放时按需请求
}

2.2 分页统一与端适配

分页是各端差异最大的交互之一。后端统一返回分页元数据,各端自行决定展示方式——PC 端渲染分页器,移动端渲染"加载更多"按钮,小程序用 onReachBottom 触发下拉加载:

// 统一分页响应结构
public class PageResult<T> {
    private List<T> list;        // 当前页数据
    private Long total;          // 总记录数
    private Integer pageNum;     // 当前页码
    private Integer pageSize;    // 每页条数
    private Integer totalPages;  // 总页数
    private Boolean hasNext;     // 是否有下一页(移动端下拉加载用)
}

// 课程列表接口(各端通用)
@GetMapping("/api/course/list")
public Result<PageResult<CourseListVO>> listCourses(
        @RequestParam(defaultValue = "1") Integer pageNum,
        @RequestParam(defaultValue = "10") Integer pageSize,
        @RequestHeader(value = "X-Client-Type", defaultValue = "WEB") String clientType) {
    // 小程序端默认每页返回 20 条,减少请求频次
    if ("MINIPROGRAM".equals(clientType) && pageSize == 10) {
        pageSize = 20;
    }
    PageResult<CourseListVO> result = courseService.listCourses(pageNum, pageSize);
    return Result.success(result);
}

通过 hasNext 字段,移动端无需计算总页数即可判断是否继续加载,简化了前端逻辑。


三、学习进度跨端同步

3.1 进度数据模型

学员在 Web 端看了 30% 的课程,切换到 App 端应该从 30% 处继续,而不是从头开始。这要求学习进度以服务端为主,客户端本地缓存为辅

进度持久化的数据表设计:

-- 学习进度记录表
CREATE TABLE `edu_learn_progress` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint NOT NULL COMMENT '学员ID',
  `course_id` bigint NOT NULL COMMENT '课程ID',
  `chapter_id` bigint NOT NULL COMMENT '章节ID',
  `watched_seconds` int NOT NULL DEFAULT 0 COMMENT '已观看秒数',
  `total_seconds` int NOT NULL DEFAULT 0 COMMENT '视频总秒数',
  `progress_percent` decimal(5,2) NOT NULL DEFAULT 0.00 COMMENT '进度百分比',
  `last_position` int NOT NULL DEFAULT 0 COMMENT '最后播放位置(秒)',
  `last_client_type` varchar(20) DEFAULT NULL COMMENT '最后上报的端类型',
  `last_report_time` datetime DEFAULT NULL COMMENT '最后上报时间',
  `completed` tinyint NOT NULL DEFAULT 0 COMMENT '是否完成: 0未完成 1已完成',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_chapter` (`user_id`, `chapter_id`),
  KEY `idx_user_course` (`user_id`, `course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习进度记录表';

关键设计: last_position 字段精确记录到秒,last_client_type 记录最后上报的端类型,便于排查跨端同步异常。唯一索引 uk_user_chapter 保证同一学员同一章节只有一条进度记录,避免多端并发写入产生脏数据。

3.2 进度上报与断点续学

客户端每隔固定间隔(如每 15 秒)上报一次观看进度,切换端时直接从服务端拉取最新进度继续播放:

// 视频学习进度上报(各端通用接口)
@PostMapping("/api/course/progress/report")
public Result<Void> reportProgress(@RequestBody ProgressReportDTO dto) {
    // dto 包含:courseId, chapterId, watchedSeconds, totalSeconds, clientType
    Long userId = getCurrentUserId();
    
    // 防回退:只更新比当前更大的进度位置
    LearnProgress existing = progressService.getProgress(userId, dto.getChapterId());
    if (existing != null && dto.getWatchedSeconds() < existing.getLastPosition()) {
        // 客户端回退(如拖动进度条),不覆盖已有进度
        return Result.success();
    }
    
    // 更新进度
    progressService.updateWatchProgress(userId, dto.getCourseId(),
            dto.getChapterId(), dto.getWatchedSeconds(), dto.getTotalSeconds(),
            dto.getClientType());
    return Result.success();
}

// 获取学习进度(各端通用接口,返回服务端最新进度)
@GetMapping("/api/course/{courseId}/progress")
public Result<CourseProgressVO> getProgress(@PathVariable Long courseId) {
    return Result.success(
        progressService.getCourseProgress(getCurrentUserId(), courseId)
    );
}

CourseProgressVO 返回课程下所有章节的进度汇总,客户端据此显示整体进度条和各章节完成状态:

// 课程进度汇总响应
public class CourseProgressVO {
    private Long courseId;
    private Integer totalChapters;        // 总章节数
    private Integer completedChapters;    // 已完成章节数
    private BigDecimal overallPercent;    // 整体进度百分比
    private List<ChapterProgressVO> chapters; // 各章节进度详情
}

public class ChapterProgressVO {
    private Long chapterId;
    private Integer lastPosition;         // 上次播放位置(秒)
    private BigDecimal percent;           // 本章进度百分比
    private Boolean completed;            // 是否完成
}

在这里插入图片描述


四、多端会话统一管理

4.1 JWT + Redis 多端会话

学员在四端使用同一账号,需要支持多端同时在线,同时要保证会话安全。方案采用 JWT Token + Redis 会话存储,按 userId + clientType 维度管理会话:

@PostMapping("/api/auth/login")
public Result<LoginVO> login(@RequestBody LoginDTO dto) {
    // 1. 校验账号密码 / 手机验证码
    User user = authService.authenticate(dto);
    
    // 2. 签发 JWT Token(统一格式,四端通用)
    String token = jwtUtil.generateToken(user.getId(), user.getRole());
    
    // 3. 写入 Redis 会话(支持多端同时在线)
    String sessionKey = "session:" + user.getId() + ":" + dto.getClientType();
    redisTemplate.opsForValue().set(sessionKey, token, 7, TimeUnit.DAYS);
    
    return Result.success(new LoginVO(token, user));
}

clientType 枚举值包括 WEBAPPMINIPROGRAMH5,各端会话独立管理,互不干扰。学员可以同时在 PC 端学习视频、在手机上做题,不会因为一端登录而踢出另一端。

4.2 会话安全设计

Redis 中的会话存储结构如下:

# Redis Key 结构
session:{userId}:{clientType} → JWT Token(TTL 7天)

# 示例
session:10001:WEB          → "eyJhbGciOiJIUzI1NiJ9..."(PC 端会话)
session:10001:APP          → "eyJhbGciOiJIUzI1NiJ9..."(手机端会话)
session:10001:MINIPROGRAM  → "eyJhbGciOiJIUzI1NiJ9..."(小程序会话)
session:10001:H5           → "eyJhbGciOiJIUzI1NiJ9..."(H5 会话)
// 统一鉴权过滤器:校验 Token 有效性
@Component
public class JwtAuthFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = extractToken(exchange.getRequest());
        String clientType = exchange.getRequest().getHeaders().getFirst("X-Client-Type");
        
        if (token == null) {
            return unauthorized(exchange, "未登录");
        }
        
        // 1. 校验 JWT 签名和过期时间
        Claims claims = jwtUtil.parseToken(token);
        if (claims == null) {
            return unauthorized(exchange, "Token无效或已过期");
        }
        
        // 2. 校验 Redis 会话是否存在(防止 Token 被撤销后仍可用)
        String sessionKey = "session:" + claims.getUserId() + ":" + clientType;
        String storedToken = redisTemplate.opsForValue().get(sessionKey);
        if (!token.equals(storedToken)) {
            return unauthorized(exchange, "会话已失效,请重新登录");
        }
        
        // 3. 续期会话(活跃用户自动延长)
        redisTemplate.expire(sessionKey, 7, TimeUnit.DAYS);
        
        return chain.filter(exchange);
    }
}

双层校验机制: JWT 签名校验保证 Token 未被篡改,Redis 会话校验保证 Token 未被主动撤销(如用户修改密码后全端登出)。通过遍历 session:{userId}:* 删除所有端的会话即可实现全端登出。


五、Web 学习端 SSR 方案

Web 学习端选择 Nuxt 3 做 SSR,出发点有两个:

SEO 需求: 课程详情页、资讯文章等需要被搜索引擎收录,SSR 确保首屏 HTML 完整直出,爬虫无需执行 JS 即可获取全部内容。

首屏性能: 服务端渲染减少客户端在 hydration 前的白屏时间,学员打开课程页面直接看到完整内容,而非先看到骨架屏再等待数据加载。

// Nuxt 3 课程详情页 SSR 数据获取
// pages/course/[id].vue
<script setup>
const route = useRoute()
// useAsyncData 在服务端执行,数据随 HTML 一同下发
const { data: course } = await useAsyncData(
  `course-${route.params.id}`,
  () => $fetch(`/api/course/${route.params.id}`)
)
// 设置 SEO meta(服务端渲染时生效,爬虫可读取)
useSeoMeta({
  title: course.value?.title,
  description: course.value?.description,
  ogImage: course.value?.coverUrl
})
</script>

SSR 方案下,后端 API 的响应速度直接影响首屏渲染时间。系统对课程详情等高频接口做了 Redis 缓存 + 降级策略

// 课程详情接口(SSR 友好,支持缓存)
@GetMapping("/api/course/{courseId}")
public Result<CourseDetailVO> getCourseDetail(@PathVariable Long courseId) {
    String cacheKey = "course:detail:" + courseId;
    
    // 1. 先查 Redis 缓存
    CourseDetailVO cached = (CourseDetailVO) redisTemplate.opsForValue().get(cacheKey);
    if (cached != null) {
        return Result.success(cached);
    }
    
    // 2. 缓存未命中,查数据库并回填缓存
    CourseDetailVO detail = courseService.getCourseDetail(courseId);
    redisTemplate.opsForValue().set(cacheKey, detail, 30, TimeUnit.MINUTES);
    
    return Result.success(detail);
}

缓存 TTL 设为 30 分钟,课程上下架时通过事件主动清除缓存,保证数据一致性。


六、App 多端编译与差异化处理

6.1 UniApp 条件编译

App 移动端基于 UniApp 开发,一套 Vue 3 代码可编译为 H5、微信小程序、Android App、iOS App 四个目标。针对不同端的差异,通过条件编译处理:

<template>
  <view>
    <!-- 通用内容 -->
    <video-player :src="videoUrl" />
    
    <!-- #ifdef MP-WEIXIN -->
    <!-- 微信小程序特有:分享按钮 -->
    <button open-type="share">分享给朋友</button>
    <!-- #endif -->
    
    <!-- #ifdef APP-PLUS -->
    <!-- 原生App特有:画中画浮窗学习 -->
    <pip-button @click="enablePip" />
    <!-- #endif -->
  </view>
</template>

6.2 多端视频播放器适配

视频播放是多端差异最大的场景。不同端使用不同的播放器 SDK,但播放凭证(PlayAuth)通过统一的后端接口获取,实现"同源凭证、各端播放":

// 统一播放凭证获取接口(各端通用)
@GetMapping("/api/course/chapter/{chapterId}/playauth")
public Result<PlayAuthVO> getPlayAuth(
        @PathVariable Long chapterId,
        @RequestHeader("X-Client-Type") String clientType) {
    Long userId = getCurrentUserId();
    
    // 1. 校验学习权限
    courseService.checkAccess(userId, chapterId);
    
    // 2. 获取阿里云 VOD 播放凭证
    String playAuth = vodService.getPlayAuth(chapterId);
    
    // 3. 记录播放日志(含端类型)
    learnLogService.logPlay(userId, chapterId, clientType);
    
    return Result.success(new PlayAuthVO(playAuth, videoId));
}

各端的播放器适配策略:

播放器方案凭证使用特有能力
Web 端Aliplayer Web SDKPlayAuth倍速播放、清晰度切换
H5 端Aliplayer H5PlayAuth适配移动浏览器
小程序端<video> 组件 + VOD 小程序插件PlayAuth微信内分享
原生 AppVOD iOS/Android SDKPlayAuth + STS画中画、后台播放、下载离线

在这里插入图片描述

6.3 进度同步实测效果

多端进度同步的实际表现:

场景:学员在 PC 端 Web 学习端观看《Python 入门》第 3 章,看到 00:28:45

操作:关闭 PC 端,打开手机 App 端进入同一课程

结果:
  - App 端显示:"上次看到 00:28:45,是否从此处继续?"
  - 学员点击"继续观看",从 28 分 45 秒精确续播
  - 学习进度条显示同步后的状态

数据流:
  PC 端每 15s 上报进度 → 写入 MySQL edu_learn_progress 表
  App 端打开课程 → 查询 edu_learn_progress 表获取 last_position
  → 精确定位播放位置 → 继续上报进度

七、多端覆盖带来的实际价值

用户角色典型场景多端带来的价值
出差中的销售人员高铁上用手机 App 看产品培训不受地点限制,碎片时间利用
倒班制一线员工休息时间微信小程序学习无需安装 App,即用即走
需要备考的员工通勤路上 H5 端刷题分享链接直接打开,零门槛
管理者手机端查看各部门培训进度随时掌握团队学习数据

对运营团队而言,一套内容录制一次、上架一次,自动覆盖全渠道,维护成本相比多套独立系统降低 60% 以上。


八、总结

织码在线教育系统的多端统一方案在技术实现上重点解决了以下问题:

  1. 后端 API 统一:一套 RESTful API + 标准化数据结构,四端按需取用字段,避免多套接口的维护灾难
  2. 会话管理统一:JWT + Redis 按 userId + clientType 维度管理,支持多端同时在线与全端登出
  3. 进度数据统一:服务端 MySQL 持久化为主,客户端定期上报,防回退机制保证进度不丢失,切端无缝衔接
  4. 前端按场景选型:Nuxt 3 SSR 服务 SEO 与首屏性能需求,UniApp 一套代码覆盖移动四端,各端用最合适的技术

"多端覆盖"在今天的企业培训场景下已经不是加分项,而是基本要求。学员的学习时间碎片化、设备多样化是既成现实,培训系统若只能在电脑上运行,就等于自动放弃了大量的学习时机。如果你对多端架构设计的某个技术点有疑问,欢迎评论区交流。

如需私有化部署报价、远程产品演示,可访问官网https://www.weavecodes.com/,私信作者领取企业落地案例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值