201 lines
55 KiB
JavaScript
201 lines
55 KiB
JavaScript
import{_ as i,o as a,c as n,a2 as p}from"./chunks/framework.BbmSSMjL.js";const g=JSON.parse('{"title":"7.1 文件上传全流程","description":"","frontmatter":{},"headers":[],"relativePath":"zh/tools/upload.md","filePath":"zh/tools/upload.md"}'),l={name:"zh/tools/upload.md"};function t(h,s,e,k,d,r){return a(),n("div",null,[...s[0]||(s[0]=[p(`<h1 id="_7-1-文件上传全流程" tabindex="-1">7.1 文件上传全流程 <a class="header-anchor" href="#_7-1-文件上传全流程" aria-label="Permalink to "7.1 文件上传全流程""></a></h1><div class="tip custom-block"><p class="custom-block-title">概述</p><p>文件处理分为三个阶段:<strong>上传</strong>(<code>uploadFile</code> 接口校验后存入 <code>temp/</code> 临时目录)→ <strong>入库</strong>(Service 的 <code>fileFields</code> 经 <code>save_file()</code> 归一化:去域名、迁移临时文件到正式目录)→ <strong>读取</strong>(<code>get_file_url()</code> 补全域名)。上传与业务逻辑解耦:前端先上传拿到 URL/路径,提交表单时框架自动完成字段处理。</p></div><h2 id="完整流程图" tabindex="-1">完整流程图 <a class="header-anchor" href="#完整流程图" aria-label="Permalink to "完整流程图""></a></h2><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span>┌─────────────────────────────────────────────────────────────────────┐</span></span>
|
||
<span class="line"><span>│ 第一阶段:上传 │</span></span>
|
||
<span class="line"><span>└─────────────────────────────────────────────────────────────────────┘</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>前端 后端</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │── POST /api/upload/ ───────►│ (auth.jwt 组内,需 sys:upload:file 权限)</span></span>
|
||
<span class="line"><span> │ uploadFile │</span></span>
|
||
<span class="line"><span> │ Content-Type: │</span></span>
|
||
<span class="line"><span> │ multipart/form-data │</span></span>
|
||
<span class="line"><span> │ file: <二进制> │</span></span>
|
||
<span class="line"><span> │ type: image | file │</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │ ┌──────┴───────────┐</span></span>
|
||
<span class="line"><span> │ │ UploadController │</span></span>
|
||
<span class="line"><span> │ │ ::uploadFile() │</span></span>
|
||
<span class="line"><span> │ └──────┬───────────┘</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │ 1. 校验 type 合法性(image / file)</span></span>
|
||
<span class="line"><span> │ 2. 文件存在且上传有效(isValid)</span></span>
|
||
<span class="line"><span> │ 3. 按类型读取扩展名白名单(config/file.php)</span></span>
|
||
<span class="line"><span> │ 4. 校验文件大小(file.max_size,默认 10MB)</span></span>
|
||
<span class="line"><span> │ 5. 生成路径:temp/Ymd/His_随机串.扩展名</span></span>
|
||
<span class="line"><span> │ 6. 采集元数据:原始名 / MIME(finfo)/ 大小</span></span>
|
||
<span class="line"><span> │ 7. mkdir + move 保存</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │◄── { fileUrl, filePath, ────│</span></span>
|
||
<span class="line"><span> │ originalName, fileSize, │</span></span>
|
||
<span class="line"><span> │ fileExtension, fileType, │</span></span>
|
||
<span class="line"><span> │ fileName } │</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │ 前端暂存 URL/路径 │</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>┌─────────────────────────────────────────────────────────────────────┐</span></span>
|
||
<span class="line"><span>│ 第二阶段:入库 │</span></span>
|
||
<span class="line"><span>└─────────────────────────────────────────────────────────────────────┘</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>前端 后端</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │── POST /api/example/add ──►│</span></span>
|
||
<span class="line"><span> │ { name: "测试", │</span></span>
|
||
<span class="line"><span> │ avatar: "temp/ │</span></span>
|
||
<span class="line"><span> │ 20261002/123456_ab.jpg" }│</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │ ┌──────┴──────────────┐</span></span>
|
||
<span class="line"><span> │ │ ExampleService::add │</span></span>
|
||
<span class="line"><span> │ │ (use HasFileProcessing)│</span></span>
|
||
<span class="line"><span> │ └──────┬──────────────┘</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │ ┌──────┴──────────────┐</span></span>
|
||
<span class="line"><span> │ │ processFileFieldsOnSave │</span></span>
|
||
<span class="line"><span> │ │ → save_file() │</span></span>
|
||
<span class="line"><span> │ │ 1. 去掉域名得相对路径 │</span></span>
|
||
<span class="line"><span> │ │ 2. temp/ 开头 → 迁移 │</span></span>
|
||
<span class="line"><span> │ │ 3. 外部URL → 下载保存 │</span></span>
|
||
<span class="line"><span> │ │ 4. 正式路径 → 直接返回│</span></span>
|
||
<span class="line"><span> │ └──────┬──────────────┘</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │◄── { code:0, data:{id:1} }──│</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>┌─────────────────────────────────────────────────────────────────────┐</span></span>
|
||
<span class="line"><span>│ 第三阶段:读取 │</span></span>
|
||
<span class="line"><span>└─────────────────────────────────────────────────────────────────────┘</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>前端 后端</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │── GET /api/example/detail/1►│</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │ ┌──────┴───────────────┐</span></span>
|
||
<span class="line"><span> │ │ processFileFieldsOnRead │</span></span>
|
||
<span class="line"><span> │ │ → get_file_url() │</span></span>
|
||
<span class="line"><span> │ │ 拼接 FILE_DOMAIN_URL │</span></span>
|
||
<span class="line"><span> │ └──────┬───────────────┘</span></span>
|
||
<span class="line"><span> │ │</span></span>
|
||
<span class="line"><span> │◄── { avatar: "https://──────│</span></span>
|
||
<span class="line"><span> │ cdn.example.com/uploads/ │</span></span>
|
||
<span class="line"><span> │ example/20261002/..." } │</span></span></code></pre></div><div class="tip custom-block"><p class="custom-block-title">临时目录中转</p><p>上传接口把文件写入 <code>temp/</code> 临时目录,<strong>不直接落正式目录</strong>;提交表单时 <code>save_file()</code> 依据 <code>temp/</code> 前缀把文件迁移到 Service 的 <code>fileSaveDir</code> 子目录(<code>user/</code>、<code>article/</code>、<code>example/</code> …),正式目录与业务模块一一对应。上传后未被表单引用的文件停留在 <code>temp/</code>,可按周期清理。</p></div><h2 id="上传接口" tabindex="-1">上传接口 <a class="header-anchor" href="#上传接口" aria-label="Permalink to "上传接口""></a></h2><h3 id="上传文件" tabindex="-1">上传文件 <a class="header-anchor" href="#上传文件" aria-label="Permalink to "上传文件""></a></h3><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">POST</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;"> /api/upload/uploadFile</span></span>
|
||
<span class="line"><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">Content-Type:</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;"> multipart/form-data</span></span>
|
||
<span class="line"><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">Authorization:</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;"> Bearer</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> <</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">toke</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">n</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">></span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">file:</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> <</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">二进制文</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">件</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">></span></span></code></pre></div><p><strong>参数:</strong></p><table tabindex="0"><thead><tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr></thead><tbody><tr><td><code>file</code></td><td>File</td><td>是</td><td>上传的文件</td></tr><tr><td><code>type</code></td><td>string</td><td>否</td><td><code>image</code>(校验图片后缀白名单)/ <code>file</code>(校验全量文件后缀白名单,默认)</td></tr></tbody></table><p><strong>响应(来自项目实际代码):</strong></p><div class="language-json vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">{</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> "</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">code</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 0</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> "</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">ok</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#1E754F;"> true</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> "</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">msg</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> "</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">上传成功</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> "</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">data</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> "</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">originalName</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> "</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">photo.jpg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> "</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">fileExtension</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> "</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">jpg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> "</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">fileType</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> "</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">image/jpeg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> "</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">fileSize</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 102400</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> "</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">fileName</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> "</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">123456_a1b2c3d4e5f6.jpg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> "</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">filePath</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> "</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">temp/20261002/123456_a1b2c3d4e5f6.jpg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> "</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">fileUrl</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">"</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> "</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">http://cdn.example.com/temp/20261002/123456_a1b2c3d4e5f6.jpg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> }</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">}</span></span></code></pre></div><p><strong>响应字段说明:</strong></p><table tabindex="0"><thead><tr><th>字段</th><th>说明</th><th>示例</th></tr></thead><tbody><tr><td><code>originalName</code></td><td>原始文件名</td><td><code>photo.jpg</code></td></tr><tr><td><code>fileExtension</code></td><td>扩展名</td><td><code>jpg</code></td></tr><tr><td><code>fileType</code></td><td>MIME 类型(<code>getMimeType()</code>)</td><td><code>image/jpeg</code></td></tr><tr><td><code>fileSize</code></td><td>文件大小(字节)</td><td><code>102400</code></td></tr><tr><td><code>fileName</code></td><td>服务器文件名(时间+随机串)</td><td><code>123456_a1b2c3d4e5f6.jpg</code></td></tr><tr><td><code>filePath</code></td><td>相对存储路径(<strong>提交表单建议用此字段</strong>)</td><td><code>temp/20261002/...</code></td></tr><tr><td><code>fileUrl</code></td><td>可访问 URL(<code>FILE_DOMAIN_URL</code> 为空时等于相对路径)</td><td><code>http://cdn.example.com/...</code></td></tr></tbody></table><h3 id="存储规则" tabindex="-1">存储规则 <a class="header-anchor" href="#存储规则" aria-label="Permalink to "存储规则""></a></h3><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span>{upload_dir}/ ← config/file.php → upload_dir(默认 uploads)</span></span>
|
||
<span class="line"><span>└── temp/ ← 所有类型统一存入(config/file.php → temp_dir)</span></span>
|
||
<span class="line"><span> └── 20261002/ ← Ymd 分目录</span></span>
|
||
<span class="line"><span> └── 123456_a1b2c3d4e5f6.jpg ← His_ + 随机8字节hex + 扩展名</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>{upload_dir}/ ← 正式目录,落库时由 save_file() 迁移生成</span></span>
|
||
<span class="line"><span>├── user/ article/ example/ … ← Service 的 fileSaveDir</span></span>
|
||
<span class="line"><span>└── 20261002/</span></span>
|
||
<span class="line"><span> └── 143052_a1b2c3d4e5f6.jpg</span></span></code></pre></div><ul><li><code>type</code> 只影响后缀白名单,存储目录始终是 <code>temp/</code></li><li>随机文件名避免重名与<strong>可预测性攻击</strong>(时间戳 + <code>random_bytes(8)</code>)</li><li>目录不存在时自动 <code>mkdir(0755, true)</code> 创建</li></ul><h3 id="安全检查链" tabindex="-1">安全检查链 <a class="header-anchor" href="#安全检查链" aria-label="Permalink to "安全检查链""></a></h3><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span>上传文件安全检查(UploadController::uploadFile):</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 1. 权限校验(auth.jwt + #[Permission('sys:upload:file')])</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 2. type 参数合法性(仅 image / file)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 3. 文件存在且上传有效(UploadedFile::isValid,捕获 PHP 上传错误)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 4. 扩展名白名单(image → file.image_ext;file → file.file_ext)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 5. 文件大小限制(config/file.php → max_size,默认 10MB)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 6. 服务端生成随机路径(防路径遍历)</span></span>
|
||
<span class="line"><span> │</span></span>
|
||
<span class="line"><span> ▼ 7. Nginx 仅允许入口 index.php 执行 —— 上传的 .php 一律 404</span></span></code></pre></div><div class="warning custom-block"><p class="custom-block-title">svg 已移除</p><p><code>FILE_IMAGE_EXT</code> 默认白名单<strong>不含 svg</strong>——svg 可内嵌 JavaScript,上传后直接作为静态资源访问存在 XSS 风险。如业务必须使用,请做内容清洗或以 attachment 方式返回。</p></div><h2 id="入库处理-HasFileProcessing-" tabindex="-1">入库处理(HasFileProcessing) <a class="header-anchor" href="#入库处理-HasFileProcessing-" aria-label="Permalink to "入库处理(HasFileProcessing)""></a></h2><p>Service 配置文件字段后,保存/读取自动处理:</p><div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// app/Services/ExampleService.php(形态示例)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">class</span><span style="--shiki-dark:#97E1F1;--shiki-light:#2E8F82;"> ExampleService</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> extends</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#59873A;--shiki-light-font-style:inherit;"> BaseService</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">{</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> use</span><span style="--shiki-dark:#97E1F1;--shiki-dark-font-style:italic;--shiki-light:#998418;--shiki-light-font-style:inherit;"> HasFileProcessing</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> /* 其他 Trait */</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // 需要自动处理的文件字段</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> protected</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> array</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">fileFields</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> [</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">avatar</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">];</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // 多文件字段(逗号分隔的多个 URL)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> protected</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> array</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">multiFileFields</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> [</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">images</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">];</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // 文件保存子目录(未指定时按模型类名小写推导)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> protected</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> string</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">fileSaveDir</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">example</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">}</span></span></code></pre></div><h3 id="自动行为" tabindex="-1">自动行为 <a class="header-anchor" href="#自动行为" aria-label="Permalink to "自动行为""></a></h3><table tabindex="0"><thead><tr><th>操作</th><th>自动行为</th></tr></thead><tbody><tr><td><code>add()</code> / <code>update()</code></td><td><code>processFileFieldsOnSave()</code> → <code>save_file()</code> 归一化(去域名、迁移 temp、下载外部 URL)</td></tr><tr><td><code>detail()</code></td><td><code>processFileFieldsOnRead()</code> → <code>get_file_url()</code> 补全域名</td></tr><tr><td><code>pageList()</code> / <code>allList()</code></td><td><code>processFileFieldsOnReadList()</code> 逐条补全域名</td></tr></tbody></table><p><strong>处理细节(来自 <code>HasFileProcessing</code> 源码):</strong></p><ul><li>空值跳过;<code>save_file()</code> 返回 <code>false</code> 时<strong>保留原值</strong>(避免误清空)</li><li>多文件按逗号拆分逐个处理,单个失败保留原 URL</li><li>未 <code>use HasFileProcessing</code> 时占位方法直接透传(见 <a href="/zh/architecture/cross-cutting.html">4.7 横切机制</a>)</li></ul><h2 id="核心辅助函数-app-Helpers-helpers-php-" tabindex="-1">核心辅助函数(app/Helpers/helpers.php) <a class="header-anchor" href="#核心辅助函数-app-Helpers-helpers-php-" aria-label="Permalink to "核心辅助函数(app/Helpers/helpers.php)""></a></h2><div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">/**</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> * 保存文件(归一化入库路径)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> *</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> * 处理逻辑:</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> * 1. 带域名 URL → 去掉域名得到相对路径</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> * 2. 包含 temp/ → 移动到正式目录(fileSaveDir)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> * 3. 外部 URL → save_remote_file() 下载保存</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> * 4. 已在正式目录 → 直接返回</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> */</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">function</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> save_file</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">string</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">fileUrl</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> string</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">saveDir</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> ''</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">:</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> string</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">|</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">false</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">/**</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> * 获取文件完整 URL</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> * - 空值 → '';已是 http(s) 开头 → 原样返回</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> * - 相对路径 → 拼接 config/file.php 的 domain_url</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> */</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">function</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> get_file_url</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">string</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">path</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">:</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> string</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">/** 生成存储路径:{saveDir}/Ymd/random.扩展名 */</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">function</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> create_file_path</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">string</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">saveDir</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> ''</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> string</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">ext</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> ''</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">:</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> string</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">/** 下载外部 URL 并保存(配合 is_internal_ip 做内网地址拦截,防 SSRF) */</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">function</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> save_remote_file</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">string</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">url</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> string</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> $</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">saveDir</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> ''</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">:</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> string</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">|</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">false</span></span></code></pre></div><h2 id="前端集成" tabindex="-1">前端集成 <a class="header-anchor" href="#前端集成" aria-label="Permalink to "前端集成""></a></h2><h3 id="NaiveUI-上传组件" tabindex="-1">NaiveUI 上传组件 <a class="header-anchor" href="#NaiveUI-上传组件" aria-label="Permalink to "NaiveUI 上传组件""></a></h3><div class="language-vue vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"><</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">template</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">></span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> <</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">n-upload</span></span>
|
||
<span class="line"><span style="--shiki-dark:#62E884;--shiki-dark-font-style:italic;--shiki-light:#B07D48;--shiki-light-font-style:inherit;"> :action</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">'/api/upload/uploadFile'</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span></span>
|
||
<span class="line"><span style="--shiki-dark:#62E884;--shiki-dark-font-style:italic;--shiki-light:#B07D48;--shiki-light-font-style:inherit;"> :data</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">{ type: 'image' }</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span></span>
|
||
<span class="line"><span style="--shiki-dark:#62E884;--shiki-dark-font-style:italic;--shiki-light:#B07D48;--shiki-light-font-style:inherit;"> :headers</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">uploadHeaders</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span></span>
|
||
<span class="line"><span style="--shiki-dark:#62E884;--shiki-dark-font-style:italic;--shiki-light:#B07D48;--shiki-light-font-style:inherit;"> @before-upload</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">beforeUpload</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span></span>
|
||
<span class="line"><span style="--shiki-dark:#62E884;--shiki-dark-font-style:italic;--shiki-light:#B07D48;--shiki-light-font-style:inherit;"> @finish</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">handleUploadFinish</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> ></span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> <</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">n-button</span><span style="--shiki-dark:#62E884;--shiki-dark-font-style:italic;--shiki-light:#B07D48;--shiki-light-font-style:inherit;"> type</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">primary</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">"</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">上传图片</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"></</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">n-button</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">></span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> </</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">n-upload</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">></span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"></</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">template</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">></span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"><</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">script</span><span style="--shiki-dark:#62E884;--shiki-dark-font-style:italic;--shiki-light:#B07D48;--shiki-light-font-style:inherit;"> setup</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">></span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">import</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> useMessage</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> }</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> from</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">@/hooks/web/useMessage</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">const</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> $message</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> useMessage</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">();</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">const</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> token</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> localStorage</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">.</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">getItem</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">access_token</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">);</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">const</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> uploadHeaders</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#998418;"> Authorization</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B5695977;"> \`</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">Bearer </span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">\${</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B56959;">token</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">}</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B5695977;">\`</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> };</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// n-upload 的 before-upload 参数为 { file },原生文件对象在 file.file</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">const</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> beforeUpload</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> ({</span><span style="--shiki-dark:#FFB86C;--shiki-dark-font-style:italic;--shiki-light:#B07D48;--shiki-light-font-style:inherit;"> file</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> })</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> const</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> isLt10M</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> file</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">.</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">file</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">.</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">size</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> /</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 1024</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> /</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 1024</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> <</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#2F798A;"> 10</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> if</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> (</span><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">!</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">isLt10M</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">)</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> $message</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">.</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">error</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">文件大小不能超过 10MB</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">);</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> }</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;"> return</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> isLt10M</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">};</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 上传结束,finish 事件拿到原始 event,需自行解析响应</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">const</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;"> handleUploadFinish</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> ({</span><span style="--shiki-dark:#FFB86C;--shiki-dark-font-style:italic;--shiki-light:#B07D48;--shiki-light-font-style:inherit;"> event</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> })</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =></span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;"> const</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> response</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#BF9EEE;--shiki-light:#B07D48;"> JSON</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">.</span><span style="--shiki-dark:#62E884;--shiki-light:#59873A;">parse</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">(</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">event</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">.</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">target</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">.</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">response</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">);</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> // 提交表单时保存相对路径(filePath)最稳妥</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> form</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">.</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">value</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">.</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">avatar</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> response</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">.</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">data</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">.</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;">filePath</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">;</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">};</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"></</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">script</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">></span></span></code></pre></div><h3 id="提交表单" tabindex="-1">提交表单 <a class="header-anchor" href="#提交表单" aria-label="Permalink to "提交表单""></a></h3><div class="language-typescript vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#AB5959;">const</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#B07D48;"> formData</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> {</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#998418;"> name</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">测试</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#998418;"> avatar</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> '</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">temp/20261002/123456_a1b2c3d4e5f6.jpg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">'</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">};</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 后端 Service 层自动:</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 1. 识别 avatar 为 fileFields 字段</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 2. save_file() 归一化(去域名 / 迁移 temp 到 fileSaveDir / 下载外部 URL)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;">// 3. 正式目录相对路径入库(如 example/20261002/123456_a1b2c3d4e5f6.jpg)</span></span></code></pre></div><h2 id="配置项" tabindex="-1">配置项 <a class="header-anchor" href="#配置项" aria-label="Permalink to "配置项""></a></h2><div class="language-ini vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ini</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># .env(config/file.php 读取)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">FILE_UPLOAD_DIR</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">uploads </span><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># 上传根目录(相对 public/)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">FILE_DOMAIN_URL</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"> # 文件访问域名(空则返回相对路径)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">FILE_MAX_SIZE</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">10 </span><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># 单文件大小上限(MB)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">FILE_IMAGE_EXT</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">gif,jpg,jpeg,png,bmp,webp </span><span style="--shiki-dark:#7B7F8B;--shiki-light:#A0ADA0;"># 图片后缀白名单(svg 已移除)</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">FILE_EXT</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">=</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv</span></span></code></pre></div><p>详见 <a href="/zh/tools/upload-config.html">7.2 上传配置详解</a>。</p><div class="warning custom-block"><p class="custom-block-title">同时调整 PHP 限制</p><p>业务层 <code>FILE_MAX_SIZE=10</code> 生效的前提是 PHP 层放行,确认 <code>php.ini</code>:</p><div class="language-ini vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ini</span><pre class="shiki shiki-themes dracula-soft vitesse-light vp-code" tabindex="0"><code><span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">upload_max_filesize</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;"> 50M</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">post_max_size</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;"> 50M</span></span>
|
||
<span class="line"><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">max_execution_time</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;"> =</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;"> 300</span></span></code></pre></div></div>`,38)])])}const c=i(l,[["render",t]]);export{g as __pageData,c as default};
|