From 97224739746d13504299969e78d33df55e73b659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B7=A6=E8=BE=B0=E5=8B=87?= Date: Thu, 23 Jul 2026 22:51:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E5=89=8D=E7=AB=AF=EF=BC=9A?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=96=B0=E6=89=8B=E5=BC=95=E5=AF=BC=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配置页增加「新手引导视频」分区(GuideVideoConfig.tsx):上传 MP4、设置 生效次数、开关启停。types.ts 补齐对应类型。nginx 配置放宽该上传接口的 client_max_body_size,否则大视频在网关就被 413 挡下、到不了后端。 Co-Authored-By: Claude Opus 4.8 (1M context) --- deploy/nginx/admin.shaguabijia.com.conf | 14 ++ src/app/(main)/config/GuideVideoConfig.tsx | 222 +++++++++++++++++++++ src/app/(main)/config/page.tsx | 2 + src/lib/types.ts | 12 ++ 4 files changed, 250 insertions(+) create mode 100644 src/app/(main)/config/GuideVideoConfig.tsx diff --git a/deploy/nginx/admin.shaguabijia.com.conf b/deploy/nginx/admin.shaguabijia.com.conf index f76eb32..0d66ae1 100644 --- a/deploy/nginx/admin.shaguabijia.com.conf +++ b/deploy/nginx/admin.shaguabijia.com.conf @@ -32,6 +32,20 @@ server { proxy_read_timeout 60s; } + # 新手引导视频上传:视频比图片大一个量级,单独放宽到 100MB(对齐后端 + # settings.GUIDE_VIDEO_MAX_BYTES)。不放宽全站上限,避免其它接口被大 body 打。 + # 上传大文件耗时长,读超时同步放宽到 300s。 + location = /admin/api/guide-video/video { + client_max_body_size 100m; + proxy_pass http://127.0.0.1:8771; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_request_buffering off; + } + # 用户上传媒体(上报截图 / 反馈截图 / 反馈二维码)由 App 后端(:8770)的 /media 托管。 # admin 页面要展示这些图,经此同域反代过去(免跨域 + 免 https 页面引 http 图被拦)。 # 前提:App 后端与本 admin 同机;若分机,把 127.0.0.1:8770 换成 App 后端可达地址。 diff --git a/src/app/(main)/config/GuideVideoConfig.tsx b/src/app/(main)/config/GuideVideoConfig.tsx new file mode 100644 index 0000000..dcf8b86 --- /dev/null +++ b/src/app/(main)/config/GuideVideoConfig.tsx @@ -0,0 +1,222 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Button, Card, Space, Spin, Switch, Tag, Typography, Upload, message } from 'antd'; +import { DeleteOutlined, UploadOutlined } from '@ant-design/icons'; +import { api, errMsg } from '@/lib/api'; +import { canDo } from '@/lib/auth'; +import { mediaUrl } from '@/lib/media'; +import type { GuideVideoConfig as GuideCfg } from '@/lib/types'; + +const { Text } = Typography; + +/** 后端 media.save_guide_video 只认 MP4 魔数;这里先在浏览器挡一道,省得白传 100MB。 */ +const MAX_BYTES = 100 * 1024 * 1024; + +/** + * 领券等候浮层的「新手引导视频」配置。 + * + * 用户点首页「一键自动领取」→ 出等候浮层,浮层下方那块位置**前 3 次**放这支引导视频 + * (而不是广告),每次固定发 120 金币;播完若浮层还开着,自动接着放广告(原逻辑)。 + * 「系统配置 → 领券引导视频」tab 的一个区块。 + * + * 后台只管两件事:**开关** 和 **换片**。次数(3)/ 金币(120)走服务端默认值,产品已拍板不再 + * 开放配置,所以这里不渲染输入框、PATCH 也不带这两个字段(后端仍保留字段与默认值)。 + */ +export default function GuideVideoConfig() { + const [cfg, setCfg] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [uploading, setUploading] = useState(false); + + // 本地编辑态,保存时一次性 PATCH + const [enabled, setEnabled] = useState(true); + + const canEdit = canDo(['operator']); + + const sync = (c: GuideCfg) => { + setCfg(c); + setEnabled(c.enabled); + }; + + const load = async () => { + setLoading(true); + try { + const { data } = await api.get('/admin/api/guide-video'); + sync(data); + } finally { + setLoading(false); + } + }; + useEffect(() => { + load(); + }, []); + + const save = async () => { + setSaving(true); + try { + const { data } = await api.patch('/admin/api/guide-video', { enabled }); + sync(data); + message.success('已保存,用户下一次进入领券浮层即生效'); + } catch (e) { + message.error(errMsg(e)); + } finally { + setSaving(false); + } + }; + + // antd Upload:beforeUpload 里自行 POST(multipart),返回 false 阻止其默认上传。 + const beforeUpload = (file: File) => { + // 有些系统给 .mp4 的 type 是空串,不能只看 type,再兜一层扩展名。 + const looksMp4 = file.type === 'video/mp4' || /\.mp4$/i.test(file.name); + if (!looksMp4) { + message.error('仅支持 MP4 视频(H.264 编码)'); + return Upload.LIST_IGNORE; + } + if (file.size > MAX_BYTES) { + message.error('视频不能超过 100MB'); + return Upload.LIST_IGNORE; + } + void uploadVideo(file); + return false; + }; + + const uploadVideo = async (file: File) => { + const form = new FormData(); + form.append('file', file); + setUploading(true); + try { + const { data } = await api.post('/admin/api/guide-video/video', form); + sync(data); + message.success('引导视频已更新,用户下一次进入领券浮层即生效'); + } catch (e) { + message.error(errMsg(e)); + } finally { + setUploading(false); + } + }; + + const removeVideo = async () => { + setUploading(true); + try { + const { data } = await api.delete('/admin/api/guide-video/video'); + sync(data); + message.success('已移除引导视频,领券浮层恢复为只放广告'); + } catch (e) { + message.error(errMsg(e)); + } finally { + setUploading(false); + } + }; + + return ( + + 更新于 {new Date(cfg.updated_at).toLocaleString('zh-CN')} + + ) : null + } + > +

