v8:观影落地(MovieLog 整体迁入统一事件原语)+ 三处 UI 撤除

- Schema v8:events / movie_details / event_attendees / event_photos 四张表,
  uk_event_source(family_id, source_ref) 作迁移幂等锚
- Movie 模型:时间线/详情/增删改、同行人整组替换、照片逐张管理、感受英→中映射
- ImageSlot 新增 poster(2:3) / ticket(free) / photo(free) 三槽位;
  free 槽位不裁剪、整图等比缩进框(只缩不放大)
- OssDriver::copyObject:同桶 CopyObject(手写 V1 签名,x-oss-* 头按字典序进签名串)
- 路由:movies 时间线/详情/新增/编辑/删除/补照片/删照片;同行人按家庭过滤;
  访客只读(requireWrite)
- UI 撤除:首页「我的一句话」、家谱页「补充我的亲属关系 / 看整个名册」两个按钮;
  顶栏加「观影」入口
- 修潜伏 bug:ImageSlot::parseRatio 的正则用 / 作分隔符,字符类里的 / 把模式
  提前截断(Unknown modifier ']')—— ratio() 只在上传 POST 里被调到,一直没被观察到
- 线上数据迁移(临时 token 门禁页,用完即删):MovieLog 219 条观影 → events,
  14 位观影人新增进名册(带 MovieLog 头像),431 个 OSS 对象同桶拷贝
  (movielog/uploads/<dir>/x → family/<dir>/x),review/feelings/影院三段/照片全量搬
This commit is contained in:
2026-09-30 20:22:46 +08:00
parent 809443d429
commit ff353b1eee
13 changed files with 1112 additions and 43 deletions
+48
View File
@@ -262,6 +262,54 @@ final class OssDriver implements StorageDriver
return ['ok' => true, 'error' => ''];
}
/**
* 服务端拷贝(CopyObject)—— v8 迁移用:同一个桶里把 movielog/ 前缀的对象
* 复制成 family/ 前缀,不走流量、不用下载重传。
*
* @param string $srcObjectKey 源对象**完整键**(含前缀,如 movielog/uploads/posters/x.jpg)
* @param string $destRelKey 目标**相对键**(不含本站前缀,如 posters/x.jpg)
*
* ⚠️ 签名注意:所有随请求发送的 x-oss-* 头都必须按字典序拼进签名串
* (copy-source 在 security-token 之前),漏一个就是 SignatureDoesNotMatch。
* CopyObject 不需要 Content-Type(拷贝沿用源对象的元数据),签名串里留空。
*/
public function copyObject(string $srcObjectKey, string $destRelKey): array
{
if ($err = $this->validateConfig()) {
return ['ok' => false, 'error' => $err];
}
$cred = $this->credentials();
if (empty($cred['ok'])) {
return ['ok' => false, 'error' => $cred['error']];
}
$bucket = trim((string)$this->cfg['bucket']);
$srcKey = ltrim($srcObjectKey, '/');
$destKey = $this->objectKey($destRelKey);
$date = gmdate('D, d M Y H:i:s \G\M\T');
$copySource = '/' . $bucket . '/' . Http::encodeKey($srcKey);
$canonical = "x-oss-copy-source:{$copySource}\n";
if ((string)($cred['token'] ?? '') !== '') {
$canonical .= "x-oss-security-token:{$cred['token']}\n";
}
$stringToSign = "PUT\n\n\n{$date}\n{$canonical}/{$bucket}/{$destKey}";
$signature = base64_encode(hash_hmac('sha1', $stringToSign, $cred['secret'], true));
$h = [
'Date: ' . $date,
'x-oss-copy-source: ' . $copySource,
'Authorization: OSS ' . $cred['id'] . ':' . $signature,
];
if ((string)($cred['token'] ?? '') !== '') {
$h[] = 'x-oss-security-token: ' . $cred['token'];
}
$res = Http::send('PUT', $this->objectUrl($destKey), $h, null, 60);
if (!$res['ok']) {
return ['ok' => false, 'error' => 'OSS CopyObject 失败:' . Http::shortError($res)];
}
return ['ok' => true, 'error' => ''];
}
public function delete(string $key): array
{
$cred = $this->credentials();