feat(community): 友圈动态支持上传短视频,可与图片同发

- 新增 /api/upload/video 独立上传接口:仅 MP4/MOV、单条 30MB、独立限流、登录鉴权

- posts 表新增 video_url / video_cover,发布接口支持视频与图片同时落库

- 小程序发布器支持选择/预览/删除视频(≤60 秒),动态卡片与详情页支持播放

- 后台动态管理新增视频预览弹窗

- 新增短视频链路与上传鉴权回归测试,125 套件/818 用例全绿
这个提交包含在:
xcbyy
2026-10-31 10:43:43 +08:00
父节点 1681d5d086
当前提交 23f500f623
共有 11 个文件被更改,包括 478 次插入 和 36 次删除

查看文件

@@ -0,0 +1,63 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 友圈短视频发布链路回归闸门:
* - 上传必须走独立接口、独立限流与体积/格式白名单;
* - posts 表必须持久化 video_url / video_cover;
* - 视频与图片二选一,避免混排;
* - 小程序与后台都必须能播放/预览视频。
*/
describe('友圈短视频发布链路', () => {
const root = path.resolve(__dirname, '../../');
const read = (rel: string) => fs.readFileSync(path.join(root, rel), 'utf-8');
const indexCode = read('server/src/index.ts');
const mysql = read('server/src/db/mysql.ts');
const repo = read('server/src/db/repository.ts');
const security = read('server/src/core/security/content-security.service.ts');
const api = read('pwuniapp/common/api.js');
const community = read('pwuniapp/pages/community/index.vue');
const detail = read('pwuniapp/pages/community/detail.vue');
const admin = read('admin/arco-design-pro-vite/src/views/community/post-manage/index.vue');
it('短视频上传必须使用独立接口、独立限流与 30MB 上限', () => {
expect(indexCode).toContain("app.post('/api/upload/video'");
expect(indexCode).toContain('uploadVideo');
expect(indexCode).toContain('30 * 1024 * 1024');
expect(indexCode).toContain('video/mp4');
});
it('短视频上传必须要求登录并做格式/体积校验', () => {
expect(security).toContain('public checkVideo');
expect(security).toContain('视频体积超过 30MB 上限');
expect(indexCode).toContain("ensureUploadAuthorized(req, res, '上传视频')");
});
it('posts 表必须持久化 video_url / video_cover', () => {
expect(mysql).toContain('video_url VARCHAR(512)');
expect(mysql).toContain('video_cover VARCHAR(512)');
expect(repo).toContain('video_url as videoUrl');
expect(repo).toContain('INSERT INTO posts');
expect(repo).toContain('video_url, video_cover');
});
it('视频与图片可同发:两条媒体链路都要落库', () => {
expect(repo).toContain("const images = Array.isArray(data.images) ? data.images : [];");
expect(repo).toContain('videoUrl, videoCover, actionText, category');
});
it('小程序必须支持选择、上传与播放短视频', () => {
expect(api).toContain('export function uploadVideo');
expect(api).toContain("'/api/upload/video'");
expect(community).toContain('uni.chooseVideo');
expect(community).toContain('uploadVideo(newPostForm.value.video.path)');
expect(community).toContain('post.videoUrl');
expect(detail).toContain('post.videoUrl');
});
it('后台动态管理必须可预览短视频', () => {
expect(admin).toContain('openVideoPreview');
expect(admin).toContain('record.videoUrl');
});
});

查看文件

@@ -10,11 +10,16 @@ describe('第四十五批上传鉴权闸门', () => {
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
it('上传必须要求登录(安全闸门不依赖 NODE_ENV)', () => {
const start = indexCode.indexOf('const handleFileUpload =');
const start = indexCode.indexOf('const ensureUploadAuthorized =');
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 500);
const block = indexCode.slice(start, start + 700);
expect(block).toContain('SECURE_MODE');
expect(block).toContain("resolveSecureUserId(req, 0, '上传文件')");
expect(block).toContain('resolveSecureUserId(req, 0, scene)');
expect(block).toContain('请先登录后再上传');
});
it('图片与视频上传都必须走同一登录闸门', () => {
expect(indexCode).toContain("ensureUploadAuthorized(req, res, '上传文件')");
expect(indexCode).toContain("ensureUploadAuthorized(req, res, '上传视频')");
});
});