CRMEB/backend/app/services/driver/upload/UploadDriverServices.php
2026-09-23 15:15:02 +08:00

625 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace app\services\driver\upload;
use app\dao\driver\upload\UploadDriverDao;
use app\dao\driver\upload\UploadStorageDao;
use crmeb\basic\BaseService;
use crmeb\exceptions\AdminException;
use crmeb\services\CacheService;
use crmeb\services\driver\DriverRegistry;
/**
* 存储驱动管理服务
* Class UploadDriverServices
* @package app\services\driver\upload
*/
class UploadDriverServices extends BaseService
{
/** 配置界面保留已保存敏感值的固定占位符。 */
public const CONFIG_MASK = '********';
/**
* @var UploadDriverDao
*/
protected $dao;
/**
* UploadDriverServices constructor.
* @param UploadDriverDao $dao
*/
public function __construct(UploadDriverDao $dao)
{
$this->dao = $dao;
}
/**
* 获取驱动列表
* @param array $where
* @return array
*/
public function getList(array $where = [])
{
[$page, $limit, $defaultLimit] = $this->getPageValue();
$page = $page ?: 1;
$limit = $limit ?: $defaultLimit;
$list = $this->dao->getList($where, ['*'], $page, $limit, 'sort desc');
$list = array_map([$this, 'redactDriver'], $list);
$count = $this->dao->count($where);
return compact('list', 'count');
}
/**
* 获取所有启用的驱动
* @return array
* @throws \ReflectionException
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getEnabledDrivers()
{
return $this->dao->getEnabledDrivers();
}
/**
* 根据 driver_key 获取驱动信息
* @param string $driverKey
* @return array|\think\Model|null
* @throws \ReflectionException
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getByDriverKey(string $driverKey)
{
return $this->dao->getByDriverKey($driverKey);
}
/**
* 根据ID获取驱动信息
* @param int $id
* @return array|\think\Model|null
* @throws \ReflectionException
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getById(int $id)
{
return $this->dao->get($id);
}
/**
* 扫描驱动目录并同步到数据库
* 扫描 driver/upload/(第三方扩展 + 内置驱动 + 插件安装的驱动)
* @return array 返回扫描到的驱动列表
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function scanDrivers()
{
$drivers = [];
// 统一走驱动注册中心(读取 plugin.json 元数据),输出按原契约键白名单构造
foreach (DriverRegistry::scan('upload') as $info) {
$drivers[] = [
'driver_key' => $info['driver_key'],
'driver_name' => $info['name'],
'class_name' => $info['class_name'],
'domain_template' => $info['domain_template'] ?? '',
'config_fields' => json_encode($info['config_fields'], JSON_UNESCAPED_UNICODE),
'bucket_fields' => json_encode($info['bucket_fields'] ?? [], JSON_UNESCAPED_UNICODE),
'help_doc' => $info['help_doc'],
'sort' => $info['sort'],
'is_builtin' => $info['is_builtin'],
];
}
// 按 sort 降序排序
usort($drivers, function ($a, $b) {
return $b['sort'] - $a['sort'];
});
return $drivers;
}
/**
* 同步驱动信息到数据库
* @return array 返回同步结果
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function syncDrivers()
{
$scanned = $this->scanDrivers();
$updated = $this->transaction(function () use ($scanned) {
$updated = 0;
foreach ($scanned as $driver) {
$current = $this->dao->getByDriverKey($driver['driver_key']);
// Registry 只提供已安装且启用的插件;新增/删除字典行必须由插件生命周期管理。
if (!$current) {
continue;
}
$savedFields = array_column($current->config_fields ?: [], null, 'field');
$fields = json_decode($driver['config_fields'], true, 512, JSON_THROW_ON_ERROR);
foreach ($fields as &$field) {
$saved = $savedFields[$field['field']] ?? [];
if (array_key_exists('value', $saved)) {
$field['value'] = $saved['value'];
}
}
unset($field);
$driver['config_fields'] = $fields;
unset($driver['is_builtin']);
if ($this->dao->update($driver['driver_key'], $driver, 'driver_key') === false) {
throw new \RuntimeException('同步存储驱动失败');
}
++$updated;
}
return $updated;
});
$this->invalidateUploadCache();
return [
'scanned' => count($scanned),
'added' => 0,
'updated' => $updated,
'deleted' => 0,
];
}
/**
* 更新驱动状态
* @param string $driverKey
* @param int $status
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function updateStatus(string $driverKey, int $status)
{
if (!in_array($status, [0, 1], true)) {
throw new AdminException('无效的驱动状态');
}
$driver = $this->dao->getByDriverKey($driverKey);
if (!$driver) {
throw new AdminException('驱动不存在');
}
$this->update((int)$driver->id, ['status' => $status]);
return true;
}
/**
* 获取驱动的配置字段定义
* @param string $driverKey
* @param bool $redact 是否脱敏界面返回值
* @return array
* @throws \ReflectionException
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getConfigFields(string $driverKey, bool $redact = false)
{
$driver = $this->dao->getByDriverKey($driverKey);
if (!$driver) {
return [];
}
$fields = $driver->config_fields ?? [];
return $redact ? $this->redactConfigFields($fields) : $fields;
}
/**
* 获取供配置界面使用的脱敏字段
* @param string $driverKey 驱动标识
* @return array
*/
public function getPublicConfigFields(string $driverKey): array
{
return $this->getConfigFields($driverKey, true);
}
/**
* 获取已安装且插件可用的驱动能力声明
* @param string $driverKey 驱动标识
* @return array
*/
public function getCapabilities(string $driverKey): array
{
$info = DriverRegistry::get('upload', $driverKey);
return is_array($info['capabilities'] ?? null) ? array_values($info['capabilities']) : [];
}
/**
* 获取驱动的存储空间配置字段
* @param string $driverKey
* @return array
* @throws \ReflectionException
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getBucketFields(string $driverKey)
{
$driver = $this->dao->getByDriverKey($driverKey);
if (!$driver) {
return [];
}
return $driver->bucket_fields ?? [];
}
/**
* 获取 type 到 driver_key 的映射
* 从数据库动态读取,不硬编码
* @return array
*/
public function getTypeDriverKeyMap()
{
$map = [];
$drivers = $this->dao->getList([], ['id', 'driver_key'], 0, 0, 'id asc');
foreach ($drivers as $index => $driver) {
$map[$index + 1] = $driver['driver_key'];
}
return $map;
}
/**
* 根据 type 获取 driver_key
* @param int $type
* @return string
*/
public function getDriverKeyByType(int $type)
{
$map = $this->getTypeDriverKeyMap();
return $map[$type] ?? 'local';
}
/**
* 根据 driver_key 获取 type
* @param string $driverKey
* @return int
*/
public function getTypeByDriverKey(string $driverKey)
{
$map = $this->getTypeDriverKeyMap();
return array_search($driverKey, $map) ?: 1;
}
/**
* 获取上传公共配置(缩略图和水印)
* @param string $driverKey
* @return array
*/
public function getCommonConfig(string $driverKey)
{
$defaultConfig = $this->getDefaultCommonConfig();
$driver = $this->dao->getByDriverKey($driverKey);
return array_merge($defaultConfig, array_intersect_key($driver->thumb_watermark_config ?? [], $defaultConfig));
}
/**
* 上传公共配置的固定字段与默认值
* @return array
*/
protected function getDefaultCommonConfig(): array
{
return [
'image_thumb_status' => 1,
'thumb_big_width' => 800,
'thumb_big_height' => 800,
'thumb_mid_width' => 300,
'thumb_mid_height' => 300,
'thumb_small_width' => 150,
'thumb_small_height' => 150,
'image_watermark_status' => 1,
'watermark_type' => 2,
'watermark_opacity' => '',
'watermark_rotate' => '',
'watermark_position' => 1,
'watermark_x' => '',
'watermark_y' => '',
'watermark_image' => '',
'watermark_text' => 'CRMEB',
'watermark_text_size' => '28',
'watermark_text_color' => '#ffffff',
'watermark_text_angle' => '7',
];
}
/**
* 保存上传公共配置(缩略图和水印)到 eb_driver 表
* @param string $driverKey
* @param array $config
* @return bool
*/
public function saveCommonConfig(string $driverKey, array $config)
{
$driver = $this->dao->getByDriverKey($driverKey);
if (!$driver) {
throw new AdminException('驱动不存在');
}
$driver->thumb_watermark_config = $this->prepareCommonConfig($driver->thumb_watermark_config ?: [], $config);
$driver->update_time = time();
if ($driver->save() === false) {
throw new \RuntimeException('保存上传公共配置失败');
}
$this->invalidateUploadCache();
return true;
}
/**
* 保存驱动配置到 eb_driver 表的 config_fields 中
* @param string $driverKey
* @param array $configValues
* @return bool
*/
public function saveDriverConfig(string $driverKey, array $configValues)
{
$driver = $this->dao->getByDriverKey($driverKey);
if (!$driver) {
throw new AdminException('驱动不存在');
}
$driver->config_fields = $this->validateDriverConfig($driver->config_fields ?: [], $configValues);
$driver->update_time = time();
if ($driver->save() === false) {
throw new \RuntimeException('保存存储驱动配置失败');
}
$this->invalidateUploadCache();
return true;
}
/**
* 更新驱动信息
* @param int $id
* @param array $data
* @param array|null $configValues 同时保存的配置字段
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function update(int $id, array $data, ?array $configValues = null)
{
if (array_diff_key($data, array_flip(['driver_name', 'sort', 'status', 'thumb_watermark_config']))) {
throw new AdminException('不支持的存储驱动字段');
}
if (array_key_exists('status', $data)) {
$data['status'] = $this->configInteger($data['status'], 0, 1, '驱动状态');
}
if (array_key_exists('sort', $data)) {
$data['sort'] = $this->configInteger($data['sort'], 0, 9999, '排序');
}
if (array_key_exists('driver_name', $data) && (!is_string($data['driver_name']) || trim($data['driver_name']) === '')) {
throw new AdminException('请输入驱动名称');
}
$driver = $this->dao->get($id);
if (!$driver) {
throw new AdminException('驱动不存在');
}
if (($data['status'] ?? 0) === 1 && !DriverRegistry::get('upload', (string)$driver->driver_key)) {
throw new AdminException('请先启用对应的驱动插件');
}
if ($configValues !== null || ($data['status'] ?? 0) === 1) {
$data['config_fields'] = $this->validateDriverConfig($driver->config_fields ?: [], $configValues ?? []);
}
if (array_key_exists('thumb_watermark_config', $data)) {
if (!is_array($data['thumb_watermark_config'])) {
throw new AdminException('上传公共配置类型错误');
}
$data['thumb_watermark_config'] = $this->prepareCommonConfig($driver->thumb_watermark_config ?: [], $data['thumb_watermark_config']);
}
$this->transaction(function () use ($id, $driver, $data) {
// 状态切换与配置保存必须一起成功,失败时回滚其他驱动的停用。
if (($data['status'] ?? 0) === 1 && $driver->driver_key !== 'local'
&& !app()->make(UploadStorageDao::class)->count(['driver_key' => $driver->driver_key, 'status' => 1, 'is_delete' => 0])) {
throw new AdminException('未有正在使用的存储空间');
}
if (($data['status'] ?? 0) === 1 && $this->dao->disableAllExcept($id) === false) {
throw new \RuntimeException('切换存储驱动失败');
}
foreach ($data as $key => $value) {
$driver->$key = $value;
}
$driver->update_time = time();
if ($driver->save() === false) {
throw new \RuntimeException('保存存储驱动失败');
}
});
$this->invalidateUploadCache();
return true;
}
/**
* 校验驱动配置字段定义中的白名单、类型和必填值
* @param array $fields 字段定义及已保存值
* @param array $values 本次配置值
* @return array
*/
protected function validateDriverConfig(array $fields, array $values): array
{
if (array_diff_key($values, array_column($fields, null, 'field'))) {
throw new AdminException('不支持的驱动配置字段');
}
foreach ($fields as &$field) {
$name = $field['field'];
$value = array_key_exists($name, $values) ? $values[$name] : ($field['value'] ?? $field['default'] ?? '');
// 固定占位符仅代表保留既有敏感值,绝不能保存为新凭据。
if ($this->isSensitiveConfigField($field) && $value === self::CONFIG_MASK) {
$value = $field['value'] ?? $field['default'] ?? '';
}
if (!is_string($value) && !is_int($value) && !is_float($value) && !is_bool($value)) {
throw new AdminException('驱动配置字段类型错误');
}
if (($field['required'] ?? false) && (is_string($value) ? trim($value) === '' : $value === null)) {
throw new AdminException('请填写必填驱动配置');
}
if (!($field['required'] ?? false) && $value === '') {
$field['value'] = '';
continue;
}
$type = $field['type'] ?? 'text';
if (in_array($type, ['text', 'password', 'textarea'], true)) {
if (!is_string($value) && !is_int($value)) throw new AdminException('驱动配置字段类型错误');
$value = (string)$value;
} elseif ($type === 'number') {
if (!is_numeric($value) || !is_finite((float)$value)) throw new AdminException('驱动配置字段类型错误');
$value = 0 + $value;
} elseif (in_array($type, ['radio', 'select'], true)) {
if (!in_array((string)$value, array_map('strval', array_column($field['options'] ?? [], 'value')), true)) {
throw new AdminException('驱动配置选项无效');
}
} elseif ($type === 'switch') {
$value = $this->configInteger($value, 0, 1, '驱动配置');
} else {
throw new AdminException('不支持的驱动配置类型');
}
$field['value'] = $value;
}
unset($field);
return $fields;
}
/**
* 校验整数型配置
* @param mixed $value 配置值
* @param int $min 最小值
* @param int $max 最大值
* @param string $label 配置名称
* @return int
*/
protected function configInteger($value, int $min, int $max, string $label): int
{
$integer = is_bool($value) ? (int)$value : filter_var($value, FILTER_VALIDATE_INT);
if ($integer === false || $integer < $min || $integer > $max) {
throw new AdminException('上传配置 {name} 数值无效', ['name' => $label]);
}
return $integer;
}
/**
* 合并公共配置并校验字段、类型和范围,不读取或修改持久化数据
* @param array $current 已保存配置
* @param array $config 本次配置
* @return array
*/
protected function prepareCommonConfig(array $current, array $config): array
{
$defaults = $this->getDefaultCommonConfig();
if (array_diff_key($config, $defaults)) {
throw new AdminException('不支持的上传公共配置字段');
}
return $this->validateCommonConfig(array_merge($defaults, array_intersect_key($current, $defaults), $config));
}
/**
* 校验公共图片处理配置
* @param array $config 合并后的配置
* @return array
*/
protected function validateCommonConfig(array $config): array
{
foreach (['image_thumb_status', 'image_watermark_status'] as $name) {
$config[$name] = $this->configInteger($config[$name], 0, 1, $name);
}
$config['watermark_type'] = $this->configInteger($config['watermark_type'], 1, 2, '水印类型');
$config['watermark_position'] = $this->configInteger($config['watermark_position'], 1, 9, '水印位置');
foreach (['thumb_big_width' => 800, 'thumb_big_height' => 800, 'thumb_mid_width' => 300, 'thumb_mid_height' => 300,
'thumb_small_width' => 150, 'thumb_small_height' => 150] as $name => $default) {
if ($config[$name] === '' && !$config['image_thumb_status']) continue;
if ($config[$name] === '') $config[$name] = $default;
$config[$name] = $this->configInteger($config[$name], $config['image_thumb_status'] ? 1 : 0, 10000, $name);
}
foreach (['watermark_opacity' => [0, 100], 'watermark_rotate' => [-360, 360], 'watermark_text_angle' => [-360, 360],
'watermark_text_size' => [1, 1000], 'watermark_x' => [0, 10000], 'watermark_y' => [0, 10000]] as $name => [$min, $max]) {
if ($config[$name] !== '') $config[$name] = $this->configInteger($config[$name], $min, $max, $name);
}
foreach (['watermark_image', 'watermark_text', 'watermark_text_color'] as $name) {
if (!is_string($config[$name])) throw new AdminException('水印配置类型错误');
}
if (!preg_match('/^#[a-fA-F0-9]{6}$/', $config['watermark_text_color'])) {
throw new AdminException('水印文字颜色无效');
}
if ($config['image_watermark_status']) {
$required = $config['watermark_type'] === 1 ? 'watermark_image' : 'watermark_text';
if (trim($config[$required]) === '') throw new AdminException('请填写水印内容');
if ($config['watermark_type'] === 2 && $config['watermark_text_size'] === '') throw new AdminException('请填写水印文字大小');
}
return $config;
}
/**
* 判断凭据型配置字段
* @param array $field 字段定义
* @return bool
*/
protected function isSensitiveConfigField(array $field): bool
{
return ($field['type'] ?? '') === 'password'
|| preg_match('/(?:access.?key|secret|token|password|private.?key)/i', $field['field'] ?? '') === 1;
}
/**
* 脱敏配置界面数据,保持原有字段定义结构
* @param array $fields 字段定义
* @return array
*/
protected function redactConfigFields(array $fields): array
{
foreach ($fields as &$field) {
if ($this->isSensitiveConfigField($field)) {
foreach (['value', 'default'] as $name) {
if (isset($field[$name]) && $field[$name] !== '') $field[$name] = self::CONFIG_MASK;
}
}
}
unset($field);
return $fields;
}
/**
* 脱敏管理端驱动详情及列表中的凭据
* @param array $driver 驱动数据
* @return array
*/
public function redactDriver(array $driver): array
{
$driver['config_fields'] = $this->redactConfigFields(is_array($driver['config_fields'] ?? null) ? $driver['config_fields'] : []);
return $driver;
}
/**
* 仅失效上传空间缓存与进程内驱动字典
* @return void
*/
protected function invalidateUploadCache(): void
{
CacheService::delete('storage_list');
DriverRegistry::resetCache();
}
}