+ 用户点「一键自动领取」后出现的等候浮层,下方那块广告位前 3 次改放这支引导视频。 + 视频不可快进;播完或中途关闭都算看完,各发一次 120 金币;播完若浮层还开着,会自动接着放广告。 + 次数按账号计(换设备不重置),视频一开播就算用掉一次。 + 次数与金币走固定值、后台不开放调整;这里只管开关换片,每次改动进审计日志。 +

+ {loading || !cfg ? ( + + ) : ( + + {/* 左:视频预览 */} +
+ + 当前引导视频 + +
+ {cfg.video_url ? ( + // eslint-disable-next-line jsx-a11y/media-has-caption +
+ {!enabled && cfg.video_url && ( +
+ 当前为「关闭」:浮层不放引导视频,直接放广告 +
+ )} +
+ + {/* 右:编辑控件 */} + + + 启用引导视频: + + {!enabled && 已关闭} + + + + + + + {cfg.video_url && ( + + )} + + MP4(H.264),≤100MB + + + + + {!canEdit && 仅 operator / super_admin 可修改} + +
+ )} +
+ ); +} diff --git a/src/app/(main)/config/page.tsx b/src/app/(main)/config/page.tsx index 3f48f83..c7a0344 100644 --- a/src/app/(main)/config/page.tsx +++ b/src/app/(main)/config/page.tsx @@ -6,6 +6,7 @@ import { api, errMsg } from '@/lib/api'; import HomeMarqueeSeeds from './HomeMarqueeSeeds'; import HomeStatsConfig from './HomeStatsConfig'; import FeedbackQrConfig from './FeedbackQrConfig'; +import GuideVideoConfig from './GuideVideoConfig'; interface ConfigItem { key: string; @@ -164,6 +165,7 @@ export default function ConfigPage() { }, { key: 'welfare', label: '福利页', children: welfareConfig }, { key: 'feedback-qr', label: '反馈二维码', children: }, + { key: 'guide-video', label: '领券引导视频', children: }, ]} /> diff --git a/src/lib/types.ts b/src/lib/types.ts index 9274e27..d3118a6 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -317,6 +317,18 @@ export interface FeedbackQrConfig { updated_at: string | null; } +/** 领券等候浮层的「新手引导视频」配置(前 3 次用它替代广告,每次固定 120 金币)。 + * 后台只开放 enabled + 换片;下面几个字段接口仍返回,但页面不展示、也不回传。 */ +export interface GuideVideoConfig { + enabled: boolean; + video_url: string | null; // /media/guide_video/xxx.mp4;null = 未配片(浮层照旧只放广告) + max_plays: number; // 每个账号前 N 次浮层放引导视频(服务端默认 3,后台不开放调整) + reward_coin: number; // 每次固定金币(服务端默认 120,播完 / 中途关闭都发) + updated_at: string | null; + total_plays: number; // 只读统计:全站已播次数(后台不再展示) + granted_plays: number; // 只读统计:其中已发币次数(后台不再展示) +} + export interface AuditLog { id: number; admin_id: number;