java/wangpan-gateway/target/classes/META-INF/resources/api-doc.html
liRQ 9f19d64afb feat: 新增OpenList集成功能,完善TMDB刮削与配置加密
1.  新增用户OpenList连接配置模块,包括数据库表、实体类与相关服务
2.  为驱动服务添加定时心跳检测功能,监控OpenList连接状态
3.  重构电影实体与数据库字段,移除冗余的overview字段,统一使用description存储简介
4.  完善TMDB刮削逻辑,增加空指针校验与详细失败日志
5.  配置文件敏感信息加密,添加加密脚本工具
6.  新增网关路由规则支持OpenList接口
7.  完善刮削日志查询功能,支持查看详细执行结果
2026-06-27 17:29:36 +08:00

4683 lines
260 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网盘系统 API 接口文档</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 1000px;
margin: 0 auto;
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: white;
padding: 30px 40px;
text-align: center;
}
.header h1 { font-size: 28px; margin-bottom: 8px; }
.header p { color: #a0aec0; font-size: 14px; }
.gateway-badge {
display: inline-block;
background: #48bb78;
color: white;
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
margin-top: 12px;
font-family: monospace;
}
.nav {
display: flex;
background: #f7fafc;
border-bottom: 1px solid #e2e8f0;
overflow-x: auto;
}
.nav-item {
padding: 14px 24px;
cursor: pointer;
border-bottom: 3px solid transparent;
white-space: nowrap;
font-size: 14px;
color: #4a5568;
transition: all 0.2s;
}
.nav-item:hover { background: #edf2f7; }
.nav-item.active {
border-bottom-color: #667eea;
color: #667eea;
font-weight: 600;
}
.badge-danger { background: #e53e3e; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px; }
.content { padding: 24px 40px; }
.section { display: none; }
.section.active { display: block; }
.service-card {
background: #f7fafc;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid #667eea;
}
.service-card h2 {
font-size: 18px;
color: #2d3748;
margin-bottom: 4px;
}
.service-card .port {
font-size: 12px;
color: #718096;
font-family: monospace;
}
.api-item {
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
margin: 12px 0;
overflow: hidden;
}
.api-header {
display: flex;
align-items: center;
padding: 14px 18px;
cursor: pointer;
transition: background 0.2s;
}
.api-header:hover { background: #f7fafc; }
.method {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
font-family: monospace;
margin-right: 12px;
min-width: 65px;
text-align: center;
}
.method.GET { background: #c6f6d5; color: #276749; }
.method.POST { background: #bee3f8; color: #2a4365; }
.method.PUT { background: #fefcbf; color: #744210; }
.method.DELETE { background: #fed7d7; color: #742a2a; }
.api-path {
font-family: monospace;
font-size: 14px;
color: #2d3748;
flex: 1;
}
.api-desc {
font-size: 13px;
color: #718096;
margin-left: 12px;
}
.api-detail {
display: none;
padding: 0 18px 18px;
border-top: 1px solid #e2e8f0;
}
.api-detail.show { display: block; }
.detail-section { margin: 12px 0; }
.detail-section h4 {
font-size: 13px;
color: #4a5568;
margin-bottom: 6px;
font-weight: 600;
}
.param-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.param-table th, .param-table td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
.param-table th {
background: #f7fafc;
color: #4a5568;
font-weight: 600;
}
.param-table td { color: #2d3748; }
.code-block {
background: #1a202c;
color: #a0aec0;
padding: 14px 18px;
border-radius: 6px;
font-family: 'Fira Code', monospace;
font-size: 12px;
overflow-x: auto;
line-height: 1.6;
}
.code-block .key { color: #63b3ed; }
.code-block .string { color: #68d391; }
.code-block .number { color: #f6ad55; }
.code-block .keyword { color: #fc8181; }
.status-table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
}
.status-table th, .status-table td {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid #e2e8f0;
font-size: 13px;
}
.status-table th { background: #f7fafc; color: #4a5568; }
.arch-diagram {
background: #f7fafc;
border-radius: 8px;
padding: 20px;
text-align: center;
margin: 16px 0;
}
.arch-flow {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
font-size: 13px;
}
.arch-box {
padding: 8px 16px;
border-radius: 6px;
color: white;
font-weight: 600;
}
.arch-box.gateway { background: #48bb78; }
.arch-box.user { background: #4299e1; }
.arch-box.drive { background: #ed8936; }
.arch-box.tmdb { background: #9f7aea; }
.arch-arrow { color: #a0aec0; font-size: 18px; }
/* 认证标识 */
.auth-badge {
display: inline-block;
background: #ed8936;
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
margin-left: 8px;
vertical-align: middle;
}
/* 搜索框样式 */
.search-container {
padding: 16px 40px;
background: #f7fafc;
border-bottom: 1px solid #e2e8f0;
}
.search-box {
display: flex;
align-items: center;
background: white;
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 10px 16px;
transition: all 0.2s;
}
.search-box:focus-within {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.search-icon {
color: #a0aec0;
margin-right: 10px;
font-size: 16px;
}
.search-input {
flex: 1;
border: none;
outline: none;
font-size: 14px;
color: #2d3748;
}
.search-input::placeholder {
color: #a0aec0;
}
.search-clear {
cursor: pointer;
color: #a0aec0;
font-size: 18px;
padding: 0 4px;
display: none;
}
.search-clear:hover {
color: #e53e3e;
}
.search-clear.show {
display: block;
}
.search-result-count {
font-size: 12px;
color: #718096;
margin-top: 8px;
display: none;
}
.search-result-count.show {
display: block;
}
/* 搜索高亮 */
.highlight {
background: #fefcbf;
padding: 1px 2px;
border-radius: 2px;
}
/* 搜索时隐藏不匹配的项 */
.api-item.search-hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>网盘系统 API 接口文档</h1>
<p>微服务架构 · 统一网关入口</p>
<div class="gateway-badge">网关地址http://localhost:8888</div>
</div>
<div class="nav">
<div class="nav-item active" data-section="overview">架构总览</div>
<div class="nav-item" data-section="user">用户服务</div>
<div class="nav-item" data-section="users">用户管理</div>
<div class="nav-item" data-section="profile">个人资料</div>
<div class="nav-item" data-section="drive">网盘服务</div>
<div class="nav-item" data-section="icon">Icon服务</div>
<div class="nav-item" data-section="tmdb">TMDB服务</div>
<div class="nav-item" data-section="openlist">OpenList集成</div>
<div class="nav-item" data-section="status">状态码</div>
</div>
<!-- 搜索框 -->
<div class="search-container">
<div class="search-box">
<span class="search-icon">🔍</span>
<input type="text" class="search-input" id="apiSearch" placeholder="搜索接口路径、描述或功能...">
<span class="search-clear" id="searchClear">×</span>
</div>
<div class="search-result-count" id="searchResultCount"></div>
</div>
<div class="content">
<!-- 架构总览 -->
<div class="section active" id="overview">
<div class="arch-diagram">
<h3 style="margin-bottom: 16px; color: #2d3748;">服务架构图</h3>
<div class="arch-flow">
<div class="arch-box" style="background: #4a5568;">前端</div>
<span class="arch-arrow"></span>
<div class="arch-box gateway">Gateway:8888</div>
<span class="arch-arrow"></span>
<div class="arch-box user">User:8081</div>
</div>
<div class="arch-flow" style="margin-top: 8px;">
<div class="arch-box" style="background: #4a5568; visibility: hidden;">前端</div>
<span class="arch-arrow" style="visibility: hidden;"></span>
<div class="arch-box gateway" style="visibility: hidden;">Gateway</div>
<span class="arch-arrow"></span>
<div class="arch-box drive">Drive:8082</div>
</div>
<div class="arch-flow" style="margin-top: 8px;">
<div class="arch-box" style="background: #4a5568; visibility: hidden;">前端</div>
<span class="arch-arrow" style="visibility: hidden;"></span>
<div class="arch-box gateway" style="visibility: hidden;">Gateway</div>
<span class="arch-arrow"></span>
<div class="arch-box tmdb">TMDB:8083</div>
</div>
<div class="arch-flow" style="margin-top: 8px;">
<div class="arch-box" style="background: #4a5568; visibility: hidden;">前端</div>
<span class="arch-arrow" style="visibility: hidden;"></span>
<div class="arch-box gateway" style="visibility: hidden;">Gateway</div>
<span class="arch-arrow"></span>
<div class="arch-box drive">Drive:8082</div>
<span class="arch-arrow"></span>
<div class="arch-box" style="background: #38b2ac;">OpenList:13000</div>
<span class="arch-arrow"></span>
<div class="arch-box" style="background: #e53e3e;">各网盘</div>
</div>
</div>
<div class="service-card">
<h2>路由规则</h2>
<table class="param-table">
<tr><th>请求路径</th><th>转发服务</th><th>说明</th></tr>
<tr><td>/api/login, /api/register</td><td>user-service</td><td>登录注册</td></tr>
<tr><td>/api/token/**, /api/login-logs/**</td><td>user-service</td><td>令牌/日志</td></tr>
<tr><td>/api/users/**</td><td>user-service</td><td>用户管理</td></tr>
<tr><td>/api/profile/**</td><td>user-service</td><td>个人资料</td></tr>
<tr><td>/api/drive/**</td><td>drive-service</td><td>网盘管理</td></tr>
<tr><td>/api/drive/**/sync-files</td><td>drive-service</td><td>OpenList 文件同步</td></tr>
<tr><td>/api/drive/**/play-url</td><td>drive-service</td><td>OpenList 播放链接</td></tr>
<tr><td>/api/icon/**</td><td>drive-service</td><td>Icon管理</td></tr>
<tr><td>/api/movies/**</td><td>tmdb-service</td><td>电影管理</td></tr>
<tr><td>/api/tmdb/**, /api/tmdb-scraper/**</td><td>tmdb-service</td><td>TMDB刮削</td></tr>
</table>
</div>
</div>
<!-- 用户服务 -->
<div class="section" id="user">
<div class="service-card">
<h2>用户认证服务</h2>
<span class="port">wangpan-user-service : 8081</span>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/login</span>
<span class="api-desc">用户登录</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"username"</span>: <span class="string">"用户名"</span>,
<span class="key">"password"</span>: <span class="string">"密码"</span>
}</div>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"登录成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">7</span>,
<span class="key">"username"</span>: <span class="string">"zhangsan"</span>,
<span class="key">"userType"</span>: <span class="number">0</span>, <span style="color:#718096">// 0=普通用户, 1=VIP, 2=管理员</span>
<span class="key">"status"</span>: <span class="number">1</span>,
<span class="key">"email"</span>: <span class="string">"xxx@xx.com"</span>,
<span class="key">"phone"</span>: <span class="string">"138****0000"</span>,
<span class="key">"realName"</span>: <span class="string">"张三"</span>,
<span class="key">"avatarUrl"</span>: <span class="string">""</span>,
<span class="key">"lastLoginIp"</span>: <span class="string">"127.0.0.1"</span>,
<span class="key">"lastLoginTime"</span>: <span class="string">"2026-06-10T11:20:21"</span>,
<span class="key">"hasTmdbKey"</span>: <span class="keyword">false</span>,
<span class="key">"token"</span>: <span class="string">"tk_xxx..."</span>,
<span class="key">"tokenType"</span>: <span class="string">"Bearer"</span>,
<span class="key">"expireIn"</span>: <span class="number">604800</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 用户名或密码错误</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"用户名或密码错误"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 账号被禁用</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">403</span>,
<span class="key">"message"</span>: <span class="string">"账号已被禁用,请联系管理员"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 参数缺失</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"用户名和密码不能为空"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>userType 说明</h4>
<table class="param-table">
<tr><th></th><th>角色</th><th>权限说明</th></tr>
<tr><td><strong>0</strong></td><td>普通用户</td><td>仅访问自己的数据(如日志只看自己的)</td></tr>
<tr><td><strong>1</strong></td><td>VIP 用户</td><td>普通用户 + 更多存储空间</td></tr>
<tr><td><strong>2</strong></td><td>管理员</td><td>可查看所有用户数据(如全部日志)</td></tr>
</table>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/register</span>
<span class="api-desc">用户注册</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"username"</span>: <span class="string">"用户名"</span>,
<span class="key">"password"</span>: <span class="string">"密码"</span>
}</div>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"注册成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">10</span>,
<span class="key">"username"</span>: <span class="string">"newuser"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 用户名已存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">409</span>,
<span class="key">"message"</span>: <span class="string">"用户名已被占用"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 参数缺失</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"用户名和密码不能为空"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/token/validate</span>
<span class="api-desc">令牌验证</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求头</h4>
<div class="code-block">Authorization: Bearer &lt;token&gt;</div>
</div>
<div class="detail-section">
<h4>成功响应示例 — Token 有效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"Token 有效"</span>,
<span class="key">"data"</span>: {
<span class="key">"valid"</span>: <span class="keyword">true</span>,
<span class="key">"userId"</span>: <span class="number">7</span>,
<span class="key">"username"</span>: <span class="string">"zhangsan"</span>,
<span class="key">"expireTime"</span>: <span class="string">"2026-06-17T11:20:21"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — Token 无效或已过期</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"Token 无效或已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/login-logs</span>
<span class="api-desc">登录日志查询</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>权限说明</h4>
<table class="param-table">
<tr><th>用户类型</th><th>可见范围</th></tr>
<tr><td>普通用户userType=0/1</td><td style="color:#e53e3e">仅自己的登录记录</td></tr>
<tr><td>管理员userType=2</td><td style="color:#38a169">所有用户的登录记录</td></tr>
</table>
</div>
<div class="detail-section">
<h4>查询参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>username</td><td>String</td><td></td><td>按用户名模糊筛选(管理员可用)</td></tr>
<tr><td>result</td><td>String</td><td></td><td>登录结果success / fail</td></tr>
<tr><td>startDate</td><td>String</td><td></td><td>开始日期格式yyyy-MM-dd如 2026-06-01</td></tr>
<tr><td>endDate</td><td>String</td><td></td><td>结束日期格式yyyy-MM-dd如 2026-06-10</td></tr>
<tr><td>pageNo</td><td>int</td><td></td><td>页码,默认 1</td></tr>
<tr><td>pageSize</td><td>int</td><td></td><td>每页条数,默认 20</td></tr>
</table>
<p style="color:#718096; font-size:13px; margin-top:8px;">提示:可组合使用多个筛选条件,如 ?result=fail&startDate=2026-06-01&endDate=2026-06-10</p>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"total"</span>: <span class="number">10</span>,
<span class="key">"list"</span>: [
{
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"userId"</span>: <span class="number">7</span>,
<span class="key">"username"</span>: <span class="string">"zhangsan"</span>,
<span class="key">"loginIp"</span>: <span class="string">"127.0.0.1"</span>,
<span class="key">"loginTime"</span>: <span class="string">"2026-06-10T11:20:21"</span>,
<span class="key">"result"</span>: <span class="string">"success"</span>,
<span class="key">"userAgent"</span>: <span class="string">"Mozilla/5.0..."</span>
}
],
<span class="key">"pageNo"</span>: <span class="number">1</span>,
<span class="key">"pageSize"</span>: <span class="number">20</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 无效"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 权限不足</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">403</span>,
<span class="key">"message"</span>: <span class="string">"权限不足,无法查看其他用户的登录日志"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
</div>
<!-- 用户管理(管理员专用) -->
<div class="section" id="users">
<div class="service-card">
<h2>用户管理</h2>
<span class="port">wangpan-user-service : 8081</span>
<span class="badge badge-danger">仅管理员 (userType=2)</span>
</div>
<!-- 查询用户列表 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/users</span>
<span class="api-desc">查询用户列表</span><span class="auth-badge">需Token + 管理员</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>权限说明</h4>
<table class="param-table">
<tr><th>角色</th><th>访问权限</th></tr>
<tr><td>普通/VIP 用户</td><td style="color:#e53e3e">403 权限不足</td></tr>
<tr><td><strong>管理员</strong></td><td style="color:#38a169">可查看所有用户列表</td></tr>
</table>
</div>
<div class="detail-section">
<h4>查询参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>username</td><td>String</td><td></td><td>用户名模糊搜索</td></tr>
<tr><td>status</td><td>int</td><td></td><td>状态0=禁用, 1=正常, 2=锁定</td></tr>
<tr><td>userType</td><td>int</td><td></td><td>角色0=普通, 1=VIP, 2=管理员</td></tr>
<tr><td>pageNo</td><td>int</td><td></td><td>页码,默认 1</td></tr>
<tr><td>pageSize</td><td>int</td><td></td><td>每页条数,默认 20</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"total"</span>: <span class="number">5</span>,
<span class="key">"list"</span>: [
{ <span class="key">"id"</span>: <span class="number">7</span>, <span class="key">"username"</span>: <span class="string">"zhangsan"</span>, <span class="key">"userType"</span>: <span class="number">0</span>, <span class="key">"status"</span>: <span class="number">1</span>, ... }
],
<span class="key">"pageNo"</span>: <span class="number">1</span>,
<span class="key">"pageSize"</span>: <span class="number">20</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 权限不足</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">403</span>,
<span class="key">"message"</span>: <span class="string">"权限不足,仅管理员可访问"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 无效"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- 新增用户 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/users</span>
<span class="api-desc">新增用户</span><span class="auth-badge">需Token + 管理员</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"username"</span>: <span class="string">"必填"</span>,
<span class="key">"password"</span>: <span class="string">"必填"</span>,
<span class="key">"email"</span>: <span class="string">"可选"</span>,
<span class="key">"phone"</span>: <span class="string">"可选"</span>,
<span class="key">"realName"</span>: <span class="string">"可选"</span>,
<span class="key">"status"</span>: <span class="number">1</span>, <span style="color:#718096">// 可选0=禁用, 1=正常, 默认1</span>
<span class="key">"userType"</span>: <span class="number">0</span> <span style="color:#718096">// 可选0=普通, 1=VIP, 2=管理员, 默认0</span>
}</div>
</div>
<div class="detail-section">
<h4>成功响应</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"用户创建成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">10</span>,
<span class="key">"username"</span>: <span class="string">"newuser"</span>,
<span class="key">"email"</span>: <span class="string">"new@test.com"</span>,
<span class="key">"status"</span>: <span class="number">1</span>,
<span class="key">"userType"</span>: <span class="number">0</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 用户名已存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">409</span>,
<span class="key">"message"</span>: <span class="string">"用户名已被占用"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 参数缺失</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"用户名或密码不能为空"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 权限不足</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">403</span>,
<span class="key">"message"</span>: <span class="string">"非管理员,无权限操作"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 服务器错误</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"服务器内部错误"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- 修改用户 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method PUT">PUT</span>
<span class="api-path">/api/users/{userId}</span>
<span class="api-desc">修改用户信息</span><span class="auth-badge">需Token + 管理员</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>userId</td><td>Long</td><td>目标用户 ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>请求体(部分更新,只传需要修改的字段)</h4>
<div class="code-block">{
<span class="key">"email"</span>: <span class="string">"新邮箱"</span>, <span style="color:#718096">// 可选</span>
<span class="key">"phone"</span>: <span class="string">"新手机号"</span>, <span style="color:#718096">// 可选</span>
<span class="key">"realName"</span>: <span class="string">"新姓名"</span>, <span style="color:#718096">// 可选</span>
<span class="key">"status"</span>: <span class="number">0</span>, <span style="color:#718096">// 可选0=禁用, 1=正常, 2=锁定</span>
<span class="key">"userType"</span>: <span class="number">1</span> <span style="color:#718096">// 可选0=普通, 1=VIP, 2=管理员</span>
}</div>
</div>
<div class="detail-section">
<h4>成功响应</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"用户信息更新成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">10</span>,
<span class="key">"username"</span>: <span class="string">"newuser"</span>,
<span class="key">"email"</span>: <span class="string">"new@test.com"</span>,
<span class="key">"status"</span>: <span class="number">1</span>,
<span class="key">"userType"</span>: <span class="number">0</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 用户不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"目标用户不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 参数缺失</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"没有需要更新的字段"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 权限不足</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">403</span>,
<span class="key">"message"</span>: <span class="string">"非管理员,无权限操作"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 服务器错误</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"服务器内部错误"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- 删除用户 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method DELETE">DELETE</span>
<span class="api-path">/api/users/{userId}</span>
<span class="api-desc">删除用户(软删除)</span><span class="auth-badge">需Token + 管理员</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>userId</td><td>Long</td><td>要删除的用户 ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>说明</h4>
<p>软删除操作,将 <code>is_deleted</code> 设为 1不物理删除数据。</p>
</div>
<div class="detail-section">
<h4>成功响应</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"用户删除成功"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 用户不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"目标用户不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 删除失败</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"删除失败,用户可能已被删除"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 权限不足</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">403</span>,
<span class="key">"message"</span>: <span class="string">"非管理员,无权限操作"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 服务器错误</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"服务器内部错误"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
</div>
<!-- 个人资料 -->
<div class="section" id="profile">
<div class="service-card">
<h2>个人资料</h2>
<span class="port">wangpan-user-service : 8081</span>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/profile</span>
<span class="api-desc">获取当前用户个人资料</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>获取当前登录用户的基本信息(不含密码等敏感字段)。</p>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"username"</span>: <span class="string">"admin"</span>,
<span class="key">"email"</span>: <span class="string">"admin@example.com"</span>,
<span class="key">"phone"</span>: <span class="string">"13800138000"</span>,
<span class="key">"realName"</span>: <span class="string">"管理员"</span>,
<span class="key">"avatarUrl"</span>: <span class="string">"http://..."</span>,
<span class="key">"userType"</span>: <span class="number">2</span>,
<span class="key">"status"</span>: <span class="number">1</span>,
<span class="key">"createTime"</span>: <span class="string">"2024-01-01T00:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 无效"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 用户不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"用户不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method PUT">PUT</span>
<span class="api-path">/api/profile/username</span>
<span class="api-desc">修改用户名</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>修改当前登录用户的用户名。</p>
<p><strong>校验规则:</strong></p>
<ul>
<li>用户名不能为空</li>
<li>新用户名不能与当前用户名相同</li>
<li>新用户名不能被其他用户占用</li>
</ul>
</div>
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"username"</span>: <span class="string">"新用户名"</span>
}</div>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"用户名修改成功"</span>,
<span class="key">"data"</span>: null
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 用户名已存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">409</span>,
<span class="key">"message"</span>: <span class="string">"用户名已被其他用户使用"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 参数错误</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"新用户名不能为空或与当前用户名相同"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method PUT">PUT</span>
<span class="api-path">/api/profile/avatar</span>
<span class="api-desc">修改头像</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>修改当前登录用户的头像。</p>
<p><strong>流程:</strong></p>
<ol>
<li>接收 Base64 编码的图片数据</li>
<li>解析 MIME 类型和 Base64 内容</li>
<li>上传到 MinIO路径avatar/{userId}_{timestamp}.{ext}</li>
<li>更新数据库中的 avatar_url 字段</li>
</ol>
<p><strong>支持的图片格式:</strong>PNG、JPG/JPEG、GIF、WebP</p>
</div>
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"avatarData"</span>: <span class="string">"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."</span>
}</div>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"头像更新成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"avatarUrl"</span>: <span class="string">"http://your-server:39000/wangpan/avatar/1_1719234567890.png?..."</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 图片格式不支持</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"不支持的图片格式,仅支持 PNG、JPG、GIF、WebP"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — MinIO 上传失败</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"头像上传失败,请稍后重试"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
</div>
<!-- 网盘服务 -->
<div class="section" id="drive">
<div class="service-card">
<h2>网盘管理服务</h2>
<span class="port">wangpan-drive-service : 8082</span>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/drive</span>
<span class="api-desc">创建网盘</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"name"</span>: <span class="string">"网盘名称"</span>,
<span class="key">"description"</span>: <span class="string">"网盘描述"</span>,
<span class="key">"logoData"</span>: <span class="string">"data:image/png;base64,iVBOR..."</span>
}</div>
</div>
<div class="detail-section">
<h4>参数说明</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>name</td><td>String</td><td></td><td>网盘名称</td></tr>
<tr><td>description</td><td>String</td><td></td><td>网盘描述</td></tr>
<tr><td>logoData</td><td>String</td><td></td><td>Logo图片Base64编码支持 data:image/xxx;base64, 前缀格式上传后存储到MinIO数据库仅保存URL</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"创建成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"name"</span>: <span class="string">"我的网盘"</span>,
<span class="key">"description"</span>: <span class="string">"个人网盘"</span>,
<span class="key">"logoUrl"</span>: <span class="string">"http://your-server:39000/wangpan/drive/1/logo.png"</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 参数缺失</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"网盘名称不能为空"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 无效"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/drive</span>
<span class="api-desc">查询网盘列表</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>查询参数</h4>
<table class="param-table">
<tr><th>参数</th><th>默认值</th><th>说明</th></tr>
<tr><td>pageNo</td><td>1</td><td>页码</td></tr>
<tr><td>pageSize</td><td>20</td><td>每页条数</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>网盘ID</td></tr>
<tr><td>name</td><td>String</td><td>网盘名称</td></tr>
<tr><td>description</td><td>String</td><td>网盘描述</td></tr>
<tr><td>logoUrl</td><td>String</td><td>网盘Logo图片链接MinIO存储地址</td></tr>
<tr><td>folderCount</td><td>Integer</td><td>文件夹数量</td></tr>
<tr><td>fileCount</td><td>Integer</td><td>文件数量</td></tr>
<tr><td>totalSize</td><td>Long</td><td>总大小(字节)</td></tr>
<tr><td>status</td><td>String</td><td>状态</td></tr>
<tr><td>createTime</td><td>String</td><td>创建时间</td></tr>
<tr><td>updateTime</td><td>String</td><td>更新时间</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"total"</span>: <span class="number">2</span>,
<span class="key">"list"</span>: [
{
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"name"</span>: <span class="string">"我的网盘"</span>,
<span class="key">"description"</span>: <span class="string">"个人网盘"</span>,
<span class="key">"logoUrl"</span>: <span class="string">"http://your-server:39000/wangpan/drive/1/logo.png"</span>,
<span class="key">"folderCount"</span>: <span class="number">5</span>,
<span class="key">"fileCount"</span>: <span class="number">20</span>,
<span class="key">"totalSize"</span>: <span class="number">10737418240</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>
}
],
<span class="key">"pageNo"</span>: <span class="number">1</span>,
<span class="key">"pageSize"</span>: <span class="number">20</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 无效"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/drive/{id}</span>
<span class="api-desc">查询网盘详情</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>网盘ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>网盘ID</td></tr>
<tr><td>name</td><td>String</td><td>网盘名称</td></tr>
<tr><td>description</td><td>String</td><td>网盘描述</td></tr>
<tr><td>logoUrl</td><td>String</td><td>网盘Logo图片链接MinIO存储地址</td></tr>
<tr><td>folderCount</td><td>Integer</td><td>文件夹数量</td></tr>
<tr><td>fileCount</td><td>Integer</td><td>文件数量</td></tr>
<tr><td>totalSize</td><td>Long</td><td>总大小(字节)</td></tr>
<tr><td>status</td><td>String</td><td>状态</td></tr>
<tr><td>createTime</td><td>String</td><td>创建时间</td></tr>
<tr><td>updateTime</td><td>String</td><td>更新时间</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"name"</span>: <span class="string">"我的网盘"</span>,
<span class="key">"description"</span>: <span class="string">"个人网盘"</span>,
<span class="key">"logoUrl"</span>: <span class="string">"http://your-server:39000/wangpan/drive/1/logo.png"</span>,
<span class="key">"folderCount"</span>: <span class="number">5</span>,
<span class="key">"fileCount"</span>: <span class="number">20</span>,
<span class="key">"totalSize"</span>: <span class="number">10737418240</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 网盘不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"网盘不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 无效"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method PUT">PUT</span>
<span class="api-path">/api/drive/{id}</span>
<span class="api-desc">更新网盘</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"name"</span>: <span class="string">"新网盘名称"</span>,
<span class="key">"description"</span>: <span class="string">"新描述"</span>,
<span class="key">"logoData"</span>: <span class="string">"data:image/png;base64,iVBOR..."</span>
}</div>
</div>
<div class="detail-section">
<h4>参数说明</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>name</td><td>String</td><td></td><td>网盘名称</td></tr>
<tr><td>description</td><td>String</td><td></td><td>网盘描述</td></tr>
<tr><td>logoData</td><td>String</td><td></td><td>新的Logo图片Base64编码传入后覆盖旧Logo不传则保留原Logo</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"更新成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"name"</span>: <span class="string">"新网盘名称"</span>,
<span class="key">"description"</span>: <span class="string">"新描述"</span>,
<span class="key">"logoUrl"</span>: <span class="string">"http://your-server:39000/wangpan/drive/1/logo.png"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-26T11:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 网盘不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"网盘不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 权限不足</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">403</span>,
<span class="key">"message"</span>: <span class="string">"无权修改该网盘"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method DELETE">DELETE</span>
<span class="api-path">/api/drive/{id}</span>
<span class="api-desc">删除网盘</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>网盘ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"删除成功"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 网盘不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"网盘不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 权限不足</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">403</span>,
<span class="key">"message"</span>: <span class="string">"无权删除该网盘"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/drive/{driveId}/folders</span>
<span class="api-desc">创建文件夹</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"name"</span>: <span class="string">"文件夹名称"</span>,
<span class="key">"parentId"</span>: <span class="number">0</span>
}</div>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"创建成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"parentId"</span>: <span class="number">0</span>,
<span class="key">"name"</span>: <span class="string">"我的电影"</span>,
<span class="key">"description"</span>: <span class="string">""</span>,
<span class="key">"depth"</span>: <span class="number">0</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 参数缺失</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"文件夹名称不能为空"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 网盘不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"网盘不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/drive/{driveId}/folders</span>
<span class="api-desc">查询文件夹列表</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>查询参数</h4>
<table class="param-table">
<tr><th>参数</th><th>默认值</th><th>说明</th></tr>
<tr><td>parentId</td><td>0</td><td>父文件夹 ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: [
{
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"parentId"</span>: <span class="number">0</span>,
<span class="key">"name"</span>: <span class="string">"我的电影"</span>,
<span class="key">"description"</span>: <span class="string">""</span>,
<span class="key">"depth"</span>: <span class="number">0</span>,
<span class="key">"childFolderCount"</span>: <span class="number">2</span>,
<span class="key">"fileCount"</span>: <span class="number">5</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>
}
]
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 网盘不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"网盘不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 无效"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/drive/{driveId}/folders/{folderId}</span>
<span class="api-desc">查询文件夹详情</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>driveId</td><td>Long</td><td>网盘ID</td></tr>
<tr><td>folderId</td><td>Long</td><td>文件夹ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>文件夹ID</td></tr>
<tr><td>driveId</td><td>Long</td><td>所属网盘ID</td></tr>
<tr><td>parentId</td><td>Long</td><td>父文件夹ID0表示根目录</td></tr>
<tr><td>name</td><td>String</td><td>文件夹名称</td></tr>
<tr><td>description</td><td>String</td><td>文件夹描述</td></tr>
<tr><td>depth</td><td>Integer</td><td>层级深度</td></tr>
<tr><td>childFolderCount</td><td>Integer</td><td>子文件夹数量</td></tr>
<tr><td>fileCount</td><td>Integer</td><td>文件数量</td></tr>
<tr><td>totalSize</td><td>Long</td><td>总大小(字节)</td></tr>
<tr><td>createTime</td><td>String</td><td>创建时间</td></tr>
<tr><td>updateTime</td><td>String</td><td>更新时间</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"parentId"</span>: <span class="number">0</span>,
<span class="key">"name"</span>: <span class="string">"我的电影"</span>,
<span class="key">"description"</span>: <span class="string">"收藏的电影"</span>,
<span class="key">"depth"</span>: <span class="number">0</span>,
<span class="key">"childFolderCount"</span>: <span class="number">2</span>,
<span class="key">"fileCount"</span>: <span class="number">5</span>,
<span class="key">"totalSize"</span>: <span class="number">5368709120</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 文件夹不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"文件夹不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 无效"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method PUT">PUT</span>
<span class="api-path">/api/drive/{driveId}/folders/{folderId}</span>
<span class="api-desc">更新文件夹</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>driveId</td><td>Long</td><td>网盘ID</td></tr>
<tr><td>folderId</td><td>Long</td><td>文件夹ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>请求体(部分更新)</h4>
<div class="code-block">{
<span class="key">"name"</span>: <span class="string">"新文件夹名称"</span>,
<span class="key">"description"</span>: <span class="string">"新描述"</span>,
<span class="key">"parentId"</span>: <span class="number">0</span>
}</div>
</div>
<div class="detail-section">
<h4>参数说明</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>name</td><td>String</td><td></td><td>文件夹名称</td></tr>
<tr><td>description</td><td>String</td><td></td><td>文件夹描述</td></tr>
<tr><td>parentId</td><td>Long</td><td></td><td>父文件夹ID可移动文件夹</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"更新成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"parentId"</span>: <span class="number">0</span>,
<span class="key">"name"</span>: <span class="string">"新文件夹名称"</span>,
<span class="key">"description"</span>: <span class="string">"新描述"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-26T11:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 文件夹不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"文件夹不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 权限不足</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">403</span>,
<span class="key">"message"</span>: <span class="string">"无权修改该文件夹"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method DELETE">DELETE</span>
<span class="api-path">/api/drive/{driveId}/folders/{folderId}</span>
<span class="api-desc">删除文件夹</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>driveId</td><td>Long</td><td>网盘ID</td></tr>
<tr><td>folderId</td><td>Long</td><td>文件夹ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>说明</h4>
<p>删除文件夹前会检查文件夹是否为空,如果包含子文件夹或电影文件则拒绝删除。</p>
</div>
<div class="detail-section">
<h4>错误码</h4>
<table class="param-table">
<tr><th>code</th><th>说明</th></tr>
<tr><td>400</td><td>文件夹不为空或数据库操作失败</td></tr>
<tr><td>401</td><td>未登录或Token无效</td></tr>
<tr><td>403</td><td>无权访问该文件夹</td></tr>
</table>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/drive/{driveId}/movies</span>
<span class="api-desc">创建电影(只传文件名)</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>driveId</td><td>Long</td><td>网盘ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"title"</span>: <span class="string">"文件名.mp4"</span>, <span style="color:#718096">// 必填,文件名(同时作为电影标题)</span>
<span class="key">"folderId"</span>: <span class="number">0</span> <span style="color:#718096">// 可选默认0根目录</span>
}</div>
</div>
<div class="detail-section">
<h4>参数说明</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>title</td><td>String</td><td></td><td>文件名(同时作为电影标题,后续通过 TMDB 刮削补充信息)</td></tr>
<tr><td>folderId</td><td>Long</td><td></td><td>目标文件夹ID默认0根目录</td></tr>
</table>
</div>
<div class="detail-section">
<h4>说明</h4>
<p>只记录文件名,不上传文件数据。创建后通过 TMDB 刮削接口补充电影的海报、描述、评分等信息。自动检测 MIME 类型mp4/mkv/avi → video/mp4</p>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"创建成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"folderId"</span>: <span class="number">0</span>,
<span class="key">"title"</span>: <span class="string">"电影.mp4"</span>,
<span class="key">"originalName"</span>: <span class="string">"电影.mp4"</span>,
<span class="key">"mimeType"</span>: <span class="string">"video/mp4"</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 参数缺失</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"文件名不能为空"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 网盘不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"网盘不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/drive/{driveId}/folders/{folderId}/movies</span>
<span class="api-desc">查询电影文件列表</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>driveId</td><td>Long</td><td>网盘ID</td></tr>
<tr><td>folderId</td><td>Long</td><td>文件夹ID0表示根目录</td></tr>
</table>
</div>
<div class="detail-section">
<h4>查询参数</h4>
<table class="param-table">
<tr><th>参数</th><th>默认值</th><th>说明</th></tr>
<tr><td>pageNo</td><td>1</td><td>页码</td></tr>
<tr><td>pageSize</td><td>20</td><td>每页条数最大100</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>list[].id</td><td>Long</td><td>电影ID</td></tr>
<tr><td>list[].title</td><td>String</td><td>电影名称</td></tr>
<tr><td>list[].originalName</td><td>String</td><td>原始文件名</td></tr>
<tr><td>list[].mimeType</td><td>String</td><td>MIME类型</td></tr>
<tr><td>list[].fileSize</td><td>Long</td><td>文件大小(字节)</td></tr>
<tr><td>list[].extension</td><td>String</td><td>文件扩展名</td></tr>
<tr><td>list[].minioUrl</td><td>String</td><td>MinIO文件访问URL</td></tr>
<tr><td>list[].rating</td><td>Integer</td><td>评分</td></tr>
<tr><td>list[].tags</td><td>String</td><td>标签</td></tr>
<tr><td>total</td><td>Integer</td><td>总数</td></tr>
<tr><td>pageNo</td><td>Integer</td><td>当前页码</td></tr>
<tr><td>pageSize</td><td>Integer</td><td>每页条数</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"total"</span>: <span class="number">5</span>,
<span class="key">"pageNo"</span>: <span class="number">1</span>,
<span class="key">"pageSize"</span>: <span class="number">20</span>,
<span class="key">"list"</span>: [
{
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"title"</span>: <span class="string">"电影.mp4"</span>,
<span class="key">"originalName"</span>: <span class="string">"电影.mp4"</span>,
<span class="key">"mimeType"</span>: <span class="string">"video/mp4"</span>,
<span class="key">"fileSize"</span>: <span class="number">1073741824</span>,
<span class="key">"extension"</span>: <span class="string">"mp4"</span>,
<span class="key">"minioUrl"</span>: <span class="string">"http://..."</span>,
<span class="key">"rating"</span>: <span class="number">85</span>,
<span class="key">"tags"</span>: <span class="string">"动作,科幻"</span>
}
]
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 网盘不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"网盘不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 无效"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/drive/{driveId}/movies/{movieId}</span>
<span class="api-desc">查询电影文件详情</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>driveId</td><td>Long</td><td>网盘ID</td></tr>
<tr><td>movieId</td><td>Long</td><td>电影ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>电影ID</td></tr>
<tr><td>driveId</td><td>Long</td><td>所属网盘ID</td></tr>
<tr><td>folderId</td><td>Long</td><td>所属文件夹ID</td></tr>
<tr><td>title</td><td>String</td><td>电影名称</td></tr>
<tr><td>mimeType</td><td>String</td><td>MIME类型</td></tr>
<tr><td>fileSize</td><td>Long</td><td>文件大小(字节)</td></tr>
<tr><td>extension</td><td>String</td><td>文件扩展名</td></tr>
<tr><td>description</td><td>String</td><td>描述</td></tr>
<tr><td>maintainer</td><td>String</td><td>维护者</td></tr>
<tr><td>status</td><td>String</td><td>状态</td></tr>
<tr><td>scrapeType</td><td>String</td><td>刮削类型</td></tr>
<tr><td>scrapeStatus</td><td>String</td><td>刮削状态</td></tr>
<tr><td>rating</td><td>Integer|null</td><td>评分(刮削成功时返回)</td></tr>
<tr><td>tags</td><td>String</td><td>标签</td></tr>
<tr><td>casts</td><td>String|null</td><td>主演(刮削成功时返回)</td></tr>
<tr><td>directors</td><td>String|null</td><td>导演(刮削成功时返回)</td></tr>
<tr><td>posterUrl</td><td>String|null</td><td>海报URL刮削成功时返回</td></tr>
<tr><td>backdropUrl</td><td>String|null</td><td>背景图URL刮削成功时返回</td></tr>
<tr><td>createTime</td><td>String</td><td>创建时间</td></tr>
<tr><td>updateTime</td><td>String</td><td>更新时间</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例(刮削成功)</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"folderId"</span>: <span class="number">0</span>,
<span class="key">"title"</span>: <span class="string">"阿凡达.mp4"</span>,
<span class="key">"mimeType"</span>: <span class="string">"video/mp4"</span>,
<span class="key">"fileSize"</span>: <span class="number">1073741824</span>,
<span class="key">"extension"</span>: <span class="string">"mp4"</span>,
<span class="key">"description"</span>: <span class="string">"电影描述"</span>,
<span class="key">"maintainer"</span>: <span class="string">"admin"</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>,
<span class="key">"scrapeType"</span>: <span class="string">"tmdb"</span>,
<span class="key">"scrapeStatus"</span>: <span class="string">"success"</span>,
<span class="key">"rating"</span>: <span class="number">85</span>,
<span class="key">"tags"</span>: <span class="string">"动作,科幻"</span>,
<span class="key">"casts"</span>: <span class="string">"主演1,主演2"</span>,
<span class="key">"directors"</span>: <span class="string">"导演1"</span>,
<span class="key">"posterUrl"</span>: <span class="string">"http://..."</span>,
<span class="key">"backdropUrl"</span>: <span class="string">"http://..."</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>成功响应示例(未刮削/刮削失败)</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">2</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"folderId"</span>: <span class="number">0</span>,
<span class="key">"title"</span>: <span class="string">"未刮削电影.mp4"</span>,
<span class="key">"mimeType"</span>: <span class="string">"video/mp4"</span>,
<span class="key">"fileSize"</span>: <span class="number">536870912</span>,
<span class="key">"extension"</span>: <span class="string">"mp4"</span>,
<span class="key">"description"</span>: <span class="keyword">null</span>,
<span class="key">"maintainer"</span>: <span class="keyword">null</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>,
<span class="key">"scrapeType"</span>: <span class="string">"none"</span>,
<span class="key">"scrapeStatus"</span>: <span class="string">"pending"</span>,
<span class="key">"rating"</span>: <span class="keyword">null</span>,
<span class="key">"tags"</span>: <span class="keyword">null</span>,
<span class="key">"casts"</span>: <span class="keyword">null</span>,
<span class="key">"directors"</span>: <span class="keyword">null</span>,
<span class="key">"posterUrl"</span>: <span class="keyword">null</span>,
<span class="key">"backdropUrl"</span>: <span class="keyword">null</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-26T10:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 电影不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"电影不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 无效"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method PUT">PUT</span>
<span class="api-path">/api/drive/{driveId}/movies/{movieId}</span>
<span class="api-desc">更新电影文件信息</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>driveId</td><td>Long</td><td>网盘ID</td></tr>
<tr><td>movieId</td><td>Long</td><td>电影ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>请求体(部分更新,所有字段可选)</h4>
<div class="code-block">{
<span class="key">"title"</span>: <span class="string">"新电影名称"</span>,
<span class="key">"description"</span>: <span class="string">"新描述"</span>,
<span class="key">"rating"</span>: <span class="number">90</span>,
<span class="key">"tags"</span>: <span class="string">"新标签"</span>,
<span class="key">"posterUrl"</span>: <span class="string">"海报URL"</span>,
<span class="key">"backdropUrl"</span>: <span class="string">"背景图URL"</span>,
<span class="key">"casts"</span>: <span class="string">"主演列表"</span>,
<span class="key">"directors"</span>: <span class="string">"导演列表"</span>,
<span class="key">"resourceLibrary"</span>: <span class="string">"资源库来源"</span>,
<span class="key">"maintainer"</span>: <span class="string">"维护者"</span>
}</div>
</div>
<div class="detail-section">
<h4>参数说明</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>title</td><td>String</td><td>电影名称</td></tr>
<tr><td>description</td><td>String</td><td>描述</td></tr>
<tr><td>rating</td><td>Integer</td><td>评分 0-100</td></tr>
<tr><td>tags</td><td>String</td><td>标签</td></tr>
<tr><td>posterUrl</td><td>String</td><td>海报URL</td></tr>
<tr><td>backdropUrl</td><td>String</td><td>背景图URL</td></tr>
<tr><td>casts</td><td>String</td><td>主演(逗号分隔)</td></tr>
<tr><td>directors</td><td>String</td><td>导演(逗号分隔)</td></tr>
<tr><td>resourceLibrary</td><td>String</td><td>资源库来源</td></tr>
<tr><td>maintainer</td><td>String</td><td>维护者</td></tr>
</table>
</div>
<div class="detail-section">
<h4>说明</h4>
<p>用于 TMDB 刮削有误时,用户可自定义修改文件名等信息。所有字段均为可选,只更新传入的字段。</p>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"更新成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"title"</span>: <span class="string">"新电影名称"</span>,
<span class="key">"rating"</span>: <span class="number">90</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-26T11:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 电影不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"电影不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 权限不足</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">403</span>,
<span class="key">"message"</span>: <span class="string">"无权修改该电影"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method DELETE">DELETE</span>
<span class="api-path">/api/drive/{driveId}/movies/{movieId}</span>
<span class="api-desc">删除电影文件</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>driveId</td><td>Long</td><td>网盘ID</td></tr>
<tr><td>movieId</td><td>Long</td><td>电影ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>说明</h4>
<p>逻辑删除(软删除),删除后自动更新网盘和文件夹的统计信息。</p>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"删除成功"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 电影不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"电影不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 权限不足</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">403</span>,
<span class="key">"message"</span>: <span class="string">"无权删除该电影"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- 重复电影检查 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/drive/movies/duplicates</span>
<span class="api-desc">检查重复电影</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>查询当前用户所有网盘下所有文件夹中的电影按标题title分组找出重复的电影。返回每组重复电影的详细信息包括所在文件夹的完整路径前端可让用户选择删除其中一个。</p>
</div>
<div class="detail-section">
<h4>成功响应</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"duplicateGroups"</span>: [
{
<span class="key">"title"</span>: <span class="string">"电影名称"</span>,
<span class="key">"count"</span>: <span class="number">2</span>,
<span class="key">"movies"</span>: [
{
<span class="key">"id"</span>: <span class="number">10</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"folderId"</span>: <span class="number">5</span>,
<span class="key">"title"</span>: <span class="string">"电影名称"</span>,
<span class="key">"originalName"</span>: <span class="string">"电影名称.mp4"</span>,
<span class="key">"fileSize"</span>: <span class="number">1073741824</span>,
<span class="key">"extension"</span>: <span class="string">"mp4"</span>,
<span class="key">"category"</span>: <span class="string">"sci-fi"</span>,
<span class="key">"rating"</span>: <span class="number">85</span>,
<span class="key">"createTime"</span>: <span class="string">"2025-01-01T10:00:00"</span>,
<span class="key">"folderPath"</span>: <span class="string">"我的电影库/科幻/2024"</span>
},
{
<span class="key">"id"</span>: <span class="number">20</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"folderId"</span>: <span class="number">8</span>,
<span class="key">"title"</span>: <span class="string">"电影名称"</span>,
<span class="key">"originalName"</span>: <span class="string">"电影名称.mkv"</span>,
<span class="key">"fileSize"</span>: <span class="number">2147483648</span>,
<span class="key">"extension"</span>: <span class="string">"mkv"</span>,
<span class="key">"category"</span>: <span class="string">"sci-fi"</span>,
<span class="key">"rating"</span>: <span class="keyword">null</span>,
<span class="key">"createTime"</span>: <span class="string">"2025-02-01T15:00:00"</span>,
<span class="key">"folderPath"</span>: <span class="string">"我的电影库/动作"</span>
}
]
}
],
<span class="key">"totalGroups"</span>: <span class="number">1</span>,
<span class="key">"totalDuplicates"</span>: <span class="number">2</span>
}
}</div>
</div>
<div class="detail-section">
<h4>返回字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>duplicateGroups</td><td>Array</td><td>重复电影分组列表</td></tr>
<tr><td>duplicateGroups[].title</td><td>String</td><td>重复的电影标题</td></tr>
<tr><td>duplicateGroups[].count</td><td>Integer</td><td>该标题下的重复数量</td></tr>
<tr><td>duplicateGroups[].movies</td><td>Array</td><td>该标题下的所有电影记录</td></tr>
<tr><td>duplicateGroups[].movies[].id</td><td>Long</td><td>电影ID可用于删除接口</td></tr>
<tr><td>duplicateGroups[].movies[].folderPath</td><td>String</td><td>所在文件夹完整路径,如"网盘名/文件夹1/文件夹2"</td></tr>
<tr><td>totalGroups</td><td>Integer</td><td>重复组总数</td></tr>
<tr><td>totalDuplicates</td><td>Integer</td><td>涉及重复的电影总数</td></tr>
</table>
</div>
<div class="detail-section">
<h4>错误码</h4>
<table class="param-table">
<tr><th>code</th><th>说明</th></tr>
<tr><td>401</td><td>未登录或 Token 无效</td></tr>
<tr><td>500</td><td>服务器内部错误</td></tr>
</table>
</div>
</div>
</div>
</div>
<!-- Icon服务 -->
<div class="section" id="icon">
<div class="service-card">
<h2>Icon 管理服务</h2>
<span class="port">wangpan-drive-service : 8082</span>
</div>
<!-- 创建 Icon -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/icon</span>
<span class="api-desc">创建 Icon</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求体JSON</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>name</td><td>String</td><td></td><td>Icon 名称最大100字符</td></tr>
<tr><td>url</td><td>String</td><td>条件必填</td><td>外网链接 URL最大500字符linkType为external或both时必填</td></tr>
<tr><td>internalUrl</td><td>String</td><td>条件必填</td><td>内网链接 URL最大500字符linkType为internal或both时必填</td></tr>
<tr><td>linkType</td><td>String</td><td></td><td>链接类型external仅外网默认/ internal仅内网/ both内外网</td></tr>
<tr><td>description</td><td>String</td><td></td><td>Icon 描述最大500字符</td></tr>
<tr><td>sortOrder</td><td>Integer</td><td></td><td>排序值数字越小越靠前默认0</td></tr>
</table>
</div>
<div class="detail-section">
<h4>请求示例(仅外网)</h4>
<div class="code-block">{
<span class="key">"name"</span>: <span class="string">"百度"</span>,
<span class="key">"url"</span>: <span class="string">"https://www.example.com"</span>,
<span class="key">"linkType"</span>: <span class="string">"external"</span>,
<span class="key">"description"</span>: <span class="string">"搜索引擎"</span>,
<span class="key">"sortOrder"</span>: <span class="number">1</span>
}</div>
</div>
<div class="detail-section">
<h4>请求示例(内外网都有)</h4>
<div class="code-block">{
<span class="key">"name"</span>: <span class="string">"内部系统"</span>,
<span class="key">"url"</span>: <span class="string">"https://system.example.com"</span>,
<span class="key">"internalUrl"</span>: <span class="string">"http://192.168.x.x:8080"</span>,
<span class="key">"linkType"</span>: <span class="string">"both"</span>,
<span class="key">"description"</span>: <span class="string">"内部管理系统"</span>,
<span class="key">"sortOrder"</span>: <span class="number">2</span>
}</div>
</div>
<div class="detail-section">
<h4>成功响应200</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"创建成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"name"</span>: <span class="string">"百度"</span>,
<span class="key">"url"</span>: <span class="string">"https://www.example.com"</span>,
<span class="key">"internalUrl"</span>: <span class="keyword">null</span>,
<span class="key">"linkType"</span>: <span class="string">"external"</span>,
<span class="key">"description"</span>: <span class="string">"搜索引擎"</span>,
<span class="key">"sortOrder"</span>: <span class="number">1</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>,
<span class="key">"logoUrl"</span>: <span class="string">"http://your-server:39000/wangpan/icons/2/1718265600000.png"</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-13T16:00:00"</span>
}
}</div>
</div>
</div>
</div>
<!-- 查询 Icon 列表 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/icon</span>
<span class="api-desc">查询当前用户的 Icon 列表</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>查询当前登录用户的所有 Icon按 sortOrder 升序、createTime 降序排列。每个用户只能看到自己创建的 Icon。</p>
</div>
<div class="detail-section">
<h4>成功响应200</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"list"</span>: [
{
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"name"</span>: <span class="string">"百度"</span>,
<span class="key">"url"</span>: <span class="string">"https://www.example.com"</span>,
<span class="key">"internalUrl"</span>: <span class="keyword">null</span>,
<span class="key">"linkType"</span>: <span class="string">"external"</span>,
<span class="key">"description"</span>: <span class="string">"搜索引擎"</span>,
<span class="key">"sortOrder"</span>: <span class="number">1</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>,
<span class="key">"logoUrl"</span>: <span class="string">"http://your-server:39000/wangpan/icons/2/1718265600000.png"</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-13T16:00:00"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-13T16:00:00"</span>
}
],
<span class="key">"total"</span>: <span class="number">1</span>
}
}</div>
</div>
</div>
</div>
<!-- 查询单个 Icon -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/icon/{id}</span>
<span class="api-desc">查询单个 Icon 详情</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>Icon ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应200</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"name"</span>: <span class="string">"百度"</span>,
<span class="key">"url"</span>: <span class="string">"https://www.example.com"</span>,
<span class="key">"internalUrl"</span>: <span class="keyword">null</span>,
<span class="key">"linkType"</span>: <span class="string">"external"</span>,
<span class="key">"description"</span>: <span class="string">"搜索引擎"</span>,
<span class="key">"sortOrder"</span>: <span class="number">1</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>,
<span class="key">"logoUrl"</span>: <span class="string">"http://your-server:39000/wangpan/icons/2/1718265600000.png"</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-13T16:00:00"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-13T16:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — Icon 不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"Icon 不存在或无权访问"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 无效"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- 更新 Icon -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method PUT">PUT</span>
<span class="api-path">/api/icon/{id}</span>
<span class="api-desc">更新 Icon</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>Icon ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>请求体JSON所有字段可选</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>name</td><td>String</td><td>Icon 名称</td></tr>
<tr><td>url</td><td>String</td><td>外网链接</td></tr>
<tr><td>internalUrl</td><td>String</td><td>内网链接</td></tr>
<tr><td>linkType</td><td>String</td><td>链接类型external / internal / both</td></tr>
<tr><td>description</td><td>String</td><td>Icon 描述</td></tr>
<tr><td>sortOrder</td><td>Integer</td><td>排序值</td></tr>
</table>
</div>
<div class="detail-section">
<h4>请求示例</h4>
<div class="code-block">{
<span class="key">"name"</span>: <span class="string">"新名称"</span>,
<span class="key">"url"</span>: <span class="string">"https://new-url.com"</span>,
<span class="key">"internalUrl"</span>: <span class="string">"http://192.168.x.x:8080"</span>,
<span class="key">"linkType"</span>: <span class="string">"both"</span>,
<span class="key">"sortOrder"</span>: <span class="number">2</span>
}</div>
</div>
<div class="detail-section">
<h4>成功响应200</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"更新成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"name"</span>: <span class="string">"新名称"</span>,
<span class="key">"url"</span>: <span class="string">"https://new-url.com"</span>,
<span class="key">"internalUrl"</span>: <span class="string">"http://192.168.x.x:8080"</span>,
<span class="key">"linkType"</span>: <span class="string">"both"</span>,
<span class="key">"description"</span>: <span class="string">"搜索引擎"</span>,
<span class="key">"sortOrder"</span>: <span class="number">2</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>,
<span class="key">"logoUrl"</span>: <span class="string">"http://your-server:39000/wangpan/icons/2/1718265600000.png"</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-13T16:00:00"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-13T17:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>错误码</h4>
<table class="param-table">
<tr><th>code</th><th>说明</th></tr>
<tr><td>401</td><td>未登录或 Token 无效</td></tr>
<tr><td>500</td><td>Icon 不存在或无权修改</td></tr>
</table>
</div>
</div>
</div>
<!-- 删除 Icon -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method DELETE">DELETE</span>
<span class="api-path">/api/icon/{id}</span>
<span class="api-desc">删除 Icon</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>Icon ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应200</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"删除成功"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>错误码</h4>
<table class="param-table">
<tr><th>code</th><th>说明</th></tr>
<tr><td>401</td><td>未登录或 Token 无效</td></tr>
<tr><td>500</td><td>Icon 不存在或无权删除</td></tr>
</table>
</div>
</div>
</div>
<!-- 上传自定义图标 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/icon/{id}/logo</span>
<span class="api-desc">上传自定义图标图片</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>上传自定义图标图片替换自动抓取的 logo。上传后会自动删除旧的 MinIO 图片,防止硬盘空间浪费。系统每 24 小时自动清理无用图片。</p>
</div>
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>Icon ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>请求格式</h4>
<p><code>multipart/form-data</code></p>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>file</td><td>File</td><td></td><td>图片文件png/jpg/jpeg/gif/webp/ico最大 1MB</td></tr>
</table>
</div>
<div class="detail-section">
<h4>请求示例curl</h4>
<div class="code-block">curl -X POST http://localhost:8888/api/icon/1/logo \
-H "Authorization: Bearer &lt;token&gt;" \
-F "file=@/path/to/icon.png"</div>
</div>
<div class="detail-section">
<h4>成功响应200</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"图标上传成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"name"</span>: <span class="string">"百度"</span>,
<span class="key">"logoUrl"</span>: <span class="string">"http://your-server:39000/wangpan/icons/2/1718265600000.png"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-15T10:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 文件为空</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"上传文件为空"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 文件格式不支持</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"不支持的文件格式,仅支持 png/jpg/jpeg/gif/webp/ico"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 文件超过 1MB</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"文件大小超过限制(最大 1MB"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — Icon 不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"Icon 不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
</div>
<!-- TMDB服务 -->
<div class="section" id="tmdb">
<div class="service-card">
<h2>TMDB 刮削服务</h2>
<span class="port">wangpan-tmdb-service : 8083</span>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/movies/recommend</span>
<span class="api-desc">今日推荐每天固定3部电影</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>从当前用户的电影库中随机抽取3部电影用于首页展示。</p>
<p><strong>推荐规则:</strong></p>
<ul>
<li>从 00:00 到 23:59 返回固定的3部电影</li>
<li>当天不管查询多少次都返回相同的结果</li>
<li>第二天 00:00 后重新随机挑选3部</li>
<li>推荐结果缓存在 wp_daily_recommend 表中</li>
</ul>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"list"</span>: [
{
<span class="key">"id"</span>: <span class="number">10</span>,
<span class="key">"title"</span>: <span class="string">"电影名称1"</span>,
<span class="key">"posterUrl"</span>: <span class="string">"海报URL"</span>,
<span class="key">"rating"</span>: <span class="number">85</span>,
<span class="key">"category"</span>: <span class="string">"动作"</span>,
<span class="key">"tags"</span>: <span class="string">"标签1,标签2"</span>,
<span class="key">"createTime"</span>: <span class="string">"2025-01-01T10:00:00"</span>
},
{
<span class="key">"id"</span>: <span class="number">20</span>,
<span class="key">"title"</span>: <span class="string">"电影名称2"</span>,
...
},
{
<span class="key">"id"</span>: <span class="number">30</span>,
<span class="key">"title"</span>: <span class="string">"电影名称3"</span>,
...
}
],
<span class="key">"count"</span>: <span class="number">3</span>
}
}</div>
</div>
<div class="detail-section">
<h4>返回字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>list</td><td>Array</td><td>推荐的电影列表最多3部</td></tr>
<tr><td>list[].id</td><td>Long</td><td>电影ID</td></tr>
<tr><td>list[].title</td><td>String</td><td>电影标题</td></tr>
<tr><td>list[].posterUrl</td><td>String</td><td>海报URL</td></tr>
<tr><td>list[].rating</td><td>Integer</td><td>评分0-100</td></tr>
<tr><td>list[].category</td><td>String</td><td>分类</td></tr>
<tr><td>list[].tags</td><td>String</td><td>标签</td></tr>
<tr><td>list[].createTime</td><td>String</td><td>创建时间</td></tr>
<tr><td>count</td><td>Integer</td><td>实际返回的电影数量可能小于3</td></tr>
</table>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 服务器内部错误</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"服务器内部错误,请稍后重试"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/movies</span>
<span class="api-desc">电影列表查询按文件夹分组每组最多10条</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p><code>wp_drive_movie</code> 表查询当前用户所有电影,按文件夹分组返回,每个文件夹最多返回 10 条电影数据。支持分页pageNo 控制文件夹分页。</p>
</div>
<div class="detail-section">
<h4>查询参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>keyword</td><td>String</td><td></td><td>搜索关键词</td></tr>
<tr><td>status</td><td>String</td><td></td><td>电影状态active正常/ deleted已删除</td></tr>
<tr><td>scrapeType</td><td>String</td><td></td><td>刮削类型tmdb / manual / none</td></tr>
<tr><td>sortBy</td><td>String</td><td></td><td>排序字段(如 createTime、rating、title</td></tr>
<tr><td>sortOrder</td><td>String</td><td></td><td>排序方向asc / desc</td></tr>
<tr><td>pageNo</td><td>int</td><td></td><td>页码,默认 1</td></tr>
<tr><td>pageSize</td><td>int</td><td></td><td>每页条数(文件夹数),默认 20</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>totalFolders</td><td>Integer</td><td>文件夹总数</td></tr>
<tr><td>pageNo</td><td>Integer</td><td>当前页码</td></tr>
<tr><td>pageSize</td><td>Integer</td><td>每页条数</td></tr>
<tr><td>folderGroups[].folderId</td><td>Long</td><td>文件夹ID</td></tr>
<tr><td>folderGroups[].movieCount</td><td>Integer</td><td>该文件夹下返回的电影数量</td></tr>
<tr><td>folderGroups[].movies[].id</td><td>Long</td><td>电影ID</td></tr>
<tr><td>folderGroups[].movies[].driveId</td><td>Long</td><td>所属网盘ID</td></tr>
<tr><td>folderGroups[].movies[].folderId</td><td>Long</td><td>所属文件夹ID</td></tr>
<tr><td>folderGroups[].movies[].title</td><td>String</td><td>电影名称</td></tr>
<tr><td>folderGroups[].movies[].originalName</td><td>String</td><td>原始文件名</td></tr>
<tr><td>folderGroups[].movies[].mimeType</td><td>String</td><td>MIME类型</td></tr>
<tr><td>folderGroups[].movies[].fileSize</td><td>Long</td><td>文件大小(字节)</td></tr>
<tr><td>folderGroups[].movies[].extension</td><td>String</td><td>文件扩展名</td></tr>
<tr><td>folderGroups[].movies[].description</td><td>String</td><td>描述</td></tr>
<tr><td>folderGroups[].movies[].scrapeStatus</td><td>String</td><td>刮削状态</td></tr>
<tr><td>folderGroups[].movies[].rating</td><td>Integer</td><td>评分0-100</td></tr>
<tr><td>folderGroups[].movies[].tags</td><td>String</td><td>标签</td></tr>
<tr><td>folderGroups[].movies[].casts</td><td>String</td><td>主演逗号分隔TMDB刮削</td></tr>
<tr><td>folderGroups[].movies[].directors</td><td>String</td><td>导演逗号分隔TMDB刮削</td></tr>
<tr><td>folderGroups[].movies[].posterUrl</td><td>String</td><td>竖版海报URL2:3 比例)</td></tr>
<tr><td>folderGroups[].movies[].backdropUrl</td><td>String</td><td>横版背景图URL16:9 比例)</td></tr>
<tr><td>folderGroups[].movies[].minioUrl</td><td>String</td><td>MinIO 文件访问URL</td></tr>
<tr><td>folderGroups[].movies[].createTime</td><td>String</td><td>创建时间</td></tr>
<tr><td>folderGroups[].movies[].updateTime</td><td>String</td><td>更新时间</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"totalFolders"</span>: <span class="number">5</span>,
<span class="key">"pageNo"</span>: <span class="number">1</span>,
<span class="key">"pageSize"</span>: <span class="number">20</span>,
<span class="key">"folderGroups"</span>: [
{
<span class="key">"folderId"</span>: <span class="number">3</span>,
<span class="key">"movieCount"</span>: <span class="number">2</span>,
<span class="key">"movies"</span>: [
{
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"folderId"</span>: <span class="number">3</span>,
<span class="key">"title"</span>: <span class="string">"肖申克的救赎"</span>,
<span class="key">"posterUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/1/poster_xxx.jpg"</span>,
<span class="key">"backdropUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/1/backdrop_xxx.jpg"</span>,
<span class="key">"rating"</span>: <span class="number">93</span>,
...
}
]
}
]
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 参数错误</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"请求参数错误"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/movies/all</span>
<span class="api-desc">查询所有电影(不分文件夹,直接分页)</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>查询当前用户所有网盘下所有未删除的电影不进行分类直接分页返回。适用于大数据量场景1万+内置5分钟缓存机制支持关键词搜索和多字段排序。</p>
<p><strong>设计特点:</strong></p>
<ul>
<li>不进行分类,直接分页查询所有电影</li>
<li>支持关键词搜索(标题模糊匹配)</li>
<li>支持排序(按创建时间、更新时间、评分、标题)</li>
<li>内置5分钟缓存适用于1万+数据量场景</li>
<li>最大每页100条防止单次查询数据量过大</li>
<li><strong>每日随机刷新</strong>第1页前20条数据每日随机刷新无关键词搜索时生效增加内容变化性其余数据按排序规则保持不变</li>
</ul>
<p><strong>每日随机刷新机制:</strong></p>
<ul>
<li>触发条件查询第1页且无关键词搜索时</li>
<li>随机数量前20条数据从所有电影中随机选取</li>
<li>缓存策略缓存Key包含日期因子每日自动刷新</li>
<li>其余数据第21条及以后按指定排序规则返回</li>
<li>关键词搜索时:不触发随机,正常返回搜索结果</li>
</ul>
</div>
<div class="detail-section">
<h4>查询参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>keyword</td><td>String</td><td></td><td>搜索关键词(电影标题模糊匹配)</td></tr>
<tr><td>sortBy</td><td>String</td><td></td><td>排序字段createTime创建时间/ updateTime更新时间/ rating评分/ title标题</td></tr>
<tr><td>sortOrder</td><td>String</td><td></td><td>排序方向asc升序/ desc降序默认desc</td></tr>
<tr><td>pageNo</td><td>int</td><td></td><td>页码,默认 1</td></tr>
<tr><td>pageSize</td><td>int</td><td></td><td>每页条数,默认 50最大 100</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>total</td><td>Long</td><td>电影总数</td></tr>
<tr><td>pages</td><td>Long</td><td>总页数</td></tr>
<tr><td>pageNo</td><td>Long</td><td>当前页码</td></tr>
<tr><td>pageSize</td><td>Long</td><td>每页条数</td></tr>
<tr><td>list[].id</td><td>Long</td><td>电影ID</td></tr>
<tr><td>list[].driveId</td><td>Long</td><td>所属网盘ID</td></tr>
<tr><td>list[].folderId</td><td>Long</td><td>所属文件夹ID</td></tr>
<tr><td>list[].title</td><td>String</td><td>电影名称</td></tr>
<tr><td>list[].originalName</td><td>String</td><td>原始文件名</td></tr>
<tr><td>list[].mimeType</td><td>String</td><td>MIME类型</td></tr>
<tr><td>list[].fileSize</td><td>Long</td><td>文件大小(字节)</td></tr>
<tr><td>list[].extension</td><td>String</td><td>文件扩展名</td></tr>
<tr><td>list[].description</td><td>String</td><td>描述</td></tr>
<tr><td>list[].scrapeStatus</td><td>String</td><td>刮削状态pending/scraping/success/failed</td></tr>
<tr><td>list[].rating</td><td>Integer</td><td>评分0-100</td></tr>
<tr><td>list[].tags</td><td>String</td><td>标签</td></tr>
<tr><td>list[].casts</td><td>String</td><td>主演逗号分隔TMDB刮削</td></tr>
<tr><td>list[].directors</td><td>String</td><td>导演逗号分隔TMDB刮削</td></tr>
<tr><td>list[].posterUrl</td><td>String</td><td>竖版海报URL2:3 比例)</td></tr>
<tr><td>list[].backdropUrl</td><td>String</td><td>横版背景图URL16:9 比例)</td></tr>
<tr><td>list[].minioUrl</td><td>String</td><td>MinIO 文件访问URL</td></tr>
<tr><td>list[].createTime</td><td>String</td><td>创建时间</td></tr>
<tr><td>list[].updateTime</td><td>String</td><td>更新时间</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"total"</span>: <span class="number">12580</span>,
<span class="key">"pages"</span>: <span class="number">252</span>,
<span class="key">"pageNo"</span>: <span class="number">1</span>,
<span class="key">"pageSize"</span>: <span class="number">50</span>,
<span class="key">"list"</span>: [
{
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"folderId"</span>: <span class="number">3</span>,
<span class="key">"title"</span>: <span class="string">"肖申克的救赎"</span>,
<span class="key">"originalName"</span>: <span class="string">"The.Shawshank.Redemption.1994.mkv"</span>,
<span class="key">"fileSize"</span>: <span class="number">8589934592</span>,
<span class="key">"description"</span>: <span class="string">"一场冤案让银行家安迪入狱他在肖申克监狱用20年时间挖出一条自由之路..."</span>,
<span class="key">"scrapeStatus"</span>: <span class="string">"success"</span>,
<span class="key">"rating"</span>: <span class="number">93</span>,
<span class="key">"casts"</span>: <span class="string">"蒂姆·罗宾斯,摩根·弗里曼"</span>,
<span class="key">"directors"</span>: <span class="string">"弗兰克·德拉邦特"</span>,
<span class="key">"posterUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/1/poster_xxx.jpg"</span>,
<span class="key">"backdropUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/1/backdrop_xxx.jpg"</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-20 10:30:00"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-27 14:20:00"</span>
}
]
}
}</div>
</div>
<div class="detail-section">
<h4>成功响应示例 — 命中缓存</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功(缓存)"</span>,
<span class="key">"data"</span>: {
<span class="key">"total"</span>: <span class="number">12580</span>,
<span class="key">"pages"</span>: <span class="number">252</span>,
<span class="key">"pageNo"</span>: <span class="number">1</span>,
<span class="key">"pageSize"</span>: <span class="number">50</span>,
<span class="key">"list"</span>: [...]
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 查询失败</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"查询失败:数据库连接异常"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>前端调用示例</h4>
<div class="code-block">// 查询第1页每页50条
fetch('/api/movies/all?pageNo=1&pageSize=50', {
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(res => res.json())
.then(data => {
console.log('总数:', data.data.total);
console.log('电影列表:', data.data.list);
});
// 搜索电影
fetch('/api/movies/all?keyword=肖申克&pageNo=1&pageSize=50', {
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(res => res.json())
.then(data => {
console.log('搜索结果:', data.data.list);
});
// 按评分降序排序
fetch('/api/movies/all?sortBy=rating&sortOrder=desc&pageNo=1&pageSize=50', {
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(res => res.json())
.then(data => {
console.log('高分电影:', data.data.list);
});</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/movies/{id}</span>
<span class="api-desc">电影详情查询</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>查询单部电影的完整详情信息,包括基本信息、海报、评分、简介、导演、主演等。</p>
</div>
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>电影ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>电影ID</td></tr>
<tr><td>driveId</td><td>Long</td><td>所属网盘ID</td></tr>
<tr><td>folderId</td><td>Long</td><td>所属文件夹ID</td></tr>
<tr><td>title</td><td>String</td><td>电影名称</td></tr>
<tr><td>originalName</td><td>String</td><td>原始文件名</td></tr>
<tr><td>mimeType</td><td>String</td><td>MIME类型</td></tr>
<tr><td>fileSize</td><td>Long</td><td>文件大小(字节)</td></tr>
<tr><td>extension</td><td>String</td><td>文件扩展名</td></tr>
<tr><td>description</td><td>String</td><td>电影描述</td></tr>
<tr><td>scrapeStatus</td><td>String</td><td>刮削状态pending/scraping/success/failed</td></tr>
<tr><td>rating</td><td>Integer</td><td>评分0-100</td></tr>
<tr><td>casts</td><td>String</td><td>主演逗号分隔TMDB刮削</td></tr>
<tr><td>directors</td><td>String</td><td>导演逗号分隔TMDB刮削</td></tr>
<tr><td>posterUrl</td><td>String</td><td>竖版海报URL2:3 比例)</td></tr>
<tr><td>backdropUrl</td><td>String</td><td>横版背景图URL16:9 比例)</td></tr>
<tr><td>tags</td><td>String</td><td>标签(逗号分隔)</td></tr>
<tr><td>createTime</td><td>String</td><td>创建时间</td></tr>
<tr><td>updateTime</td><td>String</td><td>更新时间</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"driveId"</span>: <span class="number">1</span>,
<span class="key">"folderId"</span>: <span class="number">3</span>,
<span class="key">"title"</span>: <span class="string">"肖申克的救赎"</span>,
<span class="key">"originalName"</span>: <span class="string">"肖申克的救赎.mp4"</span>,
<span class="key">"mimeType"</span>: <span class="string">"video/mp4"</span>,
<span class="key">"fileSize"</span>: <span class="number">2147483648</span>,
<span class="key">"extension"</span>: <span class="string">"mp4"</span>,
<span class="key">"description"</span>: <span class="string">"电影描述"</span>,
<span class="key">"scrapeStatus"</span>: <span class="string">"success"</span>,
<span class="key">"rating"</span>: <span class="number">93</span>,
<span class="key">"casts"</span>: <span class="string">"Tim Robbins, Morgan Freeman"</span>,
<span class="key">"directors"</span>: <span class="string">"Frank Darabont"</span>,
<span class="key">"posterUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/1/poster_xxx.jpg"</span>,
<span class="key">"backdropUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/1/backdrop_xxx.jpg"</span>,
<span class="key">"tags"</span>: <span class="string">"剧情,犯罪"</span>,
<span class="key">"createTime"</span>: <span class="string">"2025-01-01T10:00:00"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2025-01-15T10:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 电影不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"电影不存在或无权访问"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/movies/{id}/scrape-status</span>
<span class="api-desc">查询单部电影刮削状态</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>查询单部电影的刮削状态返回三种状态刮削中scraping、刮削失败failed、刮削成功success</p>
</div>
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>电影ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>movieId</td><td>Long</td><td>电影ID</td></tr>
<tr><td>title</td><td>String</td><td>电影名称</td></tr>
<tr><td>status</td><td>String</td><td>刮削状态scraping / failed / success / unknown</td></tr>
<tr><td>message</td><td>String</td><td>状态描述信息</td></tr>
<tr><td>error</td><td>String</td><td>失败原因(仅 status=failed 时返回)</td></tr>
<tr><td>posterUrl</td><td>String</td><td>竖版海报URL仅 status=success 时返回)</td></tr>
<tr><td>backdropUrl</td><td>String</td><td>横版背景图URL仅 status=success 时返回)</td></tr>
<tr><td>rating</td><td>Integer</td><td>评分(仅 status=success 时返回)</td></tr>
<tr><td>category</td><td>String</td><td>分类(仅 status=success 时返回)</td></tr>
<tr><td>tags</td><td>String</td><td>标签(仅 status=success 时返回)</td></tr>
<tr><td>casts</td><td>String</td><td>主演(仅 status=success 时返回)</td></tr>
<tr><td>directors</td><td>String</td><td>导演(仅 status=success 时返回)</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应示例 - 刮削中</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"movieId"</span>: <span class="number">1</span>,
<span class="key">"title"</span>: <span class="string">"电影名称"</span>,
<span class="key">"status"</span>: <span class="string">"scraping"</span>,
<span class="key">"message"</span>: <span class="string">"正在刮削中,请稍候..."</span>
}
}</div>
</div>
<div class="detail-section">
<h4>响应示例 - 刮削失败</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"movieId"</span>: <span class="number">1</span>,
<span class="key">"title"</span>: <span class="string">"电影名称"</span>,
<span class="key">"status"</span>: <span class="string">"failed"</span>,
<span class="key">"message"</span>: <span class="string">"刮削失败"</span>,
<span class="key">"error"</span>: <span class="string">"TMDB API 请求失败"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>响应示例 - 刮削成功</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"movieId"</span>: <span class="number">1</span>,
<span class="key">"title"</span>: <span class="string">"电影名称"</span>,
<span class="key">"status"</span>: <span class="string">"success"</span>,
<span class="key">"message"</span>: <span class="string">"刮削成功"</span>,
<span class="key">"posterUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/1/poster_xxx.jpg"</span>,
<span class="key">"backdropUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/1/backdrop_xxx.jpg"</span>,
<span class="key">"rating"</span>: <span class="number">85</span>,
<span class="key">"category"</span>: <span class="string">"动作"</span>,
<span class="key">"tags"</span>: <span class="string">"动作,科幻"</span>,
<span class="key">"casts"</span>: <span class="string">"主演1,主演2"</span>,
<span class="key">"directors"</span>: <span class="string">"导演1"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 电影不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"电影不存在或无权访问"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/movies</span>
<span class="api-desc">创建电影</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>创建新的电影记录title 为必填字段。</p>
</div>
<div class="detail-section">
<h4>请求体所有字段可选title 必填)</h4>
<div class="code-block">{
<span class="key">"title"</span>: <span class="string">"电影名称"</span>, <span style="color:#718096">// 必填</span>
<span class="key">"maintainer"</span>: <span class="string">"维护者"</span>, <span style="color:#718096">// 可选</span>
<span class="key">"status"</span>: <span class="string">"active"</span>, <span style="color:#718096">// 可选active / deleted</span>
<span class="key">"category"</span>: <span class="string">"动作"</span>, <span style="color:#718096">// 可选</span>
<span class="key">"scrapeType"</span>: <span class="string">"tmdb"</span>, <span style="color:#718096">// 可选tmdb / manual / none</span>
<span class="key">"rating"</span>: <span class="number">85</span>, <span style="color:#718096">// 可选0-100</span>
<span class="key">"posterUrl"</span>: <span class="string">"海报URL"</span>, <span style="color:#718096">// 可选</span>
<span class="key">"tags"</span>: <span class="string">"标签1,标签2"</span> <span style="color:#718096">// 可选</span>
}</div>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"创建成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"title"</span>: <span class="string">"电影名称"</span>,
<span class="key">"createTime"</span>: <span class="string">"2025-01-01T10:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 缺少必填字段</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"电影名称不能为空"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method PUT">PUT</span>
<span class="api-path">/api/movies/{id}</span>
<span class="api-desc">更新电影</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>更新指定电影的信息,支持部分更新。</p>
</div>
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>电影ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>请求体(部分更新,所有字段可选)</h4>
<div class="code-block">{
<span class="key">"title"</span>: <span class="string">"新电影名称"</span>,
<span class="key">"maintainer"</span>: <span class="string">"新维护者"</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>,
<span class="key">"category"</span>: <span class="string">"新分类"</span>,
<span class="key">"scrapeType"</span>: <span class="string">"tmdb"</span>,
<span class="key">"rating"</span>: <span class="number">90</span>,
<span class="key">"posterUrl"</span>: <span class="string">"新海报URL"</span>,
<span class="key">"tags"</span>: <span class="string">"新标签"</span>
}</div>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"更新成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"title"</span>: <span class="string">"新电影名称"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2025-01-15T10:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 电影不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"电影不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method DELETE">DELETE</span>
<span class="api-path">/api/movies/{id}</span>
<span class="api-desc">删除电影</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>删除指定的电影记录。</p>
</div>
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>电影ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"删除成功"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 电影不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"电影不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/movies/batch-delete</span>
<span class="api-desc">批量删除电影</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>批量删除多个电影记录。</p>
</div>
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{ <span class="key">"ids"</span>: [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>] }</div>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"批量删除成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"deletedCount"</span>: <span class="number">3</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 参数错误</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">400</span>,
<span class="key">"message"</span>: <span class="string">"电影ID列表不能为空"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/movies/stats</span>
<span class="api-desc">电影统计</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>获取当前用户的电影数量统计,按刮削类型和分类分组。</p>
</div>
<div class="detail-section">
<h4>响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"data"</span>: {
<span class="key">"totalCount"</span>: <span class="number">10</span>,
<span class="key">"scrapedCount"</span>: <span class="number">8</span>,
<span class="key">"pendingCount"</span>: <span class="number">2</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/tmdb/key</span>
<span class="api-desc">获取 TMDB API Key</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>获取当前用户配置的 TMDB API Key。</p>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"tmdbApiKey"</span>: <span class="string">"your_api_key_here"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/tmdb/key</span>
<span class="api-desc">设置 TMDB API Key</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{ <span class="key">"tmdbApiKey"</span>: <span class="string">"你的API Key"</span> }</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/tmdb-scraper/scrape</span>
<span class="api-desc">单部电影刮削</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>手动触发单部电影刮削自动匹配TMDB最佳结果。刮削完成后会更新电影的简介、导演、主演、海报等信息。<strong>注意两次请求需间隔5秒防止频繁点击。</strong></p>
</div>
<div class="detail-section">
<h4>请求方式</h4>
<p>支持两种方式传递 movieId</p>
<p>1. <strong>URL 查询参数</strong><code>?movieId=1</code></p>
<p>2. <strong>请求体 JSON</strong></p>
<div class="code-block">{ <span class="key">"movieId"</span>: <span class="number">1</span> }</div>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>movieId</td><td>Long</td><td>电影ID</td></tr>
<tr><td>userId</td><td>Long</td><td>用户ID</td></tr>
<tr><td>tmdbId</td><td>Long</td><td>TMDB电影ID</td></tr>
<tr><td>title</td><td>String</td><td>电影标题来自TMDB</td></tr>
<tr><td>originalTitle</td><td>String</td><td>原始标题(外文)</td></tr>
<tr><td>releaseDate</td><td>String</td><td>上映日期</td></tr>
<tr><td>posterPath</td><td>String</td><td>TMDB海报路径</td></tr>
<tr><td>backdropPath</td><td>String</td><td>TMDB背景图路径</td></tr>
<tr><td>localPosterUrl</td><td>String</td><td>本地竖版海报URLMinIO</td></tr>
<tr><td>localBackdropUrl</td><td>String</td><td>本地横版背景图URLMinIO</td></tr>
<tr><td>tmdbRating</td><td>Integer</td><td>TMDB评分0-100分制</td></tr>
<tr><td>voteCount</td><td>Integer</td><td>投票数</td></tr>
<tr><td>runtime</td><td>Integer</td><td>片长(分钟)</td></tr>
<tr><td>genres</td><td>String</td><td>类型(逗号分隔)</td></tr>
<tr><td>directors</td><td>String</td><td>导演逗号分隔最多2位</td></tr>
<tr><td>casts</td><td>String</td><td>主演逗号分隔最多5位</td></tr>
<tr><td>originalLanguage</td><td>String</td><td>原始语言</td></tr>
<tr><td>productionCountries</td><td>String</td><td>制片国家(逗号分隔)</td></tr>
<tr><td>cacheTime</td><td>String</td><td>缓存时间</td></tr>
<tr><td>status</td><td>String</td><td>缓存状态</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"刮削成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"movieId"</span>: <span class="number">1</span>,
<span class="key">"userId"</span>: <span class="number">1</span>,
<span class="key">"tmdbId"</span>: <span class="number">597</span>,
<span class="key">"title"</span>: <span class="string">"泰坦尼克号"</span>,
<span class="key">"originalTitle"</span>: <span class="string">"Titanic"</span>,
<span class="key">"releaseDate"</span>: <span class="string">"1997-11-18"</span>,
<span class="key">"posterPath"</span>: <span class="string">"/9xjZS2rlVxm8SFx8kPC3JIGCOY.jpg"</span>,
<span class="key">"backdropPath"</span>: <span class="string">"/rTh4K5uw9HypmpGslcKd4QfHl93.jpg"</span>,
<span class="key">"localPosterUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/poster_1_597.jpg"</span>,
<span class="key">"localBackdropUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/backdrop_1_597.jpg"</span>,
<span class="key">"tmdbRating"</span>: <span class="number">78</span>,
<span class="key">"voteCount"</span>: <span class="number">24563</span>,
<span class="key">"runtime"</span>: <span class="number">194</span>,
<span class="key">"genres"</span>: <span class="string">"剧情, 爱情, 灾难"</span>,
<span class="key">"directors"</span>: <span class="string">"James Cameron"</span>,
<span class="key">"casts"</span>: <span class="string">"Leonardo DiCaprio, Kate Winslet, Billy Zane, Kathy Bates, Frances Fisher"</span>,
<span class="key">"originalLanguage"</span>: <span class="string">"en"</span>,
<span class="key">"productionCountries"</span>: <span class="string">"United States of America"</span>,
<span class="key">"cacheTime"</span>: <span class="string">"2026-06-26T16:30:00"</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 操作过于频繁</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"操作过于频繁,请等待 3 秒后再试"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未找到匹配结果</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"TMDB未找到匹配的电影: xxx"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/tmdb-scraper/search</span>
<span class="api-desc">搜索TMDB电影返回所有匹配结果供前端选择</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>根据电影ID自动获取标题搜索TMDB返回所有匹配结果。如果查询出多条结果前端应展示列表让用户选择。</p>
</div>
<div class="detail-section">
<h4>查询参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>movieId</td><td>Long</td><td></td><td>电影ID自动获取标题搜索</td></tr>
<tr><td>keyword</td><td>String</td><td></td><td>自定义搜索关键词(不传则使用电影标题)</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>movieId</td><td>Long</td><td>电影ID</td></tr>
<tr><td>keyword</td><td>String</td><td>搜索关键词</td></tr>
<tr><td>total</td><td>Integer</td><td>搜索结果总数</td></tr>
<tr><td>list[].tmdbId</td><td>Long</td><td>TMDB电影ID用于确认刮削</td></tr>
<tr><td>list[].title</td><td>String</td><td>电影标题</td></tr>
<tr><td>list[].originalTitle</td><td>String</td><td>原始标题</td></tr>
<tr><td>list[].releaseDate</td><td>String</td><td>上映日期</td></tr>
<tr><td>list[].posterPath</td><td>String</td><td>海报图片URL</td></tr>
<tr><td>list[].voteAverage</td><td>Double</td><td>评分</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"movieId"</span>: <span class="number">23</span>,
<span class="key">"keyword"</span>: <span class="string">"泰坦尼克号"</span>,
<span class="key">"total"</span>: <span class="number">3</span>,
<span class="key">"list"</span>: [
{
<span class="key">"tmdbId"</span>: <span class="number">597</span>,
<span class="key">"title"</span>: <span class="string">"泰坦尼克号"</span>,
<span class="key">"originalTitle"</span>: <span class="string">"Titanic"</span>,
<span class="key">"releaseDate"</span>: <span class="string">"1997-11-18"</span>,
<span class="key">"posterPath"</span>: <span class="string">"https://image.tmdb.org/t/p/w500/xxx.jpg"</span>,
<span class="key">"voteAverage"</span>: <span class="number">7.9</span>
}
]
}
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/tmdb-scraper/confirm</span>
<span class="api-desc">确认刮削用户选择TMDB结果后执行</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>用户从搜索结果中选择一部电影后,调用此接口执行刮削。刮削完成后会更新电影的简介、导演、主演、海报等信息。<strong>注意两次请求需间隔5秒防止频繁点击。</strong></p>
</div>
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"movieId"</span>: <span class="number">23</span>,
<span class="key">"tmdbId"</span>: <span class="number">597</span>
}</div>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>movieId</td><td>Long</td><td>电影ID</td></tr>
<tr><td>userId</td><td>Long</td><td>用户ID</td></tr>
<tr><td>tmdbId</td><td>Long</td><td>TMDB电影ID</td></tr>
<tr><td>title</td><td>String</td><td>电影标题来自TMDB</td></tr>
<tr><td>originalTitle</td><td>String</td><td>原始标题(外文)</td></tr>
<tr><td>releaseDate</td><td>String</td><td>上映日期</td></tr>
<tr><td>posterPath</td><td>String</td><td>TMDB海报路径</td></tr>
<tr><td>backdropPath</td><td>String</td><td>TMDB背景图路径</td></tr>
<tr><td>localPosterUrl</td><td>String</td><td>本地竖版海报URLMinIO</td></tr>
<tr><td>localBackdropUrl</td><td>String</td><td>本地横版背景图URLMinIO</td></tr>
<tr><td>tmdbRating</td><td>Integer</td><td>TMDB评分0-100分制</td></tr>
<tr><td>voteCount</td><td>Integer</td><td>投票数</td></tr>
<tr><td>runtime</td><td>Integer</td><td>片长(分钟)</td></tr>
<tr><td>genres</td><td>String</td><td>类型(逗号分隔)</td></tr>
<tr><td>directors</td><td>String</td><td>导演逗号分隔最多2位</td></tr>
<tr><td>casts</td><td>String</td><td>主演逗号分隔最多5位</td></tr>
<tr><td>originalLanguage</td><td>String</td><td>原始语言</td></tr>
<tr><td>productionCountries</td><td>String</td><td>制片国家(逗号分隔)</td></tr>
<tr><td>cacheTime</td><td>String</td><td>缓存时间</td></tr>
<tr><td>status</td><td>String</td><td>缓存状态</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"刮削成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"movieId"</span>: <span class="number">23</span>,
<span class="key">"userId"</span>: <span class="number">1</span>,
<span class="key">"tmdbId"</span>: <span class="number">597</span>,
<span class="key">"title"</span>: <span class="string">"泰坦尼克号"</span>,
<span class="key">"originalTitle"</span>: <span class="string">"Titanic"</span>,
<span class="key">"releaseDate"</span>: <span class="string">"1997-11-18"</span>,
<span class="key">"posterPath"</span>: <span class="string">"/9xjZS2rlVxm8SFx8kPC3JIGCOY.jpg"</span>,
<span class="key">"backdropPath"</span>: <span class="string">"/rTh4K5uw9HypmpGslcKd4QfHl93.jpg"</span>,
<span class="key">"localPosterUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/poster_23_597.jpg"</span>,
<span class="key">"localBackdropUrl"</span>: <span class="string">"http://...:39000/wangpan/posters/backdrop_23_597.jpg"</span>,
<span class="key">"tmdbRating"</span>: <span class="number">78</span>,
<span class="key">"voteCount"</span>: <span class="number">24563</span>,
<span class="key">"runtime"</span>: <span class="number">194</span>,
<span class="key">"genres"</span>: <span class="string">"剧情, 爱情, 灾难"</span>,
<span class="key">"directors"</span>: <span class="string">"James Cameron"</span>,
<span class="key">"casts"</span>: <span class="string">"Leonardo DiCaprio, Kate Winslet, Billy Zane, Kathy Bates, Frances Fisher"</span>,
<span class="key">"originalLanguage"</span>: <span class="string">"en"</span>,
<span class="key">"productionCountries"</span>: <span class="string">"United States of America"</span>,
<span class="key">"cacheTime"</span>: <span class="string">"2026-06-26T16:30:00"</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 操作过于频繁</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"操作过于频繁,请等待 3 秒后再试"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 电影不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"电影不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 未登录或 Token 无效</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">401</span>,
<span class="key">"message"</span>: <span class="string">"未登录或 Token 已过期"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/tmdb-scraper/batch-scrape</span>
<span class="api-desc">批量刮削</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求体movieIds 可选)</h4>
<div class="code-block">{ <span class="key">"movieIds"</span>: [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>] } <span style="color:#718096">// 可选:不传则自动查找需要刮削的电影</span></div>
</div>
<div class="detail-section">
<h4>响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"批量刮削完成"</span>,
<span class="key">"data"</span>: {
<span class="key">"successCount"</span>: <span class="number">2</span>,
<span class="key">"failCount"</span>: <span class="number">0</span>,
<span class="key">"total"</span>: <span class="number">2</span>
}
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/tmdb-scraper/caches</span>
<span class="api-desc">查询缓存列表</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>查询参数</h4>
<table class="param-table">
<tr><th>参数</th><th>默认值</th><th>说明</th></tr>
<tr><td>pageNo</td><td>1</td><td>页码</td></tr>
<tr><td>pageSize</td><td>20</td><td>每页条数</td></tr>
</table>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/tmdb-scraper/stats</span>
<span class="api-desc">刮削统计</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"data"</span>: {
<span class="key">"totalCaches"</span>: <span class="number">5</span>,
<span class="key">"activeCaches"</span>: <span class="number">3</span>,
<span class="key">"expiredCaches"</span>: <span class="number">2</span>,
<span class="key">"userId"</span>: <span class="number">2</span>
}
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/tmdb-scraper/trigger</span>
<span class="api-desc">手动触发定时任务</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>手动触发 TMDB 定时扫描任务,查找需要刮削的电影并执行刮削。无需请求体。</p>
</div>
<div class="detail-section">
<h4>响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"扫描任务已触发"</span>,
<span class="key">"data"</span>: {
<span class="key">"message"</span>: <span class="string">"扫描任务已触发"</span>,
<span class="key">"triggeredBy"</span>: <span class="number">2</span>,
<span class="key">"triggerType"</span>: <span class="string">"manual"</span>
}
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/tmdb-scraper/scraper-logs</span>
<span class="api-desc">查询刮削日志(分页)</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>分页查询定时任务执行记录,返回每条记录的汇总信息和详细结果(包含每部电影的刮削结果和失败原因)。</p>
</div>
<div class="detail-section">
<h4>查询参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>pageNo</td><td>Integer</td><td></td><td>页码,默认 1</td></tr>
<tr><td>pageSize</td><td>Integer</td><td></td><td>每页条数,默认 20最大 50</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>total</td><td>Long</td><td>总记录数</td></tr>
<tr><td>pageNo</td><td>Integer</td><td>当前页码</td></tr>
<tr><td>pageSize</td><td>Integer</td><td>每页条数</td></tr>
<tr><td>list[].id</td><td>Long</td><td>日志ID</td></tr>
<tr><td>list[].trigger_type</td><td>String</td><td>触发方式auto定时自动/ manual手动触发</td></tr>
<tr><td>list[].status</td><td>String</td><td>执行状态success / partial / failed / skipped</td></tr>
<tr><td>list[].total_processed</td><td>Integer</td><td>总处理电影数</td></tr>
<tr><td>list[].total_success</td><td>Integer</td><td>成功刮削数</td></tr>
<tr><td>list[].total_skipped</td><td>Integer</td><td>跳过数</td></tr>
<tr><td>list[].total_failed</td><td>Integer</td><td>失败数</td></tr>
<tr><td>list[].user_count</td><td>Integer</td><td>涉及用户数</td></tr>
<tr><td>list[].duration_ms</td><td>Long</td><td>执行耗时(毫秒)</td></tr>
<tr><td>list[].start_time</td><td>String</td><td>开始时间</td></tr>
<tr><td>list[].end_time</td><td>String</td><td>结束时间</td></tr>
<tr><td>list[].error_message</td><td>String</td><td>错误信息(如有)</td></tr>
<tr><td>list[].details</td><td>Array</td><td>详细执行结果解析后的JSON</td></tr>
<tr><td>list[].details[].username</td><td>String</td><td>用户名</td></tr>
<tr><td>list[].details[].userId</td><td>Long</td><td>用户ID</td></tr>
<tr><td>list[].details[].processed</td><td>Integer</td><td>该用户处理的电影数</td></tr>
<tr><td>list[].details[].success</td><td>Integer</td><td>该用户成功刮削数</td></tr>
<tr><td>list[].details[].skipped</td><td>Integer</td><td>该用户跳过数</td></tr>
<tr><td>list[].details[].failed</td><td>Integer</td><td>该用户失败数</td></tr>
<tr><td>list[].details[].messages</td><td>Array</td><td>失败详情列表(格式:电影名称: 失败原因)</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"total"</span>: <span class="number">10</span>,
<span class="key">"pageNo"</span>: <span class="number">1</span>,
<span class="key">"pageSize"</span>: <span class="number">20</span>,
<span class="key">"list"</span>: [
{
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"trigger_type"</span>: <span class="string">"auto"</span>,
<span class="key">"status"</span>: <span class="string">"partial"</span>,
<span class="key">"total_processed"</span>: <span class="number">5</span>,
<span class="key">"total_success"</span>: <span class="number">4</span>,
<span class="key">"total_skipped"</span>: <span class="number">0</span>,
<span class="key">"total_failed"</span>: <span class="number">1</span>,
<span class="key">"user_count"</span>: <span class="number">1</span>,
<span class="key">"duration_ms"</span>: <span class="number">12500</span>,
<span class="key">"start_time"</span>: <span class="string">"2026-06-27T10:00:00"</span>,
<span class="key">"end_time"</span>: <span class="string">"2026-06-27T10:00:12"</span>,
<span class="key">"error_message"</span>: <span class="keyword">null</span>,
<span class="key">"details"</span>: [
{
<span class="key">"username"</span>: <span class="string">"testuser"</span>,
<span class="key">"userId"</span>: <span class="number">1</span>,
<span class="key">"processed"</span>: <span class="number">5</span>,
<span class="key">"success"</span>: <span class="number">4</span>,
<span class="key">"skipped"</span>: <span class="number">0</span>,
<span class="key">"failed"</span>: <span class="number">1</span>,
<span class="key">"messages"</span>: [
<span class="string">"泰坦尼克号: TMDB未找到匹配的电影"</span>
]
}
]
}
]
}
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/tmdb-scraper/scraper-logs/{id}</span>
<span class="api-desc">查询单条刮削日志详情</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>功能说明</h4>
<p>查询单条刮削日志的详细信息,包括每个用户的刮削结果、失败电影列表及原因。</p>
</div>
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>日志ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>Long</td><td>日志ID</td></tr>
<tr><td>trigger_type</td><td>String</td><td>触发方式auto定时自动/ manual手动触发</td></tr>
<tr><td>status</td><td>String</td><td>执行状态success / partial / failed / skipped</td></tr>
<tr><td>total_processed</td><td>Integer</td><td>总处理电影数</td></tr>
<tr><td>total_success</td><td>Integer</td><td>成功刮削数</td></tr>
<tr><td>total_skipped</td><td>Integer</td><td>跳过数</td></tr>
<tr><td>total_failed</td><td>Integer</td><td>失败数</td></tr>
<tr><td>user_count</td><td>Integer</td><td>涉及用户数</td></tr>
<tr><td>duration_ms</td><td>Long</td><td>执行耗时(毫秒)</td></tr>
<tr><td>start_time</td><td>String</td><td>开始时间</td></tr>
<tr><td>end_time</td><td>String</td><td>结束时间</td></tr>
<tr><td>error_message</td><td>String</td><td>错误信息(如有)</td></tr>
<tr><td>details</td><td>Array</td><td>详细执行结果解析后的JSON</td></tr>
<tr><td>details[].username</td><td>String</td><td>用户名</td></tr>
<tr><td>details[].userId</td><td>Long</td><td>用户ID</td></tr>
<tr><td>details[].processed</td><td>Integer</td><td>该用户处理的电影数</td></tr>
<tr><td>details[].success</td><td>Integer</td><td>该用户成功刮削数</td></tr>
<tr><td>details[].skipped</td><td>Integer</td><td>该用户跳过数</td></tr>
<tr><td>details[].failed</td><td>Integer</td><td>该用户失败数</td></tr>
<tr><td>details[].messages</td><td>Array</td><td>失败详情列表(格式:电影名称: 失败原因)</td></tr>
</table>
</div>
<div class="detail-section">
<h4>响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"查询成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"trigger_type"</span>: <span class="string">"auto"</span>,
<span class="key">"status"</span>: <span class="string">"partial"</span>,
<span class="key">"total_processed"</span>: <span class="number">5</span>,
<span class="key">"total_success"</span>: <span class="number">4</span>,
<span class="key">"total_skipped"</span>: <span class="number">0</span>,
<span class="key">"total_failed"</span>: <span class="number">1</span>,
<span class="key">"user_count"</span>: <span class="number">1</span>,
<span class="key">"duration_ms"</span>: <span class="number">12500</span>,
<span class="key">"start_time"</span>: <span class="string">"2026-06-27T10:00:00"</span>,
<span class="key">"end_time"</span>: <span class="string">"2026-06-27T10:00:12"</span>,
<span class="key">"error_message"</span>: <span class="keyword">null</span>,
<span class="key">"details"</span>: [
{
<span class="key">"username"</span>: <span class="string">"testuser"</span>,
<span class="key">"userId"</span>: <span class="number">1</span>,
<span class="key">"processed"</span>: <span class="number">5</span>,
<span class="key">"success"</span>: <span class="number">4</span>,
<span class="key">"skipped"</span>: <span class="number">0</span>,
<span class="key">"failed"</span>: <span class="number">1</span>,
<span class="key">"messages"</span>: [
<span class="string">"泰坦尼克号: TMDB未找到匹配的电影"</span>,
<span class="string">"阿凡达: TMDB API请求失败请检查API Key是否有效"</span>
]
}
]
}
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例 — 日志不存在</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">404</span>,
<span class="key">"message"</span>: <span class="string">"日志不存在"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method DELETE">DELETE</span>
<span class="api-path">/api/tmdb-scraper/cache/{movieId}</span>
<span class="api-desc">删除缓存</span><span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>movieId</td><td>Long</td><td>电影ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>功能说明</h4>
<p>删除指定电影的 TMDB 刮削缓存,下次刮削时将重新从 TMDB 获取数据。</p>
</div>
<div class="detail-section">
<h4>响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"缓存删除成功"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
</div>
<!-- OpenList 集成 -->
<div class="section" id="openlist">
<div class="service-card">
<h2>OpenList 用户级连接管理</h2>
<span class="port">wangpan-drive-service : 8082 · OpenListController / DriveController</span>
</div>
<div class="service-card" style="border-left-color: #48bb78;">
<h2>功能说明</h2>
<p style="font-size: 14px; color: #4a5568; line-height: 1.8; margin-top: 8px;">
每个用户可以绑定自己的 OpenList 实例(地址、账号、密码),互不干扰。
敏感信息使用 Jasypt 加密存储Token 缓存到数据库避免重复登录。
OpenList 负责文件操作同步、播放TMDB 刮削使用当前用户的 API Key。
</p>
<div class="arch-diagram" style="margin-top: 16px;">
<div class="arch-flow">
<div class="arch-box" style="background: #4a5568;">前端</div>
<span class="arch-arrow"></span>
<div class="arch-box gateway">Gateway:8888</div>
<span class="arch-arrow"></span>
<div class="arch-box drive">Drive:8082</div>
<span class="arch-arrow"></span>
<div class="arch-box" style="background: #38b2ac;">用户A的OpenList</div>
<span class="arch-arrow"></span>
<div class="arch-box" style="background: #e53e3e;">UC网盘</div>
</div>
<div class="arch-flow" style="margin-top: 8px;">
<div class="arch-box" style="background: #4a5568; visibility: hidden;">前端</div>
<span class="arch-arrow" style="visibility: hidden;"></span>
<div class="arch-box gateway" style="visibility: hidden;">Gateway</div>
<span class="arch-arrow" style="visibility: hidden;"></span>
<div class="arch-box drive" style="visibility: hidden;">Drive</div>
<span class="arch-arrow"></span>
<div class="arch-box" style="background: #38b2ac;">用户B的OpenList</div>
<span class="arch-arrow"></span>
<div class="arch-box" style="background: #e53e3e;">阿里云盘</div>
</div>
</div>
</div>
<div class="service-card" style="border-left-color: #ed8936;">
<h2>使用流程</h2>
<p style="font-size: 14px; color: #4a5568; line-height: 1.8; margin-top: 8px;">
1. 前端调用 <code>POST /api/openlist/test</code> 测试 OpenList 连接(可选)<br>
2. 前端调用 <code>POST /api/openlist/bind</code> 绑定 OpenList传入地址、账号、密码<br>
3. 绑定成功后后端自动异步同步:递归遍历 OpenList 目录树,同步文件夹层级和视频文件到数据库<br>
4. 也可手动调用 <code>POST /api/openlist/sync</code> 触发全量同步<br>
5. 同步后电影可进行 TMDB 刮削,播放视频自动使用用户绑定的 OpenList 连接<br>
6. 后端每 24 小时自动检测 OpenList 心跳,记录连接状态<br>
7. TMDB 刮削使用当前用户的 API Key与 OpenList 连接隔离)
</p>
</div>
<!-- ==================== OpenList 连接管理接口 ==================== -->
<!-- 测试连接 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/openlist/test</span>
<span class="api-desc">测试 OpenList 连接(不保存)</span>
<span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求头</h4>
<div class="code-block">Authorization: Bearer &lt;token&gt;
Content-Type: application/json</div>
</div>
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"openlistUrl"</span>: <span class="string">"http://www.example.com:3000"</span>,
<span class="key">"openlistUsername"</span>: <span class="string">"用户名"</span>,
<span class="key">"openlistPassword"</span>: <span class="string">"密码"</span>
}</div>
</div>
<div class="detail-section">
<h4>字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>openlistUrl</td><td>String</td><td></td><td>OpenList 服务地址(如 http://example.com:13000</td></tr>
<tr><td>openlistUsername</td><td>String</td><td></td><td>OpenList 账号</td></tr>
<tr><td>openlistPassword</td><td>String</td><td></td><td>OpenList 密码</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"连接成功"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"连接失败Invalid username or password"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- 绑定 OpenList -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/openlist/bind</span>
<span class="api-desc">绑定 OpenList 连接</span>
<span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求头</h4>
<div class="code-block">Authorization: Bearer &lt;token&gt;
Content-Type: application/json</div>
</div>
<div class="detail-section">
<h4>请求体</h4>
<div class="code-block">{
<span class="key">"openlistUrl"</span>: <span class="string">"http://example.com:13000"</span>,
<span class="key">"openlistUsername"</span>: <span class="string">"your_username"</span>,
<span class="key">"openlistPassword"</span>: <span class="string">"your_password"</span>
}</div>
</div>
<div class="detail-section">
<h4>字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
<tr><td>openlistUrl</td><td>String</td><td></td><td>OpenList 服务地址</td></tr>
<tr><td>openlistUsername</td><td>String</td><td></td><td>OpenList 账号</td></tr>
<tr><td>openlistPassword</td><td>String</td><td></td><td>OpenList 密码</td></tr>
</table>
</div>
<div class="detail-section">
<h4>功能说明</h4>
<p style="font-size: 13px; color: #4a5568; line-height: 1.6;">
验证连接 → 保存配置(敏感信息 Jasypt 加密) → 缓存 Token24小时有效期<br>
每个用户只能绑定一个 OpenList 实例,重复绑定会更新配置。<br>
<strong>绑定成功后自动触发异步同步</strong>:递归遍历 OpenList 目录树,同步文件夹层级和视频文件到数据库。<br>
同步只处理视频格式文件mp4, mkv, avi 等),非视频文件自动跳过。
</p>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"绑定成功,正在后台同步文件..."</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"绑定失败连接失败Invalid username or password"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- 解绑 OpenList -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method DELETE">DELETE</span>
<span class="api-path">/api/openlist/unbind</span>
<span class="api-desc">解绑 OpenList 连接</span>
<span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求头</h4>
<div class="code-block">Authorization: Bearer &lt;token&gt;</div>
</div>
<div class="detail-section">
<h4>功能说明</h4>
<p style="font-size: 13px; color: #4a5568; line-height: 1.6;">
删除当前用户的 OpenList 连接配置(包括加密的 URL、账号、密码和 Token 缓存)。<br>
解绑后同步文件和播放链接接口将不可用。
</p>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"解绑成功"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
<div class="detail-section">
<h4>失败响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"解绑失败:未找到绑定记录"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- 查询连接信息 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/openlist/info</span>
<span class="api-desc">查询当前用户 OpenList 连接信息</span>
<span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求头</h4>
<div class="code-block">Authorization: Bearer &lt;token&gt;</div>
</div>
<div class="detail-section">
<h4>功能说明</h4>
<p style="font-size: 13px; color: #4a5568; line-height: 1.6;">
返回脱敏后的连接信息URL、用户名均脱敏显示不返回明文密码<br>
会实际调用 OpenList 登录接口验证当前连接是否可用,返回 <code>connected</code><code>connectMessage</code> 字段。<br>
未绑定时返回 <code>null</code>
</p>
</div>
<div class="detail-section">
<h4>成功响应示例(已绑定)</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"操作成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"openlistUrl"</span>: <span class="string">"http://ld***:13000"</span>,
<span class="key">"openlistUsername"</span>: <span class="string">"dj***22"</span>,
<span class="key">"storageId"</span>: <span class="string">"1"</span>,
<span class="key">"mountPath"</span>: <span class="string">"/uc"</span>,
<span class="key">"status"</span>: <span class="string">"active"</span>,
<span class="key">"tokenValid"</span>: <span class="keyword">true</span>,
<span class="key">"connected"</span>: <span class="keyword">true</span>,
<span class="key">"connectMessage"</span>: <span class="string">"连接成功"</span>,
<span class="key">"lastHeartbeatTime"</span>: <span class="string">"2026-06-27T15:00:00"</span>,
<span class="key">"heartbeatStatus"</span>: <span class="string">"success"</span>,
<span class="key">"heartbeatError"</span>: <span class="keyword">null</span>,
<span class="key">"createTime"</span>: <span class="string">"2026-06-26T15:00:00"</span>,
<span class="key">"updateTime"</span>: <span class="string">"2026-06-26T15:00:00"</span>
}
}</div>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>说明</th></tr>
<tr><td>openlistUrl</td><td>OpenList 服务地址(脱敏显示,如 http://ld***:13000</td></tr>
<tr><td>openlistUsername</td><td>OpenList 账号(脱敏显示,如 dj***22</td></tr>
<tr><td>storageId</td><td>OpenList 存储 ID用户在 OpenList 后台配置)</td></tr>
<tr><td>mountPath</td><td>挂载路径(如 /uc</td></tr>
<tr><td>status</td><td>状态active / disabled</td></tr>
<tr><td>tokenValid</td><td>本地缓存的 Token 是否有效true 表示未过期)</td></tr>
<tr><td>connected</td><td>实时连接测试结果true 表示当前账号密码可成功连接 OpenList</td></tr>
<tr><td>connectMessage</td><td>实时连接测试结果描述,如 "连接成功" 或 "连接失败Invalid username or password"</td></tr>
<tr><td>lastHeartbeatTime</td><td>最后一次定时心跳检测时间(每 24 小时自动检测一次)</td></tr>
<tr><td>heartbeatStatus</td><td>最近一次心跳检测结果success / failed</td></tr>
<tr><td>heartbeatError</td><td>心跳检测失败原因(成功时为 null</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例(未绑定)</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"操作成功"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- ==================== OpenList 文件同步接口 ==================== -->
<!-- 手动同步文件 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/openlist/sync</span>
<span class="api-desc">手动触发 OpenList 文件同步</span>
<span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求头</h4>
<div class="code-block">Authorization: Bearer &lt;token&gt;</div>
</div>
<div class="detail-section">
<h4>功能说明</h4>
<p style="font-size: 13px; color: #4a5568; line-height: 1.6;">
从 OpenList 根目录开始递归遍历目录树,同步文件夹层级和视频文件到本地数据库。<br>
<strong>同步策略</strong><br>
&nbsp;&nbsp;1. 自动为用户创建/复用关联 OpenList 的网盘Drive<br>
&nbsp;&nbsp;2. 递归遍历 OpenList 目录树在本地创建对应的文件夹DriveFolder<br>
&nbsp;&nbsp;3. <strong>只同步视频格式文件</strong>mp4, mkv, avi, mov, wmv, flv, webm 等),非视频文件自动跳过<br>
&nbsp;&nbsp;4. 视频文件创建为电影记录DriveMovie<code>storageType=openlist</code><br>
&nbsp;&nbsp;5. 不含视频文件的空文件夹也会被跳过<br>
<strong>前置条件</strong>:用户已通过 <code>/api/openlist/bind</code> 绑定 OpenList 连接。<br>
<strong>注意</strong>:绑定 OpenList 时已自动触发异步同步,此接口用于手动重新同步。
</p>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"同步完成"</span>,
<span class="key">"data"</span>: {
<span class="key">"driveCount"</span>: <span class="number">1</span>,
<span class="key">"folderCount"</span>: <span class="number">10</span>,
<span class="key">"movieCount"</span>: <span class="number">50</span>,
<span class="key">"skippedFileCount"</span>: <span class="number">5</span>,
<span class="key">"skippedFolderCount"</span>: <span class="number">2</span>,
<span class="key">"errorCount"</span>: <span class="number">0</span>,
<span class="key">"errors"</span>: []
}
}</div>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
<tr><td>driveCount</td><td>int</td><td>关联网盘数量(通常为 1</td></tr>
<tr><td>folderCount</td><td>int</td><td>本次同步创建的文件夹数量</td></tr>
<tr><td>movieCount</td><td>int</td><td>本次同步创建的视频文件(电影)数量</td></tr>
<tr><td>skippedFileCount</td><td>int</td><td>跳过的非视频文件数量</td></tr>
<tr><td>skippedFolderCount</td><td>int</td><td>跳过的不含视频的文件夹数量</td></tr>
<tr><td>errorCount</td><td>int</td><td>同步过程中的错误数量</td></tr>
<tr><td>errors</td><td>Array</td><td>错误详情列表(字符串数组)</td></tr>
</table>
</div>
<div class="detail-section">
<h4>失败响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"同步失败:用户未绑定 OpenList请先绑定"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- ==================== 网盘/电影接口(使用用户级 OpenList 连接) ==================== -->
<!-- 同步文件夹文件 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method POST">POST</span>
<span class="api-path">/api/drive/{driveId}/folders/{folderId}/sync-files</span>
<span class="api-desc">从 OpenList 同步文件夹文件</span>
<span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求头</h4>
<div class="code-block">Authorization: Bearer &lt;token&gt;</div>
</div>
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>driveId</td><td>Long</td><td>网盘 ID</td></tr>
<tr><td>folderId</td><td>Long</td><td>文件夹 ID0 表示根目录)</td></tr>
</table>
</div>
<div class="detail-section">
<h4>功能说明</h4>
<p style="font-size: 13px; color: #4a5568; line-height: 1.6;">
使用当前用户绑定的 OpenList 连接,获取指定路径下的文件列表,自动创建数据库中缺失的电影记录。<br>
已存在的文件不会重复创建。同步后文件可进行 TMDB 刮削。<br>
<strong>前置条件</strong>:用户已通过 <code>/api/openlist/bind</code> 绑定 OpenList 连接。
</p>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"同步成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"syncCount"</span>: <span class="number">5</span>,
<span class="key">"totalFiles"</span>: <span class="number">10</span>
}
}</div>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>说明</th></tr>
<tr><td>syncCount</td><td>本次新增的电影记录数量</td></tr>
<tr><td>totalFiles</td><td>OpenList 中该路径下的文件总数</td></tr>
</table>
</div>
<div class="detail-section">
<h4>失败响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"同步失败:用户未绑定 OpenList请先在设置中绑定 OpenList 连接"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- 获取电影播放链接 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/drive/{driveId}/movies/{movieId}/play-url</span>
<span class="api-desc">获取电影真实播放链接</span>
<span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求头</h4>
<div class="code-block">Authorization: Bearer &lt;token&gt;</div>
</div>
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>driveId</td><td>Long</td><td>网盘 ID</td></tr>
<tr><td>movieId</td><td>Long</td><td>电影 ID</td></tr>
</table>
</div>
<div class="detail-section">
<h4>功能说明</h4>
<p style="font-size: 13px; color: #4a5568; line-height: 1.6;">
使用当前用户绑定的 OpenList 连接,获取文件的真实下载地址,返回的 URL 可直接用于视频播放器播放。<br>
仅支持 <code>storageType</code><code>openlist</code> 的电影记录。<br>
Token 过期时自动刷新,对用户透明。
</p>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"获取成功"</span>,
<span class="key">"data"</span>: {
<span class="key">"playUrl"</span>: <span class="string">"https://uc网盘真实下载地址/xxx.mp4"</span>,
<span class="key">"fileName"</span>: <span class="string">"阿凡达.mp4"</span>,
<span class="key">"fileSize"</span>: <span class="number">1073741824</span>
}
}</div>
</div>
<div class="detail-section">
<h4>响应字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>说明</th></tr>
<tr><td>playUrl</td><td>真实播放/下载地址,可直接用于视频播放器</td></tr>
<tr><td>fileName</td><td>文件名</td></tr>
<tr><td>fileSize</td><td>文件大小(字节)</td></tr>
</table>
</div>
<div class="detail-section">
<h4>失败响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">500</span>,
<span class="key">"message"</span>: <span class="string">"获取失败:该电影不是 OpenList 管理的文件"</span>,
<span class="key">"data"</span>: <span class="keyword">null</span>
}</div>
</div>
</div>
</div>
<!-- 查看电影列表 -->
<div class="api-item">
<div class="api-header" onclick="toggleDetail(this)">
<span class="method GET">GET</span>
<span class="api-path">/api/drive/{driveId}/folders/{folderId}/movies</span>
<span class="api-desc">查看电影列表(含 OpenList 信息)</span>
<span class="auth-badge">需Token</span>
</div>
<div class="api-detail">
<div class="detail-section">
<h4>请求头</h4>
<div class="code-block">Authorization: Bearer &lt;token&gt;</div>
</div>
<div class="detail-section">
<h4>路径参数</h4>
<table class="param-table">
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>driveId</td><td>Long</td><td>网盘 ID</td></tr>
<tr><td>folderId</td><td>Long</td><td>文件夹 ID0 表示根目录)</td></tr>
</table>
</div>
<div class="detail-section">
<h4>成功响应示例</h4>
<div class="code-block">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"data"</span>: {
<span class="key">"list"</span>: [
{
<span class="key">"id"</span>: <span class="number">1</span>,
<span class="key">"title"</span>: <span class="string">"阿凡达.mp4"</span>,
<span class="key">"openlistFilePath"</span>: <span class="string">"/uc/阿凡达.mp4"</span>,
<span class="key">"storageType"</span>: <span class="string">"openlist"</span>,
<span class="key">"fileSize"</span>: <span class="number">1073741824</span>,
<span class="key">"mimeType"</span>: <span class="string">"video/mp4"</span>
}
],
<span class="key">"total"</span>: <span class="number">10</span>,
<span class="key">"page"</span>: <span class="number">1</span>,
<span class="key">"pageSize"</span>: <span class="number">20</span>
}
}</div>
</div>
<div class="detail-section">
<h4>OpenList 相关字段说明</h4>
<table class="param-table">
<tr><th>字段</th><th>说明</th></tr>
<tr><td>openlistFilePath</td><td>OpenList 文件路径(如 "/uc/阿凡达.mp4"),由系统根据用户绑定的挂载路径自动生成</td></tr>
<tr><td>storageType</td><td>存储类型:<code>metadata</code>(仅元数据)/ <code>openlist</code>OpenList 管理)</td></tr>
<tr><td>fileSize</td><td>文件大小(字节),同步时从 OpenList 获取</td></tr>
</table>
</div>
</div>
</div>
<!-- 前端调用示例 -->
<div class="service-card" style="border-left-color: #9f7aea;">
<h2>前端调用示例</h2>
<div class="detail-section">
<h4>1. 绑定 OpenList</h4>
<div class="code-block">// 先测试连接
const testResp = await fetch('/api/openlist/test', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
openlistUrl: 'http://example.com:13000',
openlistUsername: 'your_username',
openlistPassword: 'your_password'
})
});
// 测试通过后绑定
const bindResp = await fetch('/api/openlist/bind', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
openlistUrl: 'http://example.com:13000',
openlistUsername: 'your_username',
openlistPassword: 'your_password'
})
});</div>
</div>
<div class="detail-section">
<h4>2. 查询连接信息</h4>
<div class="code-block">const resp = await fetch('/api/openlist/info', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await resp.json();
if (data.data) {
console.log('已绑定:', data.data.openlistUrl);
console.log('Token有效:', data.data.tokenValid);
} else {
console.log('未绑定 OpenList');
}</div>
</div>
<div class="detail-section">
<h4>3. 手动同步文件</h4>
<div class="code-block">// 绑定后已自动触发异步同步,也可手动重新同步
const resp = await fetch('/api/openlist/sync', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await resp.json();
console.log(`同步了 ${data.data.folderCount} 个文件夹, ${data.data.movieCount} 个视频`);
if (data.data.errorCount > 0) {
console.warn('同步错误:', data.data.errors);
}</div>
</div>
<div class="detail-section">
<h4>4. 查询连接状态</h4>
<div class="code-block">const resp = await fetch('/api/openlist/info', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await resp.json();
if (data.data) {
console.log('地址:', data.data.openlistUrl);
console.log('实时连接:', data.data.connected ? '正常' : '异常');
console.log('连接信息:', data.data.connectMessage);
console.log('心跳状态:', data.data.heartbeatStatus);
} else {
console.log('未绑定 OpenList');
}</div>
</div>
<div class="detail-section">
<h4>5. 播放视频</h4>
<div class="code-block">const resp = await fetch(`/api/drive/${driveId}/movies/${movieId}/play-url`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await resp.json();
videoPlayer.src = data.data.playUrl; // 直接设置视频源
videoPlayer.play();</div>
</div>
<div class="detail-section">
<h4>6. 解绑 OpenList</h4>
<div class="code-block">const resp = await fetch('/api/openlist/unbind', {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await resp.json();
console.log(data.message); // "解绑成功"</div>
</div>
</div>
</div>
<!-- 状态码 -->
<div class="section" id="status">
<div class="service-card">
<h2>通用响应格式</h2>
<div class="code-block" style="margin-top: 12px;">{
<span class="key">"code"</span>: <span class="number">200</span>,
<span class="key">"message"</span>: <span class="string">"操作结果描述"</span>,
<span class="key">"data"</span>: { <span class="string">...</span> }
}</div>
</div>
<div class="service-card">
<h2>状态码说明</h2>
<table class="status-table">
<tr><th>状态码</th><th>说明</th></tr>
<tr><td>200</td><td>请求成功</td></tr>
<tr><td>400</td><td>请求参数错误</td></tr>
<tr><td>401</td><td>未授权 / 令牌无效</td></tr>
<tr><td>403</td><td>权限不足</td></tr>
<tr><td>404</td><td>资源不存在</td></tr>
<tr><td>500</td><td>服务器内部错误</td></tr>
</table>
</div>
<div class="service-card">
<h2>认证说明</h2>
<p style="font-size: 14px; color: #4a5568; line-height: 1.8;">
除以下 <strong>3 个接口</strong> 外,所有接口都需要在请求头中携带访问令牌:<br><br>
<strong>无需 Token 的接口:</strong><br>
&nbsp;&nbsp;<code style="background:#c6f6d5;padding:2px 8px;border-radius:4px;">POST /api/login</code>&nbsp;&nbsp;用户登录<br>
&nbsp;&nbsp;<code style="background:#c6f6d5;padding:2px 8px;border-radius:4px;">POST /api/register</code>&nbsp;&nbsp;用户注册<br>
&nbsp;&nbsp;<code style="background:#c6f6d5;padding:2px 8px;border-radius:4px;">GET /api/token/validate?token=xxx</code>&nbsp;&nbsp;令牌验证token 通过 URL 参数传递)<br><br>
<strong>需要 Token 的接口(带 <span style="background:#ed8936;color:white;padding:1px 6px;border-radius:10px;font-size:10px;font-weight:600;">需Token</span> 标识):</strong><br>
&nbsp;&nbsp;在请求头中添加:<code style="background: #edf2f7; padding: 2px 8px; border-radius: 4px;">Authorization: Bearer &lt;token&gt;</code>
</p>
</div>
</div>
</div>
</div>
<script>
// 导航切换
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', function() {
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
this.classList.add('active');
const sectionId = this.dataset.section;
const section = document.getElementById(sectionId);
if (section) {
section.classList.add('active');
} else {
console.error('Section not found:', sectionId);
}
});
});
// 页面加载时默认激活第一个导航项
document.addEventListener('DOMContentLoaded', function() {
const firstNavItem = document.querySelector('.nav-item');
if (firstNavItem && !document.querySelector('.section.active')) {
firstNavItem.click();
}
});
// API 详情展开/收起
function toggleDetail(header) {
const detail = header.nextElementSibling;
detail.classList.toggle('show');
}
// 搜索功能
const searchInput = document.getElementById('apiSearch');
const searchClear = document.getElementById('searchClear');
const searchResultCount = document.getElementById('searchResultCount');
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => performSearch(this.value), 200);
// 显示/隐藏清除按钮
searchClear.classList.toggle('show', this.value.length > 0);
});
searchClear.addEventListener('click', function() {
searchInput.value = '';
performSearch('');
searchClear.classList.remove('show');
searchInput.focus();
});
// 支持快捷键 Ctrl/Cmd + K 聚焦搜索框
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
searchInput.focus();
searchInput.select();
}
// ESC 键清除搜索
if (e.key === 'Escape' && document.activeElement === searchInput) {
searchInput.value = '';
performSearch('');
searchClear.classList.remove('show');
}
});
function performSearch(keyword) {
const apiItems = document.querySelectorAll('.api-item');
const sections = document.querySelectorAll('.section');
// 清除之前的高亮
document.querySelectorAll('.highlight').forEach(el => {
const parent = el.parentNode;
parent.replaceChild(document.createTextNode(el.textContent), el);
parent.normalize();
});
if (!keyword.trim()) {
// 恢复所有项目
apiItems.forEach(item => item.classList.remove('search-hidden'));
searchResultCount.classList.remove('show');
// 恢复导航栏
document.querySelectorAll('.nav-item').forEach(item => {
item.style.display = '';
});
// 恢复 section 显示状态
sections.forEach(section => {
section.style.display = '';
});
return;
}
const lowerKeyword = keyword.toLowerCase();
let matchCount = 0;
const matchedSections = new Set();
// 全局搜索:遍历所有分类中的所有接口
apiItems.forEach(item => {
const path = item.querySelector('.api-path');
const desc = item.querySelector('.api-desc');
const method = item.querySelector('.method');
if (!path || !desc) return;
const pathText = path.textContent.toLowerCase();
const descText = desc.textContent.toLowerCase();
const methodText = method ? method.textContent.toLowerCase() : '';
const isMatch = pathText.includes(lowerKeyword) ||
descText.includes(lowerKeyword) ||
methodText.includes(lowerKeyword);
if (isMatch) {
item.classList.remove('search-hidden');
matchCount++;
// 记录匹配的 section
const section = item.closest('.section');
if (section) {
matchedSections.add(section.id);
}
// 高亮匹配内容
highlightText(path, keyword);
highlightText(desc, keyword);
// 自动展开匹配的详情
const detail = item.querySelector('.api-detail');
if (detail && !detail.classList.contains('show')) {
detail.classList.add('show');
}
} else {
item.classList.add('search-hidden');
}
});
// 全局搜索模式:显示所有有匹配结果的分类
sections.forEach(section => {
if (section.id === 'overview' || section.id === 'status') {
// 架构总览和状态码始终显示
section.style.display = '';
} else if (matchedSections.has(section.id)) {
// 有匹配结果的分类强制显示
section.style.display = 'block';
} else {
// 无匹配结果的分类隐藏
section.style.display = 'none';
}
});
// 更新导航栏显示(只显示有匹配结果的分类)
document.querySelectorAll('.nav-item').forEach(item => {
const sectionId = item.dataset.section;
if (sectionId === 'overview' || sectionId === 'status') {
item.style.display = '';
} else if (matchedSections.has(sectionId)) {
item.style.display = '';
} else {
item.style.display = 'none';
}
});
// 显示搜索结果
searchResultCount.textContent = `找到 ${matchCount} 个匹配的接口(跨 ${matchedSections.size} 个分类)`;
searchResultCount.classList.add('show');
}
function highlightText(element, keyword) {
const text = element.textContent;
const lowerText = text.toLowerCase();
const lowerKeyword = keyword.toLowerCase();
const index = lowerText.indexOf(lowerKeyword);
if (index === -1) return;
const before = text.substring(0, index);
const match = text.substring(index, index + keyword.length);
const after = text.substring(index + keyword.length);
element.innerHTML = '';
if (before) element.appendChild(document.createTextNode(before));
const highlight = document.createElement('span');
highlight.className = 'highlight';
highlight.textContent = match;
element.appendChild(highlight);
if (after) element.appendChild(document.createTextNode(after));
}
</script>
</body>
</html>