v6:名册联系信息对齐 vCard + 头像与裁剪四项修正
- ① 头像上传失败(超限/非图片)不再挡保存,改为 flash 提示「已加进名册。头像没有保存:…」 ——这是「添加不了」的真根因(错误被塞进 errors.avatar 把整条新增打回) - ② 表单顺序:生日整块下移(身份证能读出生日时无需常改) - ③ 裁剪缩放语义反转:框在屏幕上的位置与大小不动、动它底下的图; 落库归一化矩形随倍率缩小(放大=同一块区域看得更清楚) - ④ 取消应用层 5MB 兆数上限;按像素动态上调 memory_limit(px*6+64M,封顶 768M); 头像输出恒为 512x512 - ⑤ 成员信息对齐 vCard:person_contacts 表(phone/email/address/url 可多行, value 加密落库)+ persons.org(工作单位)/title(职务职称);均可留空 Schema v5→v6:person_contacts 建表 + persons.org/title 加列
This commit is contained in:
+42
-10
@@ -14,7 +14,14 @@
|
||||
* 安全要点:
|
||||
* · 用 finfo 判断真实 MIME,不信浏览器给的扩展名
|
||||
* · 文件名随机,绝不使用用户提供的名字(防路径穿越与扩展名绕过)
|
||||
* · 内存先设上限:超大图直接判错,别把 PHP 进程撑爆
|
||||
* · 不设「兆数」上限(用户原话:上传照片不要有大小限制;真正会进来的照片
|
||||
* 是手机原图)—— 改成**按像素数动态上调 memory_limit**:
|
||||
* GD 解码一张真彩图约要 5 字节/像素,默认 128M 只够 ~2500 万像素,
|
||||
* 一张 4800 万的原图会直接 OOM Fatal(不可捕获、整页白屏)。
|
||||
* 这里按 w*h*6B 预算内存,不够就 ini_set 上调(封顶 768M),
|
||||
* 超过 1.5 亿像素(约 1.2 万 × 1.2 万)才明确拒绝 —— 那已经不是照片是扫描件了。
|
||||
* · 落库前**一律缩放到槽位尺寸**(头像 512×512):原图 30MB 还是 3MB,
|
||||
* 存下来都是一张小 JPEG —— 所以"上传大图"不会把磁盘和流量撑爆。
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
@@ -56,15 +63,13 @@ final class Upload
|
||||
return ['ok' => false, 'error' => ''];
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
return ['ok' => false, 'error' => '图片太大了(上限 ' . self::maxMbWord() . ')'];
|
||||
// 应用层不再设「兆数」上限;这条只会在超过服务器 ini 上限时发生。
|
||||
return ['ok' => false, 'error' => '图片太大了(服务器单次上传上限 '
|
||||
. self::iniUploadWord() . '),请压缩后再试'];
|
||||
default:
|
||||
return ['ok' => false, 'error' => '上传失败(错误码 ' . (int)$file['error'] . ')'];
|
||||
}
|
||||
|
||||
if ((int)$file['size'] > ImageSlot::MAX_BYTES) {
|
||||
return ['ok' => false, 'error' => '图片太大了(上限 ' . self::maxMbWord() . ')'];
|
||||
}
|
||||
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($file['tmp_name']);
|
||||
$allow = ['image/jpeg' => 'imagecreatefromjpeg', 'image/png' => 'imagecreatefrompng',
|
||||
@@ -78,8 +83,16 @@ final class Upload
|
||||
if (!$info || $info[0] < 1 || $info[1] < 1) {
|
||||
return ['ok' => false, 'error' => '图片读不出来'];
|
||||
}
|
||||
if ($info[0] * $info[1] > 40_000_000) {
|
||||
return ['ok' => false, 'error' => '图片像素太多(超过 4000 万),请先压缩再传'];
|
||||
$px = $info[0] * $info[1];
|
||||
if ($px > 150_000_000) {
|
||||
return ['ok' => false, 'error' => '图片像素实在太大(超过 1.5 亿),请先压缩再传'];
|
||||
}
|
||||
// 按需上调 memory_limit:GD 真彩解码约 5B/px,再加 64M 给缩放画布与脚本本身。
|
||||
// (memory_limit 是 PHP_INI_ALL,运行时可以调;这是"大图也能传"的关键一步。)
|
||||
$need = $px * 6 + 64 * 1024 * 1024;
|
||||
$have = self::iniBytes('memory_limit');
|
||||
if ($need > $have) {
|
||||
@ini_set('memory_limit', (string)min($need, 768 * 1024 * 1024));
|
||||
}
|
||||
|
||||
$src = @$allow[$mime]($file['tmp_name']);
|
||||
@@ -238,9 +251,28 @@ final class Upload
|
||||
return [$ix, $iy, $iw, $ih];
|
||||
}
|
||||
|
||||
private static function maxMbWord(): string
|
||||
/** 把 ini 里的 upload_max_filesize 读成人话(如 "50M")——报错信息里用 */
|
||||
private static function iniUploadWord(): string
|
||||
{
|
||||
return (int)round(ImageSlot::MAX_BYTES / 1024 / 1024) . 'MB';
|
||||
$b = self::iniBytes('upload_max_filesize');
|
||||
return $b > 0 ? (int)round($b / 1024 / 1024) . 'MB' : '50MB';
|
||||
}
|
||||
|
||||
/** 解析 ini 的简写字节数("128M" / "1G" / "65536K");读不到回退 0 */
|
||||
private static function iniBytes(string $key): int
|
||||
{
|
||||
$v = trim((string)@ini_get($key));
|
||||
if ($v === '' || !is_numeric($v[0])) {
|
||||
return 0;
|
||||
}
|
||||
$n = (float)$v;
|
||||
$unit = strtolower(substr($v, -1));
|
||||
return (int)match (true) {
|
||||
$unit === 'g' => $n * 1024 ** 3,
|
||||
$unit === 'm' => $n * 1024 ** 2,
|
||||
$unit === 'k' => $n * 1024,
|
||||
default => $n,
|
||||
};
|
||||
}
|
||||
|
||||
/** 图片访问地址(无图时回退内置剪影,保证永远不出现破图) */
|
||||
|
||||
Reference in New Issue
Block a user