AI代码生成工具
This commit is contained in:
112
app/Enum/OssDriverEnum.php
Normal file
112
app/Enum/OssDriverEnum.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
/**
|
||||
* OSS 存储驱动枚举:管理端卡片展示与上传分发共用
|
||||
*/
|
||||
enum OssDriverEnum: string
|
||||
{
|
||||
case Local = 'local';
|
||||
case Aliyun = 'aliyun';
|
||||
case Qcloud = 'qcloud';
|
||||
case Qiniu = 'qiniu';
|
||||
case Huawei = 'huawei';
|
||||
case Aws = 'aws';
|
||||
case Minio = 'minio';
|
||||
case Baidu = 'baidu';
|
||||
|
||||
/**
|
||||
* 驱动编码(与库表 driver 一致)
|
||||
*/
|
||||
public function code(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文展示名
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Local => '本地存储',
|
||||
self::Aliyun => '阿里云 OSS',
|
||||
self::Qcloud => '腾讯云 COS',
|
||||
self::Qiniu => '七牛云',
|
||||
self::Huawei => '华为云 OBS',
|
||||
self::Aws => 'AWS S3',
|
||||
self::Minio => 'MinIO',
|
||||
self::Baidu => '百度云 BOS',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片说明文案
|
||||
*/
|
||||
public function description(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Local => '文件保存到应用服务器本地磁盘',
|
||||
self::Aliyun => '阿里云对象存储 OSS',
|
||||
self::Qcloud => '腾讯云对象存储 COS',
|
||||
self::Qiniu => '七牛云对象存储 Kodo',
|
||||
self::Huawei => '华为云对象存储 OBS',
|
||||
self::Aws => '亚马逊 S3 对象存储',
|
||||
self::Minio => 'S3 兼容的私有化对象存储',
|
||||
self::Baidu => '百度智能云对象存储 BOS',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端卡片图标(iconify)
|
||||
*/
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Local => 'lucide:hard-drive',
|
||||
self::Aliyun => 'simple-icons:alibabadotcom',
|
||||
self::Qcloud => 'simple-icons:tencentqq',
|
||||
self::Qiniu => 'lucide:cloud',
|
||||
self::Huawei => 'simple-icons:huawei',
|
||||
self::Aws => 'simple-icons:amazonaws',
|
||||
self::Minio => 'simple-icons:minio',
|
||||
self::Baidu => 'simple-icons:baidu',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要 AccessKey / SecretKey
|
||||
*/
|
||||
public function needsSecret(): bool
|
||||
{
|
||||
return $this !== self::Local;
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端驱动选项列表(含图标,供卡片展示)
|
||||
*/
|
||||
public static function options(): array
|
||||
{
|
||||
$list = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$list[] = [
|
||||
'code' => $case->code(),
|
||||
'name' => $case->label(),
|
||||
'label' => $case->label(),
|
||||
'description' => $case->description(),
|
||||
'icon' => $case->icon(),
|
||||
'needs_secret' => $case->needsSecret() ? 1 : 0,
|
||||
];
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验驱动编码是否合法
|
||||
*/
|
||||
public static function isValid(string $code): bool
|
||||
{
|
||||
return self::tryFrom($code) !== null;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,33 @@ class AdminController extends BaseController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人中心更新资料
|
||||
* @Method POST
|
||||
*/
|
||||
public function updateProfile(): JsonResponse
|
||||
{
|
||||
return jok($this->service->updateProfile(request()->post()), '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人中心:登录日志
|
||||
* @Method GET
|
||||
*/
|
||||
public function loginLog(): JsonResponse
|
||||
{
|
||||
return jok($this->service->loginLogList(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人中心:操作日志
|
||||
* @Method GET
|
||||
*/
|
||||
public function opLog(): JsonResponse
|
||||
{
|
||||
return jok($this->service->opLogList(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单列表
|
||||
* @Method GET
|
||||
|
||||
75
app/Http/Controllers/Api/ApiEndpointController.php
Normal file
75
app/Http/Controllers/Api/ApiEndpointController.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\ApiEndpointService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 接口注册表控制器:仅开放 list / update
|
||||
*/
|
||||
class ApiEndpointController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = ApiEndpointService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表
|
||||
* @Method GET
|
||||
*/
|
||||
public function list(): JsonResponse
|
||||
{
|
||||
return jok($this->service->list(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新接口说明 / 是否记日志 / 状态
|
||||
* @Method POST
|
||||
*/
|
||||
public function update(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
$id = (int) ($params['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
unset($params['id']);
|
||||
return jok($this->service->update($id, $params), '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function option(): mixed
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function detail(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function delete(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
}
|
||||
111
app/Http/Controllers/Api/OssConfigController.php
Normal file
111
app/Http/Controllers/Api/OssConfigController.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\OssConfigService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* OSS 存储配置控制器:驱动选项 / 启用切换 / 配置 CRUD
|
||||
*/
|
||||
class OssConfigController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = OssConfigService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置列表(卡片,无密钥明文)
|
||||
* @Method GET
|
||||
*/
|
||||
public function list(): JsonResponse
|
||||
{
|
||||
return jok($this->service->listConfigs(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function option(): mixed
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Method NO
|
||||
*/
|
||||
public function detail(): JsonResponse
|
||||
{
|
||||
return jerr('不支持');
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增配置(覆盖基类,走加密逻辑)
|
||||
* @Method POST
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jok($this->service->createConfig(request()->post()), '创建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
* @Method POST
|
||||
*/
|
||||
public function update(): JsonResponse
|
||||
{
|
||||
$params = request()->post();
|
||||
$id = (int) ($params['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
unset($params['id']);
|
||||
return jok($this->service->updateConfig($id, $params), '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配置
|
||||
* @Method POST
|
||||
*/
|
||||
public function delete(): JsonResponse
|
||||
{
|
||||
$ids = request()->post('ids');
|
||||
if (empty($ids)) {
|
||||
return jerr('参数错误');
|
||||
}
|
||||
if (!is_array($ids)) {
|
||||
$ids = [$ids];
|
||||
}
|
||||
return jok($this->service->deleteConfigs($ids), '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 驱动枚举选项
|
||||
* @Method GET
|
||||
*/
|
||||
public function driverOptions(): JsonResponse
|
||||
{
|
||||
return jok($this->service->driverOptions(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时选项(卡片 + 当前启用)
|
||||
* @Method GET
|
||||
*/
|
||||
public function runtimeOptions(): JsonResponse
|
||||
{
|
||||
return jok($this->service->getRuntimeOptions(), '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用指定配置
|
||||
* @Method POST
|
||||
*/
|
||||
public function saveRuntime(): JsonResponse
|
||||
{
|
||||
return jok($this->service->saveRuntime(request()->post()), '保存成功');
|
||||
}
|
||||
}
|
||||
156
app/Http/Middleware/ApiOpLogMiddleware.php
Normal file
156
app/Http/Middleware/ApiOpLogMiddleware.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\ApiEndpointModel;
|
||||
use App\Models\ApiOpLogModel;
|
||||
use App\Service\common\UserAgentService;
|
||||
use Closure;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* API 操作日志中间件:响应结束后按接口注册表决定是否落库
|
||||
*/
|
||||
class ApiOpLogMiddleware
|
||||
{
|
||||
/**
|
||||
* 透传请求;真正写日志在 terminate,避免拖慢接口响应
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应结束后写操作日志
|
||||
* 匹配:去掉 /api/ 前缀的 url,且 method=0(ANY) 或等于请求方法,is_log=1、status=1
|
||||
*/
|
||||
public function terminate(Request $request, Response $response): void
|
||||
{
|
||||
try {
|
||||
$path = $this->normalizePath($request->path());
|
||||
if ($path === '') {
|
||||
return;
|
||||
}
|
||||
$noInsert = config('nl.log.no_insert', []);
|
||||
if (is_array($noInsert)) {
|
||||
$full = 'api/' . $path;
|
||||
if (in_array($path, $noInsert, true)
|
||||
|| in_array($full, $noInsert, true)
|
||||
|| in_array('/' . $full, $noInsert, true)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$methodCode = $this->mapMethod($request->method());
|
||||
$endpoint = ApiEndpointModel::where('url', $path)
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 1)
|
||||
->where('is_log', 1)
|
||||
->where(function ($q) use ($methodCode) {
|
||||
$q->where('method', 0)->orWhere('method', $methodCode);
|
||||
})
|
||||
->orderByDesc('method')
|
||||
->first();
|
||||
if (!$endpoint) {
|
||||
return;
|
||||
}
|
||||
$userId = $this->resolveUserId($request);
|
||||
$body = $response->getContent();
|
||||
$result = json_decode((string) $body, true);
|
||||
if (!is_array($result)) {
|
||||
$result = ['raw' => mb_substr((string) $body, 0, 2000)];
|
||||
}
|
||||
$code = $result['code'] ?? null;
|
||||
$type = ((int) $code === 0) ? 0 : 1;
|
||||
$ua = UserAgentService::getInstance();
|
||||
ApiOpLogModel::insert([
|
||||
'user_id' => $userId,
|
||||
'url' => $path,
|
||||
'method' => $methodCode,
|
||||
'controller' => (string) ($endpoint->controller ?? ''),
|
||||
'ip' => (string) get_ip(),
|
||||
'param' => json_encode($this->collectParams($request), JSON_UNESCAPED_UNICODE),
|
||||
'result' => json_encode($result, JSON_UNESCAPED_UNICODE),
|
||||
'type' => $type,
|
||||
'result_code' => (string) ($code ?? ''),
|
||||
'platform_type' => 0,
|
||||
'belong_id' => 0,
|
||||
'user_type' => 0,
|
||||
'equipment' => $ua->parseEquipment(),
|
||||
'browser' => $ua->parseBrowser(),
|
||||
'created_at' => time(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// 日志失败不影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径:去掉 api/ 前缀
|
||||
*/
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
$path = trim($path, '/');
|
||||
if (str_starts_with($path, 'api/')) {
|
||||
$path = substr($path, 4);
|
||||
}
|
||||
return trim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 方法映射:GET=1 POST=2 其他=0
|
||||
*/
|
||||
private function mapMethod(string $method): int
|
||||
{
|
||||
return match (strtoupper($method)) {
|
||||
'GET' => 1,
|
||||
'POST' => 2,
|
||||
default => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Bearer JWT 解析 user_id;失败返回 0(不抛鉴权异常)
|
||||
*/
|
||||
private function resolveUserId(Request $request): int
|
||||
{
|
||||
try {
|
||||
$token = $request->bearerToken();
|
||||
if (empty($token)) {
|
||||
return 0;
|
||||
}
|
||||
$secret = (string) config('nl.jwt.secret', '');
|
||||
if (strlen($secret) < 32) {
|
||||
$secret = hash('sha256', $secret !== '' ? $secret : 'nl_admin_jwt_fallback_secret');
|
||||
}
|
||||
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
|
||||
return (int) ($decoded->data->id ?? 0);
|
||||
} catch (\Throwable $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集请求参数并脱敏密码类字段
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function collectParams(Request $request): array
|
||||
{
|
||||
$all = array_merge($request->query(), $request->request->all());
|
||||
$maskKeys = [
|
||||
'password', 'old_password', 'new_password', 'confirm_password',
|
||||
'access_key', 'secret_key', 'api_key', 'token',
|
||||
];
|
||||
foreach ($maskKeys as $k) {
|
||||
if (array_key_exists($k, $all) && $all[$k] !== '' && $all[$k] !== null) {
|
||||
$all[$k] = '******';
|
||||
}
|
||||
}
|
||||
return $all;
|
||||
}
|
||||
}
|
||||
15
app/Models/ApiEndpointModel.php
Normal file
15
app/Models/ApiEndpointModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* 接口注册表模型:维护 URL / 操作名 / 是否记入操作日志
|
||||
*/
|
||||
class ApiEndpointModel extends BaseModel
|
||||
{
|
||||
protected $table = 'api_endpoint';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
15
app/Models/LoginLogModel.php
Normal file
15
app/Models/LoginLogModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* 登录日志模型(仅 created_at,无软删)
|
||||
*/
|
||||
class LoginLogModel extends BaseModel
|
||||
{
|
||||
protected $table = 'login_log';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
15
app/Models/oss/OssConfigModel.php
Normal file
15
app/Models/oss/OssConfigModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\oss;
|
||||
|
||||
use App\BaseApp\BaseModel;
|
||||
|
||||
/**
|
||||
* OSS 存储配置表模型(仅表名与字段约定,查询逻辑放 Service)
|
||||
*/
|
||||
class OssConfigModel extends BaseModel
|
||||
{
|
||||
protected $table = 'oss_config';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -148,7 +148,7 @@ class AdminService extends BaseService
|
||||
}
|
||||
$result = $this->model::where('id', $this->userId)->update([
|
||||
'password' => password_hash($newPassword, PASSWORD_DEFAULT),
|
||||
'last_reset_password_at' => get_time()
|
||||
'updated_at' => get_time(),
|
||||
]);
|
||||
if (!$result) {
|
||||
return $this->utils->errorThrow('重置密码失败');
|
||||
@@ -178,7 +178,7 @@ class AdminService extends BaseService
|
||||
}
|
||||
$result = $this->model::where('id', $id)->update([
|
||||
'password' => password_hash($newPassword, PASSWORD_DEFAULT),
|
||||
'last_reset_password_at' => get_time()
|
||||
'updated_at' => get_time(),
|
||||
]);
|
||||
if (!$result) {
|
||||
return $this->utils->errorThrow('重置密码失败');
|
||||
@@ -187,7 +187,7 @@ class AdminService extends BaseService
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* 获取用户信息(本人完整字段,供个人中心回填,不做手机号脱敏)
|
||||
* @return mixed
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -196,12 +196,121 @@ class AdminService extends BaseService
|
||||
$this->with = [
|
||||
'role'
|
||||
];
|
||||
$result = $this->getDetail($this->userId);
|
||||
$result = $this->getDetail($this->userId);
|
||||
$result['created_day_at'] = format_time(strtotime($result['created_at']), 'Y-m-d');
|
||||
$result['phone'] = desensitization($result['phone']);
|
||||
// 兼容前端 Profile 组件字段
|
||||
$result['realName'] = $result['nick_name'] ?? '';
|
||||
$result['username'] = $result['phone'] ?? '';
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人中心更新资料(昵称/头像/邮箱/备注/手机)
|
||||
* 为什么单独接口:不走管理员 CRUD,避免越权改角色等字段
|
||||
*/
|
||||
public function updateProfile(array $params): mixed
|
||||
{
|
||||
$allow = ['nick_name', 'avatar', 'email', 'desc', 'phone'];
|
||||
$data = [];
|
||||
foreach ($allow as $field) {
|
||||
if (array_key_exists($field, $params)) {
|
||||
$data[$field] = is_string($params[$field]) ? trim($params[$field]) : $params[$field];
|
||||
}
|
||||
}
|
||||
if (isset($data['nick_name']) && $data['nick_name'] === '') {
|
||||
$this->utils->errorThrow('昵称不能为空');
|
||||
}
|
||||
if (isset($data['phone']) && $data['phone'] !== '') {
|
||||
$exists = $this->model::where('phone', $data['phone'])
|
||||
->where('id', '<>', $this->userId)
|
||||
->where('deleted_at', 0)
|
||||
->exists();
|
||||
if ($exists) {
|
||||
$this->utils->errorThrow('手机号已被占用');
|
||||
}
|
||||
}
|
||||
if (isset($data['email']) && $data['email'] !== '') {
|
||||
$exists = $this->model::where('email', $data['email'])
|
||||
->where('id', '<>', $this->userId)
|
||||
->where('deleted_at', 0)
|
||||
->exists();
|
||||
if ($exists) {
|
||||
$this->utils->errorThrow('邮箱已被占用');
|
||||
}
|
||||
}
|
||||
if (empty($data)) {
|
||||
return $this->myInfo();
|
||||
}
|
||||
$data['updated_at'] = get_time();
|
||||
$this->model::where('id', $this->userId)->update($data);
|
||||
return $this->myInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户登录日志分页
|
||||
*/
|
||||
public function loginLogList(): array
|
||||
{
|
||||
$page = max(1, (int) request()->get('page', 1));
|
||||
$size = max(1, min(100, (int) request()->get('pageSize', request()->get('size', 10))));
|
||||
$query = \App\Models\LoginLogModel::where('user_id', $this->userId)->orderByDesc('id');
|
||||
$total = (clone $query)->count();
|
||||
$items = $query->forPage($page, $size)->get()->toArray();
|
||||
return [
|
||||
'page' => $page,
|
||||
'size' => $size,
|
||||
'page_count' => (int) ceil($total / max($size, 1)),
|
||||
'total' => $total,
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户操作日志分页(附带接口操作名)
|
||||
*/
|
||||
public function opLogList(): array
|
||||
{
|
||||
$page = max(1, (int) request()->get('page', 1));
|
||||
$size = max(1, min(100, (int) request()->get('pageSize', request()->get('size', 10))));
|
||||
$query = \App\Models\ApiOpLogModel::where('user_id', $this->userId)->orderByDesc('id');
|
||||
$total = (clone $query)->count();
|
||||
$items = $query->forPage($page, $size)->get()->toArray();
|
||||
$urls = array_values(array_unique(array_column($items, 'url')));
|
||||
$endpointMap = [];
|
||||
if (!empty($urls)) {
|
||||
$endpointMap = \App\Models\ApiEndpointModel::whereIn('url', $urls)
|
||||
->where('deleted_at', 0)
|
||||
->get(['url', 'name', 'description'])
|
||||
->keyBy('url')
|
||||
->toArray();
|
||||
}
|
||||
foreach ($items as &$item) {
|
||||
$ep = $endpointMap[$item['url'] ?? ''] ?? null;
|
||||
$item['name'] = $ep['name'] ?? '';
|
||||
$item['description'] = $ep['description'] ?? '';
|
||||
$item['method_text'] = match ((int) ($item['method'] ?? 0)) {
|
||||
1 => 'GET',
|
||||
2 => 'POST',
|
||||
default => 'ANY',
|
||||
};
|
||||
$item['type_text'] = ((int) ($item['type'] ?? 0) === 0) ? '成功' : '失败';
|
||||
if (is_string($item['param'] ?? null)) {
|
||||
$item['param'] = json_decode($item['param'], true) ?: [];
|
||||
}
|
||||
if (is_string($item['result'] ?? null)) {
|
||||
$item['result'] = json_decode($item['result'], true) ?: [];
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
return [
|
||||
'page' => $page,
|
||||
'size' => $size,
|
||||
'page_count' => (int) ceil($total / max($size, 1)),
|
||||
'total' => $total,
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单列表
|
||||
* @return array
|
||||
|
||||
78
app/Service/ApiEndpointService.php
Normal file
78
app/Service/ApiEndpointService.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use App\Models\ApiEndpointModel;
|
||||
|
||||
/**
|
||||
* 接口注册表 Service:分页查询 + 更新是否记日志等元信息
|
||||
*/
|
||||
class ApiEndpointService extends BaseService
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->model = ApiEndpointModel::class;
|
||||
$this->selectField = [
|
||||
'id', 'url', 'method', 'name', 'description', 'controller',
|
||||
'is_log', 'status', 'created_at', 'updated_at',
|
||||
];
|
||||
$this->queryField = [
|
||||
'url' => 'like',
|
||||
'name' => 'like',
|
||||
'is_log' => '=',
|
||||
'method' => '=',
|
||||
'status' => '=',
|
||||
];
|
||||
$this->orderBy = [
|
||||
'name' => 'id',
|
||||
'sort' => 'desc',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表;支持按 url/name/is_log/method 筛选
|
||||
*/
|
||||
public function list(): array
|
||||
{
|
||||
return $this->getPageList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新接口元信息:仅允许改 name/description/is_log/status
|
||||
* 为什么不开放改 url/method:路由由代码决定,随意改会导致日志匹配错乱
|
||||
*
|
||||
* @param int $id 接口 ID
|
||||
* @param array $params 可更新字段
|
||||
*/
|
||||
public function update(int $id, array $params): mixed
|
||||
{
|
||||
$info = ApiEndpointModel::where('id', $id)->where('deleted_at', 0)->first();
|
||||
if (!$info) {
|
||||
$this->utils->notFound('接口不存在');
|
||||
}
|
||||
$data = [];
|
||||
if (array_key_exists('name', $params)) {
|
||||
$name = trim((string) $params['name']);
|
||||
if ($name === '') {
|
||||
$this->utils->errorThrow('操作名不能为空');
|
||||
}
|
||||
$data['name'] = $name;
|
||||
}
|
||||
if (array_key_exists('description', $params)) {
|
||||
$data['description'] = trim((string) $params['description']);
|
||||
}
|
||||
if (array_key_exists('is_log', $params)) {
|
||||
$data['is_log'] = (int) $params['is_log'] ? 1 : 0;
|
||||
}
|
||||
if (array_key_exists('status', $params)) {
|
||||
$data['status'] = (int) $params['status'] ? 1 : 0;
|
||||
}
|
||||
if (empty($data)) {
|
||||
return true;
|
||||
}
|
||||
$data['updated_at'] = time();
|
||||
return ApiEndpointModel::where('id', $id)->where('deleted_at', 0)->update($data);
|
||||
}
|
||||
}
|
||||
@@ -4,50 +4,84 @@ namespace App\Service;
|
||||
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Models\AdminModel;
|
||||
use App\Models\LoginLogModel;
|
||||
use App\Service\common\JWTService;
|
||||
use App\Service\common\UserAgentService;
|
||||
use App\Service\common\UtilsService;
|
||||
use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* 登录 / 注册服务:成功与失败均写登录日志,成功时更新 last_login_time
|
||||
*/
|
||||
class LoginService extends BaseNotAuthService
|
||||
{
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param $phone
|
||||
* @param $password
|
||||
* @return array
|
||||
* 账号密码登录
|
||||
* 失败时先写日志再抛错,保证审计完整;成功始终更新 last_login_time
|
||||
*
|
||||
* @param string $phone 手机号/账号
|
||||
* @param string $password 明文密码
|
||||
* @return array 用户信息 + token
|
||||
* @throws Exception
|
||||
*/
|
||||
public function login($phone, $password): array
|
||||
{
|
||||
$ua = UserAgentService::getInstance();
|
||||
$equipment = $ua->parseEquipment();
|
||||
$browser = $ua->parseBrowser();
|
||||
$userModel = AdminModel::with(['role:id,name,value'])->where('phone', $phone)->first();
|
||||
if (empty($userModel)) {
|
||||
$this->writeLoginLog(0, (string) $phone, '', 1, '用户或密码错误', $equipment, $browser);
|
||||
UtilsService::getInstance()->errorThrow('用户或密码错误!');
|
||||
// return $this->register($phone, $password);
|
||||
}
|
||||
|
||||
if (!password_verify($password, $userModel->password)) {
|
||||
UtilsService::getInstance()->errorThrow('用户或密码错误2!');
|
||||
if (!password_verify($password, $userModel->password)) {
|
||||
$this->writeLoginLog(
|
||||
(int) $userModel->id,
|
||||
(string) $phone,
|
||||
(string) $userModel->nick_name,
|
||||
1,
|
||||
'用户或密码错误',
|
||||
$equipment,
|
||||
$browser
|
||||
);
|
||||
UtilsService::getInstance()->errorThrow('用户或密码错误!');
|
||||
}
|
||||
|
||||
$updateData = [
|
||||
'last_login_time' => get_time(),
|
||||
'updated_at' => get_time(),
|
||||
];
|
||||
if ($userModel->ip !== get_ip()) {
|
||||
$ipTable = json_decode($userModel->ip_table, true);
|
||||
if (!in_array(get_ip(), $ipTable?? [])) {
|
||||
if (!in_array(get_ip(), $ipTable ?? [])) {
|
||||
$ipTable[] = get_ip();
|
||||
}
|
||||
$update = AdminModel::where('id', $userModel->id)->update([
|
||||
'ip' => get_ip(),
|
||||
'ip_table' => json_encode($ipTable),
|
||||
'updated_at' => get_time()
|
||||
]);
|
||||
|
||||
if (!$update) {
|
||||
UtilsService::getInstance()->errorThrow('更新失败!');
|
||||
}
|
||||
|
||||
$userModel = AdminModel::where('id', $userModel->id)->first();
|
||||
$updateData['ip'] = get_ip();
|
||||
$updateData['ip_table'] = json_encode($ipTable);
|
||||
}
|
||||
$update = AdminModel::where('id', $userModel->id)->update($updateData);
|
||||
if (!$update) {
|
||||
$this->writeLoginLog(
|
||||
(int) $userModel->id,
|
||||
(string) $phone,
|
||||
(string) $userModel->nick_name,
|
||||
1,
|
||||
'更新登录信息失败',
|
||||
$equipment,
|
||||
$browser
|
||||
);
|
||||
UtilsService::getInstance()->errorThrow('更新失败!');
|
||||
}
|
||||
$userModel = AdminModel::with(['role:id,name,value'])->where('id', $userModel->id)->first();
|
||||
$this->writeLoginLog(
|
||||
(int) $userModel->id,
|
||||
(string) $userModel->phone,
|
||||
(string) $userModel->nick_name,
|
||||
0,
|
||||
'登录成功',
|
||||
$equipment,
|
||||
$browser
|
||||
);
|
||||
$result = [
|
||||
'id' => $userModel->id,
|
||||
'phone' => $userModel->phone,
|
||||
@@ -66,11 +100,12 @@ class LoginService extends BaseNotAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册
|
||||
* @param $phone
|
||||
* @param $password
|
||||
* @param $email
|
||||
* @param $code
|
||||
* 注册管理员账号
|
||||
*
|
||||
* @param string $phone 手机号
|
||||
* @param string $password 密码
|
||||
* @param string $email 邮箱
|
||||
* @param mixed $code 验证码(预留)
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -80,31 +115,27 @@ class LoginService extends BaseNotAuthService
|
||||
if ($userModel) {
|
||||
UtilsService::getInstance()->errorThrow('账号已被占用!');
|
||||
}
|
||||
|
||||
$createModel = AdminModel::create([
|
||||
'phone' => $phone,
|
||||
'email' => $email,
|
||||
'password' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'nick_name' => '新用户'. Str::random(),
|
||||
'nick_name' => '新用户' . Str::random(),
|
||||
'avatar' => 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
'role_id' => 2,
|
||||
'ip' => get_ip(),
|
||||
'ip_table' => json_encode([get_ip()]),
|
||||
'created_at' => get_time()
|
||||
'created_at' => get_time(),
|
||||
]);
|
||||
|
||||
if (!$createModel) {
|
||||
UtilsService::getInstance()->errorThrow('注册失败!');
|
||||
}
|
||||
|
||||
$userInfo = AdminModel::with(['role:id,name,value'])->where('id', $createModel->id)->first();
|
||||
|
||||
$result = [
|
||||
'id' => $userInfo->id,
|
||||
'phone' => $userInfo->phone,
|
||||
'nick_name' => $userInfo->nick_name,
|
||||
'avatar' => $userInfo->avatar,
|
||||
'email' => $userInfo->email?? '',
|
||||
'email' => $userInfo->email ?? '',
|
||||
'role_name' => $userInfo->role->name,
|
||||
'role_value' => $userInfo->role->value,
|
||||
'ip' => $userInfo->ip,
|
||||
@@ -112,7 +143,43 @@ class LoginService extends BaseNotAuthService
|
||||
];
|
||||
$token = JWTService::getInstance()->generateToken($result);
|
||||
$result['token'] = $token;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入登录日志(失败也尽量落库,内部吞异常避免掩盖业务错误)
|
||||
*
|
||||
* @param int $userId 管理员 ID,失败未知用户可为 0
|
||||
* @param string $phone 登录账号
|
||||
* @param string $nickName 昵称快照
|
||||
* @param int $status 0成功 1失败
|
||||
* @param string $message 结果说明
|
||||
* @param string $equipment 操作系统
|
||||
* @param string $browser 浏览器
|
||||
*/
|
||||
private function writeLoginLog(
|
||||
int $userId,
|
||||
string $phone,
|
||||
string $nickName,
|
||||
int $status,
|
||||
string $message,
|
||||
string $equipment,
|
||||
string $browser
|
||||
): void {
|
||||
try {
|
||||
LoginLogModel::insert([
|
||||
'user_id' => $userId,
|
||||
'phone' => $phone,
|
||||
'nick_name' => $nickName,
|
||||
'ip' => (string) get_ip(),
|
||||
'equipment' => $equipment,
|
||||
'browser' => $browser,
|
||||
'status' => $status,
|
||||
'message' => $message,
|
||||
'created_at' => time(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// 日志失败不影响登录主流程
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
279
app/Service/OssConfigService.php
Normal file
279
app/Service/OssConfigService.php
Normal file
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use App\Enum\OssDriverEnum;
|
||||
use App\Models\oss\OssConfigModel;
|
||||
use App\Service\common\FieldEncryptService;
|
||||
|
||||
/**
|
||||
* OSS 配置管理:驱动选项、启用切换、配置 CRUD(密钥入库加密)
|
||||
*/
|
||||
class OssConfigService extends BaseService
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->model = OssConfigModel::class;
|
||||
$this->selectField = [
|
||||
'id', 'driver', 'name', 'endpoint', 'region', 'bucket', 'domain',
|
||||
'path_prefix', 'is_active', 'status', 'remark', 'sort', 'created_at', 'updated_at',
|
||||
];
|
||||
$this->queryField = [
|
||||
'driver' => '=',
|
||||
'name' => 'like',
|
||||
'status' => '=',
|
||||
'is_active' => '=',
|
||||
];
|
||||
$this->orderBy = [
|
||||
'name' => 'sort',
|
||||
'sort' => 'desc',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 驱动枚举选项(前端表单 / 卡片文案)
|
||||
*/
|
||||
public function driverOptions(): array
|
||||
{
|
||||
return OssDriverEnum::options();
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时页数据:配置卡片 + 当前启用 ID(不回显密钥明文)
|
||||
*/
|
||||
public function getRuntimeOptions(): array
|
||||
{
|
||||
$items = $this->formatConfigRows(
|
||||
OssConfigModel::where('deleted_at', 0)
|
||||
->orderByDesc('sort')
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->toArray()
|
||||
);
|
||||
$activeId = 0;
|
||||
foreach ($items as $item) {
|
||||
if ((int) ($item['is_active'] ?? 0) === 1) {
|
||||
$activeId = (int) $item['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($activeId <= 0) {
|
||||
$activeId = (int) SystemConfigService::getInstance()->getValue('oss_active_config_id', 0);
|
||||
}
|
||||
return [
|
||||
'drivers' => OssDriverEnum::options(),
|
||||
'items' => $items,
|
||||
'active_id' => $activeId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换当前启用的 OSS 配置
|
||||
* 为什么双写 is_active + system_config:上传链路优先读 kv,列表页靠 is_active 展示徽章
|
||||
*/
|
||||
public function saveRuntime(array $params): array
|
||||
{
|
||||
$id = (int) ($params['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
$this->utils->errorThrow('请选择要启用的配置');
|
||||
}
|
||||
$info = OssConfigModel::where('id', $id)->where('deleted_at', 0)->first();
|
||||
if (!$info) {
|
||||
$this->utils->notFound('配置不存在');
|
||||
}
|
||||
if ((int) $info->status !== 1) {
|
||||
$this->utils->errorThrow('该配置已禁用,无法启用');
|
||||
}
|
||||
$now = time();
|
||||
OssConfigModel::where('deleted_at', 0)->update(['is_active' => 0, 'updated_at' => $now]);
|
||||
OssConfigModel::where('id', $id)->update(['is_active' => 1, 'updated_at' => $now]);
|
||||
SystemConfigService::getInstance()->setValue('oss_active_config_id', (string) $id, '当前启用的 OSS 配置 ID');
|
||||
return ['id' => $id];
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置列表(卡片,无密钥明文)
|
||||
*/
|
||||
public function listConfigs(): array
|
||||
{
|
||||
$items = $this->formatConfigRows(
|
||||
OssConfigModel::where('deleted_at', 0)
|
||||
->orderByDesc('sort')
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->toArray()
|
||||
);
|
||||
return [
|
||||
'page' => 1,
|
||||
'size' => count($items),
|
||||
'page_count' => 1,
|
||||
'total' => count($items),
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增 OSS 配置;云驱动要求填写密钥
|
||||
*/
|
||||
public function createConfig(array $params): mixed
|
||||
{
|
||||
$driver = trim((string) ($params['driver'] ?? ''));
|
||||
$name = trim((string) ($params['name'] ?? ''));
|
||||
if (!OssDriverEnum::isValid($driver)) {
|
||||
$this->utils->errorThrow('不支持的存储驱动');
|
||||
}
|
||||
if ($name === '') {
|
||||
$this->utils->errorThrow('请填写配置别名');
|
||||
}
|
||||
$enum = OssDriverEnum::from($driver);
|
||||
$accessKey = trim((string) ($params['access_key'] ?? ''));
|
||||
$secretKey = trim((string) ($params['secret_key'] ?? ''));
|
||||
if ($enum->needsSecret() && ($accessKey === '' || $secretKey === '')) {
|
||||
$this->utils->errorThrow('该驱动需要填写 AccessKey 与 SecretKey');
|
||||
}
|
||||
$encrypt = FieldEncryptService::getInstance();
|
||||
$isActive = (int) ($params['is_active'] ?? 0);
|
||||
$now = time();
|
||||
if ($isActive === 1) {
|
||||
OssConfigModel::where('deleted_at', 0)->update(['is_active' => 0, 'updated_at' => $now]);
|
||||
}
|
||||
$id = OssConfigModel::insertGetId([
|
||||
'driver' => $driver,
|
||||
'name' => $name,
|
||||
'access_key' => $accessKey !== '' ? $encrypt->encryptForStorage($accessKey) : '',
|
||||
'secret_key' => $secretKey !== '' ? $encrypt->encryptForStorage($secretKey) : '',
|
||||
'endpoint' => trim((string) ($params['endpoint'] ?? '')),
|
||||
'region' => trim((string) ($params['region'] ?? '')),
|
||||
'bucket' => trim((string) ($params['bucket'] ?? '')),
|
||||
'domain' => trim((string) ($params['domain'] ?? '')),
|
||||
'path_prefix' => trim((string) ($params['path_prefix'] ?? '')),
|
||||
'extra_json' => null,
|
||||
'is_active' => $isActive,
|
||||
'status' => (int) ($params['status'] ?? 1),
|
||||
'remark' => trim((string) ($params['remark'] ?? '')),
|
||||
'sort' => (int) ($params['sort'] ?? 0),
|
||||
'created_at' => $now,
|
||||
'updated_at' => 0,
|
||||
'deleted_at' => 0,
|
||||
]);
|
||||
if ($isActive === 1) {
|
||||
SystemConfigService::getInstance()->setValue('oss_active_config_id', (string) $id, '当前启用的 OSS 配置 ID');
|
||||
}
|
||||
return ['id' => $id];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置;access_key/secret_key 空串表示不改密钥
|
||||
*/
|
||||
public function updateConfig(int $id, array $params): mixed
|
||||
{
|
||||
$info = OssConfigModel::where('id', $id)->where('deleted_at', 0)->first();
|
||||
if (!$info) {
|
||||
$this->utils->notFound('配置不存在');
|
||||
}
|
||||
$data = [];
|
||||
if (array_key_exists('driver', $params)) {
|
||||
$driver = trim((string) $params['driver']);
|
||||
if (!OssDriverEnum::isValid($driver)) {
|
||||
$this->utils->errorThrow('不支持的存储驱动');
|
||||
}
|
||||
$data['driver'] = $driver;
|
||||
}
|
||||
if (array_key_exists('name', $params)) {
|
||||
$name = trim((string) $params['name']);
|
||||
if ($name === '') {
|
||||
$this->utils->errorThrow('配置别名不能为空');
|
||||
}
|
||||
$data['name'] = $name;
|
||||
}
|
||||
foreach (['endpoint', 'region', 'bucket', 'domain', 'path_prefix', 'remark'] as $field) {
|
||||
if (array_key_exists($field, $params)) {
|
||||
$data[$field] = trim((string) $params[$field]);
|
||||
}
|
||||
}
|
||||
if (array_key_exists('status', $params)) {
|
||||
$data['status'] = (int) $params['status'];
|
||||
}
|
||||
if (array_key_exists('sort', $params)) {
|
||||
$data['sort'] = (int) $params['sort'];
|
||||
}
|
||||
$encrypt = FieldEncryptService::getInstance();
|
||||
$accessKey = trim((string) ($params['access_key'] ?? ''));
|
||||
if ($accessKey !== '') {
|
||||
$data['access_key'] = $encrypt->encryptForStorage($accessKey);
|
||||
}
|
||||
$secretKey = trim((string) ($params['secret_key'] ?? ''));
|
||||
if ($secretKey !== '') {
|
||||
$data['secret_key'] = $encrypt->encryptForStorage($secretKey);
|
||||
}
|
||||
$now = time();
|
||||
if (array_key_exists('is_active', $params) && (int) $params['is_active'] === 1) {
|
||||
OssConfigModel::where('deleted_at', 0)->where('id', '<>', $id)->update(['is_active' => 0, 'updated_at' => $now]);
|
||||
$data['is_active'] = 1;
|
||||
SystemConfigService::getInstance()->setValue('oss_active_config_id', (string) $id, '当前启用的 OSS 配置 ID');
|
||||
} elseif (array_key_exists('is_active', $params)) {
|
||||
$data['is_active'] = (int) $params['is_active'];
|
||||
}
|
||||
if (empty($data)) {
|
||||
return true;
|
||||
}
|
||||
$data['updated_at'] = $now;
|
||||
return OssConfigModel::where('id', $id)->where('deleted_at', 0)->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除;若删的是当前启用项则回落到本地配置,避免上传无可用驱动
|
||||
*/
|
||||
public function deleteConfigs(array $ids): mixed
|
||||
{
|
||||
$ids = array_values(array_filter(array_map('intval', $ids)));
|
||||
if (empty($ids)) {
|
||||
$this->utils->errorThrow('参数错误');
|
||||
}
|
||||
$activeId = (int) SystemConfigService::getInstance()->getValue('oss_active_config_id', 0);
|
||||
$deletingActive = ($activeId > 0 && in_array($activeId, $ids, true))
|
||||
|| OssConfigModel::whereIn('id', $ids)->where('is_active', 1)->where('deleted_at', 0)->exists();
|
||||
$fallbackId = 0;
|
||||
if ($deletingActive) {
|
||||
$local = OssConfigModel::where('driver', 'local')
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 1)
|
||||
->whereNotIn('id', $ids)
|
||||
->orderBy('id')
|
||||
->first();
|
||||
if (!$local) {
|
||||
$this->utils->errorThrow('不能删除当前启用的配置:请先切换,或保留至少一条本地存储');
|
||||
}
|
||||
$fallbackId = (int) $local->id;
|
||||
}
|
||||
$ok = OssConfigModel::whereIn('id', $ids)->where('deleted_at', 0)->update([
|
||||
'deleted_at' => time(),
|
||||
'updated_at' => time(),
|
||||
'is_active' => 0,
|
||||
]);
|
||||
if ($fallbackId > 0) {
|
||||
$this->saveRuntime(['id' => $fallbackId]);
|
||||
}
|
||||
return $ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行格式化:补驱动名、密钥是否已配置标记
|
||||
*/
|
||||
private function formatConfigRows(array $rows): array
|
||||
{
|
||||
foreach ($rows as &$row) {
|
||||
$driver = (string) ($row['driver'] ?? '');
|
||||
$enum = OssDriverEnum::tryFrom($driver);
|
||||
$row['driver_name'] = $enum?->label() ?? $driver;
|
||||
$row['driver_desc'] = $enum?->description() ?? '';
|
||||
$row['has_access_key'] = trim((string) ($row['access_key'] ?? '')) !== '';
|
||||
$row['has_secret_key'] = trim((string) ($row['secret_key'] ?? '')) !== '';
|
||||
unset($row['access_key'], $row['secret_key']);
|
||||
}
|
||||
unset($row);
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
@@ -3,87 +3,72 @@
|
||||
namespace App\Service\common;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use App\Models\ProjectModel;
|
||||
use App\Models\RoleModel;
|
||||
use App\Models\AdminModel;
|
||||
use App\Service\common\upload\LocalhostStorageService;
|
||||
use App\Service\common\upload\QiniuStorageService;
|
||||
use App\Service\common\oss\OssRuntimeConfigService;
|
||||
use App\Service\common\oss\OssStorageFactory;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use App\Service\FileService;
|
||||
use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
/**
|
||||
* 统一上传入口:按数据库启用的 OSS 配置,经工厂分发到对应存储实现
|
||||
*/
|
||||
class UploadService extends BaseService
|
||||
{
|
||||
private $uploadService;
|
||||
private OssStorageInterface $uploadService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->model = RoleModel::class;
|
||||
|
||||
switch (env('ECS')) {
|
||||
case 'aliyun':
|
||||
// $this->uploadService = AliyunStorageService::getInstance();
|
||||
break;
|
||||
case 'qcloud':
|
||||
// $this->uploadService = QcloudStorageService::getInstance();
|
||||
break;
|
||||
case 'qiniu':
|
||||
$this->uploadService = QiniuStorageService::getInstance();
|
||||
break;
|
||||
default:
|
||||
$this->uploadService = LocalhostStorageService::getInstance();
|
||||
break;
|
||||
try {
|
||||
$config = OssRuntimeConfigService::getInstance()->getActiveConfig();
|
||||
$this->uploadService = OssStorageFactory::getInstance()->make($config);
|
||||
} catch (\Throwable $e) {
|
||||
// 配置异常时回退本地,避免整站上传不可用
|
||||
$this->uploadService = OssStorageFactory::getInstance()->make([
|
||||
'driver' => 'local',
|
||||
'path_prefix' => 'uploads',
|
||||
'domain' => '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传OSS图片
|
||||
* @param $file
|
||||
* @return array|bool
|
||||
* @throws Exception
|
||||
* 上传图片到当前启用的存储
|
||||
*
|
||||
* @param mixed $file 上传文件对象
|
||||
*/
|
||||
public function uploadImage($file): array|bool
|
||||
{
|
||||
// 获取文件后缀
|
||||
$ext = $file->getClientOriginalExtension();
|
||||
$key = 'spa/image/'. date('Ymd') . '/' . 'cc_upload_'. Str::random(). uniqid() . '.' . $ext;
|
||||
$key = 'spa/image/' . date('Ymd') . '/' . 'cc_upload_' . Str::random() . uniqid() . '.' . $ext;
|
||||
$result = $this->uploadService->uploadVideo($file, $key);
|
||||
// $result = [
|
||||
// 'key' => $key,
|
||||
// 'url' => 'https://img2.baidu.com/it/u=1629222614,1358629025&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500&a='. rand(1111111, 9999999)
|
||||
// ];
|
||||
if (!$result) {
|
||||
$this->utils->errorThrow('图片上传失败');
|
||||
}
|
||||
FileService::getInstance()->create([
|
||||
'user_id' => $this->userId,
|
||||
'url' => $result['url'],
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传OSS视频
|
||||
* @param $file
|
||||
* @return array|bool
|
||||
* @throws Exception
|
||||
* 上传视频到当前启用的存储
|
||||
*
|
||||
* @param mixed $file 上传文件对象
|
||||
*/
|
||||
public function uploadVideo($file): array|bool
|
||||
{
|
||||
// 获取文件后缀
|
||||
$ext = $file->getClientOriginalExtension();
|
||||
$key = 'spa/video/'. date('Ymd') . '/' . 'cc_upload_'. Str::random(). uniqid() . '.' . $ext;
|
||||
$key = 'spa/video/' . date('Ymd') . '/' . 'cc_upload_' . Str::random() . uniqid() . '.' . $ext;
|
||||
$result = $this->uploadService->uploadVideo($file, $key);
|
||||
// $result = [
|
||||
// 'key' => $key,
|
||||
// 'url' => 'http://d-jy.nailaoyun.cn//storage/video//20250327/2cc1abf9bd69410ce7c69660daf54260.mp4'
|
||||
// ];
|
||||
if (!$result) {
|
||||
$this->utils->errorThrow('视频上传失败');
|
||||
}
|
||||
FileService::getInstance()->create([
|
||||
'user_id' => $this->userId,
|
||||
'url' => $result['url'],
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
57
app/Service/common/UserAgentService.php
Normal file
57
app/Service/common/UserAgentService.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common;
|
||||
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
|
||||
/**
|
||||
* User-Agent 解析:登录日志 / 操作日志共用
|
||||
*/
|
||||
class UserAgentService extends BaseNotAuthService
|
||||
{
|
||||
/**
|
||||
* 从 UA 推断操作系统名称
|
||||
* 为什么允许空参:中间件/登录处直接取当前请求头,避免到处传 UA
|
||||
*
|
||||
* @param string|null $ua 为空时取当前请求 User-Agent
|
||||
*/
|
||||
public function parseEquipment(?string $ua = null): string
|
||||
{
|
||||
$ua = $ua ?? (string) request()->header('User-Agent', '');
|
||||
if ($ua === '') {
|
||||
return '未知';
|
||||
}
|
||||
return match (true) {
|
||||
str_contains($ua, 'Windows NT 10') => 'Windows 10/11',
|
||||
str_contains($ua, 'Windows') => 'Windows',
|
||||
str_contains($ua, 'Macintosh') || str_contains($ua, 'Mac OS') => 'macOS',
|
||||
str_contains($ua, 'Android') => 'Android',
|
||||
str_contains($ua, 'iPhone') || str_contains($ua, 'iPad') => 'iOS',
|
||||
str_contains($ua, 'Linux') => 'Linux',
|
||||
default => '其他',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 UA 推断浏览器名称
|
||||
* Edge 需先于 Chrome 匹配,避免被 Chrome/ 抢先
|
||||
*
|
||||
* @param string|null $ua 为空时取当前请求 User-Agent
|
||||
*/
|
||||
public function parseBrowser(?string $ua = null): string
|
||||
{
|
||||
$ua = $ua ?? (string) request()->header('User-Agent', '');
|
||||
if ($ua === '') {
|
||||
return '未知';
|
||||
}
|
||||
return match (true) {
|
||||
str_contains($ua, 'Edg/') => 'Edge',
|
||||
str_contains($ua, 'OPR/') || str_contains($ua, 'Opera') => 'Opera',
|
||||
str_contains($ua, 'Chrome/') && !str_contains($ua, 'Edg/') => 'Chrome',
|
||||
str_contains($ua, 'Firefox/') => 'Firefox',
|
||||
str_contains($ua, 'Safari/') && !str_contains($ua, 'Chrome/') => 'Safari',
|
||||
str_contains($ua, 'MSIE') || str_contains($ua, 'Trident/') => 'IE',
|
||||
default => '其他',
|
||||
};
|
||||
}
|
||||
}
|
||||
97
app/Service/common/oss/OssRuntimeConfigService.php
Normal file
97
app/Service/common/oss/OssRuntimeConfigService.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\oss;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use App\Models\oss\OssConfigModel;
|
||||
use App\Service\common\FieldEncryptService;
|
||||
use App\Service\SystemConfigService;
|
||||
|
||||
/**
|
||||
* OSS 运行时配置:解析当前启用的存储配置并解密密钥,供上传工厂使用
|
||||
*/
|
||||
class OssRuntimeConfigService extends BaseService
|
||||
{
|
||||
public const CFG_ACTIVE_ID = 'oss_active_config_id';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// 上传链路可能在无登录上下文触发,不强制鉴权
|
||||
$this->isAuth = false;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前启用的明文配置
|
||||
* 为什么要解密:上传客户端需要明文 access/secret;库内仅存 AES 密文
|
||||
*
|
||||
* @return array{
|
||||
* id:int,driver:string,name:string,access_key:string,secret_key:string,
|
||||
* endpoint:string,region:string,bucket:string,domain:string,
|
||||
* path_prefix:string,extra_json:mixed
|
||||
* }
|
||||
*/
|
||||
public function getActiveConfig(): array
|
||||
{
|
||||
$sys = SystemConfigService::getInstance();
|
||||
$activeId = (int) $sys->getValue(self::CFG_ACTIVE_ID, '0');
|
||||
$row = null;
|
||||
if ($activeId > 0) {
|
||||
$row = OssConfigModel::where('id', $activeId)
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
}
|
||||
if (!$row) {
|
||||
// 回落 is_active 标记,避免仅写了表未写 system_config 时上传失败
|
||||
$row = OssConfigModel::where('is_active', 1)
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 1)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
if (!$row) {
|
||||
// 最终回落本地默认行或内存默认
|
||||
$row = OssConfigModel::where('driver', 'local')
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 1)
|
||||
->orderByDesc('is_active')
|
||||
->orderBy('id')
|
||||
->first();
|
||||
}
|
||||
if (!$row) {
|
||||
return [
|
||||
'id' => 0,
|
||||
'driver' => 'local',
|
||||
'name' => '本地存储',
|
||||
'access_key' => '',
|
||||
'secret_key' => '',
|
||||
'endpoint' => '',
|
||||
'region' => '',
|
||||
'bucket' => '',
|
||||
'domain' => '',
|
||||
'path_prefix' => 'uploads',
|
||||
'extra_json' => null,
|
||||
];
|
||||
}
|
||||
$enc = FieldEncryptService::getInstance();
|
||||
$extra = $row->extra_json;
|
||||
if (is_string($extra) && $extra !== '') {
|
||||
$decoded = json_decode($extra, true);
|
||||
$extra = is_array($decoded) ? $decoded : $extra;
|
||||
}
|
||||
return [
|
||||
'id' => (int) $row->id,
|
||||
'driver' => (string) $row->driver,
|
||||
'name' => (string) $row->name,
|
||||
'access_key' => $enc->decryptFromStorage((string) ($row->access_key ?? ''), true),
|
||||
'secret_key' => $enc->decryptFromStorage((string) ($row->secret_key ?? ''), true),
|
||||
'endpoint' => (string) ($row->endpoint ?? ''),
|
||||
'region' => (string) ($row->region ?? ''),
|
||||
'bucket' => (string) ($row->bucket ?? ''),
|
||||
'domain' => (string) ($row->domain ?? ''),
|
||||
'path_prefix' => (string) ($row->path_prefix ?? ''),
|
||||
'extra_json' => $extra,
|
||||
];
|
||||
}
|
||||
}
|
||||
47
app/Service/common/oss/OssStorageFactory.php
Normal file
47
app/Service/common/oss/OssStorageFactory.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\oss;
|
||||
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Enum\OssDriverEnum;
|
||||
use App\Service\common\upload\AliyunStorageService;
|
||||
use App\Service\common\upload\LocalhostStorageService;
|
||||
use App\Service\common\upload\QcloudStorageService;
|
||||
use App\Service\common\upload\QiniuStorageService;
|
||||
use App\Service\common\upload\S3CompatibleStorageService;
|
||||
use App\Service\common\UtilsService;
|
||||
|
||||
/**
|
||||
* OSS 存储工厂:按 OssDriverEnum 分发到对应实现(工厂 + 策略)
|
||||
* 覆盖:local / aliyun / qcloud / qiniu / huawei / aws / minio / baidu
|
||||
*/
|
||||
class OssStorageFactory extends BaseNotAuthService
|
||||
{
|
||||
/**
|
||||
* 根据明文运行时配置构建上传客户端
|
||||
* 为什么集中在工厂:UploadService 不关心具体 SDK/签名差异,只拿接口用
|
||||
*
|
||||
* @param array $config getActiveConfig() 返回值(含 driver)
|
||||
*/
|
||||
public function make(array $config): OssStorageInterface
|
||||
{
|
||||
$driverCode = strtolower(trim((string) ($config['driver'] ?? OssDriverEnum::Local->value)));
|
||||
$driver = OssDriverEnum::tryFrom($driverCode);
|
||||
if ($driver === null) {
|
||||
UtilsService::getInstance()->errorThrow(
|
||||
'不支持的存储驱动:' . $driverCode . ',请改用本地存储或完善配置'
|
||||
);
|
||||
}
|
||||
// 按驱动枚举创建策略实现;S3 兼容族共用一套 SigV4 客户端
|
||||
return match ($driver) {
|
||||
OssDriverEnum::Local => LocalhostStorageService::getInstance()->withConfig($config),
|
||||
OssDriverEnum::Aliyun => AliyunStorageService::getInstance()->withConfig($config),
|
||||
OssDriverEnum::Qcloud => QcloudStorageService::getInstance()->withConfig($config),
|
||||
OssDriverEnum::Qiniu => QiniuStorageService::getInstance()->withConfig($config),
|
||||
OssDriverEnum::Huawei,
|
||||
OssDriverEnum::Aws,
|
||||
OssDriverEnum::Minio,
|
||||
OssDriverEnum::Baidu => S3CompatibleStorageService::getInstance()->withConfig($config),
|
||||
};
|
||||
}
|
||||
}
|
||||
34
app/Service/common/oss/OssStorageInterface.php
Normal file
34
app/Service/common/oss/OssStorageInterface.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\oss;
|
||||
|
||||
/**
|
||||
* OSS 存储驱动统一契约:工厂按 driver 返回实现类,UploadService 只依赖本接口
|
||||
*/
|
||||
interface OssStorageInterface
|
||||
{
|
||||
/**
|
||||
* 注入本请求的运行时配置(密钥已解密)
|
||||
*
|
||||
* @param array $config OssRuntimeConfigService::getActiveConfig()
|
||||
*/
|
||||
public function withConfig(array $config): static;
|
||||
|
||||
/**
|
||||
* 上传图片对象
|
||||
*
|
||||
* @param mixed $filePath 本地路径或 UploadedFile
|
||||
* @param string $key 对象键
|
||||
* @return array{key:string,url:string}|false
|
||||
*/
|
||||
public function uploadImage($filePath, string $key): bool|array;
|
||||
|
||||
/**
|
||||
* 上传视频/通用文件对象
|
||||
*
|
||||
* @param mixed $filePath 本地路径或 UploadedFile
|
||||
* @param string $key 对象键
|
||||
* @return array{key:string,url:string}|false
|
||||
*/
|
||||
public function uploadVideo($filePath, string $key): bool|array;
|
||||
}
|
||||
76
app/Service/common/upload/AliyunStorageService.php
Normal file
76
app/Service/common/upload/AliyunStorageService.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\upload;
|
||||
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use App\Service\common\UtilsService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* 阿里云 OSS:REST PutObject(Authorization 签名)
|
||||
*/
|
||||
class AliyunStorageService extends BaseNotAuthService implements OssStorageInterface
|
||||
{
|
||||
protected array $config = [];
|
||||
|
||||
public function withConfig(array $config): static
|
||||
{
|
||||
$this->config = $config;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function uploadImage($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
public function uploadVideo($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 OSS V1 签名上传对象
|
||||
*/
|
||||
private function uploadFile($filePath, string $key): bool|array
|
||||
{
|
||||
$accessKey = (string) ($this->config['access_key'] ?? '');
|
||||
$secretKey = (string) ($this->config['secret_key'] ?? '');
|
||||
$bucket = (string) ($this->config['bucket'] ?? '');
|
||||
$endpoint = (string) ($this->config['endpoint'] ?? '');
|
||||
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
|
||||
if ($accessKey === '' || $secretKey === '' || $bucket === '' || $endpoint === '') {
|
||||
UtilsService::getInstance()->errorThrow('阿里云 OSS 配置不完整(需要 AccessKey/Secret/Bucket/Endpoint)');
|
||||
}
|
||||
$prefix = trim((string) ($this->config['path_prefix'] ?? ''), '/');
|
||||
if ($prefix !== '' && !str_starts_with($key, $prefix . '/')) {
|
||||
$key = $prefix . '/' . ltrim($key, '/');
|
||||
}
|
||||
$path = is_object($filePath) && method_exists($filePath, 'getRealPath')
|
||||
? $filePath->getRealPath()
|
||||
: (string) $filePath;
|
||||
$content = file_get_contents($path);
|
||||
$contentType = 'application/octet-stream';
|
||||
$date = gmdate('D, d M Y H:i:s \G\M\T');
|
||||
$resource = '/' . $bucket . '/' . $key;
|
||||
$stringToSign = "PUT\n\n{$contentType}\n{$date}\n{$resource}";
|
||||
$signature = base64_encode(hash_hmac('sha1', $stringToSign, $secretKey, true));
|
||||
$host = preg_replace('#^https?://#', '', rtrim($endpoint, '/'));
|
||||
// 支持传入 oss-cn-xxx.aliyuncs.com 或带 bucket 的域名
|
||||
if (!str_starts_with($host, $bucket . '.')) {
|
||||
$host = $bucket . '.' . $host;
|
||||
}
|
||||
$url = 'https://' . $host . '/' . $key;
|
||||
$response = Http::withHeaders([
|
||||
'Date' => $date,
|
||||
'Content-Type' => $contentType,
|
||||
'Authorization' => 'OSS ' . $accessKey . ':' . $signature,
|
||||
])->withBody($content, $contentType)->put($url);
|
||||
if (!$response->successful()) {
|
||||
UtilsService::getInstance()->errorThrow('阿里云上传失败:' . $response->body());
|
||||
}
|
||||
$publicUrl = $domain !== '' ? ($domain . '/' . $key) : $url;
|
||||
return ['key' => $key, 'url' => $publicUrl];
|
||||
}
|
||||
}
|
||||
@@ -2,105 +2,72 @@
|
||||
|
||||
namespace App\Service\common\upload;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class LocalhostStorageService extends BaseService
|
||||
/**
|
||||
* 本地磁盘上传;支持 domain / path_prefix 覆盖
|
||||
*/
|
||||
class LocalhostStorageService extends BaseNotAuthService implements OssStorageInterface
|
||||
{
|
||||
/**
|
||||
* @var mixed 单例实例,确保该类只有一个全局实例
|
||||
*/
|
||||
protected static mixed $_instance;
|
||||
protected array $config = [
|
||||
'domain' => '',
|
||||
'path_prefix' => '',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
/**
|
||||
* 注入运行时配置(链式)
|
||||
*/
|
||||
public function withConfig(array $config): static
|
||||
{
|
||||
parent::__construct();
|
||||
$this->config = array_merge($this->config, $config);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实例
|
||||
* @return null|static
|
||||
*/
|
||||
public static function getInstance(): null|static
|
||||
{
|
||||
$name = get_called_class();
|
||||
if (!isset(self::$_instance[$name])) {
|
||||
self::$_instance[$name] = new static();
|
||||
}
|
||||
|
||||
return self::$_instance[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到本地存储的文件名
|
||||
* @return array|bool
|
||||
*/
|
||||
public function uploadImage($filePath, $key): bool|array
|
||||
public function uploadImage($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传视频
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到本地存储的文件名
|
||||
* @return array|bool
|
||||
*/
|
||||
public function uploadVideo($filePath, $key): bool|array
|
||||
public function uploadVideo($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图片
|
||||
*
|
||||
* @param string $key 本地存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteImage($key): bool
|
||||
{
|
||||
return $this->deleteFile($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除视频
|
||||
*
|
||||
* @param string $key 本地存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteVideo($key): bool
|
||||
{
|
||||
return $this->deleteFile($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到本地存储的文件名
|
||||
* @return array|bool
|
||||
* 写入本地 storage;兼容 UploadedFile 与路径字符串
|
||||
*/
|
||||
private function uploadFile(string $filePath, string $key): bool|array
|
||||
private function uploadFile(mixed $filePath, string $key): bool|array
|
||||
{
|
||||
if (Storage::put($key, file_get_contents($filePath))) {
|
||||
return [
|
||||
'key' => $key,
|
||||
'url' => asset('/storage/' . $key)
|
||||
];
|
||||
$prefix = trim((string) ($this->config['path_prefix'] ?? ''), '/');
|
||||
if ($prefix !== '' && !str_starts_with($key, $prefix . '/')) {
|
||||
$key = $prefix . '/' . ltrim($key, '/');
|
||||
}
|
||||
return false;
|
||||
$path = is_object($filePath) && method_exists($filePath, 'getRealPath')
|
||||
? $filePath->getRealPath()
|
||||
: (string) $filePath;
|
||||
if (!Storage::put($key, file_get_contents($path))) {
|
||||
return false;
|
||||
}
|
||||
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
|
||||
$url = $domain !== '' ? ($domain . '/' . $key) : asset('/storage/' . $key);
|
||||
return [
|
||||
'key' => $key,
|
||||
'url' => $url,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param string $key 本地存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
private function deleteFile(string $key): bool
|
||||
{
|
||||
return Storage::delete($key);
|
||||
|
||||
81
app/Service/common/upload/QcloudStorageService.php
Normal file
81
app/Service/common/upload/QcloudStorageService.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\upload;
|
||||
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use App\Service\common\UtilsService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* 腾讯云 COS:简易 PUT(Authorization 签名 v5)
|
||||
*/
|
||||
class QcloudStorageService extends BaseNotAuthService implements OssStorageInterface
|
||||
{
|
||||
protected array $config = [];
|
||||
|
||||
public function withConfig(array $config): static
|
||||
{
|
||||
$this->config = $config;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function uploadImage($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
public function uploadVideo($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* COS 对象上传(Sign Algorithm=sha1)
|
||||
*/
|
||||
private function uploadFile($filePath, string $key): bool|array
|
||||
{
|
||||
$secretId = (string) ($this->config['access_key'] ?? '');
|
||||
$secretKey = (string) ($this->config['secret_key'] ?? '');
|
||||
$bucket = (string) ($this->config['bucket'] ?? '');
|
||||
$region = (string) ($this->config['region'] ?? '');
|
||||
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
|
||||
if ($secretId === '' || $secretKey === '' || $bucket === '' || $region === '') {
|
||||
UtilsService::getInstance()->errorThrow('腾讯云 COS 配置不完整(需要 SecretId/SecretKey/Bucket/Region)');
|
||||
}
|
||||
$prefix = trim((string) ($this->config['path_prefix'] ?? ''), '/');
|
||||
if ($prefix !== '' && !str_starts_with($key, $prefix . '/')) {
|
||||
$key = $prefix . '/' . ltrim($key, '/');
|
||||
}
|
||||
$path = is_object($filePath) && method_exists($filePath, 'getRealPath')
|
||||
? $filePath->getRealPath()
|
||||
: (string) $filePath;
|
||||
$content = file_get_contents($path);
|
||||
$host = $bucket . '.cos.' . $region . '.myqcloud.com';
|
||||
$urlPath = '/' . ltrim($key, '/');
|
||||
$now = time();
|
||||
$keyTime = $now . ';' . ($now + 600);
|
||||
$signKey = hash_hmac('sha1', $keyTime, $secretKey);
|
||||
$httpString = strtolower('put') . "\n" . $urlPath . "\n\nhost=" . strtolower($host) . "\n";
|
||||
$stringToSign = "sha1\n{$keyTime}\n" . sha1($httpString) . "\n";
|
||||
$signature = hash_hmac('sha1', $stringToSign, $signKey);
|
||||
$authorization = 'q-sign-algorithm=sha1'
|
||||
. '&q-ak=' . $secretId
|
||||
. '&q-sign-time=' . $keyTime
|
||||
. '&q-key-time=' . $keyTime
|
||||
. '&q-header-list=host'
|
||||
. '&q-url-param-list='
|
||||
. '&q-signature=' . $signature;
|
||||
$url = 'https://' . $host . $urlPath;
|
||||
$response = Http::withHeaders([
|
||||
'Host' => $host,
|
||||
'Authorization' => $authorization,
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
])->withBody($content, 'application/octet-stream')->put($url);
|
||||
if (!$response->successful()) {
|
||||
UtilsService::getInstance()->errorThrow('腾讯云上传失败:' . $response->body());
|
||||
}
|
||||
$publicUrl = $domain !== '' ? ($domain . $urlPath) : $url;
|
||||
return ['key' => $key, 'url' => $publicUrl];
|
||||
}
|
||||
}
|
||||
@@ -2,131 +2,95 @@
|
||||
|
||||
namespace App\Service\common\upload;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use Exception;
|
||||
use Qiniu\Auth;
|
||||
use Qiniu\Storage\BucketManager;
|
||||
use Qiniu\Storage\UploadManager;
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use App\Service\common\UtilsService;
|
||||
|
||||
class QiniuStorageService extends BaseService
|
||||
/**
|
||||
* 七牛云上传;优先使用官方 SDK,未安装时给出明确提示
|
||||
*/
|
||||
class QiniuStorageService extends BaseNotAuthService implements OssStorageInterface
|
||||
{
|
||||
/**
|
||||
* @var mixed 单例实例,确保该类只有一个全局实例
|
||||
*/
|
||||
protected static mixed $_instance;
|
||||
private $accessKey;
|
||||
private $secretKey;
|
||||
private $bucket;
|
||||
private $domain;
|
||||
protected array $config = [
|
||||
'access_key' => '',
|
||||
'secret_key' => '',
|
||||
'bucket' => '',
|
||||
'domain' => '',
|
||||
'path_prefix' => '',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
/**
|
||||
* 注入解密后的运行时配置
|
||||
*/
|
||||
public function withConfig(array $config): static
|
||||
{
|
||||
parent::__construct();
|
||||
$this->accessKey = config('nl.oss.qiniu.access_key');
|
||||
$this->secretKey = config('nl.oss.qiniu.secret_key');
|
||||
$this->bucket = config('nl.oss.qiniu.bucket');
|
||||
$this->domain = config('nl.oss.qiniu.domain');
|
||||
$this->config = array_merge($this->config, $config);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取实例
|
||||
* @return null|static
|
||||
*/
|
||||
public static function getInstance(): null|static
|
||||
{
|
||||
$name = get_called_class();
|
||||
if (!isset(self::$_instance[$name])) {
|
||||
self::$_instance[$name] = new static();
|
||||
}
|
||||
|
||||
return self::$_instance[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到七牛云的文件名
|
||||
* @return array|bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function uploadImage($filePath, $key): bool|array
|
||||
public function uploadImage($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传视频
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到七牛云的文件名
|
||||
* @return array|bool
|
||||
*/
|
||||
public function uploadVideo($filePath, $key): bool|array
|
||||
public function uploadVideo($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图片
|
||||
*
|
||||
* @param string $key 七牛云存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteImage($key): bool
|
||||
{
|
||||
return $this->deleteFile($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除视频
|
||||
*
|
||||
* @param string $key 七牛云存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteVideo($key): bool
|
||||
{
|
||||
return $this->deleteFile($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到七牛云的文件名
|
||||
* @return array|bool
|
||||
* @throws Exception
|
||||
* 上传到七牛;无 SDK 时抛业务异常引导安装或改本地
|
||||
*/
|
||||
private function uploadFile(string $filePath, string $key): bool|array
|
||||
private function uploadFile($filePath, string $key): bool|array
|
||||
{
|
||||
$auth = new Auth($this->accessKey, $this->secretKey);
|
||||
$token = $auth->uploadToken($this->bucket);
|
||||
$uploadMgr = new UploadManager();
|
||||
|
||||
list($ret, $err) = $uploadMgr->putFile($token, $key, $filePath);
|
||||
if (!class_exists(\Qiniu\Auth::class)) {
|
||||
UtilsService::getInstance()->errorThrow('未安装 qiniu/php-sdk,请改用本地存储或安装依赖');
|
||||
}
|
||||
$accessKey = (string) ($this->config['access_key'] ?? '');
|
||||
$secretKey = (string) ($this->config['secret_key'] ?? '');
|
||||
$bucket = (string) ($this->config['bucket'] ?? '');
|
||||
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
|
||||
if ($accessKey === '' || $secretKey === '' || $bucket === '') {
|
||||
UtilsService::getInstance()->errorThrow('七牛云配置不完整');
|
||||
}
|
||||
$prefix = trim((string) ($this->config['path_prefix'] ?? ''), '/');
|
||||
if ($prefix !== '' && !str_starts_with($key, $prefix . '/')) {
|
||||
$key = $prefix . '/' . ltrim($key, '/');
|
||||
}
|
||||
$path = is_object($filePath) && method_exists($filePath, 'getRealPath')
|
||||
? $filePath->getRealPath()
|
||||
: (string) $filePath;
|
||||
$auth = new \Qiniu\Auth($accessKey, $secretKey);
|
||||
$token = $auth->uploadToken($bucket);
|
||||
$uploadMgr = new \Qiniu\Storage\UploadManager();
|
||||
list($ret, $err) = $uploadMgr->putFile($token, $key, $path);
|
||||
if ($err !== null) {
|
||||
return false;
|
||||
} else {
|
||||
return [
|
||||
'key' => $ret['key'],
|
||||
'url' => $this->domain . '/' . $ret['key']
|
||||
];
|
||||
}
|
||||
return [
|
||||
'key' => $ret['key'],
|
||||
'url' => $domain . '/' . $ret['key'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param string $key 七牛云存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
private function deleteFile(string $key): bool
|
||||
{
|
||||
$auth = new Auth($this->accessKey, $this->secretKey);
|
||||
$bucketMgr = new BucketManager($auth);
|
||||
|
||||
$err = $bucketMgr->delete($this->bucket, $key);
|
||||
if (!class_exists(\Qiniu\Auth::class)) {
|
||||
return false;
|
||||
}
|
||||
$auth = new \Qiniu\Auth($this->config['access_key'], $this->config['secret_key']);
|
||||
$bucketMgr = new \Qiniu\Storage\BucketManager($auth);
|
||||
$err = $bucketMgr->delete($this->config['bucket'], $key);
|
||||
return $err === null;
|
||||
}
|
||||
}
|
||||
|
||||
90
app/Service/common/upload/S3CompatibleStorageService.php
Normal file
90
app/Service/common/upload/S3CompatibleStorageService.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\upload;
|
||||
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use App\Service\common\UtilsService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* S3 兼容上传(AWS / MinIO / 华为 OBS / 百度 BOS)
|
||||
* 使用 AWS Signature Version 4 的 PUT Object
|
||||
*/
|
||||
class S3CompatibleStorageService extends BaseNotAuthService implements OssStorageInterface
|
||||
{
|
||||
protected array $config = [];
|
||||
|
||||
public function withConfig(array $config): static
|
||||
{
|
||||
$this->config = $config;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function uploadImage($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
public function uploadVideo($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* SigV4 PUT;endpoint 必填(如 https://s3.amazonaws.com 或 MinIO 地址)
|
||||
*/
|
||||
private function uploadFile($filePath, string $key): bool|array
|
||||
{
|
||||
$accessKey = (string) ($this->config['access_key'] ?? '');
|
||||
$secretKey = (string) ($this->config['secret_key'] ?? '');
|
||||
$bucket = (string) ($this->config['bucket'] ?? '');
|
||||
$region = (string) ($this->config['region'] ?? 'us-east-1');
|
||||
$endpoint = rtrim((string) ($this->config['endpoint'] ?? ''), '/');
|
||||
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
|
||||
$driver = (string) ($this->config['driver'] ?? 'aws');
|
||||
if ($accessKey === '' || $secretKey === '' || $bucket === '' || $endpoint === '') {
|
||||
UtilsService::getInstance()->errorThrow(strtoupper($driver) . ' 配置不完整(需要 AccessKey/Secret/Bucket/Endpoint)');
|
||||
}
|
||||
$prefix = trim((string) ($this->config['path_prefix'] ?? ''), '/');
|
||||
if ($prefix !== '' && !str_starts_with($key, $prefix . '/')) {
|
||||
$key = $prefix . '/' . ltrim($key, '/');
|
||||
}
|
||||
$path = is_object($filePath) && method_exists($filePath, 'getRealPath')
|
||||
? $filePath->getRealPath()
|
||||
: (string) $filePath;
|
||||
$payload = file_get_contents($path);
|
||||
$host = parse_url($endpoint, PHP_URL_HOST) ?: preg_replace('#^https?://#', '', $endpoint);
|
||||
// path-style: endpoint/bucket/key
|
||||
$canonicalUri = '/' . rawurlencode($bucket) . '/' . str_replace('%2F', '/', rawurlencode($key));
|
||||
// 简化:多数 MinIO/OBS 接受 path-style
|
||||
$url = $endpoint . '/' . $bucket . '/' . $key;
|
||||
$amzDate = gmdate('Ymd\THis\Z');
|
||||
$dateStamp = gmdate('Ymd');
|
||||
$payloadHash = hash('sha256', $payload);
|
||||
$canonicalHeaders = "host:{$host}\nx-amz-content-sha256:{$payloadHash}\nx-amz-date:{$amzDate}\n";
|
||||
$signedHeaders = 'host;x-amz-content-sha256;x-amz-date';
|
||||
$canonicalRequest = "PUT\n{$canonicalUri}\n\n{$canonicalHeaders}\n{$signedHeaders}\n{$payloadHash}";
|
||||
$service = $driver === 'huawei' ? 's3' : 's3';
|
||||
$credentialScope = "{$dateStamp}/{$region}/{$service}/aws4_request";
|
||||
$stringToSign = "AWS4-HMAC-SHA256\n{$amzDate}\n{$credentialScope}\n" . hash('sha256', $canonicalRequest);
|
||||
$kDate = hash_hmac('sha256', $dateStamp, 'AWS4' . $secretKey, true);
|
||||
$kRegion = hash_hmac('sha256', $region, $kDate, true);
|
||||
$kService = hash_hmac('sha256', $service, $kRegion, true);
|
||||
$kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
|
||||
$signature = hash_hmac('sha256', $stringToSign, $kSigning);
|
||||
$authorization = "AWS4-HMAC-SHA256 Credential={$accessKey}/{$credentialScope}, SignedHeaders={$signedHeaders}, Signature={$signature}";
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => $authorization,
|
||||
'x-amz-content-sha256' => $payloadHash,
|
||||
'x-amz-date' => $amzDate,
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Host' => $host,
|
||||
])->withBody($payload, 'application/octet-stream')->put($url);
|
||||
if (!$response->successful()) {
|
||||
UtilsService::getInstance()->errorThrow($driver . ' 上传失败:' . $response->body());
|
||||
}
|
||||
$publicUrl = $domain !== '' ? ($domain . '/' . $key) : $url;
|
||||
return ['key' => $key, 'url' => $publicUrl];
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,10 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
//
|
||||
// 操作日志:按 nl_api_endpoint.is_log 落库
|
||||
$middleware->api(append: [
|
||||
\App\Http\Middleware\ApiOpLogMiddleware::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
$exceptions->render(function (Throwable $e) {
|
||||
|
||||
@@ -67,10 +67,10 @@ return [
|
||||
'domain' => env('OSS_DOMAIN'),
|
||||
],
|
||||
'qiniu' => [
|
||||
'access_key' => 'RPQlZUd2yrL3kR8CC4BpTGV6FhGr11npBzf6IuCT',
|
||||
'secret_key' => 'b-796uVY1InOKSVecpY8VBpO7Cdubo1Tv4RcOfMY',
|
||||
'bucket' => 'ccspa',
|
||||
'domain' => 'http://o-spa.nailaoyun.cn',
|
||||
'access_key' => env('QINIU_ACCESS_KEY', ''),
|
||||
'secret_key' => env('QINIU_SECRET_KEY', ''),
|
||||
'bucket' => env('QINIU_BUCKET', ''),
|
||||
'domain' => env('QINIU_DOMAIN', ''),
|
||||
]
|
||||
],
|
||||
/*
|
||||
@@ -124,17 +124,23 @@ return [
|
||||
* 日志配置
|
||||
*/
|
||||
'log' => [
|
||||
// 不添加返回记录的api接口
|
||||
// 不添加返回记录的api接口(路径不含 /api/ 前缀,与中间件规范化后一致)
|
||||
'no_insert' => [
|
||||
'api/admin/auth/login',
|
||||
'api/admin/auth/send-verification-code',
|
||||
'api/admin/auth/my-info',
|
||||
'api/admin/auth/codes',
|
||||
'api/admin/auth/menu',
|
||||
'api/admin/log/api-list',
|
||||
'api/admin/log/ledger-list',
|
||||
'api/admin/log/op-list',
|
||||
'api/admin/log/prescription-list',
|
||||
'login',
|
||||
'register',
|
||||
'admin/my-info',
|
||||
'admin/codes',
|
||||
'admin/menu',
|
||||
'admin/login-log',
|
||||
'admin/op-log',
|
||||
'api-endpoint/list',
|
||||
'oss-config/runtime-options',
|
||||
'oss-config/driver-options',
|
||||
'oss-config/list',
|
||||
'ai-config/runtime-options',
|
||||
'ai-config/key-list',
|
||||
'ai-config/platform-option',
|
||||
'ai-config/generation-list',
|
||||
]
|
||||
],
|
||||
'app' => [
|
||||
|
||||
@@ -352,8 +352,8 @@ CREATE TABLE `nl_ai_api_key` (
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'AI平台API密钥' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
INSERT INTO `nl_ai_api_key` VALUES
|
||||
(1, 1, '李琦-免费', '', '李琦-免费,测试key', 1, 1, 0, 1786329600, 0, 0),
|
||||
(2, 2, 'DeepSeek-v4', '', 'DeepSeek-v4', 1, 1, 0, 1786329600, 0, 0);
|
||||
(1, 1, '李琦-免费', 'nl_ase_256_3Oy8Dxp82N0PgU2rXLrhVADcfwkqG+rzTwWboUpdYnX9Sd6acEtx95BAXVehSaGHWiDq7Zgr/SRcjHDNhPfg4Q==', '李琦-免费,测试key', 1, 1, 0, 1786329600, 0, 0),
|
||||
(2, 2, 'DeepSeek-v4', 'nl_ase_256_K5ycIstKd4nalXpGqN79TggP7t+NywXffr+gN8uZcLh1T09CWI4nLPZkAgosrUoDaDCOmgIthhLvQsqHxWLtQA==', 'DeepSeek-v4', 1, 1, 0, 1786329600, 0, 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for nl_ai_generation
|
||||
@@ -389,4 +389,164 @@ CREATE TABLE `nl_ai_generation` (
|
||||
INDEX `idx_api_key_created`(`api_key_id` ASC, `created_at` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'AI生成历史' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for nl_oss_config
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `nl_oss_config`;
|
||||
CREATE TABLE `nl_oss_config` (
|
||||
`id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`driver` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'local' COMMENT '驱动:local/aliyun/qcloud/qiniu/huawei/aws/minio/baidu',
|
||||
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '配置别名',
|
||||
`access_key` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'AccessKey(库内AES加密)',
|
||||
`secret_key` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'SecretKey(库内AES加密)',
|
||||
`endpoint` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'Endpoint/服务地址',
|
||||
`region` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '地域,如 ap-guangzhou',
|
||||
`bucket` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'Bucket 名称',
|
||||
`domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '对外访问域名(含协议)',
|
||||
`path_prefix` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '对象路径前缀',
|
||||
`extra_json` json NULL COMMENT '扩展配置 JSON',
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否当前启用 0否1是(全局仅一套)',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1启用0禁用',
|
||||
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`sort` int NOT NULL DEFAULT 0 COMMENT '排序,越大越靠前',
|
||||
`created_at` int NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` int NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` int NOT NULL DEFAULT 0 COMMENT '删除时间,0表示未删除',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_driver_status`(`driver` ASC, `status` ASC) USING BTREE,
|
||||
INDEX `idx_is_active`(`is_active` ASC, `deleted_at` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'OSS存储配置' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
INSERT INTO `nl_oss_config` VALUES
|
||||
(1, 'local', '本地存储', '', '', '', '', '', '', 'uploads', NULL, 1, 1, '默认本地磁盘,文件落到 storage/app', 100, 1786329600, 0, 0),
|
||||
(2, 'aliyun', '阿里云 OSS', '', '', 'oss-cn-hangzhou.aliyuncs.com', 'cn-hangzhou', '', '', 'uploads', NULL, 0, 1, '请填写 AccessKey/Secret、Bucket、访问域名;Endpoint 可按地域修改', 90, 1786329600, 0, 0),
|
||||
(3, 'qcloud', '腾讯云 COS', '', '', '', 'ap-guangzhou', '', '', 'uploads', NULL, 0, 1, '请填写 SecretId/SecretKey、Bucket、Region、访问域名', 80, 1786329600, 0, 0),
|
||||
(4, 'qiniu', '七牛云 Kodo', '', '', '', '', '', '', 'uploads', NULL, 0, 1, '请填写 AccessKey/SecretKey、Bucket、外链域名(含协议)', 70, 1786329600, 0, 0),
|
||||
(5, 'huawei', '华为云 OBS', '', '', 'https://obs.cn-north-4.myhuaweicloud.com', 'cn-north-4', '', '', 'uploads', NULL, 0, 1, '请填写 AK/SK、Bucket、Endpoint、访问域名', 60, 1786329600, 0, 0),
|
||||
(6, 'aws', 'AWS S3', '', '', 'https://s3.amazonaws.com', 'us-east-1', '', '', 'uploads', NULL, 0, 1, '请填写 AccessKey/Secret、Bucket、Region、访问域名', 50, 1786329600, 0, 0),
|
||||
(7, 'minio', 'MinIO', '', '', 'http://127.0.0.1:9000', 'us-east-1', '', '', 'uploads', NULL, 0, 1, '私有化 S3 兼容;请填写 Key、Bucket、Endpoint', 40, 1786329600, 0, 0),
|
||||
(8, 'baidu', '百度云 BOS', '', '', 'https://bj.bcebos.com', 'bj', '', '', 'uploads', NULL, 0, 1, '请填写 AccessKey/SecretKey、Bucket、Endpoint、访问域名', 30, 1786329600, 0, 0);
|
||||
|
||||
INSERT INTO `nl_system_config` (`key`, `value`, `remark`, `created_at`, `updated_at`, `deleted_at`) VALUES
|
||||
('oss_active_config_id', '1', '当前启用的 OSS 配置 ID', 1786329600, 0, 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for nl_api_endpoint
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `nl_api_endpoint`;
|
||||
CREATE TABLE `nl_api_endpoint` (
|
||||
`id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '接口路径,如 admin/list(不含前缀 /api/)',
|
||||
`method` tinyint(1) NOT NULL DEFAULT 0 COMMENT '请求方式 0ANY 1GET 2POST',
|
||||
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '操作名',
|
||||
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '接口说明',
|
||||
`controller` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '所属控制器',
|
||||
`is_log` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否记录操作日志 0否1是',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1启用0禁用',
|
||||
`created_at` int NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` int NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` int NOT NULL DEFAULT 0 COMMENT '删除时间,0表示未删除',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `uk_url_method`(`url` ASC, `method` ASC) USING BTREE,
|
||||
INDEX `idx_is_log`(`is_log` ASC, `status` ASC) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '接口注册表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
INSERT INTO `nl_api_endpoint` (`url`, `method`, `name`, `description`, `controller`, `is_log`, `status`, `created_at`, `updated_at`, `deleted_at`) VALUES
|
||||
('login', 2, '登录', '账号密码登录', 'LoginController', 0, 1, 1786329600, 0, 0),
|
||||
('register', 2, '注册', '注册管理员账号', 'LoginController', 0, 1, 1786329600, 0, 0),
|
||||
('logout', 0, '退出登录', '退出登录', 'LoginController', 0, 1, 1786329600, 0, 0),
|
||||
('codes', 0, '权限码', '获取权限码(登录前)', 'LoginController', 0, 1, 1786329600, 0, 0),
|
||||
('admin/list', 1, '管理员列表', '分页查询管理员', 'AdminController', 0, 1, 1786329600, 0, 0),
|
||||
('admin/option', 1, '管理员下拉', '管理员下拉选项', 'AdminController', 0, 1, 1786329600, 0, 0),
|
||||
('admin/detail', 1, '管理员详情', '管理员详情', 'AdminController', 0, 1, 1786329600, 0, 0),
|
||||
('admin/create', 2, '创建管理员', '新增管理员', 'AdminController', 1, 1, 1786329600, 0, 0),
|
||||
('admin/update', 2, '更新管理员', '编辑管理员', 'AdminController', 1, 1, 1786329600, 0, 0),
|
||||
('admin/delete', 2, '删除管理员', '删除管理员', 'AdminController', 1, 1, 1786329600, 0, 0),
|
||||
('admin/change-password', 2, '修改密码', '当前用户修改密码', 'AdminController', 1, 1, 1786329600, 0, 0),
|
||||
('admin/reset-password', 2, '重置密码', '管理员重置他人密码', 'AdminController', 1, 1, 1786329600, 0, 0),
|
||||
('admin/my-info', 1, '我的信息', '获取当前登录用户信息', 'AdminController', 0, 1, 1786329600, 0, 0),
|
||||
('admin/update-profile', 2, '更新个人资料', '个人中心编辑资料/头像', 'AdminController', 1, 1, 1786329600, 0, 0),
|
||||
('admin/menu', 1, '我的菜单', '按角色获取菜单树', 'AdminController', 0, 1, 1786329600, 0, 0),
|
||||
('admin/codes', 1, '我的权限码', '获取当前用户权限码', 'AdminController', 0, 1, 1786329600, 0, 0),
|
||||
('admin/login-log', 1, '我的登录日志', '个人中心登录日志', 'AdminController', 0, 1, 1786329600, 0, 0),
|
||||
('admin/op-log', 1, '我的操作日志', '个人中心操作日志', 'AdminController', 0, 1, 1786329600, 0, 0),
|
||||
('role/list', 1, '角色列表', '分页查询角色', 'RoleController', 0, 1, 1786329600, 0, 0),
|
||||
('role/option', 1, '角色下拉', '角色下拉选项', 'RoleController', 0, 1, 1786329600, 0, 0),
|
||||
('role/detail', 1, '角色详情', '角色详情', 'RoleController', 0, 1, 1786329600, 0, 0),
|
||||
('role/create', 2, '创建角色', '新增角色', 'RoleController', 1, 1, 1786329600, 0, 0),
|
||||
('role/update', 2, '更新角色', '编辑角色', 'RoleController', 1, 1, 1786329600, 0, 0),
|
||||
('role/delete', 2, '删除角色', '删除角色', 'RoleController', 1, 1, 1786329600, 0, 0),
|
||||
('role/get-menu-ids-by-role-ids', 1, '角色菜单ID', '按角色获取已绑菜单', 'RoleController', 0, 1, 1786329600, 0, 0),
|
||||
('role/save-role-menu', 2, '保存角色菜单', '绑定角色菜单权限', 'RoleController', 1, 1, 1786329600, 0, 0),
|
||||
('menu/list', 1, '菜单列表', '分页查询菜单', 'MenuController', 0, 1, 1786329600, 0, 0),
|
||||
('menu/option', 1, '菜单下拉', '菜单下拉选项', 'MenuController', 0, 1, 1786329600, 0, 0),
|
||||
('menu/detail', 1, '菜单详情', '菜单详情', 'MenuController', 0, 1, 1786329600, 0, 0),
|
||||
('menu/create', 2, '创建菜单', '新增菜单', 'MenuController', 1, 1, 1786329600, 0, 0),
|
||||
('menu/update', 2, '更新菜单', '编辑菜单', 'MenuController', 1, 1, 1786329600, 0, 0),
|
||||
('menu/delete', 2, '删除菜单', '删除菜单', 'MenuController', 1, 1, 1786329600, 0, 0),
|
||||
('menu/tree-option', 1, '菜单树', '菜单树形选项', 'MenuController', 0, 1, 1786329600, 0, 0),
|
||||
('upload/image', 2, '上传图片', '上传图片文件', 'UploadController', 1, 1, 1786329600, 0, 0),
|
||||
('upload/video', 2, '上传视频', '上传视频文件', 'UploadController', 1, 1, 1786329600, 0, 0),
|
||||
('database/list-tables', 1, '库表列表', '列出数据库表', 'DatabaseController', 0, 1, 1786329600, 0, 0),
|
||||
('database/get-table-info', 1, '表结构', '获取表结构信息', 'DatabaseController', 0, 1, 1786329600, 0, 0),
|
||||
('database/get-table-data', 2, '表数据查询', '查询表数据', 'DatabaseController', 0, 1, 1786329600, 0, 0),
|
||||
('database/update-table-data', 2, '更新表数据', '修改表行数据', 'DatabaseController', 1, 1, 1786329600, 0, 0),
|
||||
('database/delete-table-data', 2, '删除表数据', '删除表行数据', 'DatabaseController', 1, 1, 1786329600, 0, 0),
|
||||
('database/batch-delete-table-data', 2, '批量删除表数据', '批量删除表行', 'DatabaseController', 1, 1, 1786329600, 0, 0),
|
||||
('database/insert-table-data', 2, '插入表数据', '新增表行数据', 'DatabaseController', 1, 1, 1786329600, 0, 0),
|
||||
('database/update-table-comment', 2, '更新表注释', '修改表注释', 'DatabaseController', 1, 1, 1786329600, 0, 0),
|
||||
('database/update-column-comment', 2, '更新字段注释', '修改字段注释', 'DatabaseController', 1, 1, 1786329600, 0, 0),
|
||||
('database/add-index', 2, '添加索引', '创建表索引', 'DatabaseController', 1, 1, 1786329600, 0, 0),
|
||||
('database/drop-index', 2, '删除索引', '删除表索引', 'DatabaseController', 1, 1, 1786329600, 0, 0),
|
||||
('database/update-table-structure', 2, '改表结构', '修改表结构', 'DatabaseController', 1, 1, 1786329600, 0, 0),
|
||||
('database/execute-sql-query', 2, '执行SQL', '执行自定义 SQL', 'DatabaseController', 1, 1, 1786329600, 0, 0),
|
||||
('database/get-change-log', 1, '库变更日志', '数据库变更记录', 'DatabaseController', 0, 1, 1786329600, 0, 0),
|
||||
('ai-config/runtime-options', 1, 'AI运行时选项', 'AI平台/模型/密钥选项', 'AiConfigController', 0, 1, 1786329600, 0, 0),
|
||||
('ai-config/save-runtime', 2, '保存AI运行时', '保存默认AI组合', 'AiConfigController', 1, 1, 1786329600, 0, 0),
|
||||
('ai-config/key-list', 1, 'AI密钥列表', 'API Key 卡片列表', 'AiConfigController', 0, 1, 1786329600, 0, 0),
|
||||
('ai-config/platform-option', 1, 'AI平台下拉', 'AI平台选项', 'AiConfigController', 0, 1, 1786329600, 0, 0),
|
||||
('ai-config/create-key', 2, '创建AI密钥', '新增API Key', 'AiConfigController', 1, 1, 1786329600, 0, 0),
|
||||
('ai-config/update-key', 2, '更新AI密钥', '编辑API Key', 'AiConfigController', 1, 1, 1786329600, 0, 0),
|
||||
('ai-config/delete-key', 2, '删除AI密钥', '删除API Key', 'AiConfigController', 1, 1, 1786329600, 0, 0),
|
||||
('ai-config/generation-list', 1, 'AI生成列表', 'AI生成历史列表', 'AiConfigController', 0, 1, 1786329600, 0, 0),
|
||||
('ai-config/generation-detail', 1, 'AI生成详情', 'AI生成历史详情', 'AiConfigController', 0, 1, 1786329600, 0, 0),
|
||||
('code/generation', 2, '代码生成', '按模块生成代码', 'CodeGenerationController', 1, 1, 1786329600, 0, 0),
|
||||
('code/batch-generation', 2, '批量代码生成', '批量生成代码', 'CodeGenerationController', 1, 1, 1786329600, 0, 0),
|
||||
('code/download', 0, '下载代码包', '下载生成结果', 'CodeGenerationController', 0, 1, 1786329600, 0, 0),
|
||||
('code/download-info', 0, '下载信息', '代码包下载信息', 'CodeGenerationController', 0, 1, 1786329600, 0, 0),
|
||||
('code/get-tables', 1, '代码生成表列表', '可选数据表', 'CodeGenerationController', 0, 1, 1786329600, 0, 0),
|
||||
('code/get-table-structure', 1, '代码生成表结构', '表结构供生成', 'CodeGenerationController', 0, 1, 1786329600, 0, 0),
|
||||
('code/generate-from-table', 2, '从表生成', '按表结构生成代码', 'CodeGenerationController', 1, 1, 1786329600, 0, 0),
|
||||
('code/ai-generate', 2, 'AI代码生成', 'AI辅助代码生成', 'CodeGenerationController', 1, 1, 1786329600, 0, 0),
|
||||
('code/ai-requirement-prompt', 2, 'AI需求提示词', '生成需求提示词', 'CodeGenerationController', 1, 1, 1786329600, 0, 0),
|
||||
('oss-config/driver-options', 1, 'OSS驱动选项', '可选存储驱动列表', 'OssConfigController', 0, 1, 1786329600, 0, 0),
|
||||
('oss-config/runtime-options', 1, 'OSS运行时选项', '配置卡片+当前启用', 'OssConfigController', 0, 1, 1786329600, 0, 0),
|
||||
('oss-config/save-runtime', 2, '启用OSS配置', '切换当前存储驱动', 'OssConfigController', 1, 1, 1786329600, 0, 0),
|
||||
('oss-config/list', 1, 'OSS配置列表', '存储配置列表', 'OssConfigController', 0, 1, 1786329600, 0, 0),
|
||||
('oss-config/create', 2, '创建OSS配置', '新增存储配置', 'OssConfigController', 1, 1, 1786329600, 0, 0),
|
||||
('oss-config/update', 2, '更新OSS配置', '编辑存储配置', 'OssConfigController', 1, 1, 1786329600, 0, 0),
|
||||
('oss-config/delete', 2, '删除OSS配置', '删除存储配置', 'OssConfigController', 1, 1, 1786329600, 0, 0),
|
||||
('api-endpoint/list', 1, '接口列表', '接口注册表分页', 'ApiEndpointController', 0, 1, 1786329600, 0, 0),
|
||||
('api-endpoint/update', 2, '更新接口', '编辑接口说明/是否记日志', 'ApiEndpointController', 1, 1, 1786329600, 0, 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for nl_login_log
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `nl_login_log`;
|
||||
CREATE TABLE `nl_login_log` (
|
||||
`id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`user_id` int NOT NULL DEFAULT 0 COMMENT '管理员ID,失败时可能为0',
|
||||
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '登录账号/手机号',
|
||||
`nick_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '昵称快照',
|
||||
`ip` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '登录IP',
|
||||
`equipment` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '操作系统',
|
||||
`browser` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '浏览器',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '0成功1失败',
|
||||
`message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '结果说明',
|
||||
`created_at` int NOT NULL DEFAULT 0 COMMENT '登录时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_user_created`(`user_id` ASC, `created_at` ASC) USING BTREE,
|
||||
INDEX `idx_created_at`(`created_at` ASC) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '登录日志' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
@@ -34,6 +34,8 @@ Route::group([], function () {
|
||||
'upload' => \App\Http\Controllers\Api\UploadController::class, // 上传文件
|
||||
'database' => \App\Http\Controllers\Api\DatabaseController::class, // 数据库管理
|
||||
'ai-config' => \App\Http\Controllers\Api\AiConfigController::class, // AI 配置(平台/模型/密钥)
|
||||
'oss-config' => \App\Http\Controllers\Api\OssConfigController::class, // OSS 存储配置
|
||||
'api-endpoint' => \App\Http\Controllers\Api\ApiEndpointController::class, // 接口注册表
|
||||
// 需要登录的路由生成地址
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user