Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_NaiveVue/.vitepress/dist/assets/zh_tools_upload.md.CTM3IUUL.lean.js
T
2026-10-11 19:06:33 +08:00

201 lines
55 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
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 &quot;7.1 文件上传全流程&quot;">​</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 &quot;完整流程图&quot;">​</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: &lt;二进制&gt; │</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: &quot;测试&quot;, │</span></span>
<span class="line"><span> │ avatar: &quot;temp/ │</span></span>
<span class="line"><span> │ 20261002/123456_ab.jpg&quot; }│</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: &quot;https://──────│</span></span>
<span class="line"><span> │ cdn.example.com/uploads/ │</span></span>
<span class="line"><span> │ example/20261002/...&quot; } │</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 &quot;上传接口&quot;">​</a></h2><h3 id="上传文件" tabindex="-1">上传文件 <a class="header-anchor" href="#上传文件" aria-label="Permalink to &quot;上传文件&quot;">​</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;"> &lt;</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;">&gt;</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;"> &lt;</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;">&gt;</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;"> &quot;</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">code</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">&quot;</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;"> &quot;</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">ok</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">&quot;</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;"> &quot;</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">msg</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">&quot;</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">上传成功</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> &quot;</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">data</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">&quot;</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;"> &quot;</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">originalName</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">&quot;</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">photo.jpg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> &quot;</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">fileExtension</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">&quot;</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">jpg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> &quot;</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">fileType</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">&quot;</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">image/jpeg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> &quot;</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">fileSize</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">&quot;</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;"> &quot;</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">fileName</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">&quot;</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">123456_a1b2c3d4e5f6.jpg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> &quot;</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">filePath</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">&quot;</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">temp/20261002/123456_a1b2c3d4e5f6.jpg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">,</span></span>
<span class="line"><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;"> &quot;</span><span style="--shiki-dark:#97E1F1;--shiki-light:#998418;">fileUrl</span><span style="--shiki-dark:#97E2F2;--shiki-light:#99841877;">&quot;</span><span style="--shiki-dark:#F286C4;--shiki-light:#999999;">:</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;"> &quot;</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;">&quot;</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 &quot;存储规则&quot;">​</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 &quot;安全检查链&quot;">​</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(&#39;sys:upload:file&#39;)])</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 &quot;入库处理(HasFileProcessing)&quot;">​</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">avatar</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">images</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">example</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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 &quot;自动行为&quot;">​</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 &quot;核心辅助函数(app/Helpers/helpers.php)&quot;">​</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;"> &#39;&#39;</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;"> * - 空值 → &#39;&#39;;已是 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;"> &#39;&#39;</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;"> &#39;&#39;</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;"> &#39;&#39;</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 &quot;前端集成&quot;">​</a></h2><h3 id="NaiveUI-上传组件" tabindex="-1">NaiveUI 上传组件 <a class="header-anchor" href="#NaiveUI-上传组件" aria-label="Permalink to &quot;NaiveUI 上传组件&quot;">​</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;">&lt;</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">template</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">&gt;</span></span>
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> &lt;</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;">&quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">&#39;/api/upload/uploadFile&#39;</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</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;">&quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">{ type: &#39;image&#39; }</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</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;">&quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">uploadHeaders</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</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;">&quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">beforeUpload</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</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;">&quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">handleUploadFinish</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</span></span>
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> &gt;</span></span>
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> &lt;</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;">&quot;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">primary</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&quot;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">&gt;</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#393A34;">上传图片</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">&lt;/</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">n-button</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">&gt;</span></span>
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;"> &lt;/</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">n-upload</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">&gt;</span></span>
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">&lt;/</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">template</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">&gt;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">&lt;</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;">&gt;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">@/hooks/web/useMessage</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">access_token</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;"> =&gt;</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;"> &lt;</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;">&#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">文件大小不能超过 10MB</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;"> =&gt;</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;">&lt;/</span><span style="--shiki-dark:#F286C4;--shiki-light:#1E754F;">script</span><span style="--shiki-dark:#F6F6F4;--shiki-light:#999999;">&gt;</span></span></code></pre></div><h3 id="提交表单" tabindex="-1">提交表单 <a class="header-anchor" href="#提交表单" aria-label="Permalink to &quot;提交表单&quot;">​</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">测试</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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;"> &#39;</span><span style="--shiki-dark:#E7EE98;--shiki-light:#B56959;">temp/20261002/123456_a1b2c3d4e5f6.jpg</span><span style="--shiki-dark:#DEE492;--shiki-light:#B5695977;">&#39;</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 &quot;配置项&quot;">​</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};