config(由 OssRuntimeConfigService 注入,含 path_prefix / domain)。 */ trait ObjectKeyNormalizeTrait { /** * 压平重复斜杠,不补 path_prefix * * 列举结果必须走这里:OSS 回的 key 就是对象真实路径。 * 若再套一层 path_prefix,根目录的历史文件(如 b_xxx.jpg)会被改写成 * uploads/b_xxx.jpg,素材库入库地址全 404。 */ protected function normalizeListedKey(string $key): string { return ltrim((string) preg_replace('#/{2,}#', '/', trim($key)), '/'); } /** * 上传 / 删除用:缺 path_prefix 时补上,并压平重复斜杠 * * 为什么必须压平:uploads//spa/a.jpg 与 uploads/spa/a.jpg 在 OSS 上是两个对象, * 但拼出来的访问地址会被 CDN 归一成同一个。素材库按 path 建索引, * 不统一就会落成两条记录,回收删掉其中一条后另一条变成指向已删对象的幽灵。 */ protected function normalizeObjectKey(string $key): string { $key = $this->normalizeListedKey($key); if ($key === '') { return ''; } $prefix = trim((string) ($this->config['path_prefix'] ?? ''), '/'); if ($prefix !== '' && $key !== $prefix && !str_starts_with($key, $prefix . '/')) { $key = $prefix . '/' . $key; } return $key; } /** * 列举前缀:只认调用方传入的值,留空 = 扫整个 Bucket * * 以前空前缀会偷偷套配置里的 path_prefix。种子数据把七牛写成 uploads, * 而本站历史文件都在根目录(b_*.jpg),一拉就是空列表。 * 素材库文案也是「留空表示整个 Bucket」,这里必须跟文案一致; * 要收窄范围请在同步抽屉里显式填前缀。 */ protected function scopedListPrefix(string $prefix): string { return $this->normalizeListedKey($prefix); } /** * 拼公开访问地址;未配置 domain 时回落到调用方给的默认地址 */ protected function publicUrlOf(string $key, string $fallback = ''): string { $domain = rtrim((string) ($this->config['domain'] ?? ''), '/'); return $domain !== '' ? $domain . '/' . $key : $fallback; } /** * 单页列举条数收口,避免上游把 limit 传成 0 或者十万 */ protected function boundedLimit(int $limit): int { return max(1, min($limit, 1000)); } }