config(由 OssRuntimeConfigService 注入,含 path_prefix / domain)。 */ trait ObjectKeyNormalizeTrait { /** * 补上 path_prefix 并压平重复斜杠 * * 为什么必须压平:uploads//spa/a.jpg 与 uploads/spa/a.jpg 在 OSS 上是两个对象, * 但拼出来的访问地址会被 CDN 归一成同一个。素材库按 path 建索引, * 不统一就会落成两条记录,回收删掉其中一条后另一条变成指向已删对象的幽灵。 */ protected function normalizeObjectKey(string $key): string { $key = ltrim((string) preg_replace('#/{2,}#', '/', trim($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; } /** * 列举前缀:调用方没给就退回 path_prefix * * 同一个 bucket 常常被多个项目共用,不加这层兜底会把别人的对象也拉进素材库, * 之后走回收流程就等于跨项目删文件。 */ protected function scopedListPrefix(string $prefix): string { $prefix = trim($prefix); if ($prefix !== '') { return $this->normalizeObjectKey($prefix); } $base = trim((string) ($this->config['path_prefix'] ?? ''), '/'); return $base === '' ? '' : $base . '/'; } /** * 拼公开访问地址;未配置 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)); } }