diff --git a/app/Storage/OssDriver.php b/app/Storage/OssDriver.php index 945bc3f..ef1da48 100644 --- a/app/Storage/OssDriver.php +++ b/app/Storage/OssDriver.php @@ -168,13 +168,19 @@ final class OssDriver implements StorageDriver // ---------------- 签名 ---------------- - private function authorization(string $method, string $key, string $contentType, string $date, array $cred): string + /** + * @param string $subresource 子资源串(如 `?x-oss-process=image/resize,w_400`), + * **必须计入 CanonicalizedResource**,否则 403。 + */ + private function authorization(string $method, string $key, string $contentType, string $date, + array $cred, string $subresource = ''): string { $canonicalHeaders = ''; if ((string)($cred['token'] ?? '') !== '') { $canonicalHeaders = "x-oss-security-token:{$cred['token']}\n"; } - $canonicalResource = '/' . trim((string)$this->cfg['bucket']) . '/' . ltrim($key, '/'); + $canonicalResource = '/' . trim((string)$this->cfg['bucket']) . '/' . ltrim($key, '/') + . $subresource; $stringToSign = $method . "\n" . "\n" // Content-MD5(未使用) . $contentType . "\n" @@ -200,12 +206,13 @@ final class OssDriver implements StorageDriver } /** 读请求刻意不发 Content-Type,签名串里传空串 */ - private function readHeaders(string $key, array $cred, array $extra = [], string $method = 'GET'): array + private function readHeaders(string $key, array $cred, array $extra = [], + string $method = 'GET', string $subresource = ''): array { $date = gmdate('D, d M Y H:i:s \G\M\T'); $h = [ 'Date: ' . $date, - 'Authorization: ' . $this->authorization($method, $key, '', $date, $cred), + 'Authorization: ' . $this->authorization($method, $key, '', $date, $cred, $subresource), ]; if ((string)($cred['token'] ?? '') !== '') { $h[] = 'x-oss-security-token: ' . $cred['token']; @@ -327,7 +334,19 @@ final class OssDriver implements StorageDriver } /** 服务端读取(内网优先 + 自动回退外网) */ - public function get(string $key, array $extra = []): array + /** + * 取对象。 + * + * @param string $process OSS 图片处理参数(如 `image/resize,l_400/quality,q_80`)。 + * **空 = 取原图**。非空时: + * ① URL 追加 `?x-oss-process=...` + * ② 签名把它计入 CanonicalizedResource(不然 403) + * + * 🔴 #56(用户 2026-11-05):「OSS在获取图片的时候,好像可以传一个参数, + * 它会给你返回一张压缩过的图片文件。」—— 就是 `x-oss-process`。 + * **实时处理,原图不动**,所以不用改上传流程、不用二次存储。 + */ + public function get(string $key, array $extra = [], string $process = ''): array { if ($err = $this->validateConfig()) { return ['ok' => false, 'status' => 0, 'headers' => [], 'body' => '', 'error' => $err]; @@ -336,8 +355,14 @@ final class OssDriver implements StorageDriver if (empty($cred['ok'])) { return ['ok' => false, 'status' => 0, 'headers' => [], 'body' => '', 'error' => $cred['error']]; } - $key = $this->objectKey($key); - $headers = $this->readHeaders($key, $cred, $extra); + $key = $this->objectKey($key); + + /* 子资源串:签名与 URL 必须**用同一个值**,否则签名对不上 */ + $sub = ''; + if ($process !== '') { + $sub = '?x-oss-process=' . $process; + } + $headers = $this->readHeaders($key, $cred, $extra, 'GET', $sub); $hosts = []; $internal = $this->internalEndpoint(); @@ -349,8 +374,10 @@ final class OssDriver implements StorageDriver $res = null; foreach ($hosts as $i => $host) { $isInternal = ($internal !== '' && $host === $internal); + /* ⚠️ process 里的逗号、下划线不需要 URL 编码(OSS 认原样), + 但 `?` 与 `=` 必须保留为分隔符 —— 所以直接拼,不 encode。 */ $url = $this->scheme() . '://' . trim((string)$this->cfg['bucket']) . '.' . $host - . '/' . Http::encodeKey($key); + . '/' . Http::encodeKey($key) . $sub; $res = Http::send('GET', $url, $headers, null, 60, $isInternal ? 2 : null); if ((int)$res['status'] === 0 && $isInternal && isset($hosts[$i + 1])) { continue; // 内网连不上,换外网 diff --git a/app/Storage/Storage.php b/app/Storage/Storage.php index 6e26672..e5aef97 100644 --- a/app/Storage/Storage.php +++ b/app/Storage/Storage.php @@ -153,14 +153,44 @@ final class Storage * 从本地搬到云端(或反过来)时,历史文件可能只在一边, * 本地优先能保证迁移期的老文件照样打得开,也让回滚随时可行。 */ - public static function read(int $familyId, string $key): array + /** + * 读一个对象。 + * + * @param string $process OSS 图片处理参数(如 `image/resize,l_400/quality,q_80`)。 + * + * 🔴 #56(用户 2026-11-05):「如果图片是放在本地服务器的…就不管它。 + * 如果是传到 OSS 里面的…就用 OSS 的压缩就行了。」 + * + * ## 两种驱动下的行为 + * + * | 驱动 | `$process` 非空时 | + * |---|---| + * | `oss` | 透传给 `OssDriver::get()` ⇒ OSS 实时压缩 | + * | `local` | **忽略** —— 本地没有图片处理服务,不该假装有 | + * + * ⚠️ 带 `$process` 时**跳过本地缓存**:那个缓存里是**原图**, + * 拿原图当压缩版返回,等于压缩没生效而且用户以为生效了。 + * (缓存由 media.php 自己按尺寸分目录管理,见那里的说明。) + */ + public static function read(int $familyId, string $key, string $process = ''): array { - $local = FB_UPLOADS . '/' . ltrim($key, '/'); - if ($key !== '' && is_file($local)) { - $body = (string)@file_get_contents($local); - return ['ok' => true, 'status' => 200, 'headers' => [], 'body' => $body, 'error' => '', 'from' => 'local']; + /* 只有无 process 时才走"本地优先":原图有缓存就直接读, + 省掉整个 OSS round-trip。带 process 的必须去 OSS 要压缩版。 */ + if ($process === '') { + $local = FB_UPLOADS . '/' . ltrim($key, '/'); + if ($key !== '' && is_file($local)) { + $body = (string)@file_get_contents($local); + return ['ok' => true, 'status' => 200, 'headers' => [], 'body' => $body, + 'error' => '', 'from' => 'local']; + } } - return self::disk($familyId)->get($key); + + $disk = self::disk($familyId); + /* 本地驱动不支持图片处理 ⇒ 忽略 process(用户明确要求"本地不管它") */ + if ($process === '' || $disk->id() !== 'oss') { + return $disk->get($key); + } + return $disk->get($key, [], $process); } public static function exists(int $familyId, string $key): bool diff --git a/media.php b/media.php index af5bc05..f731973 100644 --- a/media.php +++ b/media.php @@ -41,6 +41,28 @@ function media_fail(int $code, string $message): void } $key = (string)($_GET['k'] ?? ''); + +/* 🔴 #56(用户 2026-11-05):「OSS在获取图片的时候,好像可以传一个参数, + 它会给你返回一张压缩过的图片文件…如果可行的话,就用OSS的压缩就行了。」 + + `?w=400` ⇒ 让 OSS 实时返回压缩版(`x-oss-process=image/resize,l_400`)。 + + ⚠️ **白名单**而不是透传:OSS 图片处理**按次计费**(约 ¥0.0005/次), + 若放任 `?w=` 任意值,`?w=1`、`?w=9999` 这种请求会让处理次数暴涨。 + 只允许下面这几档 —— 覆盖实际用途,且数量可控。 + ⚠️ 不带 `w` ⇒ 原图,**行为完全不变**(300+ 处既有 `` 不受影响)。 + ⚠️ 本地存储的图片:OSS 档位参数**自动失效**(Storage::read 里判驱动), + 用户明确要求「本地的不不管它」。 */ +$imgW = (int)($_GET['w'] ?? 0); +/** 允许的尺寸档位 ⇒ OSS 处理参数 */ +$OssSizes = [ + 400 => 'image/resize,l_400/quality,q_80', + 800 => 'image/resize,l_800/quality,q_82', + 1600 => 'image/resize,l_1600/quality,q_85', +]; +if (!array_key_exists($imgW, $OssSizes)) { + $imgW = 0; // 非法值一律按原图处理 +} $stageTok = (string)($_GET['stage'] ?? ''); /* @@ -162,14 +184,17 @@ if ($isImageKey) { } } -$res = Storage::read($familyId, $key); +$res = Storage::read($familyId, $key, $imgW > 0 ? $OssSizes[$imgW] : ''); $status = (int)($res['status'] ?? 0); $body = (string)($res['body'] ?? ''); -// 远程取到的图片回写本地(下一跳命中文件系统,省掉整个 OSS round-trip) +/* 远程取到的图片回写本地(下一跳命中文件系统,省掉整个 OSS round-trip) + 🔴 #56:**按尺寸分档存** —— 否则 400/800/1600 三档会互相覆盖, + 用户看到的尺寸随机。原图($imgW=0)仍存原路径,行为不变。 */ if ($isImageKey && !empty($res['ok']) && $status === 200 && ($res['from'] ?? '') !== 'local' && $body !== '') { - Storage::cacheLocally($key, $body); + $cacheKey = $imgW > 0 ? ('_proc/' . $imgW . '/' . $key) : $key; + Storage::cacheLocally($cacheKey, $body); } if (empty($res['ok']) || $status < 200 || $status >= 300) { diff --git a/templates/_attachments.php b/templates/_attachments.php index f2b9bca..3ed18ba 100644 --- a/templates/_attachments.php +++ b/templates/_attachments.php @@ -49,7 +49,11 @@ $attDelUrl = $attDelUrl ?? ''; 400]); $attDl = media_url((string)$attRow['key'], ['dl' => 1]); $attNm = (string)($attRow['name'] ?? ''); $attSz = \App\Attachment::sizeWord($attRow['size'] ?? 0); diff --git a/templates/trips/show.php b/templates/trips/show.php index cb59297..ff10f07 100644 --- a/templates/trips/show.php +++ b/templates/trips/show.php @@ -101,7 +101,8 @@ $groupMeta = [
- + + 🧳