feat: 集成阿里云 IMM (即时媒体服务) 与 OSS 跨域配置优化

主要变更:
- 新增阿里云 IMM SDK 依赖 (imm20200930 v1.28.3)
- 新增 IMM 配置类 (ImmConfig, ImmUtil, ImmController, ImmTokenVo)
- 新增前端 API 生成文件 (imm.api.ts)
- 更新 WebOffice.vue 使用新的 imm.api 导入

配置优化:
- 三环境 (dev/test/prod) 均开启 OSS CORS 跨域支持
- 添加 IMM 服务配置 (endpoint, region, project, 密钥)
- 测试/开发环境跨域域名设置为通配符

技术栈:
- 后端:Spring Boot + 阿里云 IMM SDK
- 前端:Vue 3 + TypeScript API 生成

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
En 2026-03-16 19:35:31 +08:00
parent 709e59e142
commit ce7ee34666
26 changed files with 909 additions and 4 deletions

View File

@ -0,0 +1,121 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- complementary [ref=e4]:
- generic [ref=e5]:
- generic [ref=e6]:
- img "Logo" [ref=e7]
- generic [ref=e8]:
- generic [ref=e9]: 少儿智慧阅读
- generic [ref=e10]: 服务管理后台
- menu [ref=e12]:
- menuitem "数据看板" [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic [ref=e20]: 数据看板
- menuitem "课程包管理" [ref=e21] [cursor=pointer]:
- img [ref=e22]
- generic [ref=e25]: 课程包管理
- menuitem "database 套餐管理" [ref=e26] [cursor=pointer]:
- img "database" [ref=e27]:
- img [ref=e28]
- generic [ref=e31]: 套餐管理
- menuitem "format-painter 主题字典" [ref=e32] [cursor=pointer]:
- img "format-painter" [ref=e33]:
- img [ref=e34]
- generic [ref=e37]: 主题字典
- menuitem "租户管理" [ref=e38] [cursor=pointer]:
- img [ref=e39]
- generic [ref=e44]: 租户管理
- menuitem "资源库" [ref=e45] [cursor=pointer]:
- img [ref=e46]
- generic [ref=e49]: 资源库
- menuitem "系统设置" [ref=e50] [cursor=pointer]:
- img [ref=e51]
- generic [ref=e55]: 系统设置
- generic [ref=e56]:
- generic [ref=e57]:
- img "menu-fold" [ref=e59] [cursor=pointer]:
- img [ref=e60]
- generic [ref=e63]:
- generic [ref=e65]:
- img "bell" [ref=e66] [cursor=pointer]:
- img [ref=e67]
- superscript [ref=e69]:
- paragraph [ref=e71]: "5"
- generic [ref=e73] [cursor=pointer]:
- img "user" [ref=e76]:
- img [ref=e77]
- generic [ref=e79]: 系统管理员
- img "down" [ref=e81]:
- img [ref=e82]
- main [ref=e84]:
- generic [ref=e85]:
- generic [ref=e87]:
- generic [ref=e88]:
- button "返回" [ref=e90] [cursor=pointer]:
- img "arrow-left" [ref=e91]:
- img [ref=e92]
- generic "编辑课程包" [ref=e94]
- generic [ref=e98]:
- button "保存草稿" [ref=e100] [cursor=pointer]:
- generic [ref=e101]: 保存草稿
- button "保 存" [ref=e103] [cursor=pointer]:
- generic [ref=e104]: 保 存
- generic [ref=e108]:
- generic [ref=e109]:
- button "check 基本信息" [ref=e111] [cursor=pointer]:
- img "check" [ref=e114]:
- img [ref=e115]
- generic [ref=e118]: 基本信息
- button "check 课程介绍" [ref=e120] [cursor=pointer]:
- img "check" [ref=e123]:
- img [ref=e124]
- generic [ref=e127]: 课程介绍
- button "check 排课参考" [ref=e129] [cursor=pointer]:
- img "check" [ref=e132]:
- img [ref=e133]
- generic [ref=e136]: 排课参考
- button "check 导入课" [ref=e138] [cursor=pointer]:
- img "check" [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: 导入课
- button "check 集体课" [ref=e147] [cursor=pointer]:
- img "check" [ref=e150]:
- img [ref=e151]
- generic [ref=e154]: 集体课
- button "check 领域课" [ref=e156] [cursor=pointer]:
- img "check" [ref=e159]:
- img [ref=e160]
- generic [ref=e163]: 领域课
- button "7 环创建设" [ref=e165]:
- generic [ref=e166]: "7"
- generic [ref=e168]: 环创建设
- generic [ref=e169]:
- generic [ref=e170]: 完成度
- progressbar [ref=e171]:
- img "check-circle" [ref=e176]:
- img [ref=e177]
- generic [ref=e179]:
- text: "* : * : * : : * : 26 / 200 : : : 60 / 1500 64 / 1500 71 / 1500 61 / 1500 33 / 1500 35 / 1500 33 / 1500 36 / 1500 : : : 0 / 500 : : : * : 94 / 1500 * : 64 / 1500 : 0 / 1500 : 0 / 1500 : 0 / 1500 : : : 0 / 500 : : : * : 0 / 1500 * : 0 / 1500 : 0 / 1500 : 0 / 1500 : 0 / 1500 : : : 0 / 500 : : : * : 64 / 1500 * : 0 / 1500 : 0 / 1500 : 0 / 1500 : 0 / 1500 : : : 0 / 500 : : : * : 68 / 1500 * : 0 / 1500 : 0 / 1500 : 0 / 1500 : 0 / 1500"
- generic [ref=e180]:
- generic [ref=e181]:
- generic [ref=e182]: 环创建设
- generic [ref=e183]: 已填写
- alert [ref=e185]:
- img "info-circle" [ref=e186]:
- img [ref=e187]
- generic [ref=e190]:
- generic [ref=e191]: 填写提示
- generic [ref=e192]: 环创建设内容可包括:主题环境布置、区域活动环境、阅读角创设、材料投放建议等,帮助教师更好地创设支持幼儿学习的环境。
- generic [ref=e194]:
- textbox "请输入环创建设内容,例如: - 主题墙布置建议 - 阅读区环境创设 - 材料展示区设置 - 互动区域规划 - 相关装饰物品建议等" [ref=e195]:
- /placeholder: "请输入环创建设内容,例如:\r\n- 主题墙布置建议\r\n- 阅读区环境创设\r\n- 材料展示区设置\r\n- 互动区域规划\r\n- 相关装饰物品建议等"
- text: 1. 夸夸卡展示区:展示幼儿制作的夸夸卡 2. "我的特别之处"展示墙:张贴幼儿分享的特别之处作品 3. 兔子探秘墙:张贴兔子图片和观察记录 4. 音乐角环创:张贴儿歌歌词图谱、兔子头饰 5. 健康小卫士展示区:张贴保护耳朵方法海报
- text: 116 / 3000
- generic [ref=e196]:
- button "上一步" [ref=e197] [cursor=pointer]:
- generic [ref=e198]: 上一步
- button "保 存" [active] [ref=e199] [cursor=pointer]:
- generic [ref=e200]: 保 存
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@ -0,0 +1,55 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface CourseLessonResponse {
/** ID */
id?: number;
/** 课程 ID */
courseId?: number;
/** 课程类型 */
lessonType?: string;
/** 名称 */
name?: string;
/** 描述 */
description?: string;
/** 时长(分钟) */
duration?: number;
/** 视频路径 */
videoPath?: string;
/** 视频名称 */
videoName?: string;
/** PPT 路径 */
pptPath?: string;
/** PPT 名称 */
pptName?: string;
/** PDF 路径 */
pdfPath?: string;
/** PDF 名称 */
pdfName?: string;
/** 教学目标 */
objectives?: string;
/** 教学准备 */
preparation?: string;
/** 教学延伸 */
extension?: string;
/** 教学反思 */
reflection?: string;
/** 评估数据 */
assessmentData?: string;
/** 是否使用模板 */
useTemplate?: boolean;
/** 排序号 */
sortOrder?: number;
/** 创建时间 */
createdAt?: string;
/** 更新时间 */
updatedAt?: string;
}

View File

@ -0,0 +1,21 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface CoursePackageCourseItem {
/** 课程 ID */
id?: number;
/** 课程名称 */
name?: string;
/** 适用年级 */
gradeLevel?: string;
/** 排序号 */
sortOrder?: number;
}

View File

@ -0,0 +1,56 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { CoursePackageCourseItem } from './coursePackageCourseItem';
/**
*
*/
export interface CoursePackageResponse {
/** ID */
id?: number;
/** 名称 */
name?: string;
/** 描述 */
description?: string;
/** 价格(分) */
price?: number;
/** 折后价格(分) */
discountPrice?: number;
/** 折扣类型 */
discountType?: string;
/** 年级水平(数组) */
gradeLevels?: string[];
/** 课程数量 */
courseCount?: number;
/** 使用学校数 */
tenantCount?: number;
/** 状态 */
status?: string;
/** 提交时间 */
submittedAt?: string;
/** 提交人 ID */
submittedBy?: number;
/** 审核时间 */
reviewedAt?: string;
/** 审核人 ID */
reviewedBy?: number;
/** 审核意见 */
reviewComment?: string;
/** 发布时间 */
publishedAt?: string;
/** 创建时间 */
createdAt?: string;
/** 更新时间 */
updatedAt?: string;
/** 包含的课程 */
courses?: CoursePackageCourseItem[];
/** 开始日期(租户套餐) */
startDate?: string;
/** 结束日期(租户套餐) */
endDate?: string;
}

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { CoursePackageResponse } from './coursePackageResponse';
export interface PageResultCoursePackageResponse {
list?: CoursePackageResponse[];
total?: number;
pageNum?: number;
pageSize?: number;
pages?: number;
}

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { ResourceItem } from './resourceItem';
export interface PageResultResourceItem {
list?: ResourceItem[];
total?: number;
pageNum?: number;
pageSize?: number;
pages?: number;
}

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { ResourceLibrary } from './resourceLibrary';
export interface PageResultResourceLibrary {
list?: ResourceLibrary[];
total?: number;
pageNum?: number;
pageSize?: number;
pages?: number;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { CoursePackageResponse } from './coursePackageResponse';
export interface ResultCoursePackageResponse {
code?: number;
message?: string;
data?: CoursePackageResponse;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { CoursePackageResponse } from './coursePackageResponse';
export interface ResultListCoursePackageResponse {
code?: number;
message?: string;
data?: CoursePackageResponse[];
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { PageResultCoursePackageResponse } from './pageResultCoursePackageResponse';
export interface ResultPageResultCoursePackageResponse {
code?: number;
message?: string;
data?: PageResultCoursePackageResponse;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { PageResultResourceItem } from './pageResultResourceItem';
export interface ResultPageResultResourceItem {
code?: number;
message?: string;
data?: PageResultResourceItem;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { PageResultResourceLibrary } from './pageResultResourceLibrary';
export interface ResultPageResultResourceLibrary {
code?: number;
message?: string;
data?: PageResultResourceLibrary;
}

View File

@ -0,0 +1,36 @@
/**
* IMM WebOffice API
* 线
*/
import { http } from '@/api';
/**
* WebOffice Token
* @param url OSS oss://bucket-name/path/to/file.pptx 或完整 HTTPS URL
* @param name
* @returns Token
*/
export function generateWebofficeTokenReadOnly(params: { url: string; name: string }) {
return http.get('/v1/imm/token/readonly', { params });
}
/**
* WebOffice Token
* @param url OSS oss://bucket-name/path/to/file.pptx 或完整 HTTPS URL
* @param name
* @returns Token
*/
export function generateWebofficeToken(params: { url: string; name: string }) {
return http.get('/v1/imm/token', { params });
}
/**
* WebOffice Token
* @param accessToken 访
* @param refreshToken
* @returns Token
*/
export function refreshWebofficeToken(data: { accessToken: string; refreshToken: string }) {
return http.post('/v1/imm/token/refresh', data);
}

View File

@ -46,7 +46,7 @@ import {
generateWebofficeToken,
generateWebofficeTokenReadOnly,
refreshWebofficeToken,
} from '@/views/cms/teaching-material/CmsTeachingMaterial.api';
} from '@/api/imm.api';
import { useRoute, useRouter } from 'vue-router';
// import { usePermission } from '@/hooks/web/usePermission';
// import { insertPPTImage, insertWordImage } from './webOffice';

View File

@ -149,6 +149,12 @@
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.1</version>
</dependency>
<!-- Aliyun IMM SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>imm20200930</artifactId>
<version>1.28.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension-spring6</artifactId>

View File

@ -0,0 +1,42 @@
package com.reading.platform.common.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 阿里云 IMM智能媒体服务配置类
*
* @author reading-platform
* @since 2026-03-16
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "aliyun.imm")
public class ImmConfig {
/**
* IMM 服务终端节点imm-cn-shanghai.aliyuncs.com
*/
private String endpoint;
/**
* 服务区域cn-shanghai
*/
private String region;
/**
* 访问密钥 ID
*/
private String accessKeyId;
/**
* 访问密钥秘密
*/
private String accessKeySecret;
/**
* IMM 项目名称
*/
private String projectName;
}

View File

@ -0,0 +1,241 @@
package com.reading.platform.common.util;
import com.aliyun.imm20200930.Client;
import com.aliyun.imm20200930.models.*;
import com.reading.platform.common.config.ImmConfig;
import com.reading.platform.dto.response.ImmTokenVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.Instant;
/**
* 阿里云 IMM智能媒体服务工具类
* <p>
* 封装 WebOffice Token 生成刷新等操作
* </p>
*
* @author reading-platform
* @since 2026-03-16
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ImmUtil {
private final ImmConfig immConfig;
/**
* 生成 WebOffice Token只读权限
*
* @param sourceURI OSS 文件路径oss://bucket-name/path/to/file.pptx
* @param name 文件名
* @return WebOffice Token 信息
*/
public ImmTokenVo generateWebofficeTokenReadOnly(String sourceURI, String name) {
return generateWebofficeToken(sourceURI, name, true);
}
/**
* 生成 WebOffice Token编辑权限
*
* @param sourceURI OSS 文件路径oss://bucket-name/path/to/file.pptx
* @param name 文件名
* @return WebOffice Token 信息
*/
public ImmTokenVo generateWebofficeToken(String sourceURI, String name) {
return generateWebofficeToken(sourceURI, name, false);
}
/**
* 生成 WebOffice Token
*
* @param sourceURI OSS 文件路径oss://bucket-name/path/to/file.pptx
* @param name 文件名
* @param isReadOnly 是否只读
* @return WebOffice Token 信息
*/
private ImmTokenVo generateWebofficeToken(String sourceURI, String name, boolean isReadOnly) {
try {
log.info("生成 WebOffice Token: sourceURI={}, name={}, isReadOnly={}", sourceURI, name, isReadOnly);
// 获取 IMM 客户端
Client client = getImmClient();
// 构建权限配置
WebofficePermission permission = new WebofficePermission()
.setReadonly(isReadOnly)
.setCopy(!isReadOnly)
.setExport(!isReadOnly)
.setPrint(!isReadOnly)
.setHistory(!isReadOnly)
.setRename(!isReadOnly);
// 构建请求
GetWebofficeURLRequest request = new GetWebofficeURLRequest()
.setProjectName(immConfig.getProjectName())
.setSourceURI(sourceURI)
.setFilename(name)
.setPermission(permission);
// 发送请求
GetWebofficeURLResponse response = client.getWebofficeURL(request);
// 解析响应
GetWebofficeURLResponseBody body = response.getBody();
// 计算过期时间
Long expireTime = null;
if (body.getAccessTokenExpiredTime() != null) {
try {
expireTime = Instant.parse(body.getAccessTokenExpiredTime()).toEpochMilli();
} catch (Exception e) {
log.warn("解析过期时间失败:{}", e.getMessage());
}
}
// 构建响应 VO
ImmTokenVo tokenVo = ImmTokenVo.builder()
.webofficeURL(body.getWebofficeURL())
.accessToken(body.getAccessToken())
.refreshToken(body.getRefreshToken())
.expireTime(expireTime)
.build();
log.info("WebOffice Token 生成成功webofficeURL={}", maskString(body.getWebofficeURL()));
return tokenVo;
} catch (Exception e) {
log.error("生成 WebOffice Token 失败:{}", e.getMessage(), e);
throw new RuntimeException("生成 WebOffice Token 失败:" + e.getMessage(), e);
}
}
/**
* 刷新 WebOffice Token
*
* @param accessToken 当前访问凭证
* @param refreshToken 刷新凭证
* @return 刷新后的 Token 信息
*/
public ImmTokenVo refreshWebofficeToken(String accessToken, String refreshToken) {
try {
log.info("刷新 WebOffice Token: accessToken={}, refreshToken={}",
maskString(accessToken), maskString(refreshToken));
// 获取 IMM 客户端
Client client = getImmClient();
// 构建请求
RefreshWebofficeTokenRequest request = new RefreshWebofficeTokenRequest()
.setProjectName(immConfig.getProjectName())
.setAccessToken(accessToken)
.setRefreshToken(refreshToken);
// 发送请求
RefreshWebofficeTokenResponse response = client.refreshWebofficeToken(request);
// 解析响应
RefreshWebofficeTokenResponseBody body = response.getBody();
// 计算过期时间
Long expireTime = null;
if (body.getAccessTokenExpiredTime() != null) {
try {
expireTime = Instant.parse(body.getAccessTokenExpiredTime()).toEpochMilli();
} catch (Exception e) {
log.warn("解析过期时间失败:{}", e.getMessage());
}
}
// 构建响应 VO刷新 Token 不返回 webofficeURL
ImmTokenVo tokenVo = ImmTokenVo.builder()
.accessToken(body.getAccessToken())
.refreshToken(body.getRefreshToken())
.expireTime(expireTime)
.build();
log.info("WebOffice Token 刷新成功");
return tokenVo;
} catch (Exception e) {
log.error("刷新 WebOffice Token 失败:{}", e.getMessage(), e);
throw new RuntimeException("刷新 WebOffice Token 失败:" + e.getMessage(), e);
}
}
/**
* 获取 IMM 客户端
*
* @return IMM 客户端实例
*/
private Client getImmClient() {
try {
// 配置访问参数
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setAccessKeyId(immConfig.getAccessKeyId())
.setAccessKeySecret(immConfig.getAccessKeySecret())
.setRegionId(immConfig.getRegion())
.setEndpoint(immConfig.getEndpoint());
// 创建客户端
return new Client(config);
} catch (Exception e) {
log.error("创建 IMM 客户端失败:{}", e.getMessage(), e);
throw new RuntimeException("创建 IMM 客户端失败:" + e.getMessage(), e);
}
}
/**
* 脱敏显示字符串用于日志
*
* @param str 原始字符串
* @return 脱敏后的字符串
*/
private String maskString(String str) {
if (str == null || str.length() < 10) {
return "***";
}
return str.substring(0, 3) + "..." + str.substring(str.length() - 3);
}
/**
* OSS URL 转换为 IMM 所需的 SourceURI 格式
*
* @param ossUrl OSS 文件 URL
* @return IMM SourceURI 格式
*/
public String convertOssUrlToSourceURI(String ossUrl) {
if (ossUrl == null || ossUrl.isEmpty()) {
throw new IllegalArgumentException("OSS URL 不能为空");
}
// 如果已经是 oss:// 格式直接返回
if (ossUrl.startsWith("oss://")) {
return ossUrl;
}
// 从完整 URL 提取 bucket object key
// 格式https://bucket-name.endpoint/object-key
try {
String url = ossUrl.replace("https://", "");
int firstSlash = url.indexOf("/");
if (firstSlash == -1) {
throw new IllegalArgumentException("无效的 OSS URL 格式:" + ossUrl);
}
String bucketAndEndpoint = url.substring(0, firstSlash);
String objectKey = url.substring(firstSlash + 1);
// 提取 bucket 名称endpoint 之前
String bucket = bucketAndEndpoint.split("\\.")[0];
// 构建 oss://bucket/object-key 格式
return "oss://" + bucket + "/" + objectKey;
} catch (Exception e) {
log.error("解析 OSS URL 失败:{}", e.getMessage(), e);
throw new IllegalArgumentException("无效的 OSS URL 格式:" + ossUrl, e);
}
}
}

View File

@ -0,0 +1,125 @@
package com.reading.platform.controller;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.util.ImmUtil;
import com.reading.platform.dto.response.ImmTokenVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 阿里云 IMM智能媒体服务控制器
* <p>
* 提供 WebOffice Token 生成刷新等接口
* </p>
*
* @author reading-platform
* @since 2026-03-16
*/
@RestController
@RequestMapping("/api/v1/imm")
@RequiredArgsConstructor
@Tag(name = "阿里云 IMM 服务", description = "WebOffice 文档预览和编辑相关接口")
public class ImmController {
private final ImmUtil immUtil;
/**
* 生成只读 WebOffice Token
* <p>
* 用于前端预览文档PPTWordExcel 仅支持查看不支持编辑
* </p>
*
* @param url OSS 文件路径oss://bucket-name/path/to/file.pptx 或完整 HTTPS URL
* @param name 文件名
* @return WebOffice Token 信息
*/
@GetMapping("/token/readonly")
@Operation(summary = "生成只读 WebOffice Token", description = "用于文档预览,仅支持查看")
public Result<ImmTokenVo> generateReadOnlyToken(
@RequestParam("url") String url,
@RequestParam("name") String name) {
// OSS URL 转换为 IMM 所需的 SourceURI 格式
String sourceURI = immUtil.convertOssUrlToSourceURI(url);
ImmTokenVo tokenVo = immUtil.generateWebofficeTokenReadOnly(sourceURI, name);
return Result.success(tokenVo);
}
/**
* 生成编辑 WebOffice Token
* <p>
* 用于前端编辑文档PPTWordExcel 支持查看和编辑
* </p>
*
* @param url OSS 文件路径oss://bucket-name/path/to/file.pptx 或完整 HTTPS URL
* @param name 文件名
* @return WebOffice Token 信息
*/
@GetMapping("/token")
@Operation(summary = "生成编辑 WebOffice Token", description = "用于文档编辑,支持查看和修改")
public Result<ImmTokenVo> generateEditToken(
@RequestParam("url") String url,
@RequestParam("name") String name) {
// OSS URL 转换为 IMM 所需的 SourceURI 格式
String sourceURI = immUtil.convertOssUrlToSourceURI(url);
ImmTokenVo tokenVo = immUtil.generateWebofficeToken(sourceURI, name);
return Result.success(tokenVo);
}
/**
* 刷新 WebOffice Token
* <p>
* Token 即将过期时使用 refreshToken 刷新获取新的 Token
* </p>
*
* @param request 刷新请求包含 accessToken refreshToken
* @return 刷新后的 WebOffice Token 信息
*/
@PostMapping("/token/refresh")
@Operation(summary = "刷新 WebOffice Token", description = "当 Token 即将过期时刷新")
public Result<ImmTokenVo> refreshToken(
@RequestBody RefreshTokenRequest request) {
ImmTokenVo tokenVo = immUtil.refreshWebofficeToken(
request.getAccessToken(),
request.getRefreshToken()
);
return Result.success(tokenVo);
}
/**
* 刷新 Token 请求 DTO
*/
public static class RefreshTokenRequest {
/**
* 当前访问凭证
*/
private String accessToken;
/**
* 刷新凭证
*/
private String refreshToken;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
}
}

View File

@ -0,0 +1,42 @@
package com.reading.platform.dto.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 阿里云 IMM WebOffice Token 响应 VO
* <p>
* 用于前端 WebOffice 文档预览和编辑
* </p>
*
* @author reading-platform
* @since 2026-03-16
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ImmTokenVo {
/**
* WebOffice 地址
*/
private String webofficeURL;
/**
* 访问凭证
*/
private String accessToken;
/**
* 刷新凭证
*/
private String refreshToken;
/**
* 过期时间毫秒时间戳
*/
private Long expireTime;
}

View File

@ -78,7 +78,13 @@ aliyun:
max-file-size: ${OSS_MAX_FILE_SIZE:10485760}
# 前端直传跨域:启动时自动配置 OSS CORS
cors-enabled: ${OSS_CORS_ENABLED:true}
cors-allowed-origins: ${OSS_CORS_ORIGINS:http://localhost:5173,http://localhost:5174}
cors-allowed-origins: ${OSS_CORS_ORIGINS:*}
imm:
endpoint: ${IMM_ENDPOINT:imm.cn-shenzhen.aliyuncs.com}
region: ${IMM_REGION:cn-shenzhen}
project-name: ${IMM_PROJECT_NAME:kid-course-platform-doc}
access-key-id: ${IMM_ACCESS_KEY_ID:LTAI5tKZhPofbThbSzDSiWoK}
access-key-secret: ${IMM_ACCESS_KEY_SECRET:FtcsC7oQX3T0NaChaa9FYq2aoysQFM}
# 日志配置(开发环境 - DEBUG 级别)
logging:

View File

@ -74,8 +74,15 @@ aliyun:
access-key-secret: ${OSS_ACCESS_KEY_SECRET:FtcsC7oQX3T0NaChaa9FYq2aoysQFM}
bucket-name: ${OSS_BUCKET_NAME:lesingle-kid-course}
max-file-size: ${OSS_MAX_FILE_SIZE:10485760}
cors-enabled: ${OSS_CORS_ENABLED:false}
cors-allowed-origins: ${OSS_CORS_ORIGINS:}
# 前端直传跨域:启动时自动配置 OSS CORS
cors-enabled: ${OSS_CORS_ENABLED:true}
cors-allowed-origins: ${OSS_CORS_ORIGINS:*}
imm:
endpoint: ${IMM_ENDPOINT:imm.cn-shenzhen.aliyuncs.com}
region: ${IMM_REGION:cn-shenzhen}
project-name: ${IMM_PROJECT_NAME:kid-course-platform-doc}
access-key-id: ${IMM_ACCESS_KEY_ID:LTAI5tKZhPofbThbSzDSiWoK}
access-key-secret: ${IMM_ACCESS_KEY_SECRET:FtcsC7oQX3T0NaChaa9FYq2aoysQFM}
# 日志配置(生产环境 - 降低日志级别)
logging:

View File

@ -74,6 +74,15 @@ aliyun:
access-key-secret: ${OSS_ACCESS_KEY_SECRET:FtcsC7oQX3T0NaChaa9FYq2aoysQFM}
bucket-name: ${OSS_BUCKET_NAME:lesingle-kid-course}
max-file-size: ${OSS_MAX_FILE_SIZE:10485760}
# 前端直传跨域:启动时自动配置 OSS CORS
cors-enabled: ${OSS_CORS_ENABLED:true}
cors-allowed-origins: ${OSS_CORS_ORIGINS:*}
imm:
endpoint: ${IMM_ENDPOINT:imm.cn-shenzhen.aliyuncs.com}
region: ${IMM_REGION:cn-shenzhen}
project-name: ${IMM_PROJECT_NAME:kid-course-platform-doc}
access-key-id: ${IMM_ACCESS_KEY_ID:LTAI5tKZhPofbThbSzDSiWoK}
access-key-secret: ${IMM_ACCESS_KEY_SECRET:FtcsC7oQX3T0NaChaa9FYq2aoysQFM}
# 日志配置(测试环境)
logging:

View File

@ -0,0 +1,10 @@
{
"name": "Test Course 005",
"themeId": null,
"gradeTags": "[\"class_a\"]",
"pictureBookName": "Test Picture Book",
"coreContent": "Test Content",
"durationMinutes": 25,
"domainTags": "[\"social\"]",
"introSummary": "Test Summary"
}

View File

@ -0,0 +1,10 @@
{
"name": "Test Course 007",
"themeId": null,
"gradeTags": "[]",
"pictureBookName": "Test Picture Book",
"coreContent": "Test Content",
"durationMinutes": 25,
"domainTags": "[]",
"introSummary": "Test Summary"
}