diff --git a/README.md b/README.md index e610412..3bc3a5e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,120 @@ +## Sport Era 服务器部署脚本 + +部署脚本位置: + +- Windows 原生:`scripts/deploy-server.ps1` +- Bash 兼容版:`scripts/deploy-server.sh` + +脚本通过 SSH 别名 `sbnews` 连接服务器,打包本地 `server` 代码并在服务器目录解压安装。Windows 下优先使用 `deploy-server.ps1`,不依赖 WSL。 + +### PowerShell 一次执行命令 + +在 Windows PowerShell 中执行: + +```powershell +cd D:\www\sport-era +powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -RemoteDir /www/wwwroot/test-server.sbnews.net +``` + +如果 `sbnews` 配置在 `C:\Users\Administrator\.ssh\config` 中,默认会使用 Windows 的 `ssh.exe` 读取该配置。 + +### 全量发布 + +不传 commit id 即为全量发布: + +```powershell +cd D:\www\sport-era +powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -RemoteDir /www/wwwroot/test-server.sbnews.net +``` + +全量模式默认只打包以下目录: + +```text +server/app +server/config +server/extend +server/public/dongqiudi-crawler +server/route +``` + +默认会排除运行态或较大目录,例如 `.env`、`vendor`、`runtime`、`public/uploads`、`public/admin`、`public/mobile`、爬虫 `data/logs/venv`。如确实需要包含,可追加参数: + +```bash +--include-env +--include-vendor +--include-runtime +--include-uploads +--include-public-builds +--include-crawler-data +``` + +### 增量发布 + +传入两个 commit id,脚本会对比差异,只打包 `--to` 版本中的变更文件,并在服务器删除两个 commit 之间被删除的文件: + +```powershell +cd D:\www\sport-era +powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -From -To -RemoteDir /www/wwwroot/test-server.sbnews.net +``` + +示例: + +```powershell +cd D:\www\sport-era +powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -From abc1234 -To def5678 -RemoteDir /www/wwwroot/test-server.sbnews.net +``` + +### 自动增量发布 + +如果只是发布最近一次提交的后端改动,可以使用 `--auto-incremental`,脚本会自动对比 `HEAD~1` 和 `HEAD`: + +```powershell +cd D:\www\sport-era +powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -AutoIncremental -RemoteDir /www/wwwroot/test-server.sbnews.net +``` + +先演练不上传: + +```powershell +cd D:\www\sport-era +powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -AutoIncremental -RemoteDir /www/wwwroot/test-server.sbnews.net -DryRun +``` + +注意:自动增量只包含最近两个提交之间已经提交到 Git 的差异,不会包含工作区未提交文件。如有多次提交需要一起发布,请继续使用 `--from --to `。 + +如果当前根目录 Git 只把 `server` 记录为 gitlink/submodule,且本机没有 `server/.git` 子仓库元数据,脚本无法拿到 `server` 内部的文件级差异;此时会自动降级为打包当前工作区默认的 5 个 server 目录,保证可以继续发布。 + +### 只演练不上传 + +先确认打包范围、包大小和增量删除清单: + +```powershell +cd D:\www\sport-era +powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -RemoteDir /www/wwwroot/test-server.sbnews.net -DryRun +``` + +脚本会在打包后打印 `update log`:增量模式包含 commit 范围、commit 摘要、变更文件和删除文件;全量模式包含当前最新 commit 和默认打包目录。 + +每条部署日志都会带耗时,例如 `[+0.123s][total 1.456s]`:`+` 表示距离上一条日志的耗时,`total` 表示本次脚本启动后的累计耗时。服务器解压安装阶段会额外打印 `[deploy][remote]` 日志,用来判断耗时是在本地、上传还是远端解压。 + +### 发布后执行命令 + +如果需要解压后在服务器项目目录执行命令,可使用 `--post-install`: + +```powershell +cd D:\www\sport-era +powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -RemoteDir /www/wwwroot/test-server.sbnews.net -PostInstall "php think optimize:clear" +``` + +### 常见问题 + +1. `ssh: Could not resolve hostname sbnews`:检查 Windows 的 `C:\Users\Administrator\.ssh\config` 是否存在 `Host sbnews`,或直接用 `-HostName <服务器IP>`。 +2. PowerShell 禁止执行脚本:使用示例中的 `-ExecutionPolicy Bypass -File`,或以管理员身份调整当前用户执行策略。 +3. `tar: Cannot utime` 或 `Cannot change mode`:远端目录权限不足,请加 `-Sudo`。脚本会使用 `sudo tar` 覆盖解压,并默认跳过还原 owner/permission,减少权限冲突。 +4. 增量发布提示 commit 无效:确认 commit id 属于当前根目录 Git 可识别的历史。 +5. `server is tracked as a root gitlink`:脚本检测到根 Git 只记录了 `server` 指针,无法做文件级增量,会自动改为打包当前工作区默认 5 个 server 目录。 +6. 仍需使用 Bash 兼容版时,可继续运行 `bash ./scripts/deploy-server.sh ...`。 + ![likeshop全开源商城](https://server.likeshop.cn/uploads/gitad/fastbuildAI.png)
![likeshop全开源商城](https://server.likeshop.cn/uploads/gitad/likeshop.png)
![BuildingAI](https://server.likeshop.cn/uploads/gitad/fastbuildAI.png)
diff --git a/admin b/admin deleted file mode 160000 index 44251a6..0000000 --- a/admin +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 44251a602e262cb9009ab3d444592d867018cb36 diff --git a/docs/sql/create_ai_assistant_tables.sql b/docs/sql/create_ai_assistant_tables.sql new file mode 100644 index 0000000..60d8e71 --- /dev/null +++ b/docs/sql/create_ai_assistant_tables.sql @@ -0,0 +1,56 @@ +CREATE TABLE IF NOT EXISTS `la_ai_chat_session` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL DEFAULT 0 COMMENT '登录用户ID,匿名为0', + `client_id` varchar(80) NOT NULL DEFAULT '' COMMENT '匿名客户端ID', + `title` varchar(100) NOT NULL DEFAULT '' COMMENT '会话标题', + `last_message` varchar(500) NOT NULL DEFAULT '' COMMENT '最后用户消息', + `last_answer` varchar(500) NOT NULL DEFAULT '' COMMENT '最后助手回复', + `message_count` int unsigned NOT NULL DEFAULT 0 COMMENT '消息数', + `last_active_time` int unsigned NOT NULL DEFAULT 0 COMMENT '最后活跃时间', + `create_time` int unsigned NOT NULL DEFAULT 0, + `update_time` int unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_user_active` (`user_id`, `last_active_time`), + KEY `idx_client_active` (`client_id`, `last_active_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI助手会话表'; + +CREATE TABLE IF NOT EXISTS `la_ai_chat_message` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `session_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '会话ID', + `user_id` int unsigned NOT NULL DEFAULT 0 COMMENT '登录用户ID', + `client_id` varchar(80) NOT NULL DEFAULT '' COMMENT '匿名客户端ID', + `role` varchar(20) NOT NULL DEFAULT 'user' COMMENT 'user/assistant', + `content` mediumtext NULL COMMENT '消息内容', + `sources_json` json NULL COMMENT '引用来源', + `model` varchar(100) NOT NULL DEFAULT '' COMMENT '模型', + `tokens_used` int unsigned NOT NULL DEFAULT 0 COMMENT 'token消耗', + `cost_ms` int unsigned NOT NULL DEFAULT 0 COMMENT '耗时毫秒', + `status` tinyint unsigned NOT NULL DEFAULT 1 COMMENT '1成功2失败', + `error_msg` varchar(500) NOT NULL DEFAULT '' COMMENT '错误信息', + `create_time` int unsigned NOT NULL DEFAULT 0, + `update_time` int unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_session_id` (`session_id`), + KEY `idx_user_time` (`user_id`, `create_time`), + KEY `idx_client_time` (`client_id`, `create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI助手消息表'; + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'assistant_system_prompt', '你是世博头条 AI 助手,面向普通用户回答站内体育资讯、赛事、社区、彩票和加密行情相关问题。必须优先依据提供的站内资料和实时上下文回答;资料不足时要直接说明不足并建议用户换关键词。不要声称可以访问后台、源码、服务器或未提供的内部数据。回答使用中文,结构清晰,语气专业克制。涉及赛事预测、彩票或加密行情时,必须说明仅供参考,不构成投资或购彩建议。', '世博头条 AI 助手系统提示词', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'assistant_system_prompt'); + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'assistant_kb_topk', '6', 'AI助手知识库召回数量', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'assistant_kb_topk'); + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'assistant_history_limit', '8', 'AI助手发送给模型的历史消息数量', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'assistant_history_limit'); + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'assistant_rate_limit_per_minute', '10', 'AI助手单用户/客户端每分钟请求上限', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'assistant_rate_limit_per_minute'); + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'assistant_enable_crypto_realtime', '1', 'AI助手是否启用加密行情实时查询:1启用0关闭', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'assistant_enable_crypto_realtime'); diff --git a/docs/sql/create_ai_kb_tables.sql b/docs/sql/create_ai_kb_tables.sql index 230de34..8c87ea8 100644 --- a/docs/sql/create_ai_kb_tables.sql +++ b/docs/sql/create_ai_kb_tables.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS `la_ai_kb_document` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `domain` varchar(32) NOT NULL DEFAULT '' COMMENT 'article/post/lottery', - `subtype` varchar(32) NOT NULL DEFAULT '' COMMENT 'article/post/draw_result/ai_history', + `domain` varchar(32) NOT NULL DEFAULT '' COMMENT 'article/post/lottery/match', + `subtype` varchar(32) NOT NULL DEFAULT '' COMMENT 'article/post/draw_result/ai_history/event', `source_id` bigint unsigned NOT NULL DEFAULT 0, `title` varchar(255) NOT NULL DEFAULT '', `summary` text NULL, diff --git a/docs/sql/create_private_chat_tables.sql b/docs/sql/create_private_chat_tables.sql new file mode 100644 index 0000000..36098a4 --- /dev/null +++ b/docs/sql/create_private_chat_tables.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS `la_private_chat_session` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_a_id` int unsigned NOT NULL DEFAULT 0 COMMENT '较小用户ID', + `user_b_id` int unsigned NOT NULL DEFAULT 0 COMMENT '较大用户ID', + `last_message_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '最后消息ID', + `last_message_type` varchar(20) NOT NULL DEFAULT 'text' COMMENT '最后消息类型 text/image', + `last_message_content` varchar(500) NOT NULL DEFAULT '' COMMENT '最后消息内容', + `last_sender_id` int unsigned NOT NULL DEFAULT 0 COMMENT '最后发送者', + `user_a_unread` int unsigned NOT NULL DEFAULT 0 COMMENT '用户A未读数', + `user_b_unread` int unsigned NOT NULL DEFAULT 0 COMMENT '用户B未读数', + `last_active_time` int unsigned NOT NULL DEFAULT 0 COMMENT '最后活跃时间', + `create_time` int unsigned NOT NULL DEFAULT 0, + `update_time` int unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_pair` (`user_a_id`, `user_b_id`), + KEY `idx_user_a_active` (`user_a_id`, `last_active_time`), + KEY `idx_user_b_active` (`user_b_id`, `last_active_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户私聊会话表'; + +CREATE TABLE IF NOT EXISTS `la_private_chat_message` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `session_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '会话ID', + `sender_id` int unsigned NOT NULL DEFAULT 0 COMMENT '发送者', + `receiver_id` int unsigned NOT NULL DEFAULT 0 COMMENT '接收者', + `message_type` varchar(20) NOT NULL DEFAULT 'text' COMMENT '消息类型 text/image', + `content` text NULL COMMENT '消息内容', + `read_time` int unsigned NOT NULL DEFAULT 0 COMMENT '接收者阅读时间', + `create_time` int unsigned NOT NULL DEFAULT 0, + `update_time` int unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_session_id` (`session_id`, `id`), + KEY `idx_receiver_read` (`receiver_id`, `read_time`), + KEY `idx_sender_time` (`sender_id`, `create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户私聊消息表'; diff --git a/docs/sql/insert_qwen_ai_config.sql b/docs/sql/insert_qwen_ai_config.sql new file mode 100644 index 0000000..a9ee206 --- /dev/null +++ b/docs/sql/insert_qwen_ai_config.sql @@ -0,0 +1,35 @@ +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'qwen_api_key', '', 'DashScope Qwen API Key(留空时读取 server/.env 的 DASHSCOPE_API_KEY)', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'qwen_api_key'); + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'qwen_base_url', 'https://dashscope.aliyuncs.com/compatible-mode', 'DashScope OpenAI-compatible Base URL(不带 /v1)', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'qwen_base_url'); + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'qwen_model', 'qwen3.7-plus', '默认文本生成模型', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'qwen_model'); + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'ai_request_timeout', '300', 'AI 模型请求超时时间(秒,30-300)', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'ai_request_timeout'); + +UPDATE `la_ai_config` +SET `value` = '300', `update_time` = UNIX_TIMESTAMP() +WHERE `name` = 'ai_request_timeout'; + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'embedding_api_key', '', '知识库 Embedding API Key(留空时读取 server/.env 的 DASHSCOPE_API_KEY)', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'embedding_api_key'); + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'embedding_base_url', 'https://dashscope.aliyuncs.com/compatible-mode', '知识库 Embedding API Base URL(不带 /v1)', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'embedding_base_url'); + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'embedding_model', 'text-embedding-v4', '知识库 Embedding 模型名', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'embedding_model'); + +INSERT INTO `la_ai_config` (`name`, `value`, `remark`, `create_time`, `update_time`) +SELECT 'embedding_dim', '1024', '知识库 Embedding 维度', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS (SELECT 1 FROM `la_ai_config` WHERE `name` = 'embedding_dim'); diff --git a/qa/backend/test_private_chat_api.py b/qa/backend/test_private_chat_api.py new file mode 100644 index 0000000..815dc97 --- /dev/null +++ b/qa/backend/test_private_chat_api.py @@ -0,0 +1,101 @@ +""" +接口验证: 社区私聊 v1 +路径: +- GET /api/chat/sessions +- POST /api/chat/open +- GET /api/chat/messages +- POST /api/chat/send + +运行前请通过环境变量提供两个已登录用户 token: +PRIVATE_CHAT_TOKEN_A、PRIVATE_CHAT_TOKEN_B。 +""" +import os + +import requests + + +BASE_URL = os.getenv("BASE_URL", "http://127.0.0.1:8000") +TOKEN_A = os.getenv("PRIVATE_CHAT_TOKEN_A", "") +TOKEN_B = os.getenv("PRIVATE_CHAT_TOKEN_B", "") +TARGET_USER_ID = int(os.getenv("PRIVATE_CHAT_TARGET_USER_ID", "0")) + + +def assert_api_ok(resp): + assert resp.status_code == 200, resp.text + data = resp.json() + assert data["code"] == 1, data + return data["data"] + + +def require_env(): + assert TOKEN_A, "请设置 PRIVATE_CHAT_TOKEN_A" + assert TOKEN_B, "请设置 PRIVATE_CHAT_TOKEN_B" + assert TARGET_USER_ID > 0, "请设置 PRIVATE_CHAT_TARGET_USER_ID" + + +def test_private_chat_flow(): + require_env() + + headers_a = {"token": TOKEN_A} + headers_b = {"token": TOKEN_B} + + session = assert_api_ok( + requests.post( + f"{BASE_URL}/api/chat/open", + json={"target_user_id": TARGET_USER_ID}, + headers=headers_a, + timeout=10, + ) + ) + assert session["target_user"]["id"] == TARGET_USER_ID + assert "is_mutual" in session["relationship"] + assert "relationship_text" in session["relationship"] + + message = assert_api_ok( + requests.post( + f"{BASE_URL}/api/chat/send", + json={ + "session_id": session["id"], + "message_type": "text", + "content": "私聊接口冒烟测试", + }, + headers=headers_a, + timeout=10, + ) + ) + assert message["message_type"] == "text" + assert message["content"] == "私聊接口冒烟测试" + + messages = assert_api_ok( + requests.get( + f"{BASE_URL}/api/chat/messages", + params={"session_id": session["id"], "after_id": 0}, + headers=headers_b, + timeout=10, + ) + ) + assert any(item["id"] == message["id"] for item in messages["lists"]) + + image_message = assert_api_ok( + requests.post( + f"{BASE_URL}/api/chat/send", + json={ + "session_id": session["id"], + "message_type": "image", + "content": "/uploads/private-chat-test.png", + }, + headers=headers_a, + timeout=10, + ) + ) + assert image_message["message_type"] == "image" + + sessions = assert_api_ok( + requests.get(f"{BASE_URL}/api/chat/sessions", headers=headers_a, timeout=10) + ) + assert any(item["id"] == session["id"] for item in sessions["lists"]) + + +if __name__ == "__main__": + test_private_chat_flow() + print("私聊接口冒烟通过") diff --git a/scripts/deploy-server.ps1 b/scripts/deploy-server.ps1 new file mode 100644 index 0000000..f922f40 --- /dev/null +++ b/scripts/deploy-server.ps1 @@ -0,0 +1,708 @@ +[CmdletBinding()] +param( + [string]$SourceDir = "server", + [Parameter(Mandatory = $true)] + [string]$RemoteDir, + [string]$HostName = "sbnews", + [string]$SshBin = "ssh.exe", + [switch]$AutoIncremental, + [string]$From = "", + [string]$To = "", + [string]$RemoteTmp = "/tmp", + [string]$PostInstall = "", + [switch]$Sudo, + [switch]$KeepRemoteTemp, + [switch]$IncludeVendor, + [switch]$IncludeEnv, + [switch]$IncludeRuntime, + [switch]$IncludeUploads, + [switch]$IncludePublicBuilds, + [switch]$IncludeCrawlerData, + [switch]$DryRun +) + +$ErrorActionPreference = "Stop" +$Utf8NoBom = [System.Text.UTF8Encoding]::new($false) +[Console]::OutputEncoding = $Utf8NoBom +$OutputEncoding = $Utf8NoBom + +$DeployWatch = [System.Diagnostics.Stopwatch]::StartNew() +$script:LastLogMs = 0.0 + +function Format-Duration { + param([Int64]$Milliseconds) + $span = [TimeSpan]::FromMilliseconds([Math]::Max(0, $Milliseconds)) + if ($span.TotalMinutes -ge 1) { + return "{0}m{1:00}.{2:000}s" -f [Math]::Floor($span.TotalMinutes), $span.Seconds, $span.Milliseconds + } + return "{0}.{1:000}s" -f [Math]::Floor($span.TotalSeconds), $span.Milliseconds +} + +function Write-DeployLog { + param([string]$Message) + $total = [Int64]$DeployWatch.ElapsedMilliseconds + $step = $total - $script:LastLogMs + $script:LastLogMs = $total + Write-Host ("[deploy][+{0}][total {1}] {2}" -f (Format-Duration $step), (Format-Duration $total), $Message) +} + +function Fail { + param([string]$Message) + throw "[deploy][error] $Message" +} + +function Require-Command { + param([string]$Name) + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { + Fail "missing command: $Name" + } +} + +function Normalize-DeployDir { + param([string]$Value) + return $Value.TrimEnd("/", "\") +} + +function Normalize-ComparePath { + param([string]$Value) + return ([System.IO.Path]::GetFullPath($Value).TrimEnd("\", "/") -replace "\\", "/").ToLowerInvariant() +} + +function ConvertTo-RemoteSingleQuoted { + param([string]$Value) + return "'" + $Value.Replace("'", "'\''") + "'" +} + +function Invoke-Native { + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + [string[]]$Arguments = @() + ) + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + Fail "command failed ($LASTEXITCODE): $FilePath $($Arguments -join ' ')" + } +} + +function Invoke-NativeOutput { + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + [string[]]$Arguments = @() + ) + $output = & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + Fail "command failed ($LASTEXITCODE): $FilePath $($Arguments -join ' ')" + } + return $output +} + +function ConvertTo-WindowsArgument { + param([string]$Value) + if ($null -eq $Value) { + return '""' + } + if ($Value -notmatch '[\s"]') { + return $Value + } + + $result = '"' + $slashes = 0 + foreach ($ch in $Value.ToCharArray()) { + if ($ch -eq '\') { + $slashes++ + continue + } + if ($ch -eq '"') { + $result += ('\' * (($slashes * 2) + 1)) + $result += '"' + $slashes = 0 + continue + } + if ($slashes -gt 0) { + $result += ('\' * $slashes) + $slashes = 0 + } + $result += $ch + } + if ($slashes -gt 0) { + $result += ('\' * ($slashes * 2)) + } + $result += '"' + return $result +} + +function Set-ProcessArguments { + param( + [System.Diagnostics.ProcessStartInfo]$ProcessStartInfo, + [string[]]$Arguments + ) + $argumentListProperty = $ProcessStartInfo.GetType().GetProperty("ArgumentList") + $argumentList = $null + if ($null -ne $argumentListProperty) { + $argumentList = $argumentListProperty.GetValue($ProcessStartInfo, $null) + } + if ($null -ne $argumentList) { + foreach ($arg in $Arguments) { + [void]$argumentList.Add($arg) + } + return + } + $ProcessStartInfo.Arguments = (($Arguments | ForEach-Object { ConvertTo-WindowsArgument $_ }) -join " ") +} + +function Start-ProcessWithInputFile { + param( + [string]$FilePath, + [string[]]$Arguments, + [string]$InputFile + ) + $psi = [System.Diagnostics.ProcessStartInfo]::new() + $psi.FileName = $FilePath + $psi.UseShellExecute = $false + $psi.RedirectStandardInput = $true + Set-ProcessArguments -ProcessStartInfo $psi -Arguments $Arguments + + $process = [System.Diagnostics.Process]::Start($psi) + try { + $fileStream = [System.IO.File]::OpenRead($InputFile) + try { + $fileStream.CopyTo($process.StandardInput.BaseStream) + } finally { + $fileStream.Dispose() + } + $process.StandardInput.Close() + $process.WaitForExit() + if ($process.ExitCode -ne 0) { + Fail "command failed ($($process.ExitCode)): $FilePath $($Arguments -join ' ')" + } + } finally { + $process.Dispose() + } +} + +function Start-ProcessWithInputText { + param( + [string]$FilePath, + [string[]]$Arguments, + [string]$InputText + ) + $psi = [System.Diagnostics.ProcessStartInfo]::new() + $psi.FileName = $FilePath + $psi.UseShellExecute = $false + $psi.RedirectStandardInput = $true + Set-ProcessArguments -ProcessStartInfo $psi -Arguments $Arguments + + $process = [System.Diagnostics.Process]::Start($psi) + try { + $process.StandardInput.Write($InputText) + $process.StandardInput.Close() + $process.WaitForExit() + if ($process.ExitCode -ne 0) { + Fail "command failed ($($process.ExitCode)): $FilePath $($Arguments -join ' ')" + } + } finally { + $process.Dispose() + } +} + +function Remove-IfExists { + param([string]$Path) + if (Test-Path -LiteralPath $Path) { + Remove-Item -LiteralPath $Path -Recurse -Force + } +} + +function Copy-FullPayload { + Write-DeployLog "building full payload from current working tree: $SourceDir" + Write-DeployLog "full include paths: $($FullIncludePaths -join ' ')" + + $payloadTar = Join-Path $WorkDir "payload-copy.tar" + $tarArgs = @( + "-cf", $payloadTar, + "-C", $SourceAbs, + "--exclude=.git", + "--exclude=.idea", + "--exclude=.vscode", + "--exclude=.tmp", + "--exclude=.gitignore", + "--exclude=*.log", + "--exclude=release-*.zip", + "--exclude=*.zip", + "--exclude=__pycache__", + "--exclude=*.pyc" + ) + + if (-not $IncludeEnv) { + $tarArgs += "--exclude=.env" + } + if (-not $IncludeVendor) { + $tarArgs += "--exclude=vendor" + } + if (-not $IncludeRuntime) { + $tarArgs += "--exclude=runtime" + } + if (-not $IncludeUploads) { + $tarArgs += "--exclude=public/uploads" + } + if (-not $IncludePublicBuilds) { + $tarArgs += @("--exclude=public/admin", "--exclude=public/mobile") + } + if (-not $IncludeCrawlerData) { + $tarArgs += @( + "--exclude=public/dongqiudi-crawler/data", + "--exclude=public/dongqiudi-crawler/logs", + "--exclude=public/dongqiudi-crawler/venv", + "--exclude=public/dongqiudi-crawler/__pycache__" + ) + } + + $tarArgs += $FullIncludePaths + Invoke-Native "tar" $tarArgs + Invoke-Native "tar" @("-xf", $payloadTar, "-C", $PayloadDir) +} + +function Strip-SourcePrefix { + param([string]$Path) + if ([string]::IsNullOrEmpty($GitPathPrefix)) { + return $Path + } + $prefix = "$GitPathPrefix/" + if ($Path.StartsWith($prefix)) { + return $Path.Substring($prefix.Length) + } + return $Path +} + +function Test-DeployIncludedPath { + param([string]$RelativePath) + foreach ($include in $FullIncludePaths) { + if ($RelativePath -eq $include -or $RelativePath.StartsWith("$include/")) { + return $true + } + } + return $false +} + +function Test-UnderSourceDir { + param([string]$GitPath) + if ([string]::IsNullOrEmpty($GitPathPrefix)) { + return (Test-DeployIncludedPath $GitPath) + } + if (-not $GitPath.StartsWith("$GitPathPrefix/")) { + return $false + } + return (Test-DeployIncludedPath (Strip-SourcePrefix $GitPath)) +} + +function Get-ShortCommit { + param([string]$Commit) + try { + return (Invoke-NativeOutput "git" @("-C", $GitCwd, "rev-parse", "--short", $Commit) | Select-Object -First 1) + } catch { + return $Commit + } +} + +function Write-FileListLog { + param( + [string]$Title, + [string]$Prefix, + [string[]]$Items + ) + Write-DeployLog $Title + $list = @($Items | Where-Object { $_ } | Sort-Object -Unique) + if ($list.Count -eq 0) { + Write-Host " (none)" + return + } + foreach ($item in $list) { + Write-Host " $Prefix $item" + } +} + +function Write-UpdateLog { + Write-DeployLog "update log:" + if ($Mode -eq "incremental") { + $fromShort = Get-ShortCommit $FromCommit + $toShort = Get-ShortCommit $ToCommit + Write-DeployLog "commit range: $fromShort -> $toShort" + + $logArgs = @("-C", $GitCwd, "-c", "i18n.logOutputEncoding=utf-8", "log", "--encoding=UTF-8", "--no-merges", "--date=format:%Y-%m-%d %H:%M:%S", "--pretty=format: * %h %ad %an %s", "$FromCommit..$ToCommit") + if ($SourceIsGitLink) { + Write-DeployLog "$SourceDir is a root gitlink, showing root repository commits for the selected range" + } elseif (-not [string]::IsNullOrEmpty($GitPathPrefix)) { + $logArgs += @("--", $GitPathPrefix) + } + + $commitLines = @(& git @logArgs) + Write-DeployLog "commits:" + if ($commitLines.Count -gt 0) { + $commitLines | ForEach-Object { Write-Host $_ } + } else { + Write-Host " (none)" + } + + Write-FileListLog "changed files:" "+" $ChangedDisplayPaths + Write-FileListLog "deleted files:" "-" $DeletedDisplayPaths + } else { + Write-DeployLog "mode: full deploy from current working tree" + Write-DeployLog "full include paths: $($FullIncludePaths -join ' ')" + $commitLines = @(& git -C $GitCwd -c i18n.logOutputEncoding=utf-8 log -1 --encoding=UTF-8 "--date=format:%Y-%m-%d %H:%M:%S" "--pretty=format: * %h %ad %an %s") + Write-DeployLog "latest commit:" + if ($commitLines.Count -gt 0) { + $commitLines | ForEach-Object { Write-Host $_ } + } else { + Write-Host " (none)" + } + } +} + +function Build-IncrementalPayload { + if ($SourceIsGitLink) { + Write-DeployLog "$SourceDir is tracked as a root gitlink; exact file-level diff is unavailable without $SourceDir/.git" + Write-DeployLog "building fallback payload from current working tree include paths: $($FullIncludePaths -join ' ')" + $script:ChangedDisplayPaths = @($FullIncludePaths) + Copy-FullPayload + return + } + + $diffArgs = @("-C", $GitCwd, "diff", "--name-status", "--find-renames", $FromCommit, $ToCommit) + if (-not [string]::IsNullOrEmpty($GitPathPrefix)) { + $diffArgs += @("--", $GitPathPrefix) + } + + $rawDiff = @(& git @diffArgs) + if ($LASTEXITCODE -ne 0) { + Fail "git diff failed" + } + + $changedGitPaths = New-Object System.Collections.Generic.List[string] + $script:ChangedDisplayPaths = @() + $script:DeletedDisplayPaths = @() + + foreach ($line in $rawDiff) { + if ([string]::IsNullOrWhiteSpace($line)) { + continue + } + $parts = $line -split "`t" + $status = $parts[0] + if ($status -eq "D") { + $path1 = $parts[1] + if (Test-UnderSourceDir $path1) { + $script:DeletedDisplayPaths += (Strip-SourcePrefix $path1) + } + } elseif ($status -like "R*" -or $status -like "C*") { + if ($parts.Count -lt 3) { + continue + } + $path1 = $parts[1] + $path2 = $parts[2] + if (Test-UnderSourceDir $path2) { + [void]$changedGitPaths.Add($path2) + $script:ChangedDisplayPaths += (Strip-SourcePrefix $path2) + if ($status -like "R*" -and (Test-UnderSourceDir $path1)) { + $script:DeletedDisplayPaths += (Strip-SourcePrefix $path1) + } + } + } else { + $path1 = $parts[1] + if (Test-UnderSourceDir $path1) { + [void]$changedGitPaths.Add($path1) + $script:ChangedDisplayPaths += (Strip-SourcePrefix $path1) + } + } + } + + $script:ChangedDisplayPaths = @($script:ChangedDisplayPaths | Sort-Object -Unique) + $script:DeletedDisplayPaths = @($script:DeletedDisplayPaths | Sort-Object -Unique) + $changedGitPaths = @($changedGitPaths | Sort-Object -Unique) + + if ($changedGitPaths.Count -eq 0 -and $script:DeletedDisplayPaths.Count -eq 0) { + Write-DeployLog "no changed files detected between $FromCommit and $ToCommit under $SourceDir" + $script:NoChanges = $true + return + } + + if ($script:DeletedDisplayPaths.Count -gt 0) { + Set-Content -LiteralPath $DeletedPathsFile -Value $script:DeletedDisplayPaths -Encoding UTF8 + } + + if ($changedGitPaths.Count -gt 0) { + $incrementalTar = Join-Path $WorkDir "incremental.tar" + Write-DeployLog "building incremental payload from commit $ToCommit" + Invoke-Native "git" (@("-C", $GitCwd, "archive", "--format=tar", "--output=$incrementalTar", $ToCommit, "--") + $changedGitPaths) + Invoke-Native "tar" @("-xf", $incrementalTar, "-C", $ExtractDir) + + if ($SourceIsGitRepo) { + $extractSource = $ExtractDir + } else { + $extractSource = Join-Path $ExtractDir $SourceDir + } + if (Test-Path -LiteralPath $extractSource) { + Get-ChildItem -LiteralPath $extractSource -Force | Copy-Item -Destination $PayloadDir -Recurse -Force + } + } +} + +$SourceDir = Normalize-DeployDir $SourceDir +$RemoteDir = Normalize-DeployDir $RemoteDir +$RemoteTmp = Normalize-DeployDir $RemoteTmp + +if ($AutoIncremental -and ($From -or $To)) { + Fail "-AutoIncremental cannot be used with -From/-To" +} + +if ($AutoIncremental) { + $Mode = "incremental" + $FromCommit = "HEAD~1" + $ToCommit = "HEAD" +} elseif ($From -or $To) { + if (-not $From -or -not $To) { + Fail "incremental deploy requires both -From and -To" + } + $Mode = "incremental" + $FromCommit = $From + $ToCommit = $To +} else { + $Mode = "full" + $FromCommit = "" + $ToCommit = "" +} + +Require-Command "git" +Require-Command "tar" +Require-Command $SshBin + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = (Resolve-Path (Join-Path $ScriptDir "..")).Path +$SourceAbs = Join-Path $RepoRoot $SourceDir +if (-not (Test-Path -LiteralPath $SourceAbs -PathType Container)) { + Fail "source directory not found: $SourceDir" +} + +$FullIncludePaths = @("app", "config", "extend", "public/dongqiudi-crawler", "route") +$GitCwd = $RepoRoot +$GitPathPrefix = $SourceDir +$SourceIsGitRepo = $false +$SourceIsGitLink = $false + +$sourceGitTop = (& git -C $SourceAbs rev-parse --show-toplevel 2>$null | Select-Object -First 1) +if ($LASTEXITCODE -eq 0 -and $sourceGitTop -and (Normalize-ComparePath $sourceGitTop) -eq (Normalize-ComparePath $SourceAbs)) { + $GitCwd = $SourceAbs + $GitPathPrefix = "" + $SourceIsGitRepo = $true +} else { + & git -C $RepoRoot rev-parse --show-toplevel *> $null + if ($LASTEXITCODE -ne 0 -and $Mode -eq "incremental") { + Fail "incremental deploy requires git metadata in $SourceDir or repo root" + } +} + +$lsFiles = @(& git -C $RepoRoot ls-files -s -- $SourceDir) +if (-not $SourceIsGitRepo -and ($lsFiles | Where-Object { $_ -match "^160000\s" })) { + $SourceIsGitLink = $true +} + +if ($Mode -eq "incremental") { + & git -C $GitCwd rev-parse --verify "$FromCommit^{commit}" *> $null + if ($LASTEXITCODE -ne 0) { + Fail "invalid -From commit: $FromCommit" + } + & git -C $GitCwd rev-parse --verify "$ToCommit^{commit}" *> $null + if ($LASTEXITCODE -ne 0) { + Fail "invalid -To commit: $ToCommit" + } + $FromCommit = (Invoke-NativeOutput "git" @("-C", $GitCwd, "rev-parse", $FromCommit) | Select-Object -First 1) + $ToCommit = (Invoke-NativeOutput "git" @("-C", $GitCwd, "rev-parse", $ToCommit) | Select-Object -First 1) +} + +$TempRoot = Join-Path ([System.IO.Path]::GetTempPath()) "sport-era-deploy" +New-Item -ItemType Directory -Force -Path $TempRoot | Out-Null +$WorkDir = Join-Path $TempRoot ("deploy-server-" + [System.Guid]::NewGuid().ToString("N")) +$PackageRoot = Join-Path $WorkDir "package" +$PayloadDir = Join-Path $PackageRoot "payload" +$ExtractDir = Join-Path $WorkDir "extract" +$ArchivePath = Join-Path $WorkDir ("{0}-{1}.tar.gz" -f ($SourceDir -replace "[\\/]", "-"), $Mode) +$DeletedPathsFile = Join-Path $PackageRoot ".deleted-paths.txt" +$DeployInfoFile = Join-Path $PackageRoot ".deploy-info.txt" +$script:ChangedDisplayPaths = @() +$script:DeletedDisplayPaths = @() +$script:NoChanges = $false + +try { + New-Item -ItemType Directory -Force -Path $PayloadDir, $ExtractDir | Out-Null + New-Item -ItemType File -Force -Path $DeletedPathsFile | Out-Null + + if ($Mode -eq "full") { + Copy-FullPayload + } else { + Build-IncrementalPayload + if ($script:NoChanges) { + return + } + } + + Write-UpdateLog + + $deployInfo = @( + "mode=$Mode", + "source_dir=$SourceDir", + "host=$HostName", + "remote_dir=$RemoteDir", + "built_at=$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + ) + if ($Mode -eq "incremental") { + $deployInfo += @( + "auto_incremental=$([int]$AutoIncremental.IsPresent)", + "source_is_gitlink=$([int]$SourceIsGitLink)", + "from_commit=$FromCommit", + "to_commit=$ToCommit", + "changed_count=$($script:ChangedDisplayPaths.Count)", + "deleted_count=$($script:DeletedDisplayPaths.Count)" + ) + } + Set-Content -LiteralPath $DeployInfoFile -Value $deployInfo -Encoding UTF8 + + Write-DeployLog "packing archive: $ArchivePath" + Invoke-Native "tar" @("-czf", $ArchivePath, "-C", $PackageRoot, ".") + $packageSizeKb = [Math]::Ceiling((Get-Item -LiteralPath $ArchivePath).Length / 1KB) + Write-DeployLog "package size: ${packageSizeKb}K" + + if ($DryRun) { + Write-DeployLog "dry-run enabled, skip upload/install" + return + } + + $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + $pidValue = $PID + $remoteWorkDir = "$RemoteTmp/$($SourceDir -replace '[\\/]', '-')-deploy-$timestamp-$pidValue" + $remoteArchive = "$RemoteTmp/$($SourceDir -replace '[\\/]', '-')-$Mode-$timestamp.tar.gz" + + $remoteTmpQ = ConvertTo-RemoteSingleQuoted $RemoteTmp + $remoteArchiveQ = ConvertTo-RemoteSingleQuoted $remoteArchive + + Write-DeployLog "uploading archive to ${HostName}:$remoteArchive" + $uploadCommand = "mkdir -p $remoteTmpQ && cat > $remoteArchiveQ" + Start-ProcessWithInputFile -FilePath $SshBin -Arguments @($HostName, $uploadCommand) -InputFile $ArchivePath + + $postInstallB64 = "" + if ($PostInstall) { + $postInstallB64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($PostInstall)) + } + + $useSudoValue = if ($Sudo) { "1" } else { "0" } + $keepRemoteTempValue = if ($KeepRemoteTemp) { "1" } else { "0" } + $remoteCommand = @( + "REMOTE_DIR=$(ConvertTo-RemoteSingleQuoted $RemoteDir)", + "REMOTE_ARCHIVE=$(ConvertTo-RemoteSingleQuoted $remoteArchive)", + "REMOTE_WORK_DIR=$(ConvertTo-RemoteSingleQuoted $remoteWorkDir)", + "KEEP_REMOTE_TEMP='$keepRemoteTempValue'", + "POST_INSTALL_B64='$postInstallB64'", + "USE_SUDO='$useSudoValue'", + "bash -s" + ) -join " " + + $remoteScript = @' +set -euo pipefail + +remote_now_ms() { + date +%s%3N +} + +remote_format_duration() { + local ms="$1" + local seconds=$((ms / 1000)) + local millis=$((ms % 1000)) + local minutes=$((seconds / 60)) + seconds=$((seconds % 60)) + + if [[ "$minutes" -gt 0 ]]; then + printf '%dm%02d.%03ds' "$minutes" "$seconds" "$millis" + else + printf '%d.%03ds' "$seconds" "$millis" + fi +} + +REMOTE_START_MS="$(remote_now_ms)" +REMOTE_LAST_LOG_MS="$REMOTE_START_MS" + +remote_log() { + local current_ms + local total_ms + local step_ms + current_ms="$(remote_now_ms)" + total_ms=$((current_ms - REMOTE_START_MS)) + step_ms=$((current_ms - REMOTE_LAST_LOG_MS)) + REMOTE_LAST_LOG_MS="$current_ms" + printf '[deploy][remote][+%s][total %s] %s\n' "$(remote_format_duration "$step_ms")" "$(remote_format_duration "$total_ms")" "$*" +} + +run_priv() { + if [[ "${USE_SUDO:-0}" == "1" ]]; then + sudo "$@" + else + "$@" + fi +} + +tar_extract_flags=(--no-same-owner --no-same-permissions --no-overwrite-dir) + +remote_log "prepare remote work dir: $REMOTE_WORK_DIR" +mkdir -p "$REMOTE_WORK_DIR" +remote_log "extract uploaded archive" +tar -xzf "$REMOTE_ARCHIVE" -C "$REMOTE_WORK_DIR" +remote_log "ensure remote dir: $REMOTE_DIR" +run_priv mkdir -p "$REMOTE_DIR" + +if [[ -d "$REMOTE_WORK_DIR/payload" ]]; then + remote_log "install payload into remote dir" + tar -cf - -C "$REMOTE_WORK_DIR/payload" . | run_priv tar "${tar_extract_flags[@]}" -xf - -C "$REMOTE_DIR" +fi + +if [[ -f "$REMOTE_WORK_DIR/.deleted-paths.txt" ]]; then + remote_log "apply deleted paths" + while IFS= read -r rel; do + [[ -n "$rel" ]] || continue + case "$rel" in + *".."*|/*) + echo "[deploy][remote][warn] skip unsafe delete path: $rel" >&2 + continue + ;; + esac + run_priv rm -rf "$REMOTE_DIR/$rel" + done < "$REMOTE_WORK_DIR/.deleted-paths.txt" +fi + +if [[ -n "${POST_INSTALL_B64:-}" ]]; then + remote_log "run post-install command" + POST_INSTALL_CMD="$(printf '%s' "$POST_INSTALL_B64" | base64 -d)" + cd "$REMOTE_DIR" + if [[ "${USE_SUDO:-0}" == "1" ]]; then + sudo bash -lc "$POST_INSTALL_CMD" + else + bash -lc "$POST_INSTALL_CMD" + fi +fi + +if [[ "${KEEP_REMOTE_TEMP:-0}" != "1" ]]; then + remote_log "clean remote temp files" + rm -rf "$REMOTE_WORK_DIR" "$REMOTE_ARCHIVE" +fi + +remote_log "remote install finished" +'@ + + Write-DeployLog "installing on server: $RemoteDir" + Start-ProcessWithInputText -FilePath $SshBin -Arguments @($HostName, $remoteCommand) -InputText $remoteScript + + Write-DeployLog "deploy finished" + Write-DeployLog "mode: $Mode" + Write-DeployLog "source: $SourceDir" + Write-DeployLog "remote: ${HostName}:$RemoteDir" +} finally { + if ($WorkDir -and (Test-Path -LiteralPath $WorkDir)) { + Remove-Item -LiteralPath $WorkDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/scripts/deploy-server.sh b/scripts/deploy-server.sh new file mode 100644 index 0000000..1761952 --- /dev/null +++ b/scripts/deploy-server.sh @@ -0,0 +1,699 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + ./scripts/deploy-server.sh --remote-dir [options] + +Modes: + 1. Full deploy (no commit ids) + ./scripts/deploy-server.sh --remote-dir /www/wwwroot/test-server.sbnews.net + + 2. Incremental deploy (compare two commits) + ./scripts/deploy-server.sh \ + --from \ + --to \ + --remote-dir /www/wwwroot/test-server.sbnews.net + + 3. Auto incremental deploy (compare latest two commits) + ./scripts/deploy-server.sh \ + --auto-incremental \ + --remote-dir /www/wwwroot/test-server.sbnews.net + +Options: + --source-dir Local source directory, default: server + --remote-dir Remote install directory, required + --host SSH host/alias, default: sbnews + --ssh-bin SSH command, default: ssh. Use ssh.exe in WSL to reuse Windows SSH config + --auto-incremental Incremental deploy using HEAD~1 as --from and HEAD as --to + --from Base commit for incremental deploy + --to Target commit for incremental deploy + --remote-tmp Remote temp directory, default: /tmp + --post-install Run command on server after extract/install + --sudo Use sudo on server when extracting/removing files + --keep-remote-temp Keep remote temp files after install + --include-vendor Include vendor directory in full package + --include-env Include .env in full package + --include-runtime Include runtime directory in full package + --include-uploads Include public/uploads directory in full package + --include-public-builds Include public/admin and public/mobile in full package + --include-crawler-data Include crawler data/logs/venv cache directories in full package + --dry-run Build package and print changed/deleted files, do not upload + -h, --help Show this help + +Notes: + - Full deploy packages only these server paths by default: + app, config, extend, public/dongqiudi-crawler, route. + - Auto incremental deploy compares HEAD~1 and HEAD. + - Incremental deploy compares two commit ids and packages exact file contents from --to. + - If server is a root gitlink without server/.git metadata, incremental deploy falls back + to packaging the current working tree for the default server include paths. + - Deleted files between --from and --to are removed on the server. + - Upload uses ssh stdin instead of scp: ssh "cat > archive" < local.tar.gz +EOF +} + +now_ms() { + if command -v python3 >/dev/null 2>&1; then + python3 - <<'PY' +import time +print(int(time.time() * 1000)) +PY + elif command -v python >/dev/null 2>&1; then + python - <<'PY' +import time +print(int(time.time() * 1000)) +PY + else + printf '%s000\n' "$(date +%s)" + fi +} + +format_duration() { + local ms="$1" + local seconds=$((ms / 1000)) + local millis=$((ms % 1000)) + local minutes=$((seconds / 60)) + seconds=$((seconds % 60)) + + if [[ "$minutes" -gt 0 ]]; then + printf '%dm%02d.%03ds' "$minutes" "$seconds" "$millis" + else + printf '%d.%03ds' "$seconds" "$millis" + fi +} + +DEPLOY_START_MS="$(now_ms)" +DEPLOY_LAST_LOG_MS="$DEPLOY_START_MS" + +log() { + local current_ms + local total_ms + local step_ms + current_ms="$(now_ms)" + total_ms=$((current_ms - DEPLOY_START_MS)) + step_ms=$((current_ms - DEPLOY_LAST_LOG_MS)) + DEPLOY_LAST_LOG_MS="$current_ms" + printf '[deploy][+%s][total %s] %s\n' "$(format_duration "$step_ms")" "$(format_duration "$total_ms")" "$*" +} + +fail() { + printf '[deploy][error] %s\n' "$*" >&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || fail "missing command: $1" +} + +normalize_dir() { + local value="$1" + value="${value%/}" + printf '%s' "$value" +} + +safe_remote_single_quote() { + printf "%s" "$1" | sed "s/'/'\\\\''/g" +} + +SOURCE_DIR="server" +REMOTE_DIR="" +HOST="sbnews" +SSH_BIN="ssh" +AUTO_INCREMENTAL=0 +FROM_COMMIT="" +TO_COMMIT="" +REMOTE_TMP="/tmp" +POST_INSTALL="" +USE_SUDO=0 +KEEP_REMOTE_TEMP=0 +INCLUDE_VENDOR=0 +INCLUDE_ENV=0 +INCLUDE_RUNTIME=0 +INCLUDE_UPLOADS=0 +INCLUDE_PUBLIC_BUILDS=0 +INCLUDE_CRAWLER_DATA=0 +DRY_RUN=0 +FULL_INCLUDE_PATHS=( + "app" + "config" + "extend" + "public/dongqiudi-crawler" + "route" +) + +while [[ $# -gt 0 ]]; do + case "$1" in + --source-dir) + [[ $# -ge 2 ]] || fail "--source-dir requires a value" + SOURCE_DIR="$2" + shift 2 + ;; + --remote-dir) + [[ $# -ge 2 ]] || fail "--remote-dir requires a value" + REMOTE_DIR="$2" + shift 2 + ;; + --host) + [[ $# -ge 2 ]] || fail "--host requires a value" + HOST="$2" + shift 2 + ;; + --ssh-bin) + [[ $# -ge 2 ]] || fail "--ssh-bin requires a value" + SSH_BIN="$2" + shift 2 + ;; + --auto-incremental) + AUTO_INCREMENTAL=1 + shift + ;; + --from) + [[ $# -ge 2 ]] || fail "--from requires a value" + FROM_COMMIT="$2" + shift 2 + ;; + --to) + [[ $# -ge 2 ]] || fail "--to requires a value" + TO_COMMIT="$2" + shift 2 + ;; + --remote-tmp) + [[ $# -ge 2 ]] || fail "--remote-tmp requires a value" + REMOTE_TMP="$2" + shift 2 + ;; + --post-install) + [[ $# -ge 2 ]] || fail "--post-install requires a value" + POST_INSTALL="$2" + shift 2 + ;; + --sudo) + USE_SUDO=1 + shift + ;; + --keep-remote-temp) + KEEP_REMOTE_TEMP=1 + shift + ;; + --include-vendor) + INCLUDE_VENDOR=1 + shift + ;; + --include-env) + INCLUDE_ENV=1 + shift + ;; + --include-runtime) + INCLUDE_RUNTIME=1 + shift + ;; + --include-uploads) + INCLUDE_UPLOADS=1 + shift + ;; + --include-public-builds) + INCLUDE_PUBLIC_BUILDS=1 + shift + ;; + --include-crawler-data) + INCLUDE_CRAWLER_DATA=1 + shift + ;; + --dry-run) + DRY_RUN=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "unknown argument: $1" + ;; + esac +done + +[[ -n "$REMOTE_DIR" ]] || { + usage + fail "--remote-dir is required" +} + +SOURCE_DIR="$(normalize_dir "$SOURCE_DIR")" +REMOTE_DIR="$(normalize_dir "$REMOTE_DIR")" +REMOTE_TMP="$(normalize_dir "$REMOTE_TMP")" + +if [[ "$AUTO_INCREMENTAL" -eq 1 ]]; then + [[ -z "$FROM_COMMIT" && -z "$TO_COMMIT" ]] || fail "--auto-incremental cannot be used with --from/--to" + FROM_COMMIT="HEAD~1" + TO_COMMIT="HEAD" + MODE="incremental" +elif [[ -n "$FROM_COMMIT" || -n "$TO_COMMIT" ]]; then + [[ -n "$FROM_COMMIT" && -n "$TO_COMMIT" ]] || fail "incremental deploy requires both --from and --to" + MODE="incremental" +else + MODE="full" +fi + +require_cmd git +require_cmd tar +require_cmd "$SSH_BIN" +if [[ -n "$POST_INSTALL" ]]; then + require_cmd base64 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" +SOURCE_ABS="$REPO_ROOT/$SOURCE_DIR" + +cd "$REPO_ROOT" + +[[ -d "$SOURCE_ABS" ]] || fail "source directory not found: $SOURCE_DIR" + +GIT_CWD="$REPO_ROOT" +GIT_PATH_PREFIX="$SOURCE_DIR" +SOURCE_IS_GIT_REPO=0 +SOURCE_IS_GITLINK=0 +if SOURCE_GIT_TOP="$(git -C "$SOURCE_ABS" rev-parse --show-toplevel 2>/dev/null)" && [[ "$(cd "$SOURCE_GIT_TOP" && pwd -P)" == "$SOURCE_ABS" ]]; then + GIT_CWD="$SOURCE_ABS" + GIT_PATH_PREFIX="" + SOURCE_IS_GIT_REPO=1 +elif ! git -C "$REPO_ROOT" rev-parse --show-toplevel >/dev/null 2>&1; then + [[ "$MODE" == "full" ]] || fail "incremental deploy requires git metadata in $SOURCE_DIR or repo root" +fi + +if [[ "$SOURCE_IS_GIT_REPO" -ne 1 ]] && git -C "$REPO_ROOT" ls-files -s -- "$SOURCE_DIR" | awk '{print $1}' | grep -qx '160000'; then + SOURCE_IS_GITLINK=1 +fi + +if [[ "$MODE" == "incremental" ]]; then + git -C "$GIT_CWD" rev-parse --verify "${FROM_COMMIT}^{commit}" >/dev/null 2>&1 || fail "invalid --from commit: $FROM_COMMIT" + git -C "$GIT_CWD" rev-parse --verify "${TO_COMMIT}^{commit}" >/dev/null 2>&1 || fail "invalid --to commit: $TO_COMMIT" + FROM_COMMIT="$(git -C "$GIT_CWD" rev-parse "$FROM_COMMIT")" + TO_COMMIT="$(git -C "$GIT_CWD" rev-parse "$TO_COMMIT")" +fi + +mkdir -p "$REPO_ROOT/.tmp" +WORK_DIR="$(mktemp -d "$REPO_ROOT/.tmp/deploy-server.XXXXXX")" +PACKAGE_ROOT="$WORK_DIR/package" +PAYLOAD_DIR="$PACKAGE_ROOT/payload" +EXTRACT_DIR="$WORK_DIR/extract" +ARCHIVE_PATH="$WORK_DIR/${SOURCE_DIR//\//-}-${MODE}.tar.gz" +DELETED_PATHS_FILE="$PACKAGE_ROOT/.deleted-paths.txt" +DEPLOY_INFO_FILE="$PACKAGE_ROOT/.deploy-info.txt" +CHANGED_LOG="$WORK_DIR/changed-paths.txt" +DELETED_LOG="$WORK_DIR/deleted-paths.txt" + +cleanup_local() { + if [[ -n "${WORK_DIR:-}" && -d "$WORK_DIR" ]]; then + chmod -R u+rwX "$WORK_DIR" 2>/dev/null || true + rm -rf "$WORK_DIR" || true + fi +} +trap cleanup_local EXIT + +mkdir -p "$PAYLOAD_DIR" "$EXTRACT_DIR" +touch "$DELETED_PATHS_FILE" "$CHANGED_LOG" "$DELETED_LOG" + +copy_full_payload() { + local tar_args=() + tar_args+=(--exclude=.git) + tar_args+=(--exclude=.idea) + tar_args+=(--exclude=.vscode) + tar_args+=(--exclude=.tmp) + tar_args+=(--exclude=.gitignore) + tar_args+=(--exclude='*.log') + tar_args+=(--exclude='release-*.zip') + tar_args+=(--exclude='*.zip') + tar_args+=(--exclude='__pycache__') + tar_args+=(--exclude='*.pyc') + + if [[ "$INCLUDE_ENV" -ne 1 ]]; then + tar_args+=(--exclude=.env) + fi + + if [[ "$INCLUDE_VENDOR" -ne 1 ]]; then + tar_args+=(--exclude=vendor) + fi + + if [[ "$INCLUDE_RUNTIME" -ne 1 ]]; then + tar_args+=(--exclude=runtime) + fi + + if [[ "$INCLUDE_UPLOADS" -ne 1 ]]; then + tar_args+=(--exclude=public/uploads) + fi + + if [[ "$INCLUDE_PUBLIC_BUILDS" -ne 1 ]]; then + tar_args+=(--exclude=public/admin) + tar_args+=(--exclude=public/mobile) + fi + + if [[ "$INCLUDE_CRAWLER_DATA" -ne 1 ]]; then + tar_args+=(--exclude=public/dongqiudi-crawler/data) + tar_args+=(--exclude=public/dongqiudi-crawler/logs) + tar_args+=(--exclude=public/dongqiudi-crawler/venv) + tar_args+=(--exclude=public/dongqiudi-crawler/__pycache__) + fi + + log "building full payload from current working tree: $SOURCE_DIR" + log "full include paths: ${FULL_INCLUDE_PATHS[*]}" + ( + cd "$SOURCE_ABS" + tar "${tar_args[@]}" -cf - "${FULL_INCLUDE_PATHS[@]}" + ) | ( + cd "$PAYLOAD_DIR" + tar -xf - + ) +} + +strip_source_prefix() { + local path="$1" + if [[ -z "$GIT_PATH_PREFIX" ]]; then + printf '%s' "$path" + else + printf '%s' "${path#"$GIT_PATH_PREFIX"/}" + fi +} + +is_under_source_dir() { + local path="$1" + if [[ -z "$GIT_PATH_PREFIX" ]]; then + is_deploy_included_path "$path" + return $? + fi + [[ "$path" == "$GIT_PATH_PREFIX/"* ]] || return 1 + is_deploy_included_path "$(strip_source_prefix "$path")" +} + +is_deploy_included_path() { + local rel="$1" + local include + for include in "${FULL_INCLUDE_PATHS[@]}"; do + if [[ "$rel" == "$include" || "$rel" == "$include/"* ]]; then + return 0 + fi + done + return 1 +} + +short_commit() { + git -C "$GIT_CWD" rev-parse --short "$1" 2>/dev/null || printf '%s' "$1" +} + +print_incremental_file_list() { + local title="$1" + local prefix="$2" + local file="$3" + + log "$title" + if [[ -s "$file" ]]; then + sort -u "$file" | sed '/^$/d' | sed "s/^/ $prefix /" + else + printf ' (none)\n' + fi +} + +print_update_log() { + local from_short="" + local to_short="" + local commit_lines="" + + if [[ "$MODE" == "incremental" ]]; then + from_short="$(short_commit "$FROM_COMMIT")" + to_short="$(short_commit "$TO_COMMIT")" + + log "update log:" + log "commit range: $from_short -> $to_short" + + if [[ "$SOURCE_IS_GITLINK" -eq 1 ]]; then + log "$SOURCE_DIR is a root gitlink, showing root repository commits for the selected range" + commit_lines="$(git -C "$GIT_CWD" log --no-merges --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' "$FROM_COMMIT..$TO_COMMIT" 2>/dev/null || true)" + elif [[ -n "$GIT_PATH_PREFIX" ]]; then + commit_lines="$(git -C "$GIT_CWD" log --no-merges --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' "$FROM_COMMIT..$TO_COMMIT" -- "$GIT_PATH_PREFIX" 2>/dev/null || true)" + else + commit_lines="$(git -C "$GIT_CWD" log --no-merges --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' "$FROM_COMMIT..$TO_COMMIT" 2>/dev/null || true)" + fi + + log "commits:" + if [[ -n "$commit_lines" ]]; then + printf '%s\n' "$commit_lines" + else + printf ' (none)\n' + fi + + print_incremental_file_list "changed files:" "+" "$CHANGED_LOG" + print_incremental_file_list "deleted files:" "-" "$DELETED_LOG" + else + log "update log:" + log "mode: full deploy from current working tree" + log "full include paths: ${FULL_INCLUDE_PATHS[*]}" + commit_lines="$(git -C "$GIT_CWD" log -1 --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' 2>/dev/null || true)" + log "latest commit:" + if [[ -n "$commit_lines" ]]; then + printf '%s\n' "$commit_lines" + else + printf ' (none)\n' + fi + fi +} + +build_incremental_payload() { + local raw_diff="$WORK_DIR/diff-name-status.txt" + local archive_include="$WORK_DIR/incremental.tar" + local diff_args=() + + if [[ "$SOURCE_IS_GITLINK" -eq 1 ]]; then + log "$SOURCE_DIR is tracked as a root gitlink; exact file-level diff is unavailable without $SOURCE_DIR/.git" + log "building fallback payload from current working tree include paths: ${FULL_INCLUDE_PATHS[*]}" + printf '%s\n' "${FULL_INCLUDE_PATHS[@]}" > "$CHANGED_LOG" + copy_full_payload + return + fi + + if [[ -n "$GIT_PATH_PREFIX" ]]; then + diff_args+=(-- "$GIT_PATH_PREFIX") + fi + + git -C "$GIT_CWD" diff --name-status --find-renames "$FROM_COMMIT" "$TO_COMMIT" "${diff_args[@]}" > "$raw_diff" + + while IFS=$'\t' read -r status path1 path2; do + [[ -n "${status:-}" ]] || continue + case "$status" in + D) + is_under_source_dir "$path1" || continue + strip_source_prefix "$path1" >> "$DELETED_LOG" + ;; + R*|C*) + [[ -n "${path2:-}" ]] || continue + is_under_source_dir "$path2" || continue + printf '%s\n' "$path2" >> "$CHANGED_LOG" + if [[ "$status" == R* ]] && is_under_source_dir "$path1"; then + strip_source_prefix "$path1" >> "$DELETED_LOG" + fi + ;; + *) + is_under_source_dir "$path1" || continue + printf '%s\n' "$path1" >> "$CHANGED_LOG" + ;; + esac + done < "$raw_diff" + + mapfile -t changed_paths < <(sort -u "$CHANGED_LOG" | sed '/^$/d') + mapfile -t deleted_paths < <(sort -u "$DELETED_LOG" | sed '/^$/d') + + if [[ "${#changed_paths[@]}" -eq 0 && "${#deleted_paths[@]}" -eq 0 ]]; then + log "no changed files detected between $FROM_COMMIT and $TO_COMMIT under $SOURCE_DIR" + exit 0 + fi + + if [[ "${#deleted_paths[@]}" -gt 0 ]]; then + printf '%s\n' "${deleted_paths[@]}" > "$DELETED_PATHS_FILE" + fi + + if [[ "${#changed_paths[@]}" -gt 0 ]]; then + log "building incremental payload from commit $TO_COMMIT" + git -C "$GIT_CWD" archive --format=tar --output="$archive_include" "$TO_COMMIT" -- "${changed_paths[@]}" + tar -xf "$archive_include" -C "$EXTRACT_DIR" + + if [[ "$SOURCE_IS_GIT_REPO" -eq 1 ]]; then + ( + cd "$EXTRACT_DIR" + tar -cf - . + ) | ( + cd "$PAYLOAD_DIR" + tar -xf - + ) + elif [[ -d "$EXTRACT_DIR/$SOURCE_DIR" ]]; then + ( + cd "$EXTRACT_DIR/$SOURCE_DIR" + tar -cf - . + ) | ( + cd "$PAYLOAD_DIR" + tar -xf - + ) + fi + fi +} + +if [[ "$MODE" == "full" ]]; then + copy_full_payload +else + build_incremental_payload +fi + +print_update_log + +{ + printf 'mode=%s\n' "$MODE" + printf 'source_dir=%s\n' "$SOURCE_DIR" + printf 'host=%s\n' "$HOST" + printf 'remote_dir=%s\n' "$REMOTE_DIR" + printf 'built_at=%s\n' "$(date '+%Y-%m-%d %H:%M:%S')" + if [[ "$MODE" == "incremental" ]]; then + printf 'auto_incremental=%s\n' "$AUTO_INCREMENTAL" + printf 'source_is_gitlink=%s\n' "$SOURCE_IS_GITLINK" + printf 'from_commit=%s\n' "$FROM_COMMIT" + printf 'to_commit=%s\n' "$TO_COMMIT" + printf 'changed_count=%s\n' "$(sort -u "$CHANGED_LOG" | sed '/^$/d' | wc -l | tr -d ' ')" + printf 'deleted_count=%s\n' "$(sort -u "$DELETED_LOG" | sed '/^$/d' | wc -l | tr -d ' ')" + fi +} > "$DEPLOY_INFO_FILE" + +log "packing archive: $ARCHIVE_PATH" +( + cd "$PACKAGE_ROOT" + tar -czf "$ARCHIVE_PATH" . +) + +log "package size: $(du -h "$ARCHIVE_PATH" | awk '{print $1}')" + +if [[ "$DRY_RUN" -eq 1 ]]; then + log "dry-run enabled, skip upload/install" + exit 0 +fi + +TIMESTAMP="$(date '+%Y%m%d-%H%M%S')" +REMOTE_WORK_DIR="$REMOTE_TMP/${SOURCE_DIR//\//-}-deploy-$TIMESTAMP-$$" +REMOTE_ARCHIVE="$REMOTE_TMP/${SOURCE_DIR//\//-}-${MODE}-$TIMESTAMP.tar.gz" +REMOTE_TMP_Q="$(safe_remote_single_quote "$REMOTE_TMP")" +REMOTE_ARCHIVE_Q="$(safe_remote_single_quote "$REMOTE_ARCHIVE")" + +log "uploading archive to $HOST:$REMOTE_ARCHIVE" +"$SSH_BIN" "$HOST" "mkdir -p '$REMOTE_TMP_Q' && cat > '$REMOTE_ARCHIVE_Q'" < "$ARCHIVE_PATH" + +POST_INSTALL_B64="" +if [[ -n "$POST_INSTALL" ]]; then + POST_INSTALL_B64="$(printf '%s' "$POST_INSTALL" | base64 | tr -d '\r\n')" +fi + +log "installing on server: $REMOTE_DIR" +"$SSH_BIN" "$HOST" \ + "REMOTE_DIR='$(safe_remote_single_quote "$REMOTE_DIR")' REMOTE_ARCHIVE='$(safe_remote_single_quote "$REMOTE_ARCHIVE")' REMOTE_WORK_DIR='$(safe_remote_single_quote "$REMOTE_WORK_DIR")' KEEP_REMOTE_TEMP='$KEEP_REMOTE_TEMP' POST_INSTALL_B64='$POST_INSTALL_B64' USE_SUDO='$USE_SUDO' bash -s" <<'REMOTE_SCRIPT' +set -euo pipefail + +remote_now_ms() { + if command -v python3 >/dev/null 2>&1; then + python3 - <<'PY' +import time +print(int(time.time() * 1000)) +PY + elif command -v python >/dev/null 2>&1; then + python - <<'PY' +import time +print(int(time.time() * 1000)) +PY + else + printf '%s000\n' "$(date +%s)" + fi +} + +remote_format_duration() { + local ms="$1" + local seconds=$((ms / 1000)) + local millis=$((ms % 1000)) + local minutes=$((seconds / 60)) + seconds=$((seconds % 60)) + + if [[ "$minutes" -gt 0 ]]; then + printf '%dm%02d.%03ds' "$minutes" "$seconds" "$millis" + else + printf '%d.%03ds' "$seconds" "$millis" + fi +} + +REMOTE_START_MS="$(remote_now_ms)" +REMOTE_LAST_LOG_MS="$REMOTE_START_MS" + +remote_log() { + local current_ms + local total_ms + local step_ms + current_ms="$(remote_now_ms)" + total_ms=$((current_ms - REMOTE_START_MS)) + step_ms=$((current_ms - REMOTE_LAST_LOG_MS)) + REMOTE_LAST_LOG_MS="$current_ms" + printf '[deploy][remote][+%s][total %s] %s\n' "$(remote_format_duration "$step_ms")" "$(remote_format_duration "$total_ms")" "$*" +} + +run_priv() { + if [[ "${USE_SUDO:-0}" == "1" ]]; then + sudo "$@" + else + "$@" + fi +} + +tar_extract_flags=(--no-same-owner --no-same-permissions --no-overwrite-dir) + +remote_log "prepare remote work dir: $REMOTE_WORK_DIR" +mkdir -p "$REMOTE_WORK_DIR" +remote_log "extract uploaded archive" +tar -xzf "$REMOTE_ARCHIVE" -C "$REMOTE_WORK_DIR" +remote_log "ensure remote dir: $REMOTE_DIR" +run_priv mkdir -p "$REMOTE_DIR" + +if [[ -d "$REMOTE_WORK_DIR/payload" ]]; then + remote_log "install payload into remote dir" + tar -cf - -C "$REMOTE_WORK_DIR/payload" . | run_priv tar "${tar_extract_flags[@]}" -xf - -C "$REMOTE_DIR" +fi + +if [[ -f "$REMOTE_WORK_DIR/.deleted-paths.txt" ]]; then + remote_log "apply deleted paths" + while IFS= read -r rel; do + [[ -n "$rel" ]] || continue + case "$rel" in + *".."*|/*) + echo "[deploy][remote][warn] skip unsafe delete path: $rel" >&2 + continue + ;; + esac + run_priv rm -rf "$REMOTE_DIR/$rel" + done < "$REMOTE_WORK_DIR/.deleted-paths.txt" +fi + +if [[ -n "${POST_INSTALL_B64:-}" ]]; then + remote_log "run post-install command" + POST_INSTALL_CMD="$(printf '%s' "$POST_INSTALL_B64" | base64 -d)" + cd "$REMOTE_DIR" + if [[ "${USE_SUDO:-0}" == "1" ]]; then + sudo bash -lc "$POST_INSTALL_CMD" + else + bash -lc "$POST_INSTALL_CMD" + fi +fi + +if [[ "${KEEP_REMOTE_TEMP:-0}" != "1" ]]; then + remote_log "clean remote temp files" + rm -rf "$REMOTE_WORK_DIR" "$REMOTE_ARCHIVE" +fi + +remote_log "remote install finished" +REMOTE_SCRIPT + +log "deploy finished" +log "mode: $MODE" +log "source: $SOURCE_DIR" +log "remote: $HOST:$REMOTE_DIR" diff --git a/server b/server deleted file mode 160000 index 187d583..0000000 --- a/server +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 187d583d217f5c4ebd162fe45dd7bd03253a2690 diff --git a/server/.example.env b/server/.example.env new file mode 100644 index 0000000..e73b326 --- /dev/null +++ b/server/.example.env @@ -0,0 +1,24 @@ +APP_DEBUG = true +DASHSCOPE_API_KEY = your_dashscope_api_key + +[APP] +DEFAULT_TIMEZONE = Asia/Shanghai + +[DATABASE] +TYPE = mysql +HOSTNAME = 127.0.0.1 +DATABASE = test +USERNAME = username +PASSWORD = password +HOSTPORT = 3306 +CHARSET = utf8mb4 +DEBUG = true +PREFIX = la_ + +[LANG] +default_lang = zh-cn + +[PROJECT] +UNIQUE_IDENTIFICATION = likeadmin +# 演示环境 +DEMO_ENV = false diff --git a/server/.gitee/ISSUE_TEMPLATE.zh-CN.md b/server/.gitee/ISSUE_TEMPLATE.zh-CN.md new file mode 100644 index 0000000..f09d98d --- /dev/null +++ b/server/.gitee/ISSUE_TEMPLATE.zh-CN.md @@ -0,0 +1,13 @@ +### 该问题是怎么引起的? + + + +### 重现步骤 + + + +### 报错信息 + + + + diff --git a/server/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md b/server/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md new file mode 100644 index 0000000..6199c1e --- /dev/null +++ b/server/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md @@ -0,0 +1,12 @@ +### 相关的Issue + + +### 原因(目的、解决的问题等) + + +### 描述(做了什么,变更了什么) + + +### 测试用例(新增、改动、可能影响的功能) + + diff --git a/server/.github/workflows/truth-social-crawler.yml b/server/.github/workflows/truth-social-crawler.yml new file mode 100644 index 0000000..183058d --- /dev/null +++ b/server/.github/workflows/truth-social-crawler.yml @@ -0,0 +1,43 @@ +# 已停用:改由 la_dev_crontab 定时任务调用(params=crawl) +# 原配置保留供参考,不删除以便回溯 +# +# name: Truth Social Crawler +# +# on: +# schedule: +# # 每小时执行一次 (UTC 时间) +# - cron: '0 * * * *' +# workflow_dispatch: # 允许手动触发 +# +# jobs: +# crawl: +# runs-on: ubuntu-latest +# permissions: +# contents: write +# +# steps: +# - name: Checkout +# uses: actions/checkout@v4 +# +# - name: Setup Python +# uses: actions/setup-python@v5 +# with: +# python-version: '3.12' +# +# - name: Install dependencies +# run: pip install -r public/truth-social/requirements.txt +# +# - name: Run crawler +# run: python public/truth-social/crawler.py +# +# - name: Commit & Push data +# run: | +# git config user.name "github-actions[bot]" +# git config user.email "github-actions[bot]@users.noreply.github.com" +# git add public/truth-social/data/ +# if git diff --staged --quiet; then +# echo "No new data to commit" +# else +# git commit -m "data: update truth social posts $(date -u +%Y-%m-%dT%H:%M:%SZ)" +# git push +# fi diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..51c56f5 Binary files /dev/null and b/server/.gitignore differ diff --git a/server/.travis.yml b/server/.travis.yml new file mode 100644 index 0000000..36f7b6f --- /dev/null +++ b/server/.travis.yml @@ -0,0 +1,42 @@ +sudo: false + +language: php + +branches: + only: + - stable + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - composer self-update + +install: + - composer install --no-dev --no-interaction --ignore-platform-reqs + - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip . + - composer require --update-no-dev --no-interaction "topthink/think-image:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0" + - composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0" + - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip . + +script: + - php think unit + +deploy: + provider: releases + api_key: + secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw= + file: + - ThinkPHP_Core.zip + - ThinkPHP_Full.zip + skip_cleanup: true + on: + tags: true diff --git a/server/AGENTS.md b/server/AGENTS.md new file mode 100644 index 0000000..ac6dcfb --- /dev/null +++ b/server/AGENTS.md @@ -0,0 +1,116 @@ +# Server Agent — Sport Era 后端 + +ThinkPHP / likeadmin 多应用后台系统。**本模块是项目核心,定义 adminapi、api、定时任务、数据库模型与业务逻辑,其他端依赖此模块。** + +## 技术栈 + +- **PHP** 8.x +- **框架** ThinkPHP + likeadmin +- **数据库** MySQL(库名 `sbnews`,表前缀 `la_`) +- **缓存** Redis(ThinkPHP Cache facade) +- **Web 服务** Nginx + PHP-FPM + +## 项目结构 + +```text +app/ +├── adminapi/ # 后台管理 API → admin/ 消费 +├── api/ # 前台公开 API → uniapp/ + pc/ 消费 +├── common/ # 公共模型/服务/缓存/中间件/监听器/枚举 +├── command/ # 控制台命令与定时任务 +├── service/ # 公共服务封装 +├── common.php # 全局函数 +├── middleware.php # 全局中间件 +└── event.php # 事件监听 +config/ # 框架配置 +route/ # 路由定义 +public/ # Web 入口、上传目录、脚本与静态资源 +``` + +## 模块职责 + +| 应用 | 路由前缀 | 消费方 | 控制器目录 | +| -------- | ------------- | --------------- | -------------------------- | +| adminapi | `/adminapi/*` | admin/ 管理后台 | `app/adminapi/controller/` | +| api | `/api/*` | uniapp/ + pc/ | `app/api/controller/` | + +## 目录约定 + +- `controller/` — 控制器(类名后缀 `Controller`) +- `logic/` — 业务逻辑层 +- `service/` — 服务层 +- `validate/` — 验证器层 +- `lists/` — 列表查询封装 +- `model/` — ORM 模型 +- `cache/` — Redis 缓存 +- `http/middleware/` — 中间件 + +## 运行命令 + +```bash +composer install +php think run +php think crontab +``` + +--- + +## 并行协作协议 + +### 新功能开发流程 + +```text +Step 1: 后端定义契约 + + 1.1 确认是否已有数据表、模型、字典或配置可复用 + 1.2 创建或调整 Model / Logic / Validate / Lists / Controller + 1.3 输出接口清单: + + ## 新功能: XXX + ### adminapi 接口 + | 方法 | 路径 | 说明 | + | GET | /adminapi/xxx/lists | 列表 | + | POST | /adminapi/xxx/add | 新增 | + | POST | /adminapi/xxx/edit | 编辑 | + | POST | /adminapi/xxx/del | 删除 | + + ### api 接口 + | 方法 | 路径 | 说明 | + | GET | /api/xxx/lists | 前台列表 | + | GET | /api/xxx/detail | 前台详情 | + +Step 2: 前端并行消费契约 + + ┌─ server/ 实现 controller + logic + validate + lists + ├─ admin/ 查看 adminapi controller → 写管理页面 + ├─ uniapp/ 查看 api controller → 写手机端页面 + └─ pc/ 查看 api controller → 写 PC 页面 + +Step 3: 集成联调与文档同步 + + - 检查接口响应格式、权限、字段、字典 + - 同步更新 `业务进度管理.md` +``` + +## 与前端 Agent 的协作方式 + +1. **API 契约即文档**:前端优先查看 controller、validate、logic 获取接口签名、请求参数、响应结构。 +2. **Model 即数据结构**:前端查看 `app/common/model/` 了解字段与访问器。 +3. **枚举共享**:业务枚举优先使用字典表或配置表,不在 PHP / TS 中重复硬编码。 +4. **上传接口统一**:后台使用 adminapi 上传接口,前台使用 api 上传接口,沿用现有封装。 + +## 后端开发硬性规范 + +1. **字典/配置驱动枚举**:状态、类型、分类、开关等优先落入字典表或配置表。 +2. **列表查询沿用 Lists 封装**:后台列表页继承 `BaseAdminDataLists`,实现 `lists()` 和 `count()`。 +3. **控制器沿用基类能力**:后台控制器继承 `BaseAdminController`,列表使用 `$this->dataLists()`。 +4. **菜单与权限**:新增管理后台模块必须同步菜单和按钮权限,SQL 初始化必须幂等。 +5. **接口响应格式**:沿用 likeadmin 统一响应结构,不随意增减顶层字段。 +6. **时间字段处理**:BaseModel 自动转换 `create_time` / `update_time`,不要对已转字符串再次 `date()`。 +7. **SQL 保留字**:原生 SQL 中 `system`、`order`、`group`、`key` 等字段必须用反引号包裹。 +8. **定时任务**:注册在 `la_dev_crontab`,通过 `php think crontab` 统一调度。 + +## 近期踩坑 + +- `Validate::only()` 只限制校验字段范围,不会自动过滤请求参数;新增字段要同时检查 validate、logic、model、保存白名单和数据库列。 +- `adminapi/match.match/edit` 保存失败时,先确认测试库或线上库 `la_match` 是否真的已补上目标字段,测试库缺列会直接导致写入失败。 diff --git a/server/LICENSE.txt b/server/LICENSE.txt new file mode 100644 index 0000000..574a39c --- /dev/null +++ b/server/LICENSE.txt @@ -0,0 +1,32 @@ + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 +版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) +All rights reserved。 +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +Apache Licence是著名的非盈利开源组织Apache采用的协议。 +该协议和BSD类似,鼓励代码共享和尊重原作者的著作权, +允许代码修改,再作为开源或商业软件发布。需要满足 +的条件: +1. 需要给代码的用户一份Apache Licence ; +2. 如果你修改了代码,需要在被修改的文件中说明; +3. 在延伸的代码中(修改和有源代码衍生的代码中)需要 +带有原来代码中的协议,商标,专利声明和其他原来作者规 +定需要包含的说明; +4. 如果再发布的产品中包含一个Notice文件,则在Notice文 +件中需要带有本协议内容。你可以在Notice中增加自己的 +许可,但不可以表现为对Apache Licence构成更改。 +具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0 + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/server/README.en.md b/server/README.en.md new file mode 100644 index 0000000..43844ba --- /dev/null +++ b/server/README.en.md @@ -0,0 +1,36 @@ +# sport-era-service + +#### Description +{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**} + +#### Software Architecture +Software architecture description + +#### Installation + +1. xxxx +2. xxxx +3. xxxx + +#### Instructions + +1. xxxx +2. xxxx +3. xxxx + +#### Contribution + +1. Fork the repository +2. Create Feat_xxx branch +3. Commit your code +4. Create Pull Request + + +#### Gitee Feature + +1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md +2. Gitee blog [blog.gitee.com](https://blog.gitee.com) +3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) +4. The most valuable open source project [GVP](https://gitee.com/gvp) +5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) +6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..e85a1cf --- /dev/null +++ b/server/README.md @@ -0,0 +1,40 @@ +# sport-era-service + +#### 介绍 + +{**以下是 Gitee 平台说明,您可以替换此简介** +Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台 +无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)} + +#### 软件架构 + +软件架构说明 + +#### 安装教程 + +上传项目 通过 rsync + scp 将完整 dongqiudi-crawler/ 上传到服务器 +安装 pip curl get-pip.py | python3 → pip 26.0.1 +安装依赖 pip3 install aiohttp requests httpx ... 等核心依赖(跳过了 playwright/curl_cffi 等重量级浏览器包,API 通道足够用) +验证运行 python3 main.py test ✅ 全部通过(API通道、比赛菜单、数据库连接) + +#### 使用说明 + +1. xxxx +2. xxxx +3. xxxx + +#### 参与贡献 + +1. Fork 本仓库 +2. 新建 Feat_xxx 分支 +3. 提交代码 +4. 新建 Pull Request + +#### 特技 + +1. 使用 Readme_XXX.md 来支持不同的语言,例如 Readme_en.md, Readme_zh.md +2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) +3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 +4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 +5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) +6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/server/app/.htaccess b/server/app/.htaccess new file mode 100644 index 0000000..3418e55 --- /dev/null +++ b/server/app/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/server/app/AppService.php b/server/app/AppService.php new file mode 100644 index 0000000..96556e8 --- /dev/null +++ b/server/app/AppService.php @@ -0,0 +1,22 @@ +app = $app; + $this->request = $this->app->request; + + // 控制器初始化 + $this->initialize(); + } + + // 初始化 + protected function initialize() + {} + + /** + * 验证数据 + * @access protected + * @param array $data 数据 + * @param string|array $validate 验证器名或者验证规则数组 + * @param array $message 提示信息 + * @param bool $batch 是否批量验证 + * @return array|string|true + * @throws ValidateException + */ + protected function validate(array $data, $validate, array $message = [], bool $batch = false) + { + if (is_array($validate)) { + $v = new Validate(); + $v->rule($validate); + } else { + if (strpos($validate, '.')) { + // 支持场景 + [$validate, $scene] = explode('.', $validate); + } + $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate); + $v = new $class(); + if (!empty($scene)) { + $v->scene($scene); + } + } + + $v->message($message); + + // 是否批量验证 + if ($batch || $this->batchValidate) { + $v->batch(true); + } + + return $v->failException(true)->check($data); + } + + +} diff --git a/server/app/ExceptionHandle.php b/server/app/ExceptionHandle.php new file mode 100644 index 0000000..453d126 --- /dev/null +++ b/server/app/ExceptionHandle.php @@ -0,0 +1,58 @@ + [ + // 初始化 + app\adminapi\http\middleware\InitMiddleware::class, + // 登录验证 + app\adminapi\http\middleware\LoginMiddleware::class, + // 权限认证 + app\adminapi\http\middleware\AuthMiddleware::class, + // 演示模式 - 禁止提交数据 + app\adminapi\http\middleware\CheckDemoMiddleware::class, + // 演示模式 - 不返回敏感数据 + app\adminapi\http\middleware\EncryDemoDataMiddleware::class, + ], +]; diff --git a/server/app/adminapi/controller/BaseAdminController.php b/server/app/adminapi/controller/BaseAdminController.php new file mode 100644 index 0000000..66e8cce --- /dev/null +++ b/server/app/adminapi/controller/BaseAdminController.php @@ -0,0 +1,40 @@ +request->adminInfo) && $this->request->adminInfo) { + $this->adminInfo = $this->request->adminInfo; + $this->adminId = $this->request->adminInfo['admin_id']; + } + } + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/ConfigController.php b/server/app/adminapi/controller/ConfigController.php new file mode 100644 index 0000000..bce4705 --- /dev/null +++ b/server/app/adminapi/controller/ConfigController.php @@ -0,0 +1,61 @@ +data($data); + } + + + /** + * @notes 根据类型获取字典数据 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/27 19:10 + */ + public function dict() + { + $type = $this->request->get('type', ''); + $data = ConfigLogic::getDictByType($type); + return $this->data($data); + } + + + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/DownloadController.php b/server/app/adminapi/controller/DownloadController.php new file mode 100644 index 0000000..b61e81c --- /dev/null +++ b/server/app/adminapi/controller/DownloadController.php @@ -0,0 +1,50 @@ +get('file'); + + //通过文件缓存的key获取文件储存的路径 + $exportCache = new ExportCache(); + $fileInfo = $exportCache->getFile($fileKey); + + if (empty($fileInfo)) { + return JsonService::fail('下载文件不存在'); + } + + //下载前删除缓存 + $exportCache->delete($fileKey); + + return download($fileInfo['src'] . $fileInfo['name'], $fileInfo['name']); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/FileController.php b/server/app/adminapi/controller/FileController.php new file mode 100644 index 0000000..830bef1 --- /dev/null +++ b/server/app/adminapi/controller/FileController.php @@ -0,0 +1,137 @@ +dataLists(new FileLists()); + } + + + /** + * @notes 文件移动成功 + * @return Json + * @author 段誉 + * @date 2021/12/29 14:30 + */ + public function move() + { + $params = (new FileValidate())->post()->goCheck('move'); + FileLogic::move($params); + return $this->success('移动成功', [], 1, 1); + } + + + /** + * @notes 重命名文件 + * @return Json + * @author 段誉 + * @date 2021/12/29 14:31 + */ + public function rename() + { + $params = (new FileValidate())->post()->goCheck('rename'); + FileLogic::rename($params); + return $this->success('重命名成功', [], 1, 1); + } + + + /** + * @notes 删除文件 + * @return Json + * @author 段誉 + * @date 2021/12/29 14:31 + */ + public function delete() + { + $params = (new FileValidate())->post()->goCheck('delete'); + FileLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + + /** + * @notes 分类列表 + * @return Json + * @author 段誉 + * @date 2021/12/29 14:31 + */ + public function listCate() + { + return $this->dataLists(new FileCateLists()); + } + + + /** + * @notes 添加文件分类 + * @return Json + * @author 段誉 + * @date 2021/12/29 14:31 + */ + public function addCate() + { + $params = (new FileValidate())->post()->goCheck('addCate'); + FileLogic::addCate($params); + return $this->success('添加成功', [], 1, 1); + } + + + /** + * @notes 编辑文件分类 + * @return Json + * @author 段誉 + * @date 2021/12/29 14:31 + */ + public function editCate() + { + $params = (new FileValidate())->post()->goCheck('editCate'); + FileLogic::editCate($params); + return $this->success('编辑成功', [], 1, 1); + } + + + /** + * @notes 删除文件分类 + * @return Json + * @author 段誉 + * @date 2021/12/29 14:32 + */ + public function delCate() + { + $params = (new FileValidate())->post()->goCheck('id'); + FileLogic::delCate($params); + return $this->success('删除成功', [], 1, 1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/LoginController.php b/server/app/adminapi/controller/LoginController.php new file mode 100644 index 0000000..9d7ecc3 --- /dev/null +++ b/server/app/adminapi/controller/LoginController.php @@ -0,0 +1,59 @@ +post()->goCheck(); + return $this->data((new LoginLogic())->login($params)); + } + + /** + * @notes 退出登录 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 令狐冲 + * @date 2021/7/8 00:36 + */ + public function logout() + { + //退出登录情况特殊,只有成功的情况,也不需要token验证 + (new LoginLogic())->logout($this->adminInfo); + return $this->success(); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/UploadController.php b/server/app/adminapi/controller/UploadController.php new file mode 100644 index 0000000..12c38bf --- /dev/null +++ b/server/app/adminapi/controller/UploadController.php @@ -0,0 +1,80 @@ +request->post('cid', 0); + $result = UploadService::image($cid); + return $this->success('上传成功', $result); + } catch (Exception $e) { + return $this->fail($e->getMessage()); + } + } + + /** + * @notes 上传视频 + * @return Json + * @author 段誉 + * @date 2021/12/29 16:27 + */ + public function video() + { + try { + $cid = $this->request->post('cid', 0); + $result = UploadService::video($cid); + return $this->success('上传成功', $result); + } catch (Exception $e) { + return $this->fail($e->getMessage()); + } + } + + /** + * @notes 上传文件 + * @return Json + * @author dw + * @date 2023/06/26 + */ + public function file() + { + try { + $cid = $this->request->post('cid', 0); + $result = UploadService::file($cid); + return $this->success('上传成功', $result); + } catch (Exception $e) { + return $this->fail($e->getMessage()); + } + } + +} diff --git a/server/app/adminapi/controller/WorkbenchController.php b/server/app/adminapi/controller/WorkbenchController.php new file mode 100644 index 0000000..d482add --- /dev/null +++ b/server/app/adminapi/controller/WorkbenchController.php @@ -0,0 +1,38 @@ +data($result); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/ai/AiAnalysisController.php b/server/app/adminapi/controller/ai/AiAnalysisController.php new file mode 100644 index 0000000..a66c534 --- /dev/null +++ b/server/app/adminapi/controller/ai/AiAnalysisController.php @@ -0,0 +1,14 @@ +dataLists(new AiAnalysisLists()); + } +} diff --git a/server/app/adminapi/controller/ai/AiConfigController.php b/server/app/adminapi/controller/ai/AiConfigController.php new file mode 100644 index 0000000..2ad5a03 --- /dev/null +++ b/server/app/adminapi/controller/ai/AiConfigController.php @@ -0,0 +1,33 @@ +dataLists(new AiConfigLists()); + } + + public function detail() + { + $params = (new AiConfigValidate())->goCheck('detail'); + $result = AiConfigLogic::detail($params); + return $this->data($result); + } + + public function edit() + { + $params = (new AiConfigValidate())->post()->goCheck('edit'); + $result = AiConfigLogic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(AiConfigLogic::getError()); + } +} diff --git a/server/app/adminapi/controller/ai/AiLogController.php b/server/app/adminapi/controller/ai/AiLogController.php new file mode 100644 index 0000000..a66ee25 --- /dev/null +++ b/server/app/adminapi/controller/ai/AiLogController.php @@ -0,0 +1,14 @@ +dataLists(new AiLogLists()); + } +} diff --git a/server/app/adminapi/controller/ai/KbConfigController.php b/server/app/adminapi/controller/ai/KbConfigController.php new file mode 100644 index 0000000..d97d7ad --- /dev/null +++ b/server/app/adminapi/controller/ai/KbConfigController.php @@ -0,0 +1,32 @@ +dataLists(new KbConfigLists()); + } + + public function detail() + { + $params = (new KbConfigValidate())->goCheck('detail'); + return $this->data(KbConfigLogic::detail($params)); + } + + public function edit() + { + $params = (new KbConfigValidate())->post()->goCheck(); + $result = KbConfigLogic::edit($params); + if ($result === true) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(KbConfigLogic::getError()); + } +} diff --git a/server/app/adminapi/controller/ai/KbDocumentController.php b/server/app/adminapi/controller/ai/KbDocumentController.php new file mode 100644 index 0000000..1efbd31 --- /dev/null +++ b/server/app/adminapi/controller/ai/KbDocumentController.php @@ -0,0 +1,14 @@ +dataLists(new KbDocumentLists()); + } +} diff --git a/server/app/adminapi/controller/ai/KbQueryLogController.php b/server/app/adminapi/controller/ai/KbQueryLogController.php new file mode 100644 index 0000000..f345fc4 --- /dev/null +++ b/server/app/adminapi/controller/ai/KbQueryLogController.php @@ -0,0 +1,14 @@ +dataLists(new KbQueryLogLists()); + } +} diff --git a/server/app/adminapi/controller/ai/KbSyncJobController.php b/server/app/adminapi/controller/ai/KbSyncJobController.php new file mode 100644 index 0000000..17e30ac --- /dev/null +++ b/server/app/adminapi/controller/ai/KbSyncJobController.php @@ -0,0 +1,23 @@ +dataLists(new KbSyncJobLists()); + } + + public function rebuild() + { + $params = (new KbSyncJobValidate())->post()->goCheck(); + Console::call('kb:rebuild', ['--domain' => $params['domain']]); + return $this->success('已触发重建任务'); + } +} diff --git a/server/app/adminapi/controller/app/AppVersionController.php b/server/app/adminapi/controller/app/AppVersionController.php new file mode 100644 index 0000000..9681c3d --- /dev/null +++ b/server/app/adminapi/controller/app/AppVersionController.php @@ -0,0 +1,65 @@ +dataLists(new AppVersionLists()); + } + + public function add() + { + $params = (new AppVersionValidate())->post()->goCheck('add'); + $result = AppVersionLogic::add($params); + if ($result === true) { + return $this->success('添加成功', [], 1, 1); + } + return $this->fail(AppVersionLogic::getError()); + } + + public function edit() + { + $params = (new AppVersionValidate())->post()->goCheck('edit'); + $result = AppVersionLogic::edit($params); + if ($result === true) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(AppVersionLogic::getError()); + } + + public function delete() + { + $params = (new AppVersionValidate())->post()->goCheck('delete'); + AppVersionLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + public function detail() + { + $params = (new AppVersionValidate())->goCheck('detail'); + $result = AppVersionLogic::detail($params); + return $this->data($result); + } + + public function updateStatus() + { + $params = (new AppVersionValidate())->post()->goCheck('status'); + $result = AppVersionLogic::updateStatus($params); + if ($result === true) { + return $this->success('修改成功', [], 1, 1); + } + return $this->fail(AppVersionLogic::getError()); + } +} diff --git a/server/app/adminapi/controller/article/ArticleAiCommentTaskController.php b/server/app/adminapi/controller/article/ArticleAiCommentTaskController.php new file mode 100644 index 0000000..0593063 --- /dev/null +++ b/server/app/adminapi/controller/article/ArticleAiCommentTaskController.php @@ -0,0 +1,33 @@ +dataLists(new ArticleAiCommentTaskLists()); + } + + public function delete() + { + $params = (new ArticleAiCommentTaskValidate())->post()->goCheck('delete'); + ArticleAiCommentTaskLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + public function retry() + { + $params = (new ArticleAiCommentTaskValidate())->post()->goCheck('retry'); + $result = ArticleAiCommentTaskLogic::retry($params); + if (true === $result) { + return $this->success('重置成功', [], 1, 1); + } + return $this->fail(ArticleAiCommentTaskLogic::getError()); + } +} diff --git a/server/app/adminapi/controller/article/ArticleCateController.php b/server/app/adminapi/controller/article/ArticleCateController.php new file mode 100644 index 0000000..89d7c83 --- /dev/null +++ b/server/app/adminapi/controller/article/ArticleCateController.php @@ -0,0 +1,134 @@ +dataLists(new ArticleCateLists()); + } + + + /** + * @notes 添加资讯分类 + * @return \think\response\Json + * @author heshihu + * @date 2022/2/21 17:31 + */ + public function add() + { + $params = (new ArticleCateValidate())->post()->goCheck('add'); + ArticleCateLogic::add($params); + return $this->success('添加成功', [], 1, 1); + } + + + /** + * @notes 编辑资讯分类 + * @return \think\response\Json + * @author heshihu + * @date 2022/2/21 17:49 + */ + public function edit() + { + $params = (new ArticleCateValidate())->post()->goCheck('edit'); + $result = ArticleCateLogic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(ArticleCateLogic::getError()); + } + + + /** + * @notes 删除资讯分类 + * @return \think\response\Json + * @author heshihu + * @date 2022/2/21 17:52 + */ + public function delete() + { + $params = (new ArticleCateValidate())->post()->goCheck('delete'); + ArticleCateLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + + /** + * @notes 资讯分类详情 + * @return \think\response\Json + * @author heshihu + * @date 2022/2/21 17:54 + */ + public function detail() + { + $params = (new ArticleCateValidate())->goCheck('detail'); + $result = ArticleCateLogic::detail($params); + return $this->data($result); + } + + + /** + * @notes 更改资讯分类状态 + * @return \think\response\Json + * @author heshihu + * @date 2022/2/21 10:15 + */ + public function updateStatus() + { + $params = (new ArticleCateValidate())->post()->goCheck('status'); + $result = ArticleCateLogic::updateStatus($params); + if (true === $result) { + return $this->success('修改成功', [], 1, 1); + } + return $this->fail(ArticleCateLogic::getError()); + } + + + /** + * @notes 获取文章分类 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 10:54 + */ + public function all() + { + $result = ArticleCateLogic::getAllData(); + return $this->data($result); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/article/ArticleController.php b/server/app/adminapi/controller/article/ArticleController.php new file mode 100644 index 0000000..e30099a --- /dev/null +++ b/server/app/adminapi/controller/article/ArticleController.php @@ -0,0 +1,132 @@ +dataLists(new ArticleLists()); + } + + /** + * @notes 添加资讯 + * @return \think\response\Json + * @author heshihu + * @date 2022/2/22 9:57 + */ + public function add() + { + $params = (new ArticleValidate())->post()->goCheck('add'); + $result = ArticleLogic::add($params); + if (true === $result) { + return $this->success('添加成功', [], 1, 1); + } + return $this->fail(ArticleLogic::getError()); + } + + /** + * @notes 编辑资讯 + * @return \think\response\Json + * @author heshihu + * @date 2022/2/22 10:12 + */ + public function edit() + { + $params = (new ArticleValidate())->post()->goCheck('edit'); + $result = ArticleLogic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(ArticleLogic::getError()); + } + + /** + * @notes 删除资讯 + * @return \think\response\Json + * @author heshihu + * @date 2022/2/22 10:17 + */ + public function delete() + { + $params = (new ArticleValidate())->post()->goCheck('delete'); + ArticleLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + /** + * @notes 资讯详情 + * @return \think\response\Json + * @author heshihu + * @date 2022/2/22 10:15 + */ + public function detail() + { + $params = (new ArticleValidate())->goCheck('detail'); + $result = ArticleLogic::detail($params); + return $this->data($result); + } + + + /** + * @notes 更改资讯状态 + * @return \think\response\Json + * @author heshihu + * @date 2022/2/22 10:18 + */ + public function updateStatus() + { + $params = (new ArticleValidate())->post()->goCheck('status'); + $result = ArticleLogic::updateStatus($params); + if (true === $result) { + return $this->success('修改成功', [], 1, 1); + } + return $this->fail(ArticleLogic::getError()); + } + + /** + * @notes 批量推荐 + * @return \think\response\Json + * @author heshihu + * @date 2022/2/22 10:18 + */ + public function batchRecommend() + { + $params = (new ArticleValidate())->post()->goCheck('batchRecommend'); + $result = ArticleLogic::batchRecommend($params); + if (true === $result) { + return $this->success('设置成功', [], 1, 1); + } + return $this->fail(ArticleLogic::getError()); + } + +} diff --git a/server/app/adminapi/controller/auth/AdminController.php b/server/app/adminapi/controller/auth/AdminController.php new file mode 100644 index 0000000..b687a89 --- /dev/null +++ b/server/app/adminapi/controller/auth/AdminController.php @@ -0,0 +1,134 @@ +dataLists(new AdminLists()); + } + + + /** + * @notes 添加管理员 + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/29 10:21 + */ + public function add() + { + $params = (new AdminValidate())->post()->goCheck('add'); + $result = AdminLogic::add($params); + if (true === $result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(AdminLogic::getError()); + } + + + /** + * @notes 编辑管理员 + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/29 11:03 + */ + public function edit() + { + $params = (new AdminValidate())->post()->goCheck('edit'); + $result = AdminLogic::edit($params); + if (true === $result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(AdminLogic::getError()); + } + + + /** + * @notes 删除管理员 + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/29 11:03 + */ + public function delete() + { + $params = (new AdminValidate())->post()->goCheck('delete'); + $result = AdminLogic::delete($params); + if (true === $result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(AdminLogic::getError()); + } + + + /** + * @notes 查看管理员详情 + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/29 11:07 + */ + public function detail() + { + $params = (new AdminValidate())->goCheck('detail'); + $result = AdminLogic::detail($params); + return $this->data($result); + } + + + /** + * @notes 获取当前管理员信息 + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/31 10:53 + */ + public function mySelf() + { + $result = AdminLogic::detail(['id' => $this->adminId], 'auth'); + return $this->data($result); + } + + + /** + * @notes 编辑超级管理员信息 + * @return \think\response\Json + * @author 段誉 + * @date 2022/4/8 17:54 + */ + public function editSelf() + { + $params = (new editSelfValidate())->post()->goCheck('', ['admin_id' => $this->adminId]); + $result = AdminLogic::editSelf($params); + return $this->success('操作成功', [], 1, 1); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/auth/MenuController.php b/server/app/adminapi/controller/auth/MenuController.php new file mode 100644 index 0000000..831039a --- /dev/null +++ b/server/app/adminapi/controller/auth/MenuController.php @@ -0,0 +1,142 @@ +adminId); + return $this->data($result); + } + + + /** + * @notes 获取菜单列表 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/29 17:23 + */ + public function lists() + { + return $this->dataLists(new MenuLists()); + } + + + /** + * @notes 菜单详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/30 10:07 + */ + public function detail() + { + $params = (new MenuValidate())->goCheck('detail'); + return $this->data(MenuLogic::detail($params)); + } + + + /** + * @notes 添加菜单 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/30 10:07 + */ + public function add() + { + $params = (new MenuValidate())->post()->goCheck('add'); + MenuLogic::add($params); + return $this->success('操作成功', [], 1, 1); + } + + + /** + * @notes 编辑菜单 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/30 10:07 + */ + public function edit() + { + $params = (new MenuValidate())->post()->goCheck('edit'); + MenuLogic::edit($params); + return $this->success('操作成功', [], 1, 1); + } + + + /** + * @notes 删除菜单 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/30 10:07 + */ + public function delete() + { + $params = (new MenuValidate())->post()->goCheck('delete'); + MenuLogic::delete($params); + return $this->success('操作成功', [], 1, 1); + } + + + /** + * @notes 更新状态 + * @return \think\response\Json + * @author 段誉 + * @date 2022/7/6 17:04 + */ + public function updateStatus() + { + $params = (new MenuValidate())->post()->goCheck('status'); + MenuLogic::updateStatus($params); + return $this->success('操作成功', [], 1, 1); + } + + + /** + * @notes 获取菜单数据 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 11:03 + */ + public function all() + { + $result = MenuLogic::getAllData(); + return $this->data($result); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/auth/RoleController.php b/server/app/adminapi/controller/auth/RoleController.php new file mode 100644 index 0000000..706c41d --- /dev/null +++ b/server/app/adminapi/controller/auth/RoleController.php @@ -0,0 +1,124 @@ +dataLists(new RoleLists()); + } + + + /** + * @notes 添加权限 + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/29 11:49 + */ + public function add() + { + $params = (new RoleValidate())->post()->goCheck('add'); + $res = RoleLogic::add($params); + if (true === $res) { + return $this->success('添加成功', [], 1, 1); + } + return $this->fail(RoleLogic::getError()); + } + + + /** + * @notes 编辑角色 + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/29 14:18 + */ + public function edit() + { + $params = (new RoleValidate())->post()->goCheck('edit'); + $res = RoleLogic::edit($params); + if (true === $res) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(RoleLogic::getError()); + } + + + /** + * @notes 删除角色 + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/29 14:18 + */ + public function delete() + { + $params = (new RoleValidate())->post()->goCheck('del'); + RoleLogic::delete($params['id']); + return $this->success('删除成功', [], 1, 1); + } + + + /** + * @notes 查看角色详情 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2021/12/29 14:18 + */ + public function detail() + { + $params = (new RoleValidate())->goCheck('detail'); + $detail = RoleLogic::detail($params['id']); + return $this->data($detail); + } + + + /** + * @notes 获取角色数据 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 10:39 + */ + public function all() + { + $result = RoleLogic::getAllData(); + return $this->data($result); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/channel/AppSettingController.php b/server/app/adminapi/controller/channel/AppSettingController.php new file mode 100644 index 0000000..c2d0036 --- /dev/null +++ b/server/app/adminapi/controller/channel/AppSettingController.php @@ -0,0 +1,53 @@ +data($result); + } + + + /** + * @notes App设置 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:25 + */ + public function setConfig() + { + $params = $this->request->post(); + AppSettingLogic::setConfig($params); + return $this->success('操作成功', [], 1, 1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/channel/MnpSettingsController.php b/server/app/adminapi/controller/channel/MnpSettingsController.php new file mode 100644 index 0000000..82ff61d --- /dev/null +++ b/server/app/adminapi/controller/channel/MnpSettingsController.php @@ -0,0 +1,52 @@ +getConfig(); + return $this->data($result); + } + + /** + * @notes 设置小程序配置 + * @return \think\response\Json + * @author ljj + * @date 2022/2/16 9:51 上午 + */ + public function setConfig() + { + $params = (new MnpSettingsValidate())->post()->goCheck(); + (new MnpSettingsLogic())->setConfig($params); + return $this->success('操作成功', [], 1, 1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/channel/OfficialAccountMenuController.php b/server/app/adminapi/controller/channel/OfficialAccountMenuController.php new file mode 100644 index 0000000..92b76d7 --- /dev/null +++ b/server/app/adminapi/controller/channel/OfficialAccountMenuController.php @@ -0,0 +1,74 @@ +request->post(); + $result = OfficialAccountMenuLogic::save($params); + if(false === $result) { + return $this->fail(OfficialAccountMenuLogic::getError()); + } + return $this->success('保存成功',[],1,1); + } + + + /** + * @notes 保存发布菜单 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:42 + */ + public function saveAndPublish() + { + $params = $this->request->post(); + $result = OfficialAccountMenuLogic::saveAndPublish($params); + if($result) { + return $this->success('保存并发布成功',[],1,1); + } + return $this->fail(OfficialAccountMenuLogic::getError()); + } + + + + /** + * @notes 查看菜单详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:42 + */ + public function detail() + { + $result = OfficialAccountMenuLogic::detail(); + return $this->data($result); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/channel/OfficialAccountReplyController.php b/server/app/adminapi/controller/channel/OfficialAccountReplyController.php new file mode 100644 index 0000000..2adb33b --- /dev/null +++ b/server/app/adminapi/controller/channel/OfficialAccountReplyController.php @@ -0,0 +1,148 @@ +dataLists(new OfficialAccountReplyLists()); + } + + + /** + * @notes 添加回复(关注/关键词/默认) + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:58 + */ + public function add() + { + $params = (new OfficialAccountReplyValidate())->post()->goCheck('add'); + $result = OfficialAccountReplyLogic::add($params); + if ($result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(OfficialAccountReplyLogic::getError()); + } + + + /** + * @notes 查看回复详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:58 + */ + public function detail() + { + $params = (new OfficialAccountReplyValidate())->goCheck('detail'); + $result = OfficialAccountReplyLogic::detail($params); + return $this->data($result); + } + + + /** + * @notes 编辑回复(关注/关键词/默认) + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:58 + */ + public function edit() + { + $params = (new OfficialAccountReplyValidate())->post()->goCheck('edit'); + $result = OfficialAccountReplyLogic::edit($params); + if ($result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(OfficialAccountReplyLogic::getError()); + } + + + /** + * @notes 删除回复(关注/关键词/默认) + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:59 + */ + public function delete() + { + $params = (new OfficialAccountReplyValidate())->post()->goCheck('delete'); + OfficialAccountReplyLogic::delete($params); + return $this->success('操作成功', [], 1, 1); + } + + + /** + * @notes 更新排序 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:59 + */ + public function sort() + { + $params = (new OfficialAccountReplyValidate())->post()->goCheck('sort'); + OfficialAccountReplyLogic::sort($params); + return $this->success('操作成功', [], 1, 1); + } + + + /** + * @notes 更新状态 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:59 + */ + public function status() + { + $params = (new OfficialAccountReplyValidate())->post()->goCheck('status'); + OfficialAccountReplyLogic::status($params); + return $this->success('操作成功', [], 1, 1); + } + + + /** + * @notes 微信公众号回调 + * @throws \ReflectionException + * @author 段誉 + * @date 2022/3/29 10:59 + */ + public function index() + { + $result = OfficialAccountReplyLogic::index(); + return response($result->getBody())->header([ + 'Content-Type' => 'text/plain;charset=utf-8' + ]); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/channel/OfficialAccountSettingController.php b/server/app/adminapi/controller/channel/OfficialAccountSettingController.php new file mode 100644 index 0000000..dcb7018 --- /dev/null +++ b/server/app/adminapi/controller/channel/OfficialAccountSettingController.php @@ -0,0 +1,52 @@ +getConfig(); + return $this->data($result); + } + + /** + * @notes 设置公众号配置 + * @return \think\response\Json + * @author ljj + * @date 2022/2/16 10:09 上午 + */ + public function setConfig() + { + $params = (new OfficialAccountSettingValidate())->post()->goCheck(); + (new OfficialAccountSettingLogic())->setConfig($params); + return $this->success('操作成功',[],1,1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/channel/OpenSettingController.php b/server/app/adminapi/controller/channel/OpenSettingController.php new file mode 100644 index 0000000..608a138 --- /dev/null +++ b/server/app/adminapi/controller/channel/OpenSettingController.php @@ -0,0 +1,54 @@ +data($result); + } + + + /** + * @notes 微信开放平台设置 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 11:03 + */ + public function setConfig() + { + $params = (new OpenSettingValidate())->post()->goCheck(); + OpenSettingLogic::setConfig($params); + return $this->success('操作成功', [], 1, 1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/channel/WebPageSettingController.php b/server/app/adminapi/controller/channel/WebPageSettingController.php new file mode 100644 index 0000000..57f2e4c --- /dev/null +++ b/server/app/adminapi/controller/channel/WebPageSettingController.php @@ -0,0 +1,54 @@ +data($result); + } + + + /** + * @notes H5设置 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:36 + */ + public function setConfig() + { + $params = (new WebPageSettingValidate())->post()->goCheck(); + WebPageSettingLogic::setConfig($params); + return $this->success('操作成功', [], 1, 1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/community/CommunityCommentController.php b/server/app/adminapi/controller/community/CommunityCommentController.php new file mode 100644 index 0000000..f637031 --- /dev/null +++ b/server/app/adminapi/controller/community/CommunityCommentController.php @@ -0,0 +1,33 @@ +dataLists(new CommunityCommentLists()); + } + + public function updateStatus() + { + $params = (new CommunityCommentValidate())->post()->goCheck('status'); + $result = CommunityCommentLogic::updateStatus($params); + if (true === $result) { + return $this->success('修改成功', [], 1, 1); + } + return $this->fail(CommunityCommentLogic::getError()); + } + + public function delete() + { + $params = (new CommunityCommentValidate())->post()->goCheck('delete'); + CommunityCommentLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } +} diff --git a/server/app/adminapi/controller/community/CommunityPostController.php b/server/app/adminapi/controller/community/CommunityPostController.php new file mode 100644 index 0000000..15642ed --- /dev/null +++ b/server/app/adminapi/controller/community/CommunityPostController.php @@ -0,0 +1,70 @@ +dataLists(new CommunityPostLists()); + } + + public function detail() + { + $params = (new CommunityPostValidate())->goCheck('detail'); + $result = CommunityPostLogic::detail($params); + return $this->data($result); + } + + public function updateStatus() + { + $params = (new CommunityPostValidate())->post()->goCheck('status'); + $result = CommunityPostLogic::updateStatus($params); + if (true === $result) { + return $this->success('修改成功', [], 1, 1); + } + return $this->fail(CommunityPostLogic::getError()); + } + + public function setTop() + { + $params = (new CommunityPostValidate())->post()->goCheck('top'); + $result = CommunityPostLogic::setTop($params); + if (true === $result) { + return $this->success('设置成功', [], 1, 1); + } + return $this->fail(CommunityPostLogic::getError()); + } + + public function setHot() + { + $params = (new CommunityPostValidate())->post()->goCheck('hot'); + $result = CommunityPostLogic::setHot($params); + if (true === $result) { + return $this->success('设置成功', [], 1, 1); + } + return $this->fail(CommunityPostLogic::getError()); + } + + public function setRecommend() + { + $params = (new CommunityPostValidate())->post()->goCheck('recommend'); + $result = CommunityPostLogic::setRecommend($params); + if (true === $result) { + return $this->success('设置成功', [], 1, 1); + } + return $this->fail(CommunityPostLogic::getError()); + } + + public function delete() + { + $params = (new CommunityPostValidate())->post()->goCheck('delete'); + CommunityPostLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } +} diff --git a/server/app/adminapi/controller/community/CommunityTagController.php b/server/app/adminapi/controller/community/CommunityTagController.php new file mode 100644 index 0000000..739d70c --- /dev/null +++ b/server/app/adminapi/controller/community/CommunityTagController.php @@ -0,0 +1,53 @@ +dataLists(new CommunityTagLists()); + } + + public function all() + { + $result = CommunityTagLogic::all(); + return $this->data($result); + } + + public function add() + { + $params = (new CommunityTagValidate())->post()->goCheck('add'); + CommunityTagLogic::add($params); + return $this->success('添加成功', [], 1, 1); + } + + public function edit() + { + $params = (new CommunityTagValidate())->post()->goCheck('edit'); + $result = CommunityTagLogic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(CommunityTagLogic::getError()); + } + + public function delete() + { + $params = (new CommunityTagValidate())->post()->goCheck('delete'); + CommunityTagLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + public function detail() + { + $params = (new CommunityTagValidate())->goCheck('detail'); + $result = CommunityTagLogic::detail($params); + return $this->data($result); + } +} diff --git a/server/app/adminapi/controller/crawler/CrawlErrorLogController.php b/server/app/adminapi/controller/crawler/CrawlErrorLogController.php new file mode 100644 index 0000000..ebc8def --- /dev/null +++ b/server/app/adminapi/controller/crawler/CrawlErrorLogController.php @@ -0,0 +1,32 @@ +dataLists(new CrawlErrorLogLists()); + } + + public function detail() + { + $id = $this->request->get('id/d', 0); + $result = CrawlErrorLog::findOrEmpty($id)->toArray(); + return $this->data($result); + } + + public function delete() + { + $ids = $this->request->post('ids/a', []); + if (empty($ids)) { + return $this->fail('请选择要删除的记录'); + } + CrawlErrorLog::destroy($ids); + return $this->success('删除成功', [], 1, 1); + } +} diff --git a/server/app/adminapi/controller/crontab/CrontabController.php b/server/app/adminapi/controller/crontab/CrontabController.php new file mode 100644 index 0000000..8dd3a46 --- /dev/null +++ b/server/app/adminapi/controller/crontab/CrontabController.php @@ -0,0 +1,160 @@ +dataLists(new CrontabLists()); + } + + + /** + * @notes 添加定时任务 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 14:27 + */ + public function add() + { + $params = (new CrontabValidate())->post()->goCheck('add'); + $result = CrontabLogic::add($params); + if ($result) { + return $this->success('添加成功', [], 1, 1); + } + return $this->fail(CrontabLogic::getError()); + } + + + /** + * @notes 查看定时任务详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 14:27 + */ + public function detail() + { + $params = (new CrontabValidate())->goCheck('detail'); + $result = CrontabLogic::detail($params); + return $this->data($result); + } + + + /** + * @notes 编辑定时任务 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 14:27 + */ + public function edit() + { + $params = (new CrontabValidate())->post()->goCheck('edit'); + $result = CrontabLogic::edit($params); + if ($result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(CrontabLogic::getError()); + } + + + /** + * @notes 删除定时任务 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 14:27 + */ + public function delete() + { + $params = (new CrontabValidate())->post()->goCheck('delete'); + $result = CrontabLogic::delete($params); + if ($result) { + return $this->success('删除成功', [], 1, 1); + } + return $this->fail('删除失败'); + } + + + /** + * @notes 操作定时任务 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 14:28 + */ + public function operate() + { + $params = (new CrontabValidate())->post()->goCheck('operate'); + $result = CrontabLogic::operate($params); + if ($result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(CrontabLogic::getError()); + } + + + /** + * @notes 手动执行定时任务 + */ + public function run() + { + $params = (new CrontabValidate())->post()->goCheck('detail'); + $result = CrontabLogic::run($params); + if ($result) { + return $this->success('执行完成', $result); + } + return $this->fail(CrontabLogic::getError()); + } + + + /** + * @notes 执行日志列表 + */ + public function logs() + { + $params = $this->request->get(); + $result = CrontabLogic::logs($params); + return $this->success('', $result); + } + + + /** + * @notes 获取规则执行时间 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 14:28 + */ + public function expression() + { + $params = (new CrontabValidate())->goCheck('expression'); + $result = CrontabLogic::expression($params); + return $this->data($result); + } +} diff --git a/server/app/adminapi/controller/decorate/DataController.php b/server/app/adminapi/controller/decorate/DataController.php new file mode 100644 index 0000000..deee615 --- /dev/null +++ b/server/app/adminapi/controller/decorate/DataController.php @@ -0,0 +1,55 @@ +request->get('limit/d', 10); + $result = DecorateDataLogic::getArticleLists($limit); + return $this->success('获取成功', $result); + } + + /** + * @notes pc设置 + * @return Json + * @author mjf + * @date 2024/3/14 18:13 + */ + public function pc(): Json + { + $result = DecorateDataLogic::pc(); + return $this->data($result); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/decorate/PageController.php b/server/app/adminapi/controller/decorate/PageController.php new file mode 100644 index 0000000..f76d809 --- /dev/null +++ b/server/app/adminapi/controller/decorate/PageController.php @@ -0,0 +1,61 @@ +request->get('id/d'); + $result = DecoratePageLogic::getDetail($id); + return $this->success('获取成功', $result); + } + + + /** + * @notes 保存装修配置 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/15 9:57 + */ + public function save() + { + $params = (new DecoratePageValidate())->post()->goCheck(); + $result = DecoratePageLogic::save($params); + if (false === $result) { + return $this->fail(DecoratePageLogic::getError()); + } + return $this->success('操作成功', [], 1, 1); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/decorate/TabbarController.php b/server/app/adminapi/controller/decorate/TabbarController.php new file mode 100644 index 0000000..f2ff9e0 --- /dev/null +++ b/server/app/adminapi/controller/decorate/TabbarController.php @@ -0,0 +1,58 @@ +success('', $data); + } + + + /** + * @notes 底部导航保存 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/6 9:58 + */ + public function save() + { + $params = $this->request->post(); + DecorateTabbarLogic::save($params); + return $this->success('操作成功', [], 1, 1); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/dept/DeptController.php b/server/app/adminapi/controller/dept/DeptController.php new file mode 100644 index 0000000..e0dcf84 --- /dev/null +++ b/server/app/adminapi/controller/dept/DeptController.php @@ -0,0 +1,134 @@ +request->get(); + $result = DeptLogic::lists($params); + return $this->success('',$result); + } + + + /** + * @notes 上级部门 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/5/26 18:36 + */ + public function leaderDept() + { + $result = DeptLogic::leaderDept(); + return $this->success('',$result); + } + + + /** + * @notes 添加部门 + * @return \think\response\Json + * @author 段誉 + * @date 2022/5/25 18:40 + */ + public function add() + { + $params = (new DeptValidate())->post()->goCheck('add'); + DeptLogic::add($params); + return $this->success('添加成功', [], 1, 1); + } + + + /** + * @notes 编辑部门 + * @return \think\response\Json + * @author 段誉 + * @date 2022/5/25 18:41 + */ + public function edit() + { + $params = (new DeptValidate())->post()->goCheck('edit'); + $result = DeptLogic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(DeptLogic::getError()); + } + + + /** + * @notes 删除部门 + * @return \think\response\Json + * @author 段誉 + * @date 2022/5/25 18:41 + */ + public function delete() + { + $params = (new DeptValidate())->post()->goCheck('delete'); + DeptLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + + /** + * @notes 获取部门详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/5/25 18:41 + */ + public function detail() + { + $params = (new DeptValidate())->goCheck('detail'); + $result = DeptLogic::detail($params); + return $this->data($result); + } + + + /** + * @notes 获取部门数据 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 10:28 + */ + public function all() + { + $result = DeptLogic::getAllData(); + return $this->data($result); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/dept/JobsController.php b/server/app/adminapi/controller/dept/JobsController.php new file mode 100644 index 0000000..0f8b622 --- /dev/null +++ b/server/app/adminapi/controller/dept/JobsController.php @@ -0,0 +1,118 @@ +dataLists(new JobsLists()); + } + + + /** + * @notes 添加岗位 + * @return \think\response\Json + * @author 段誉 + * @date 2022/5/25 18:40 + */ + public function add() + { + $params = (new JobsValidate())->post()->goCheck('add'); + JobsLogic::add($params); + return $this->success('添加成功', [], 1, 1); + } + + + /** + * @notes 编辑岗位 + * @return \think\response\Json + * @author 段誉 + * @date 2022/5/25 18:41 + */ + public function edit() + { + $params = (new JobsValidate())->post()->goCheck('edit'); + $result = JobsLogic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(JobsLogic::getError()); + } + + + /** + * @notes 删除岗位 + * @return \think\response\Json + * @author 段誉 + * @date 2022/5/25 18:41 + */ + public function delete() + { + $params = (new JobsValidate())->post()->goCheck('delete'); + JobsLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + + /** + * @notes 获取岗位详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/5/25 18:41 + */ + public function detail() + { + $params = (new JobsValidate())->goCheck('detail'); + $result = JobsLogic::detail($params); + return $this->data($result); + } + + + /** + * @notes 获取岗位数据 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 10:31 + */ + public function all() + { + $result = JobsLogic::getAllData(); + return $this->data($result); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/device/DeviceController.php b/server/app/adminapi/controller/device/DeviceController.php new file mode 100644 index 0000000..16c1436 --- /dev/null +++ b/server/app/adminapi/controller/device/DeviceController.php @@ -0,0 +1,57 @@ +dataLists(new DeviceLists()); + } + + public function detail() + { + $id = (int)$this->request->get('id', 0); + if ($id <= 0) return $this->fail('参数错误'); + $info = UserDevice::findOrEmpty($id)->toArray(); + return $this->data($info); + } + + public function delete() + { + $id = (int)$this->request->post('id', 0); + if ($id <= 0) return $this->fail('参数错误'); + UserDevice::destroy($id); + return $this->success('删除成功', [], 1, 1); + } + + /** + * 设备统计概览 + */ + public function stats() + { + $total = UserDevice::count(); + $platforms = UserDevice::field('platform, COUNT(*) as count') + ->group('platform') + ->select() + ->toArray(); + + $today = strtotime(date('Y-m-d')); + $todayActive = UserDevice::where('last_at', '>=', $today)->count(); + $todayNew = UserDevice::where('first_at', '>=', $today)->count(); + + return $this->data([ + 'total' => $total, + 'today_active' => $todayActive, + 'today_new' => $todayNew, + 'platforms' => $platforms, + ]); + } +} diff --git a/server/app/adminapi/controller/finance/AccountLogController.php b/server/app/adminapi/controller/finance/AccountLogController.php new file mode 100644 index 0000000..5c82279 --- /dev/null +++ b/server/app/adminapi/controller/finance/AccountLogController.php @@ -0,0 +1,54 @@ +dataLists(new AccountLogLists()); + } + + + /** + * @notes 用户余额变动类型 + * @return \think\response\Json + * @author 段誉 + * @date 2023/2/24 15:25 + */ + public function getUmChangeType() + { + return $this->data(AccountLogEnum::getUserMoneyChangeTypeDesc()); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/finance/RefundController.php b/server/app/adminapi/controller/finance/RefundController.php new file mode 100644 index 0000000..6a81e14 --- /dev/null +++ b/server/app/adminapi/controller/finance/RefundController.php @@ -0,0 +1,72 @@ +success('', $result); + } + + + /** + * @notes 退款记录 + * @return \think\response\Json + * @author 段誉 + * @date 2023/3/1 9:47 + */ + public function record() + { + return $this->dataLists(new RefundRecordLists()); + } + + + /** + * @notes 退款日志 + * @return \think\response\Json + * @author 段誉 + * @date 2023/3/1 9:47 + */ + public function log() + { + $recordId = $this->request->get('record_id', 0); + $result = RefundLogic::refundLog($recordId); + return $this->success('', $result); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/lottery/LotteryCategoryController.php b/server/app/adminapi/controller/lottery/LotteryCategoryController.php new file mode 100644 index 0000000..3d059b2 --- /dev/null +++ b/server/app/adminapi/controller/lottery/LotteryCategoryController.php @@ -0,0 +1,47 @@ +dataLists(new LotteryCategoryLists()); + } + + public function add() + { + $params = (new LotteryCategoryValidate())->post()->goCheck('add'); + LotteryCategoryLogic::add($params); + return $this->success('添加成功', [], 1, 1); + } + + public function edit() + { + $params = (new LotteryCategoryValidate())->post()->goCheck('edit'); + $result = LotteryCategoryLogic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(LotteryCategoryLogic::getError()); + } + + public function delete() + { + $params = (new LotteryCategoryValidate())->post()->goCheck('delete'); + LotteryCategoryLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + public function detail() + { + $params = (new LotteryCategoryValidate())->goCheck('detail'); + $result = LotteryCategoryLogic::detail($params); + return $this->data($result); + } +} diff --git a/server/app/adminapi/controller/lottery/LotteryDrawController.php b/server/app/adminapi/controller/lottery/LotteryDrawController.php new file mode 100644 index 0000000..8320598 --- /dev/null +++ b/server/app/adminapi/controller/lottery/LotteryDrawController.php @@ -0,0 +1,30 @@ +dataLists(new LotteryDrawLists()); + } + + public function detail() + { + $params = (new LotteryDrawValidate())->goCheck('detail'); + $result = LotteryDrawLogic::detail($params); + return $this->data($result); + } + + public function delete() + { + $params = (new LotteryDrawValidate())->post()->goCheck('delete'); + LotteryDrawLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } +} diff --git a/server/app/adminapi/controller/lottery/LotteryRegionController.php b/server/app/adminapi/controller/lottery/LotteryRegionController.php new file mode 100644 index 0000000..31afa1c --- /dev/null +++ b/server/app/adminapi/controller/lottery/LotteryRegionController.php @@ -0,0 +1,47 @@ +dataLists(new LotteryRegionLists()); + } + + public function add() + { + $params = (new LotteryRegionValidate())->post()->goCheck('add'); + LotteryRegionLogic::add($params); + return $this->success('添加成功', [], 1, 1); + } + + public function edit() + { + $params = (new LotteryRegionValidate())->post()->goCheck('edit'); + $result = LotteryRegionLogic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(LotteryRegionLogic::getError()); + } + + public function delete() + { + $params = (new LotteryRegionValidate())->post()->goCheck('delete'); + LotteryRegionLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + public function detail() + { + $params = (new LotteryRegionValidate())->goCheck('detail'); + $result = LotteryRegionLogic::detail($params); + return $this->data($result); + } +} diff --git a/server/app/adminapi/controller/match/LeagueController.php b/server/app/adminapi/controller/match/LeagueController.php new file mode 100644 index 0000000..ff5b3f2 --- /dev/null +++ b/server/app/adminapi/controller/match/LeagueController.php @@ -0,0 +1,40 @@ +dataLists(new LeagueLists()); + } + + public function detail() + { + $params = (new LeagueValidate())->goCheck('detail'); + $result = LeagueLogic::detail($params); + return $this->data($result); + } + + public function edit() + { + $params = (new LeagueValidate())->post()->goCheck('edit'); + $result = LeagueLogic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(LeagueLogic::getError()); + } + + public function delete() + { + $params = (new LeagueValidate())->post()->goCheck('delete'); + LeagueLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } +} diff --git a/server/app/adminapi/controller/match/MatchController.php b/server/app/adminapi/controller/match/MatchController.php new file mode 100644 index 0000000..8a833d5 --- /dev/null +++ b/server/app/adminapi/controller/match/MatchController.php @@ -0,0 +1,40 @@ +dataLists(new MatchLists()); + } + + public function detail() + { + $params = (new MatchValidate())->goCheck('detail'); + $result = MatchLogic::detail($params); + return $this->data($result); + } + + public function edit() + { + $params = (new MatchValidate())->post()->goCheck('edit'); + $result = MatchLogic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(MatchLogic::getError()); + } + + public function delete() + { + $params = (new MatchValidate())->post()->goCheck('delete'); + MatchLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } +} diff --git a/server/app/adminapi/controller/notice/NoticeController.php b/server/app/adminapi/controller/notice/NoticeController.php new file mode 100644 index 0000000..48f2707 --- /dev/null +++ b/server/app/adminapi/controller/notice/NoticeController.php @@ -0,0 +1,70 @@ +dataLists(new NoticeSettingLists()); + } + + + /** + * @notes 查看通知设置详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 11:18 + */ + public function detail() + { + $params = (new NoticeValidate())->goCheck('detail'); + $result = NoticeLogic::detail($params); + return $this->data($result); + } + + + /** + * @notes 通知设置 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 11:18 + */ + public function set() + { + $params = $this->request->post(); + $result = NoticeLogic::set($params); + if ($result) { + return $this->success('设置成功'); + } + return $this->fail(NoticeLogic::getError()); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/notice/SmsConfigController.php b/server/app/adminapi/controller/notice/SmsConfigController.php new file mode 100644 index 0000000..86516c3 --- /dev/null +++ b/server/app/adminapi/controller/notice/SmsConfigController.php @@ -0,0 +1,69 @@ +data($result); + } + + + /** + * @notes 短信配置 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 11:36 + */ + public function setConfig() + { + $params = (new SmsConfigValidate())->post()->goCheck('setConfig'); + SmsConfigLogic::setConfig($params); + return $this->success('操作成功',[],1,1); + } + + + /** + * @notes 查看短信配置详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 11:36 + */ + public function detail() + { + $params = (new SmsConfigValidate())->goCheck('detail'); + $result = SmsConfigLogic::detail($params); + return $this->data($result); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/points/PointsProductController.php b/server/app/adminapi/controller/points/PointsProductController.php new file mode 100644 index 0000000..1b2ce4b --- /dev/null +++ b/server/app/adminapi/controller/points/PointsProductController.php @@ -0,0 +1,43 @@ +dataLists(new PointsProductLists()); + } + + public function add() + { + $params = (new PointsProductValidate())->post()->goCheck('add'); + PointsProductLogic::add($params); + return $this->success('添加成功', [], 1, 1); + } + + public function edit() + { + $params = (new PointsProductValidate())->post()->goCheck(); + PointsProductLogic::edit($params); + return $this->success('编辑成功', [], 1, 1); + } + + public function delete() + { + $params = (new PointsProductValidate())->post()->goCheck('delete'); + PointsProductLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + public function detail() + { + $params = (new PointsProductValidate())->goCheck('detail'); + return $this->data(PointsProductLogic::detail($params)); + } +} diff --git a/server/app/adminapi/controller/popup/PopupController.php b/server/app/adminapi/controller/popup/PopupController.php new file mode 100644 index 0000000..0753a3a --- /dev/null +++ b/server/app/adminapi/controller/popup/PopupController.php @@ -0,0 +1,57 @@ +dataLists(new PopupLists()); + } + + public function add() + { + $params = (new PopupValidate())->post()->goCheck('add'); + PopupLogic::add($params); + return $this->success('添加成功', [], 1, 1); + } + + public function edit() + { + $params = (new PopupValidate())->post()->goCheck('edit'); + $result = PopupLogic::edit($params); + if ($result === true) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(PopupLogic::getError()); + } + + public function delete() + { + $params = (new PopupValidate())->post()->goCheck('delete'); + PopupLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + public function detail() + { + $params = (new PopupValidate())->goCheck('detail'); + $result = PopupLogic::detail($params); + return $this->data($result); + } + + public function updateStatus() + { + $params = (new PopupValidate())->post()->goCheck('status'); + PopupLogic::updateStatus($params); + return $this->success('修改成功', [], 1, 1); + } +} diff --git a/server/app/adminapi/controller/popup/PopupLogController.php b/server/app/adminapi/controller/popup/PopupLogController.php new file mode 100644 index 0000000..06c5d25 --- /dev/null +++ b/server/app/adminapi/controller/popup/PopupLogController.php @@ -0,0 +1,70 @@ +dataLists(new PopupLogLists()); + } + + /** + * 弹窗统计概览 + * GET /popup.popup_log/stats?popup_id=xxx + * 返回:[show, click, close, ctr, days[]] + */ + public function stats() + { + $popupId = (int)$this->request->get('popup_id', 0); + $where = []; + if ($popupId > 0) $where[] = ['popup_id', '=', $popupId]; + + $show = PagePopupLog::where($where)->where('action', 1)->count(); + $click = PagePopupLog::where($where)->where('action', 2)->count(); + $close = PagePopupLog::where($where)->where('action', 3)->count(); + $ctr = $show > 0 ? round($click / $show * 100, 2) : 0; + + // 最近 7 天每日统计 + $days = []; + for ($i = 6; $i >= 0; $i--) { + $start = strtotime(date('Y-m-d', strtotime("-{$i} day"))); + $end = $start + 86400; + $dShow = PagePopupLog::where($where)->where('action', 1) + ->where('create_time', '>=', $start) + ->where('create_time', '<', $end)->count(); + $dClick = PagePopupLog::where($where)->where('action', 2) + ->where('create_time', '>=', $start) + ->where('create_time', '<', $end)->count(); + $days[] = [ + 'date' => date('m-d', $start), + 'show' => $dShow, + 'click' => $dClick, + ]; + } + + $popupName = ''; + if ($popupId > 0) { + $popup = PagePopup::find($popupId); + $popupName = $popup ? (string)$popup['name'] : ''; + } + + return $this->data([ + 'popup_id' => $popupId, + 'popup_name' => $popupName, + 'show' => $show, + 'click' => $click, + 'close' => $close, + 'ctr' => $ctr, + 'days' => $days, + ]); + } +} diff --git a/server/app/adminapi/controller/recharge/RechargeController.php b/server/app/adminapi/controller/recharge/RechargeController.php new file mode 100644 index 0000000..56a1079 --- /dev/null +++ b/server/app/adminapi/controller/recharge/RechargeController.php @@ -0,0 +1,107 @@ +data($result); + } + + + /** + * @notes 充值设置 + * @return \think\response\Json + * @author 段誉 + * @date 2023/2/22 16:48 + */ + public function setConfig() + { + $params = $this->request->post(); + $result = RechargeLogic::setConfig($params); + if($result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(RechargeLogic::getError()); + } + + + /** + * @notes 充值记录 + * @return \think\response\Json + * @author 段誉 + * @date 2023/2/24 16:01 + */ + public function lists() + { + return $this->dataLists(new RechargeLists()); + } + + + /** + * @notes 退款 + * @return \think\response\Json + * @author 段誉 + * @date 2023/2/28 17:29 + */ + public function refund() + { + $params = (new RechargeRefundValidate())->post()->goCheck('refund'); + $result = RechargeLogic::refund($params, $this->adminId); + list($flag, $msg) = $result; + if(false === $flag) { + return $this->fail($msg); + } + return $this->success($msg, [], 1, 1); + } + + + /** + * @notes 重新退款 + * @return \think\response\Json + * @author 段誉 + * @date 2023/2/28 19:17 + */ + public function refundAgain() + { + $params = (new RechargeRefundValidate())->post()->goCheck('again'); + $result = RechargeLogic::refundAgain($params, $this->adminId); + list($flag, $msg) = $result; + if(false === $flag) { + return $this->fail($msg); + } + return $this->success($msg, [], 1, 1); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/CustomerServiceController.php b/server/app/adminapi/controller/setting/CustomerServiceController.php new file mode 100644 index 0000000..5461db2 --- /dev/null +++ b/server/app/adminapi/controller/setting/CustomerServiceController.php @@ -0,0 +1,51 @@ +data($result); + } + + /** + * @notes 设置客服设置 + * @return \think\response\Json + * @author ljj + * @date 2022/2/15 12:11 下午 + */ + public function setConfig() + { + $params = $this->request->post(); + CustomerServiceLogic::setConfig($params); + return $this->success('设置成功', [], 1, 1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/HotSearchController.php b/server/app/adminapi/controller/setting/HotSearchController.php new file mode 100644 index 0000000..772c457 --- /dev/null +++ b/server/app/adminapi/controller/setting/HotSearchController.php @@ -0,0 +1,56 @@ +data($result); + } + + + /** + * @notes 设置热门搜索 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/5 19:00 + */ + public function setConfig() + { + $params = $this->request->post(); + $result = HotSearchLogic::setConfig($params); + if (false === $result) { + return $this->fail(HotSearchLogic::getError() ?: '系统错误'); + } + return $this->success('设置成功', [], 1, 1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/SmsUploadController.php b/server/app/adminapi/controller/setting/SmsUploadController.php new file mode 100644 index 0000000..503c415 --- /dev/null +++ b/server/app/adminapi/controller/setting/SmsUploadController.php @@ -0,0 +1,14 @@ +dataLists(new SmsUploadLists()); + } +} diff --git a/server/app/adminapi/controller/setting/StorageController.php b/server/app/adminapi/controller/setting/StorageController.php new file mode 100644 index 0000000..6f21221 --- /dev/null +++ b/server/app/adminapi/controller/setting/StorageController.php @@ -0,0 +1,86 @@ +success('获取成功', StorageLogic::lists()); + } + + + /** + * @notes 存储配置信息 + * @return Json + * @author 段誉 + * @date 2022/4/20 16:19 + */ + public function detail() + { + $param = (new StorageValidate())->get()->goCheck('detail'); + return $this->success('获取成功', StorageLogic::detail($param)); + } + + + /** + * @notes 设置存储参数 + * @return Json + * @author 段誉 + * @date 2022/4/20 16:19 + */ + public function setup() + { + $params = (new StorageValidate())->post()->goCheck('setup'); + $result = StorageLogic::setup($params); + if (true === $result) { + return $this->success('配置成功', [], 1, 1); + } + return $this->success($result, [], 1, 1); + } + + + /** + * @notes 切换存储引擎 + * @return Json + * @author 段誉 + * @date 2022/4/20 16:19 + */ + public function change() + { + $params = (new StorageValidate())->post()->goCheck('change'); + StorageLogic::change($params); + return $this->success('切换成功', [], 1, 1); + } +} diff --git a/server/app/adminapi/controller/setting/TransactionSettingsController.php b/server/app/adminapi/controller/setting/TransactionSettingsController.php new file mode 100644 index 0000000..406a6f2 --- /dev/null +++ b/server/app/adminapi/controller/setting/TransactionSettingsController.php @@ -0,0 +1,53 @@ +data($result); + } + + /** + * @notes 设置交易设置 + * @return \think\response\Json + * @author ljj + * @date 2022/2/15 11:50 上午 + */ + public function setConfig() + { + $params = (new TransactionSettingsValidate())->post()->goCheck('setConfig'); + TransactionSettingsLogic::setConfig($params); + return $this->success('操作成功',[],1,1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/dict/DictDataController.php b/server/app/adminapi/controller/setting/dict/DictDataController.php new file mode 100644 index 0000000..c9d1285 --- /dev/null +++ b/server/app/adminapi/controller/setting/dict/DictDataController.php @@ -0,0 +1,99 @@ +dataLists(new DictDataLists()); + } + + + /** + * @notes 添加字典数据 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/20 17:13 + */ + public function add() + { + $params = (new DictDataValidate())->post()->goCheck('add'); + DictDataLogic::save($params); + return $this->success('添加成功', [], 1, 1); + } + + + /** + * @notes 编辑字典数据 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/20 17:13 + */ + public function edit() + { + $params = (new DictDataValidate())->post()->goCheck('edit'); + DictDataLogic::save($params); + return $this->success('编辑成功', [], 1, 1); + } + + + /** + * @notes 删除字典数据 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/20 17:13 + */ + public function delete() + { + $params = (new DictDataValidate())->post()->goCheck('id'); + DictDataLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + + /** + * @notes 获取字典详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/20 17:14 + */ + public function detail() + { + $params = (new DictDataValidate())->goCheck('id'); + $result = DictDataLogic::detail($params); + return $this->data($result); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/dict/DictTypeController.php b/server/app/adminapi/controller/setting/dict/DictTypeController.php new file mode 100644 index 0000000..18efbf8 --- /dev/null +++ b/server/app/adminapi/controller/setting/dict/DictTypeController.php @@ -0,0 +1,116 @@ +dataLists(new DictTypeLists()); + } + + + /** + * @notes 添加字典类型 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/20 16:24 + */ + public function add() + { + $params = (new DictTypeValidate())->post()->goCheck('add'); + DictTypeLogic::add($params); + return $this->success('添加成功', [], 1, 1); + } + + + /** + * @notes 编辑字典类型 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/20 16:25 + */ + public function edit() + { + $params = (new DictTypeValidate())->post()->goCheck('edit'); + DictTypeLogic::edit($params); + return $this->success('编辑成功', [], 1, 1); + } + + + /** + * @notes 删除字典类型 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/20 16:25 + */ + public function delete() + { + $params = (new DictTypeValidate())->post()->goCheck('delete'); + DictTypeLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + + /** + * @notes 获取字典详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/20 16:25 + */ + public function detail() + { + $params = (new DictTypeValidate())->goCheck('detail'); + $result = DictTypeLogic::detail($params); + return $this->data($result); + } + + + /** + * @notes 获取字典类型数据 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 10:46 + */ + public function all() + { + $result = DictTypeLogic::getAllData(); + return $this->data($result); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/pay/PayConfigController.php b/server/app/adminapi/controller/setting/pay/PayConfigController.php new file mode 100644 index 0000000..de3bdb4 --- /dev/null +++ b/server/app/adminapi/controller/setting/pay/PayConfigController.php @@ -0,0 +1,69 @@ +post()->goCheck(); + PayConfigLogic::setConfig($params); + return $this->success('设置成功', [], 1, 1); + } + + + /** + * @notes 获取支付配置 + * @return Json + * @author 段誉 + * @date 2023/2/23 16:14 + */ + public function getConfig(): Json + { + $id = (new PayConfigValidate())->goCheck('get'); + $result = PayConfigLogic::getConfig($id); + return $this->success('获取成功', $result); + } + + + /** + * @notes + * @return Json + * @author 段誉 + * @date 2023/2/23 16:15 + */ + public function lists(): Json + { + return $this->dataLists(new PayConfigLists()); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/pay/PayWayController.php b/server/app/adminapi/controller/setting/pay/PayWayController.php new file mode 100644 index 0000000..e2e9147 --- /dev/null +++ b/server/app/adminapi/controller/setting/pay/PayWayController.php @@ -0,0 +1,61 @@ +success('获取成功',$result); + } + + + /** + * @notes 设置支付方式 + * @return \think\response\Json + * @throws \Exception + * @author 段誉 + * @date 2023/2/23 16:27 + */ + public function setPayWay() + { + $params = $this->request->post(); + $result = (new PayWayLogic())->setPayWay($params); + if (true !== $result) { + return $this->fail($result); + } + return $this->success('操作成功',[],1, 1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/system/CacheController.php b/server/app/adminapi/controller/setting/system/CacheController.php new file mode 100644 index 0000000..b4a83f9 --- /dev/null +++ b/server/app/adminapi/controller/setting/system/CacheController.php @@ -0,0 +1,39 @@ +success('清除成功', [], 1, 1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/system/LogController.php b/server/app/adminapi/controller/setting/system/LogController.php new file mode 100644 index 0000000..5a26e31 --- /dev/null +++ b/server/app/adminapi/controller/setting/system/LogController.php @@ -0,0 +1,38 @@ +dataLists(new LogLists()); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/system/SystemController.php b/server/app/adminapi/controller/setting/system/SystemController.php new file mode 100644 index 0000000..dbe5bc5 --- /dev/null +++ b/server/app/adminapi/controller/setting/system/SystemController.php @@ -0,0 +1,42 @@ +data($result); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/user/UserController.php b/server/app/adminapi/controller/setting/user/UserController.php new file mode 100644 index 0000000..136593a --- /dev/null +++ b/server/app/adminapi/controller/setting/user/UserController.php @@ -0,0 +1,84 @@ +getConfig(); + return $this->data($result); + } + + + /** + * @notes 设置用户设置 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:08 + */ + public function setConfig() + { + $params = (new UserConfigValidate())->post()->goCheck('user'); + (new UserLogic())->setConfig($params); + return $this->success('操作成功', [], 1, 1); + } + + + /** + * @notes 获取注册配置 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:08 + */ + public function getRegisterConfig() + { + $result = (new UserLogic())->getRegisterConfig(); + return $this->data($result); + } + + + /** + * @notes 设置注册配置 + * @return \think\response\Json + * @author 段誉 + * @date 2022/3/29 10:08 + */ + public function setRegisterConfig() + { + $params = (new UserConfigValidate())->post()->goCheck('register'); + (new UserLogic())->setRegisterConfig($params); + return $this->success('操作成功', [], 1, 1); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/controller/setting/web/WebSettingController.php b/server/app/adminapi/controller/setting/web/WebSettingController.php new file mode 100644 index 0000000..936468c --- /dev/null +++ b/server/app/adminapi/controller/setting/web/WebSettingController.php @@ -0,0 +1,137 @@ +data($result); + } + + + /** + * @notes 设置网站信息 + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/28 15:45 + */ + public function setWebsite() + { + $params = (new WebSettingValidate())->post()->goCheck('website'); + WebSettingLogic::setWebsiteInfo($params); + return $this->success('设置成功', [], 1, 1); + } + + + + /** + * @notes 获取备案信息 + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/28 16:10 + */ + public function getCopyright() + { + $result = WebSettingLogic::getCopyright(); + return $this->data($result); + } + + + /** + * @notes 设置备案信息 + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/28 16:10 + */ + public function setCopyright() + { + $params = $this->request->post(); + $result = WebSettingLogic::setCopyright($params); + if (false === $result) { + return $this->fail(WebSettingLogic::getError() ?: '操作失败'); + } + return $this->success('设置成功', [], 1, 1); + } + + + /** + * @notes 设置政策协议 + * @return \think\response\Json + * @author ljj + * @date 2022/2/15 11:00 上午 + */ + public function setAgreement() + { + $params = $this->request->post(); + WebSettingLogic::setAgreement($params); + return $this->success('设置成功', [], 1, 1); + } + + + /** + * @notes 获取政策协议 + * @return \think\response\Json + * @author ljj + * @date 2022/2/15 11:16 上午 + */ + public function getAgreement() + { + $result = WebSettingLogic::getAgreement(); + return $this->data($result); + } + + /** + * @notes 获取站点统计配置 + * @return \think\response\Json + * @author yfdong + * @date 2024/09/20 22:24 + */ + public function getSiteStatistics() + { + $result = WebSettingLogic::getSiteStatistics(); + return $this->data($result); + } + + /** + * @notes 获取站点统计配置 + * @return \think\response\Json + * @author yfdong + * @date 2024/09/20 22:51 + */ + public function setSiteStatistics() + { + $params = (new WebSettingValidate())->post()->goCheck('siteStatistics'); + WebSettingLogic::setSiteStatistics($params); + return $this->success('设置成功', [], 1, 1); + } +} \ No newline at end of file diff --git a/server/app/adminapi/controller/shortlink/ShortLinkController.php b/server/app/adminapi/controller/shortlink/ShortLinkController.php new file mode 100644 index 0000000..0bf0967 --- /dev/null +++ b/server/app/adminapi/controller/shortlink/ShortLinkController.php @@ -0,0 +1,91 @@ +dataLists(new ShortLinkLists()); + } + + /** + * @notes 短链接点击日志 + */ + public function logLists() + { + return $this->dataLists(new ShortLinkLogLists()); + } + + /** + * @notes 删除短链接 + */ + public function delete() + { + $id = $this->request->post('id/d', 0); + if (empty($id)) { + return $this->fail('参数错误'); + } + ShortLink::destroy($id); + return $this->success('删除成功', [], 1, 1); + } + + /** + * @notes 重新生成二维码 + */ + public function regenerateQrcode() + { + $id = $this->request->post('id/d', 0); + if (empty($id)) { + return $this->fail('参数错误'); + } + + $link = ShortLink::findOrEmpty($id); + if ($link->isEmpty()) { + return $this->fail('短链接不存在'); + } + + // 删除旧二维码与海报合成文件 + $oldUri = (string) $link->getData('qrcode'); + $candidates = []; + if ($oldUri !== '') { + $candidates[] = $oldUri; + } + // 同时清理可能的同名残留文件(海报版与纯二维码版) + $candidates[] = 'uploads/qrcode/shortlink/' . $link->code . '.png'; + $candidates[] = 'uploads/qrcode/shortlink/' . $link->code . '_poster.png'; + $candidates[] = 'uploads/qrcode/shortlink/' . $link->code . '.svg'; + foreach (array_unique($candidates) as $uri) { + $abs = public_path() . $uri; + if (is_file($abs)) { + @unlink($abs); + } + } + + $shortUrl = request()->domain() . '/s/' . $link->code; + $posterData = ShortLinkPosterDataService::build( + (string) $link->page_type, + $link->page_id, + (int) $link->user_id + ); + $qrcode = ShortLinkQrCodeService::create($shortUrl, $link->code, $posterData['poster_image_uri'], $posterData); + + $link->qrcode = $qrcode['uri']; + $link->save(); + + return $this->success('重新生成成功', [ + 'qrcode' => $qrcode['uri'], + 'qrcode_url' => $qrcode['url'], + ], 1, 1); + } +} diff --git a/server/app/adminapi/controller/tools/GeneratorController.php b/server/app/adminapi/controller/tools/GeneratorController.php new file mode 100644 index 0000000..9a469df --- /dev/null +++ b/server/app/adminapi/controller/tools/GeneratorController.php @@ -0,0 +1,206 @@ +dataLists(new DataTableLists()); + } + + + /** + * @notes 获取已选择的数据表 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/14 10:57 + */ + public function generateTable() + { + return $this->dataLists(new GenerateTableLists()); + } + + + /** + * @notes 选择数据表 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/15 10:09 + */ + public function selectTable() + { + $params = (new GenerateTableValidate())->post()->goCheck('select'); + $result = GeneratorLogic::selectTable($params, $this->adminId); + if (true === $result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(GeneratorLogic::getError()); + } + + + /** + * @notes 生成代码 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/23 19:08 + */ + public function generate() + { + $params = (new GenerateTableValidate())->post()->goCheck('id'); + $result = GeneratorLogic::generate($params); + if (false === $result) { + return $this->fail(GeneratorLogic::getError()); + } + return $this->success('操作成功', $result, 1, 1); + } + + + /** + * @notes 下载文件 + * @return \think\response\File|\think\response\Json + * @author 段誉 + * @date 2022/6/24 9:51 + */ + public function download() + { + $params = (new GenerateTableValidate())->goCheck('download'); + $result = GeneratorLogic::download($params['file']); + if (false === $result) { + return $this->fail(GeneratorLogic::getError() ?: '下载失败'); + } + return download($result, 'likeadmin-curd.zip'); + } + + + /** + * @notes 预览代码 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/23 19:07 + */ + public function preview() + { + $params = (new GenerateTableValidate())->post()->goCheck('id'); + $result = GeneratorLogic::preview($params); + if (false === $result) { + return $this->fail(GeneratorLogic::getError()); + } + return $this->data($result); + } + + + /** + * @notes 同步字段 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/17 15:22 + */ + public function syncColumn() + { + $params = (new GenerateTableValidate())->post()->goCheck('id'); + $result = GeneratorLogic::syncColumn($params); + if (true === $result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(GeneratorLogic::getError()); + } + + + /** + * @notes 编辑表信息 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/20 10:44 + */ + public function edit() + { + $params = (new EditTableValidate())->post()->goCheck(); + $result = GeneratorLogic::editTable($params); + if (true === $result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(GeneratorLogic::getError()); + } + + + /** + * @notes 获取已选择的数据表详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/15 19:00 + */ + public function detail() + { + $params = (new GenerateTableValidate())->goCheck('id'); + $result = GeneratorLogic::getTableDetail($params); + return $this->success('', $result); + } + + + /** + * @notes 删除已选择的数据表信息 + * @return \think\response\Json + * @author 段誉 + * @date 2022/6/15 19:00 + */ + public function delete() + { + $params = (new GenerateTableValidate())->post()->goCheck('id'); + $result = GeneratorLogic::deleteTable($params); + if (true === $result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(GeneratorLogic::getError()); + } + + + /** + * @notes 获取模型 + * @return \think\response\Json + * @author 段誉 + * @date 2022/12/14 11:07 + */ + public function getModels() + { + $result = GeneratorLogic::getAllModels(); + return $this->success('', $result, 1, 1); + } + +} + diff --git a/server/app/adminapi/controller/user/UserController.php b/server/app/adminapi/controller/user/UserController.php new file mode 100644 index 0000000..c65656f --- /dev/null +++ b/server/app/adminapi/controller/user/UserController.php @@ -0,0 +1,112 @@ +dataLists(new UserLists()); + } + + + /** + * @notes 获取用户详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/22 16:34 + */ + public function detail() + { + $params = (new UserValidate())->goCheck('detail'); + $detail = UserLogic::detail($params['id']); + return $this->success('', $detail); + } + + + /** + * @notes 编辑用户信息 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/22 16:34 + */ + public function edit() + { + $params = (new UserValidate())->post()->goCheck('setInfo'); + UserLogic::setUserInfo($params); + return $this->success('操作成功', [], 1, 1); + } + + + /** + * @notes 调整用户余额 + * @return \think\response\Json + * @author 段誉 + * @date 2023/2/23 14:33 + */ + public function adjustMoney() + { + $params = (new AdjustUserMoney())->post()->goCheck(); + $res = UserLogic::adjustUserMoney($params); + if (true === $res) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail($res); + } + + + /** + * @notes 调整用户账户(余额/积分) + */ + public function adjustAccount() + { + $params = $this->request->post(); + if (empty($params['user_id']) || empty($params['type']) || empty($params['action']) || empty($params['num'])) { + return $this->fail('参数不完整'); + } + if (!in_array($params['type'], ['money', 'points'])) { + return $this->fail('调整类型错误'); + } + if (!in_array($params['action'], [1, 2])) { + return $this->fail('操作类型错误'); + } + if ($params['num'] <= 0) { + return $this->fail('调整数量必须大于零'); + } + $res = UserLogic::adjustUserAccount($params); + if (true === $res) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail($res); + } + +} diff --git a/server/app/adminapi/controller/vip/VipLevelController.php b/server/app/adminapi/controller/vip/VipLevelController.php new file mode 100644 index 0000000..33826db --- /dev/null +++ b/server/app/adminapi/controller/vip/VipLevelController.php @@ -0,0 +1,43 @@ +dataLists(new VipLevelLists()); + } + + public function add() + { + $params = (new VipLevelValidate())->post()->goCheck('add'); + VipLevelLogic::add($params); + return $this->success('添加成功', [], 1, 1); + } + + public function edit() + { + $params = (new VipLevelValidate())->post()->goCheck(); + VipLevelLogic::edit($params); + return $this->success('编辑成功', [], 1, 1); + } + + public function delete() + { + $params = (new VipLevelValidate())->post()->goCheck('delete'); + VipLevelLogic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + public function detail() + { + $params = (new VipLevelValidate())->goCheck('detail'); + return $this->data(VipLevelLogic::detail($params)); + } +} diff --git a/server/app/adminapi/event.php b/server/app/adminapi/event.php new file mode 100644 index 0000000..02a6f2b --- /dev/null +++ b/server/app/adminapi/event.php @@ -0,0 +1,23 @@ + [ + 'AppInit' => [], + 'HttpRun' => [], + 'HttpEnd' => ['app\adminapi\listener\OperationLog'], + 'LogLevel' => [], + 'LogWrite' => [], + ] +]; \ No newline at end of file diff --git a/server/app/adminapi/http/middleware/AuthMiddleware.php b/server/app/adminapi/http/middleware/AuthMiddleware.php new file mode 100644 index 0000000..2808a10 --- /dev/null +++ b/server/app/adminapi/http/middleware/AuthMiddleware.php @@ -0,0 +1,93 @@ +controllerObject->isNotNeedLogin()) { + return $next($request); + } + + if ($request->adminInfo['login_ip'] != request()->ip()) { + return JsonService::fail('ip地址发生变化,请重新登录', [], -1); + } + + //系统默认超级管理员,无需权限验证 + if (1 === $request->adminInfo['root']) { + return $next($request); + } + + $adminAuthCache = new AdminAuthCache($request->adminInfo['admin_id']); + + // 当前访问路径 + $accessUri = strtolower($request->controller() . '/' . $request->action()); + // 全部路由 + $allUri = $this->formatUrl($adminAuthCache->getAllUri()); + + // 判断该当前访问的uri是否存在,不存在无需验证 + if (!in_array($accessUri, $allUri)) { + return $next($request); + } + + // 当前管理员拥有的路由权限 + $AdminUris = $adminAuthCache->getAdminUri() ?? []; + $AdminUris = $this->formatUrl($AdminUris); + + if (in_array($accessUri, $AdminUris)) { + return $next($request); + } + return JsonService::fail('权限不足,无法访问或操作'); + } + + + /** + * @notes 格式化URL + * @param array $data + * @return array|string[] + * @author 段誉 + * @date 2022/7/7 15:39 + */ + public function formatUrl(array $data) + { + return array_map(function ($item) { + return strtolower(Str::camel($item)); + }, $data); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/http/middleware/CheckDemoMiddleware.php b/server/app/adminapi/http/middleware/CheckDemoMiddleware.php new file mode 100644 index 0000000..e6f652c --- /dev/null +++ b/server/app/adminapi/http/middleware/CheckDemoMiddleware.php @@ -0,0 +1,50 @@ +method() != 'POST') { + return $next($request); + } + + $accessUri = strtolower($request->controller() . '/' . $request->action()); + if (!in_array($accessUri, $this->ablePost) && env('project.demo_env')) { + return JsonService::fail('演示环境不支持修改数据,请下载源码本地部署体验'); + } + + return $next($request); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/http/middleware/EncryDemoDataMiddleware.php b/server/app/adminapi/http/middleware/EncryDemoDataMiddleware.php new file mode 100644 index 0000000..96c626e --- /dev/null +++ b/server/app/adminapi/http/middleware/EncryDemoDataMiddleware.php @@ -0,0 +1,114 @@ +controller() . '/' . $request->action()); + if (!in_array($accessUri, lower_uri($this->needCheck)) || !env('project.demo_env')) { + return $response; + } + + // 非json数据 + if (!method_exists($response, 'header') || !in_array('application/json; charset=utf-8', $response->getHeader())) { + return $response; + } + + $data = $response->getData(); + if (!is_array($data) || empty($data)) { + return $response; + } + + foreach ($data['data'] as $key => $item) { + // 字符串 + if (is_string($item)) { + $data['data'][$key] = $this->getEncryData($key, $item); + continue; + } + // 数组 + if (is_array($item)) { + foreach ($item as $itemKey => $itemValue) { + $data['data'][$key][$itemKey] = $this->getEncryData($itemKey, $itemValue); + } + } + } + + return $response->data($data); + } + + + /** + * @notes 加密配置 + * @param $key + * @param $value + * @return mixed|string + * @author 段誉 + * @date 2023/3/6 11:49 + */ + protected function getEncryData($key, $value) + { + // 非隐藏字段 + if (in_array($key, $this->excludeParams)) { + return $value; + } + + // 隐藏字段 + if (is_string($value)) { + return '******'; + } + return $value; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/http/middleware/InitMiddleware.php b/server/app/adminapi/http/middleware/InitMiddleware.php new file mode 100644 index 0000000..a4cf6d1 --- /dev/null +++ b/server/app/adminapi/http/middleware/InitMiddleware.php @@ -0,0 +1,57 @@ +controller()); + $controller = '\\app\\adminapi\\controller\\' . $controller . 'Controller'; + $controllerClass = invoke($controller); + if (($controllerClass instanceof BaseAdminController) === false) { + throw new ControllerExtendException($controller, '404'); + } + } catch (ClassNotFoundException $e) { + throw new HttpException(404, 'controller not exists:' . $e->getClass()); + } + + //创建控制器对象 + $request->controllerObject = invoke($controller); + + return $next($request); + } +} \ No newline at end of file diff --git a/server/app/adminapi/http/middleware/LoginMiddleware.php b/server/app/adminapi/http/middleware/LoginMiddleware.php new file mode 100644 index 0000000..0a09cef --- /dev/null +++ b/server/app/adminapi/http/middleware/LoginMiddleware.php @@ -0,0 +1,78 @@ +header('token'); + //判断接口是否免登录 + $isNotNeedLogin = $request->controllerObject->isNotNeedLogin(); + + //不直接判断$isNotNeedLogin结果,使不需要登录的接口通过,为了兼容某些接口可以登录或不登录访问 + if (empty($token) && !$isNotNeedLogin) { + //没有token并且该地址需要登录才能访问 + return JsonService::fail('请求参数缺token', [], 0, 0); + } + + $adminInfo = (new AdminTokenCache())->getAdminInfo($token); + if (empty($adminInfo) && !$isNotNeedLogin) { + //token过期无效并且该地址需要登录才能访问 + return JsonService::fail('登录超时,请重新登录', [], -1); + } + + //token临近过期,自动续期 + if ($adminInfo) { + //获取临近过期自动续期时长 + $beExpireDuration = Config::get('project.admin_token.be_expire_duration'); + //token续期 + if (time() > ($adminInfo['expire_time'] - $beExpireDuration)) { + $result = AdminTokenService::overtimeToken($token); + //续期失败(数据表被删除导致) + if (empty($result)) { + return JsonService::fail('登录过期', [], -1); + } + } + } + + //给request赋值,用于控制器 + $request->adminInfo = $adminInfo; + $request->adminId = $adminInfo['admin_id'] ?? 0; + + return $next($request); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/listener/OperationLog.php b/server/app/adminapi/listener/OperationLog.php new file mode 100644 index 0000000..87bb054 --- /dev/null +++ b/server/app/adminapi/listener/OperationLog.php @@ -0,0 +1,77 @@ +controllerObject->isNotNeedLogin() && empty($request->adminInfo)) { + return; + } + + //不记录日志操作 + if (strtolower(str_replace('.', '\\', $request->controller())) === 'setting\system\log') { + return; + } + + //获取操作注解 + $notes = ''; + try { + $re = new ReflectionClass($request->controllerObject); + $doc = $re->getMethod($request->action())->getDocComment(); + if (empty($doc)) { + throw new Exception('请给控制器方法注释'); + } + preg_match('/\s(\w+)/u', $re->getMethod($request->action())->getDocComment(), $values); + $notes = $values[0]; + } catch (Exception $e) { + $notes = $notes ?: '无法获取操作名称,请给控制器方法注释'; + } + + $params = $request->param(); + + //过滤密码参数 + if (isset($params['password'])) { + $params['password'] = "******"; + } + //过滤密钥参数 + if(isset($params['app_secret'])){ + $params['app_secret'] = "******"; + } + + //导出数据操作进行记录 + if (isset($params['export']) && $params['export'] == 2) { + $notes .= '-数据导出'; + } + + //记录日志 + $systemLog = new \app\common\model\OperationLog(); + $systemLog->admin_id = $request->adminInfo['admin_id'] ?? 0; + $systemLog->admin_name = $request->adminInfo['name'] ?? ''; + $systemLog->action = $notes; + $systemLog->account = $request->adminInfo['account'] ?? ''; + $systemLog->url = $request->url(true); + $systemLog->type = $request->isGet() ? 'GET' : 'POST'; + $systemLog->params = json_encode($params, true); + $systemLog->ip = $request->ip(); + $systemLog->result = $response->getContent(); + return $systemLog->save(); + } +} \ No newline at end of file diff --git a/server/app/adminapi/lists/BaseAdminDataLists.php b/server/app/adminapi/lists/BaseAdminDataLists.php new file mode 100644 index 0000000..bf5b1b0 --- /dev/null +++ b/server/app/adminapi/lists/BaseAdminDataLists.php @@ -0,0 +1,39 @@ +adminInfo = $this->request->adminInfo; + $this->adminId = $this->request->adminId; + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/lists/ai/AiAnalysisLists.php b/server/app/adminapi/lists/ai/AiAnalysisLists.php new file mode 100644 index 0000000..0621f10 --- /dev/null +++ b/server/app/adminapi/lists/ai/AiAnalysisLists.php @@ -0,0 +1,38 @@ + ['type', 'status'], + ]; + } + + public function lists(): array + { + $lists = AiAnalysis::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('id', 'desc') + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['create_time'] = is_numeric($item['create_time']) ? date('Y-m-d H:i:s', $item['create_time']) : $item['create_time']; + $item['expire_time'] = !empty($item['expire_time']) && is_numeric($item['expire_time']) ? date('Y-m-d H:i:s', $item['expire_time']) : $item['expire_time']; + } + + return $lists; + } + + public function count(): int + { + return AiAnalysis::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/ai/AiConfigLists.php b/server/app/adminapi/lists/ai/AiConfigLists.php new file mode 100644 index 0000000..004ef92 --- /dev/null +++ b/server/app/adminapi/lists/ai/AiConfigLists.php @@ -0,0 +1,22 @@ +limitOffset, $this->limitLength) + ->order('id', 'asc') + ->select() + ->toArray(); + } + + public function count(): int + { + return AiConfig::count(); + } +} diff --git a/server/app/adminapi/lists/ai/AiLogLists.php b/server/app/adminapi/lists/ai/AiLogLists.php new file mode 100644 index 0000000..328d37b --- /dev/null +++ b/server/app/adminapi/lists/ai/AiLogLists.php @@ -0,0 +1,37 @@ + ['type', 'status', 'user_id'], + ]; + } + + public function lists(): array + { + $lists = AiLog::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('id', 'desc') + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['create_time'] = !empty($item['create_time']) && is_numeric($item['create_time']) ? date('Y-m-d H:i:s', $item['create_time']) : ($item['create_time'] ?: '-'); + } + + return $lists; + } + + public function count(): int + { + return AiLog::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/ai/KbConfigLists.php b/server/app/adminapi/lists/ai/KbConfigLists.php new file mode 100644 index 0000000..00d8cb1 --- /dev/null +++ b/server/app/adminapi/lists/ai/KbConfigLists.php @@ -0,0 +1,38 @@ +whereLike('name', 'embedding_%') + ->whereOrLike('name', 'kb_%') + ->whereOrLike('name', 'qwen_%') + ->whereOrLike('name', 'openai_%') + ->whereOrLike('name', 'mtranserver_%') + ->whereOrLike('name', 'assistant_%') + ->whereOr('name', '=', 'post_analysis_prompt'); + })->limit($this->limitOffset, $this->limitLength) + ->order('id', 'asc') + ->select() + ->toArray(); + } + + public function count(): int + { + return AiConfig::where(function ($query) { + $query->whereLike('name', 'embedding_%') + ->whereOrLike('name', 'kb_%') + ->whereOrLike('name', 'qwen_%') + ->whereOrLike('name', 'openai_%') + ->whereOrLike('name', 'mtranserver_%') + ->whereOrLike('name', 'assistant_%') + ->whereOr('name', '=', 'post_analysis_prompt'); + })->count(); + } +} diff --git a/server/app/adminapi/lists/ai/KbDocumentLists.php b/server/app/adminapi/lists/ai/KbDocumentLists.php new file mode 100644 index 0000000..99daba3 --- /dev/null +++ b/server/app/adminapi/lists/ai/KbDocumentLists.php @@ -0,0 +1,39 @@ + ['domain', 'subtype', 'is_active'], + '%like%' => ['title', 'keywords'], + ]; + } + + public function lists(): array + { + $lists = AiKbDocument::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('id', 'desc') + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['source_updated_at_text'] = !empty($item['source_updated_at']) ? date('Y-m-d H:i:s', (int) $item['source_updated_at']) : '-'; + $item['update_time_text'] = !empty($item['update_time']) ? date('Y-m-d H:i:s', (int) $item['update_time']) : '-'; + } + + return $lists; + } + + public function count(): int + { + return AiKbDocument::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/ai/KbQueryLogLists.php b/server/app/adminapi/lists/ai/KbQueryLogLists.php new file mode 100644 index 0000000..e42ca24 --- /dev/null +++ b/server/app/adminapi/lists/ai/KbQueryLogLists.php @@ -0,0 +1,38 @@ + ['scene', 'status', 'user_id'], + '%like%' => ['query_text'], + ]; + } + + public function lists(): array + { + $lists = AiKbQueryLog::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('id', 'desc') + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['create_time_text'] = !empty($item['create_time']) ? date('Y-m-d H:i:s', (int) $item['create_time']) : '-'; + } + + return $lists; + } + + public function count(): int + { + return AiKbQueryLog::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/ai/KbSyncJobLists.php b/server/app/adminapi/lists/ai/KbSyncJobLists.php new file mode 100644 index 0000000..b76e7e5 --- /dev/null +++ b/server/app/adminapi/lists/ai/KbSyncJobLists.php @@ -0,0 +1,38 @@ + ['domain', 'subtype', 'action', 'status'], + ]; + } + + public function lists(): array + { + $lists = AiKbSyncJob::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('id', 'desc') + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['scheduled_at_text'] = !empty($item['scheduled_at']) ? date('Y-m-d H:i:s', (int) $item['scheduled_at']) : '-'; + $item['processed_at_text'] = !empty($item['processed_at']) ? date('Y-m-d H:i:s', (int) $item['processed_at']) : '-'; + } + + return $lists; + } + + public function count(): int + { + return AiKbSyncJob::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/app/AppVersionLists.php b/server/app/adminapi/lists/app/AppVersionLists.php new file mode 100644 index 0000000..a91c32c --- /dev/null +++ b/server/app/adminapi/lists/app/AppVersionLists.php @@ -0,0 +1,38 @@ + ['version_name', 'title'], + '=' => ['platform', 'package_type', 'update_type', 'status'], + ]; + } + + public function lists(): array + { + return AppVersion::where($this->searchWhere) + ->append(['platform_text', 'package_type_text', 'update_type_text']) + ->limit($this->limitOffset, $this->limitLength) + ->order(['id' => 'desc']) + ->select() + ->toArray(); + } + + public function count(): int + { + return AppVersion::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/article/ArticleAiCommentTaskLists.php b/server/app/adminapi/lists/article/ArticleAiCommentTaskLists.php new file mode 100644 index 0000000..0b8e1ee --- /dev/null +++ b/server/app/adminapi/lists/article/ArticleAiCommentTaskLists.php @@ -0,0 +1,50 @@ + ['article_id', 'status'], + '%like%' => [], + ]; + } + + public function lists(): array + { + $lists = ArticleAiCommentTask::where($this->searchWhere) + ->whereNull('delete_time') + ->limit($this->limitOffset, $this->limitLength) + ->order('id', 'desc') + ->select() + ->toArray(); + + $userIds = array_unique(array_column($lists, 'virtual_user_id')); + $users = []; + if (!empty($userIds)) { + $users = User::whereIn('id', $userIds)->column('nickname', 'id'); + } + + foreach ($lists as &$item) { + $item['virtual_nickname'] = $users[$item['virtual_user_id']] ?? '-'; + $item['create_time_text'] = is_numeric($item['create_time']) ? date('Y-m-d H:i:s', $item['create_time']) : $item['create_time']; + $item['run_at_text'] = is_numeric($item['run_at']) ? date('Y-m-d H:i:s', $item['run_at']) : $item['run_at']; + } + + return $lists; + } + + public function count(): int + { + return ArticleAiCommentTask::where($this->searchWhere) + ->whereNull('delete_time') + ->count(); + } +} diff --git a/server/app/adminapi/lists/article/ArticleCateLists.php b/server/app/adminapi/lists/article/ArticleCateLists.php new file mode 100644 index 0000000..2f6df58 --- /dev/null +++ b/server/app/adminapi/lists/article/ArticleCateLists.php @@ -0,0 +1,112 @@ + 'create_time', 'id' => 'id']; + } + + /** + * @notes 设置默认排序 + * @return array + * @author heshihu + * @date 2022/2/9 15:08 + */ + public function setDefaultOrder(): array + { + return ['sort' => 'desc', 'id' => 'desc']; + } + + /** + * @notes 获取管理列表 + * @return array + * @author heshihu + * @date 2022/2/21 17:11 + */ + public function lists(): array + { + $lists = ArticleCate::where($this->searchWhere) + ->append(['is_show_desc', 'article_count']) + ->order($this->sortOrder) + ->select() + ->toArray(); + + $pid = 0; + if (!empty($lists)) { + $pid = min(array_column($lists, 'pid')); + } + return self::getTree($lists, $pid); + } + + public static function getTree($array, $pid = 0) + { + $list = []; + foreach ($array as $item) { + if ($item['pid'] == $pid) { + $item['children'] = self::getTree($array, $item['id']); + $list[] = $item; + } + } + return $list; + } + + /** + * @notes 获取数量 + * @return int + * @author heshihu + * @date 2022/2/9 15:12 + */ + public function count(): int + { + return ArticleCate::where($this->searchWhere)->count(); + } + + public function extend() + { + return []; + } +} diff --git a/server/app/adminapi/lists/article/ArticleLists.php b/server/app/adminapi/lists/article/ArticleLists.php new file mode 100644 index 0000000..109f380 --- /dev/null +++ b/server/app/adminapi/lists/article/ArticleLists.php @@ -0,0 +1,99 @@ + ['title'], + '=' => ['cid', 'is_show', 'is_recommend'] + ]; + } + + /** + * @notes 设置支持排序字段 + * @return array + * @author heshihu + * @date 2022/2/9 15:11 + */ + public function setSortFields(): array + { + return ['create_time' => 'create_time', 'id' => 'id']; + } + + /** + * @notes 设置默认排序 + * @return array + * @author heshihu + * @date 2022/2/9 15:08 + */ + public function setDefaultOrder(): array + { + return ['sort' => 'desc', 'id' => 'desc']; + } + + /** + * @notes 获取管理列表 + * @return array + * @author heshihu + * @date 2022/2/21 17:11 + */ + public function lists(): array + { + $ArticleLists = Article::where($this->searchWhere) + ->append(['cate_name', 'click']) + ->limit($this->limitOffset, $this->limitLength) + ->order($this->sortOrder) + ->select() + ->toArray(); + + return $ArticleLists; + } + + /** + * @notes 获取数量 + * @return int + * @author heshihu + * @date 2022/2/9 15:12 + */ + public function count(): int + { + return Article::where($this->searchWhere)->count(); + } + + public function extend() + { + return []; + } +} diff --git a/server/app/adminapi/lists/auth/AdminLists.php b/server/app/adminapi/lists/auth/AdminLists.php new file mode 100644 index 0000000..6ba8430 --- /dev/null +++ b/server/app/adminapi/lists/auth/AdminLists.php @@ -0,0 +1,206 @@ + '账号', + 'name' => '名称', + 'role_name' => '角色', + 'dept_name' => '部门', + 'create_time' => '创建时间', + 'login_time' => '最近登录时间', + 'login_ip' => '最近登录IP', + 'disable_desc' => '状态', + ]; + } + + + /** + * @notes 设置导出文件名 + * @return string + * @author 段誉 + * @date 2021/12/29 10:08 + */ + public function setFileName(): string + { + return '管理员列表'; + } + + + /** + * @notes 设置搜索条件 + * @return \string[][] + * @author 段誉 + * @date 2021/12/29 10:07 + */ + public function setSearch(): array + { + return [ + '%like%' => ['name', 'account'], + ]; + } + + + /** + * @notes 设置支持排序字段 + * @return string[] + * @author 段誉 + * @date 2021/12/29 10:07 + * @remark 格式: ['前端传过来的字段名' => '数据库中的字段名']; + */ + public function setSortFields(): array + { + return ['create_time' => 'create_time', 'id' => 'id']; + } + + + /** + * @notes 设置默认排序 + * @return string[] + * @author 段誉 + * @date 2021/12/29 10:06 + */ + public function setDefaultOrder(): array + { + return ['id' => 'desc']; + } + + /** + * @notes 查询条件 + * @return array + * @author 段誉 + * @date 2022/11/29 11:33 + */ + public function queryWhere() + { + $where = []; + if (isset($this->params['role_id']) && $this->params['role_id'] != '') { + $adminIds = AdminRole::where('role_id', $this->params['role_id'])->column('admin_id'); + if (!empty($adminIds)) { + $where[] = ['id', 'in', $adminIds]; + } + } + return $where; + } + + + /** + * @notes 获取管理列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2021/12/29 10:05 + */ + public function lists(): array + { + $field = [ + 'id', 'name', 'account', 'create_time', 'disable', 'root', + 'login_time', 'login_ip', 'multipoint_login', 'avatar' + ]; + + $adminLists = Admin::field($field) + ->where($this->searchWhere) + ->where($this->queryWhere()) + ->limit($this->limitOffset, $this->limitLength) + ->order($this->sortOrder) + ->append(['role_id', 'dept_id', 'jobs_id', 'disable_desc']) + ->select() + ->toArray(); + + // 角色数组('角色id'=>'角色名称') + $roleLists = SystemRole::column('name', 'id'); + // 部门列表 + $deptLists = Dept::column('name', 'id'); + // 岗位列表 + $jobsLists = Jobs::column('name', 'id'); + + //管理员列表增加角色名称 + foreach ($adminLists as $k => $v) { + $roleName = ''; + if ($v['root'] == 1) { + $roleName = '系统管理员'; + } else { + foreach ($v['role_id'] as $roleId) { + $roleName .= $roleLists[$roleId] ?? ''; + $roleName .= '/'; + } + } + + $deptName = ''; + foreach ($v['dept_id'] as $deptId) { + $deptName .= $deptLists[$deptId] ?? ''; + $deptName .= '/'; + } + + $jobsName = ''; + foreach ($v['jobs_id'] as $jobsId) { + $jobsName .= $jobsLists[$jobsId] ?? ''; + $jobsName .= '/'; + } + + $adminLists[$k]['role_name'] = trim($roleName, '/'); + $adminLists[$k]['dept_name'] = trim($deptName, '/'); + $adminLists[$k]['jobs_name'] = trim($jobsName, '/'); + } + + return $adminLists; + } + + /** + * @notes 获取数量 + * @return int + * @author 令狐冲 + * @date 2021/7/13 00:52 + */ + public function count(): int + { + return Admin::where($this->searchWhere) + ->where($this->queryWhere()) + ->count(); + } + + public function extend() + { + return []; + } +} \ No newline at end of file diff --git a/server/app/adminapi/lists/auth/MenuLists.php b/server/app/adminapi/lists/auth/MenuLists.php new file mode 100644 index 0000000..d622e5a --- /dev/null +++ b/server/app/adminapi/lists/auth/MenuLists.php @@ -0,0 +1,58 @@ + 'desc', 'id' => 'asc']) + ->select() + ->toArray(); + return linear_to_tree($lists, 'children'); + } + + + /** + * @notes 获取菜单数量 + * @return int + * @author 段誉 + * @date 2022/6/29 16:41 + */ + public function count(): int + { + return SystemMenu::count(); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/lists/auth/RoleLists.php b/server/app/adminapi/lists/auth/RoleLists.php new file mode 100644 index 0000000..4b47d3f --- /dev/null +++ b/server/app/adminapi/lists/auth/RoleLists.php @@ -0,0 +1,93 @@ + '角色名称', + 'desc' => '备注', + 'create_time' => '创建时间' + ]; + } + + /** + * @notes 导出表名 + * @return string + * @author Tab + * @date 2021/9/22 18:52 + */ + public function setFileName(): string + { + return '角色表'; + } + + /** + * @notes 角色列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author cjhao + * @date 2021/8/25 18:00 + */ + public function lists(): array + { + $lists = SystemRole::with(['role_menu_index']) + ->field('id,name,desc,sort,create_time') + ->limit($this->limitOffset, $this->limitLength) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->select() + ->toArray(); + + foreach ($lists as $key => $role) { + //使用角色的人数 + $lists[$key]['num'] = AdminRole::where('role_id', $role['id'])->count(); + $menuId = array_column($role['role_menu_index'], 'menu_id'); + $lists[$key]['menu_id'] = $menuId; + unset($lists[$key]['role_menu_index']); + } + + return $lists; + } + + /** + * @notes 总记录数 + * @return int + * @author Tab + * @date 2021/7/13 11:26 + */ + public function count(): int + { + return SystemRole::count(); + } +} \ No newline at end of file diff --git a/server/app/adminapi/lists/channel/OfficialAccountReplyLists.php b/server/app/adminapi/lists/channel/OfficialAccountReplyLists.php new file mode 100644 index 0000000..db93e71 --- /dev/null +++ b/server/app/adminapi/lists/channel/OfficialAccountReplyLists.php @@ -0,0 +1,80 @@ + ['reply_type'] + ]; + } + + + /** + * @notes 回复列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/3/30 15:02 + */ + public function lists(): array + { + $field = 'id,name,keyword,matching_type,content,content_type,status,sort'; + $field .= ',matching_type as matching_type_desc,content_type as content_type_desc,status as status_desc'; + + $lists = OfficialAccountReply::field($field) + ->where($this->searchWhere) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + + return $lists; + } + + + /** + * @notes 回复记录数 + * @return int + * @author 段誉 + * @date 2022/3/30 15:02 + */ + public function count(): int + { + $count = OfficialAccountReply::where($this->searchWhere)->count(); + + return $count; + } +} \ No newline at end of file diff --git a/server/app/adminapi/lists/community/CommunityCommentLists.php b/server/app/adminapi/lists/community/CommunityCommentLists.php new file mode 100644 index 0000000..69a1cb1 --- /dev/null +++ b/server/app/adminapi/lists/community/CommunityCommentLists.php @@ -0,0 +1,51 @@ + ['post_id', 'user_id', 'status'], + ]; + } + + public function lists(): array + { + $lists = CommunityComment::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('id', 'desc') + ->select() + ->toArray(); + + $userIds = array_unique(array_column($lists, 'user_id')); + $users = User::whereIn('id', $userIds)->column('nickname,avatar', 'id'); + + $postIds = array_unique(array_column($lists, 'post_id')); + $posts = CommunityPost::whereIn('id', $postIds)->column('content,user_id', 'id'); + + foreach ($lists as &$item) { + $user = $users[$item['user_id']] ?? []; + $item['nickname'] = $user['nickname'] ?? '-'; + $item['create_time'] = is_numeric($item['create_time']) ? date('Y-m-d H:i:s', $item['create_time']) : $item['create_time']; + $post = $posts[$item['post_id']] ?? []; + $item['post_content'] = isset($post['content']) ? mb_substr($post['content'], 0, 50) : '-'; + $postUser = !empty($post['user_id']) ? (User::field('nickname')->findOrEmpty($post['user_id'])->toArray()) : []; + $item['post_nickname'] = $postUser['nickname'] ?? '-'; + } + + return $lists; + } + + public function count(): int + { + return CommunityComment::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/community/CommunityPostLists.php b/server/app/adminapi/lists/community/CommunityPostLists.php new file mode 100644 index 0000000..5d17d82 --- /dev/null +++ b/server/app/adminapi/lists/community/CommunityPostLists.php @@ -0,0 +1,44 @@ + ['status', 'post_type', 'is_top', 'is_hot', 'is_recommend', 'user_id'], + ]; + } + + public function lists(): array + { + $lists = CommunityPost::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('id', 'desc') + ->select() + ->toArray(); + + $userIds = array_unique(array_column($lists, 'user_id')); + $users = User::whereIn('id', $userIds)->column('nickname,avatar', 'id'); + + foreach ($lists as &$item) { + $user = $users[$item['user_id']] ?? []; + $item['nickname'] = $user['nickname'] ?? '-'; + $item['avatar'] = $user['avatar'] ?? ''; + $item['create_time'] = is_numeric($item['create_time']) ? date('Y-m-d H:i:s', $item['create_time']) : $item['create_time']; + } + + return $lists; + } + + public function count(): int + { + return CommunityPost::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/community/CommunityTagLists.php b/server/app/adminapi/lists/community/CommunityTagLists.php new file mode 100644 index 0000000..ec154fd --- /dev/null +++ b/server/app/adminapi/lists/community/CommunityTagLists.php @@ -0,0 +1,33 @@ + ['name'], + '=' => ['status'], + ]; + } + + public function lists(): array + { + return CommunityTag::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('sort', 'desc') + ->order('id', 'desc') + ->select() + ->toArray(); + } + + public function count(): int + { + return CommunityTag::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/crawler/CrawlErrorLogLists.php b/server/app/adminapi/lists/crawler/CrawlErrorLogLists.php new file mode 100644 index 0000000..003bdf7 --- /dev/null +++ b/server/app/adminapi/lists/crawler/CrawlErrorLogLists.php @@ -0,0 +1,34 @@ + ['task_name', 'error_type', 'channel', 'http_status'], + ]; + } + + public function lists(): array + { + $lists = CrawlErrorLog::field('id,task_name,request_url,request_method,http_status,error_type,error_message,channel,notified,created_at') + ->where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('id', 'desc') + ->select() + ->toArray(); + + return $lists; + } + + public function count(): int + { + return CrawlErrorLog::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/crontab/CrontabLists.php b/server/app/adminapi/lists/crontab/CrontabLists.php new file mode 100644 index 0000000..5c5c8f1 --- /dev/null +++ b/server/app/adminapi/lists/crontab/CrontabLists.php @@ -0,0 +1,61 @@ +limit($this->limitOffset, $this->limitLength) + ->order('id', 'desc') + ->select() + ->toArray(); + + return $lists; + } + + + /** + * @notes 定时任务数量 + * @return int + * @author 段誉 + * @date 2022/3/29 14:38 + */ + public function count(): int + { + return Crontab::count(); + } +} \ No newline at end of file diff --git a/server/app/adminapi/lists/decorate/MenuLists.php b/server/app/adminapi/lists/decorate/MenuLists.php new file mode 100644 index 0000000..57e1012 --- /dev/null +++ b/server/app/adminapi/lists/decorate/MenuLists.php @@ -0,0 +1,64 @@ +field('id,name,image,link_type,link_address,sort,status') + ->order(['sort'=>'asc','id'=>'desc']) + ->append(['link_address_desc','status_desc']) + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + + foreach ($lists as &$list) { + $list['link_address_desc'] = MenuEnum::getLinkDesc($list['link_type']).':'.$list['link_address_desc']; + } + + return $lists; + } + + /** + * @notes 菜单总数 + * @return int + * @author ljj + * @date 2022/2/14 11:29 上午 + */ + public function count(): int + { + return (new Menu())->count(); + } +} \ No newline at end of file diff --git a/server/app/adminapi/lists/decorate/NavigationLists.php b/server/app/adminapi/lists/decorate/NavigationLists.php new file mode 100644 index 0000000..42abde8 --- /dev/null +++ b/server/app/adminapi/lists/decorate/NavigationLists.php @@ -0,0 +1,52 @@ +select()->toArray(); + } + + /** + * @notes 底部导航总数 + * @return int + * @author ljj + * @date 2022/2/14 10:13 上午 + */ + public function count(): int + { + return (new Navigation())->count(); + } +} \ No newline at end of file diff --git a/server/app/adminapi/lists/dept/JobsLists.php b/server/app/adminapi/lists/dept/JobsLists.php new file mode 100644 index 0000000..c33533d --- /dev/null +++ b/server/app/adminapi/lists/dept/JobsLists.php @@ -0,0 +1,105 @@ + ['name'], + '=' => ['code', 'status'] + ]; + } + + + /** + * @notes 获取管理列表 + * @return array + * @author heshihu + * @date 2022/2/21 17:11 + */ + public function lists(): array + { + $lists = Jobs::where($this->searchWhere) + ->append(['status_desc']) + ->limit($this->limitOffset, $this->limitLength) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->select() + ->toArray(); + + return $lists; + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2022/5/26 9:48 + */ + public function count(): int + { + return Jobs::where($this->searchWhere)->count(); + } + + + /** + * @notes 导出文件名 + * @return string + * @author 段誉 + * @date 2022/11/24 16:17 + */ + public function setFileName(): string + { + return '岗位列表'; + } + + + /** + * @notes 导出字段 + * @return string[] + * @author 段誉 + * @date 2022/11/24 16:17 + */ + public function setExcelFields(): array + { + return [ + 'code' => '岗位编码', + 'name' => '岗位名称', + 'remark' => '备注', + 'status_desc' => '状态', + 'create_time' => '添加时间', + ]; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/lists/device/DeviceLists.php b/server/app/adminapi/lists/device/DeviceLists.php new file mode 100644 index 0000000..28b62d1 --- /dev/null +++ b/server/app/adminapi/lists/device/DeviceLists.php @@ -0,0 +1,33 @@ + ['platform', 'user_id'], + '%like%' => ['device_id', 'model', 'brand', 'app_version', 'ip'], + ]; + } + + public function lists(): array + { + return UserDevice::where($this->searchWhere) + ->append(['platform_text']) + ->order(['last_at' => 'desc', 'id' => 'desc']) + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + } + + public function count(): int + { + return UserDevice::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/file/FileCateLists.php b/server/app/adminapi/lists/file/FileCateLists.php new file mode 100644 index 0000000..8f3c491 --- /dev/null +++ b/server/app/adminapi/lists/file/FileCateLists.php @@ -0,0 +1,74 @@ + ['type'] + ]; + } + + + /** + * @notes 获取文件分类列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2021/12/29 14:24 + */ + public function lists(): array + { + $lists = (new FileCate())->field(['id,pid,type,name']) + ->where($this->searchWhere) + ->order('id desc') + ->select()->toArray(); + + return linear_to_tree($lists, 'children'); + } + + + /** + * @notes 获取文件分类数量 + * @return int + * @author 段誉 + * @date 2021/12/29 14:24 + */ + public function count(): int + { + return (new FileCate())->where($this->searchWhere)->count(); + } +} \ No newline at end of file diff --git a/server/app/adminapi/lists/file/FileLists.php b/server/app/adminapi/lists/file/FileLists.php new file mode 100644 index 0000000..489c730 --- /dev/null +++ b/server/app/adminapi/lists/file/FileLists.php @@ -0,0 +1,108 @@ + ['type', 'source'], + '%like%' => ['name'] + ]; + } + + /** + * @notes 额外查询处理 + * @return array + * @author 段誉 + * @date 2024/2/7 10:26 + */ + public function queryWhere(): array + { + $where = []; + + if (!empty($this->params['cid'])) { + $cateChild = FileLogic::getCateIds($this->params['cid']); + array_push($cateChild, $this->params['cid']); + $where[] = ['cid', 'in', $cateChild]; + } + + return $where; + } + + + /** + * @notes 获取文件列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2021/12/29 14:27 + */ + public function lists(): array + { + $lists = (new File())->field(['id,cid,type,name,uri,create_time']) + ->order('id', 'desc') + ->where($this->searchWhere) + ->where($this->queryWhere()) +// ->where('source', FileEnum::SOURCE_ADMIN) + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['url'] = FileService::getFileUrl($item['uri']); + } + + return $lists; + } + + + /** + * @notes 获取文件数量 + * @return int + * @author 段誉 + * @date 2021/12/29 14:29 + */ + public function count(): int + { + return (new File())->where($this->searchWhere) + ->where($this->queryWhere()) +// ->where('source', FileEnum::SOURCE_ADMIN) + ->count(); + } +} diff --git a/server/app/adminapi/lists/finance/AccountLogLists.php b/server/app/adminapi/lists/finance/AccountLogLists.php new file mode 100644 index 0000000..187defa --- /dev/null +++ b/server/app/adminapi/lists/finance/AccountLogLists.php @@ -0,0 +1,131 @@ + ['al.change_type'], + ]; + } + + + /** + * @notes 搜索条件 + * @author 段誉 + * @date 2023/2/24 15:26 + */ + public function queryWhere() + { + $where = []; + // 按变动对象筛选 + if (!empty($this->params['change_object'])) { + $where[] = ['al.change_object', '=', $this->params['change_object']]; + } + // 用户余额 + if (isset($this->params['type']) && $this->params['type'] == 'um') { + $where[] = ['change_type', 'in', AccountLogEnum::getUserMoneyChangeType()]; + } + + if (!empty($this->params['user_info'])) { + $where[] = ['u.sn|u.nickname|u.mobile|u.account', 'like', '%' . $this->params['user_info'] . '%']; + } + + if (!empty($this->params['start_time'])) { + $where[] = ['al.create_time', '>=', strtotime($this->params['start_time'])]; + } + + if (!empty($this->params['end_time'])) { + $where[] = ['al.create_time', '<=', strtotime($this->params['end_time'])]; + } + + return $where; + } + + + /** + * @notes 获取列表 + * @return array + * @author 段誉 + * @date 2023/2/24 15:31 + */ + public function lists(): array + { + $field = 'u.nickname,u.account,u.sn,u.avatar,u.mobile,al.action,al.change_object,al.change_amount,al.left_amount,al.change_type,al.source_sn,al.remark,al.create_time'; + $lists = UserAccountLog::alias('al') + ->join('user u', 'u.id = al.user_id') + ->field($field) + ->where($this->searchWhere) + ->where($this->queryWhere()) + ->order('al.id', 'desc') + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + + // 变动对象字典 + $objectMap = Db::name('dict_data') + ->where('type_value', 'account_change_object') + ->where('status', 1) + ->column('name', 'value'); + + foreach ($lists as &$item) { + $item['avatar'] = FileService::getFileUrl($item['avatar']); + $item['change_type_desc'] = AccountLogEnum::getChangeTypeDesc($item['change_type']); + $item['change_object_desc'] = $objectMap[(string) $item['change_object']] ?? '-'; + $symbol = $item['action'] == AccountLogEnum::INC ? '+' : '-'; + $item['change_amount'] = $symbol . $item['change_amount']; + } + + return $lists; + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2023/2/24 15:36 + */ + public function count(): int + { + return UserAccountLog::alias('al') + ->join('user u', 'u.id = al.user_id') + ->where($this->queryWhere()) + ->where($this->searchWhere) + ->count(); + } +} diff --git a/server/app/adminapi/lists/finance/RefundLogLists.php b/server/app/adminapi/lists/finance/RefundLogLists.php new file mode 100644 index 0000000..2809bab --- /dev/null +++ b/server/app/adminapi/lists/finance/RefundLogLists.php @@ -0,0 +1,79 @@ +params['record_id'] ?? 0]; + return $where; + } + + + /** + * @notes 获取列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2023/3/1 9:56 + */ + public function lists(): array + { + $lists = (new RefundLog()) + ->order(['id' => 'desc']) + ->where($this->queryWhere()) + ->limit($this->limitOffset, $this->limitLength) + ->hidden(['refund_msg']) + ->append(['handler', 'refund_status_text']) + ->select() + ->toArray(); + return $lists; + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2023/3/1 9:56 + */ + public function count(): int + { + return (new RefundLog()) + ->where($this->queryWhere()) + ->count(); + } + +} diff --git a/server/app/adminapi/lists/finance/RefundRecordLists.php b/server/app/adminapi/lists/finance/RefundRecordLists.php new file mode 100644 index 0000000..84d097c --- /dev/null +++ b/server/app/adminapi/lists/finance/RefundRecordLists.php @@ -0,0 +1,143 @@ + ['r.sn', 'r.order_sn', 'r.refund_type'], + ]; + } + + + /** + * @notes 查询条件 + * @param bool $flag + * @return array + * @author 段誉 + * @date 2023/3/1 9:51 + */ + public function queryWhere($flag = true) + { + $where = []; + if (!empty($this->params['user_info'])) { + $where[] = ['u.sn|u.nickname|u.mobile|u.account', 'like', '%' . $this->params['user_info'] . '%']; + } + if (!empty($this->params['start_time'])) { + $where[] = ['r.create_time', '>=', strtotime($this->params['start_time'])]; + } + if (!empty($this->params['end_time'])) { + $where[] = ['r.create_time', '<=', strtotime($this->params['end_time'])]; + } + + if ($flag == true) { + if (isset($this->params['refund_status']) && $this->params['refund_status'] != '') { + $where[] = ['r.refund_status', '=', $this->params['refund_status']]; + } + } + + return $where; + } + + + /** + * @notes 获取列表 + * @return array + * @author 段誉 + * @date 2023/3/1 9:51 + */ + public function lists(): array + { + $lists = (new RefundRecord())->alias('r') + ->field('r.*,u.nickname,u.avatar') + ->join('user u', 'u.id = r.user_id') + ->order(['r.id' => 'desc']) + ->where($this->searchWhere) + ->where($this->queryWhere()) + ->limit($this->limitOffset, $this->limitLength) + ->append(['refund_type_text', 'refund_status_text', 'refund_way_text']) + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['avatar'] = FileService::getFileUrl($item['avatar']); + } + + return $lists; + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2023/3/1 9:51 + */ + public function count(): int + { + return (new RefundRecord())->alias('r') + ->join('user u', 'u.id = r.user_id') + ->where($this->searchWhere) + ->where($this->queryWhere()) + ->count(); + } + + + /** + * @notes 额外参数 + * @return mixed|null + * @author 段誉 + * @date 2023/3/1 9:51 + */ + public function extend() + { + $count = (new RefundRecord())->alias('r') + ->join('user u', 'u.id = r.user_id') + ->field([ + 'count(r.id) as total', + 'count(if(r.refund_status='.RefundEnum::REFUND_ING.', true, null)) as ing', + 'count(if(r.refund_status='.RefundEnum::REFUND_SUCCESS.', true, null)) as success', + 'count(if(r.refund_status='.RefundEnum::REFUND_ERROR.', true, null)) as error', + ]) + ->where($this->searchWhere) + ->where($this->queryWhere(false)) + ->select()->toArray(); + + return array_shift($count); + } +} diff --git a/server/app/adminapi/lists/lottery/LotteryCategoryLists.php b/server/app/adminapi/lists/lottery/LotteryCategoryLists.php new file mode 100644 index 0000000..02e336f --- /dev/null +++ b/server/app/adminapi/lists/lottery/LotteryCategoryLists.php @@ -0,0 +1,40 @@ + ['name'], + '=' => ['category', 'status', 'is_show'], + ]; + } + + public function lists(): array + { + $lists = LotteryGame::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('sort', 'asc') + ->order('id', 'asc') + ->select() + ->toArray(); + + $categories = LotteryGameCategory::column('label', 'value'); + foreach ($lists as &$item) { + $item['category_name'] = $categories[$item['category']] ?? '-'; + } + return $lists; + } + + public function count(): int + { + return LotteryGame::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/lottery/LotteryDrawLists.php b/server/app/adminapi/lists/lottery/LotteryDrawLists.php new file mode 100644 index 0000000..047f1f7 --- /dev/null +++ b/server/app/adminapi/lists/lottery/LotteryDrawLists.php @@ -0,0 +1,42 @@ + ['category_id', 'status'], + ]; + } + + public function lists(): array + { + $lists = LotteryDraw::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('draw_date', 'desc') + ->order('id', 'desc') + ->select() + ->toArray(); + + $catIds = array_unique(array_column($lists, 'category_id')); + $cats = LotteryCategory::whereIn('id', $catIds)->column('name', 'id'); + + foreach ($lists as &$item) { + $item['category_name'] = $cats[$item['category_id']] ?? '-'; + } + + return $lists; + } + + public function count(): int + { + return LotteryDraw::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/lottery/LotteryRegionLists.php b/server/app/adminapi/lists/lottery/LotteryRegionLists.php new file mode 100644 index 0000000..eafcc7f --- /dev/null +++ b/server/app/adminapi/lists/lottery/LotteryRegionLists.php @@ -0,0 +1,33 @@ + ['label'], + '=' => ['is_show'], + ]; + } + + public function lists(): array + { + return LotteryGameCategory::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('sort', 'asc') + ->order('id', 'desc') + ->select() + ->toArray(); + } + + public function count(): int + { + return LotteryGameCategory::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/match/LeagueLists.php b/server/app/adminapi/lists/match/LeagueLists.php new file mode 100644 index 0000000..963b8ab --- /dev/null +++ b/server/app/adminapi/lists/match/LeagueLists.php @@ -0,0 +1,101 @@ + ['label'], + '=' => ['type', 'is_show'], + ]; + } + + public function lists(): array + { + $lists = League::where($this->searchWhere) + ->whereIn('type', ['sport', 'league']) + ->order('sort', 'desc') + ->order('id', 'desc') + ->select() + ->toArray(); + + // 按 type 分组 + $sports = []; + $leagues = []; + foreach ($lists as $item) { + if ($item['type'] === 'sport') { + $sports[] = $item; + } else { + $leagues[] = $item; + } + } + + // 按 sport_type 将联赛挂到对应体育类型下 + $leagueMap = []; + foreach ($leagues as $lg) { + $st = $lg['sport_type']; + if (!isset($leagueMap[$st])) { + $leagueMap[$st] = []; + } + $leagueMap[$st][] = $lg; + } + + // 构建树形:sport 为父节点,league 为子节点 + $tree = []; + foreach ($sports as $sport) { + $st = $sport['sport_type']; + $children = $leagueMap[$st] ?? []; + $tree[] = [ + 'id' => $sport['id'], + 'label' => $sport['label'], + 'type' => 'sport', + 'sport_type' => $st, + 'icon' => $sport['icon'] ?? '', + 'sort' => $sport['sort'], + 'is_show' => $sport['is_show'], + 'is_group' => true, + 'children' => $children, + ]; + } + + // 未归属任何 sport 的联赛(孤儿数据) + $assignedIds = []; + foreach ($leagueMap as $group) { + foreach ($group as $lg) { + $assignedIds[] = $lg['id']; + } + } + $orphans = []; + foreach ($leagues as $lg) { + if (!in_array($lg['id'], $assignedIds)) { + $orphans[] = $lg; + } + } + if (!empty($orphans)) { + $tree[] = [ + 'id' => 'type_orphan', + 'label' => '未分类', + 'type' => 'league', + 'sport_type' => -1, + 'icon' => '', + 'sort' => '', + 'is_show' => '', + 'is_group' => true, + 'children' => $orphans, + ]; + } + + return $tree; + } + + public function count(): int + { + return League::where($this->searchWhere)->whereIn('type', ['sport', 'league'])->count(); + } +} diff --git a/server/app/adminapi/lists/match/MatchLists.php b/server/app/adminapi/lists/match/MatchLists.php new file mode 100644 index 0000000..854ee9c --- /dev/null +++ b/server/app/adminapi/lists/match/MatchLists.php @@ -0,0 +1,38 @@ + ['sport_type', 'status', 'is_hot', 'is_show'], + '%like%' => ['home_team', 'away_team', 'league_name'], + ]; + } + + public function lists(): array + { + $lists = MatchEvent::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('match_time', 'desc') + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['match_time_str'] = date('Y-m-d H:i', $item['match_time']); + } + + return $lists; + } + + public function count(): int + { + return MatchEvent::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/notice/NoticeSettingLists.php b/server/app/adminapi/lists/notice/NoticeSettingLists.php new file mode 100644 index 0000000..5d975d5 --- /dev/null +++ b/server/app/adminapi/lists/notice/NoticeSettingLists.php @@ -0,0 +1,71 @@ + ['recipient', 'type'] + ]; + } + + /** + * @notes 通知设置列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author ljj + * @date 2022/2/16 3:18 下午 + */ + public function lists(): array + { + $lists = (new NoticeSetting())->field('id,scene_name,sms_notice,type') + ->append(['sms_status_desc','type_desc']) + ->where($this->searchWhere) + ->select() + ->toArray(); + + return $lists; + } + + /** + * @notes 通知设置数量 + * @return int + * @author ljj + * @date 2022/2/16 3:18 下午 + */ + public function count(): int + { + return (new NoticeSetting())->where($this->searchWhere)->count(); + } +} \ No newline at end of file diff --git a/server/app/adminapi/lists/points/PointsProductLists.php b/server/app/adminapi/lists/points/PointsProductLists.php new file mode 100644 index 0000000..75f38cc --- /dev/null +++ b/server/app/adminapi/lists/points/PointsProductLists.php @@ -0,0 +1,30 @@ + ['name'], + ]; + } + + public function lists(): array + { + return PointsProduct::where($this->searchWhere) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + } + + public function count(): int + { + return PointsProduct::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/popup/PopupLists.php b/server/app/adminapi/lists/popup/PopupLists.php new file mode 100644 index 0000000..b804281 --- /dev/null +++ b/server/app/adminapi/lists/popup/PopupLists.php @@ -0,0 +1,55 @@ + ['name', 'page_path', 'title'], + '=' => ['status', 'popup_type'], + ]; + } + + public function lists(): array + { + return PagePopup::where($this->searchWhere) + ->field([ + 'id', + 'name', + 'page_path', + 'popup_type', + 'image', + 'title', + 'link_type', + 'link_url', + 'frequency', + 'auto_close_seconds', + 'is_closable', + 'start_time', + 'end_time', + 'target_platform', + 'sort', + 'status', + 'show_count', + 'click_count', + 'close_count', + 'create_time', + 'update_time' + ]) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + } + + public function count(): int + { + return PagePopup::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/popup/PopupLogLists.php b/server/app/adminapi/lists/popup/PopupLogLists.php new file mode 100644 index 0000000..cc27794 --- /dev/null +++ b/server/app/adminapi/lists/popup/PopupLogLists.php @@ -0,0 +1,33 @@ + ['popup_id', 'action', 'platform', 'user_id'], + '%like%' => ['device_id', 'page_path'], + ]; + } + + public function lists(): array + { + return PagePopupLog::where($this->searchWhere) + ->append(['action_text']) + ->order(['id' => 'desc']) + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + } + + public function count(): int + { + return PagePopupLog::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/recharge/RechargeLists.php b/server/app/adminapi/lists/recharge/RechargeLists.php new file mode 100644 index 0000000..2c5429e --- /dev/null +++ b/server/app/adminapi/lists/recharge/RechargeLists.php @@ -0,0 +1,146 @@ + '充值单号', + 'nickname' => '用户昵称', + 'order_amount' => '充值金额', + 'pay_way_text' => '支付方式', + 'pay_status_text' => '支付状态', + 'pay_time' => '支付时间', + 'create_time' => '下单时间', + ]; + } + + + /** + * @notes 导出表名 + * @return string + * @author 段誉 + * @date 2023/2/24 16:07 + */ + public function setFileName(): string + { + return '充值记录'; + } + + + /** + * @notes 搜索条件 + * @return \string[][] + * @author 段誉 + * @date 2023/2/24 16:08 + */ + public function setSearch(): array + { + return [ + '=' => ['ro.sn', 'ro.pay_way', 'ro.pay_status'], + ]; + } + + + /** + * @notes 搜索条件 + * @author 段誉 + * @date 2023/2/24 16:08 + */ + public function queryWhere() + { + $where = []; + // 用户编号 + if (!empty($this->params['user_info'])) { + $where[] = ['u.sn|u.nickname|u.mobile|u.account', 'like', '%' . $this->params['user_info'] . '%']; + } + + // 下单时间 + if (!empty($this->params['start_time']) && !empty($this->params['end_time'])) { + $time = [strtotime($this->params['start_time']), strtotime($this->params['end_time'])]; + $where[] = ['ro.create_time', 'between', $time]; + } + + return $where; + } + + + /** + * @notes 获取列表 + * @return array + * @author 段誉 + * @date 2023/2/24 16:13 + */ + public function lists(): array + { + $field = 'ro.id,ro.sn,ro.order_amount,ro.pay_way,ro.pay_time,ro.pay_status,ro.create_time,ro.refund_status'; + $field .= ',u.avatar,u.nickname,u.account'; + $lists = RechargeOrder::alias('ro') + ->join('user u', 'u.id = ro.user_id') + ->field($field) + ->where($this->queryWhere()) + ->where($this->searchWhere) + ->order('ro.id', 'desc') + ->limit($this->limitOffset, $this->limitLength) + ->append(['pay_status_text', 'pay_way_text']) + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['avatar'] = FileService::getFileUrl($item['avatar']); + $item['pay_time'] = empty($item['pay_time']) ? '' : date('Y-m-d H:i:s', $item['pay_time']); + } + + return $lists; + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2023/2/24 16:13 + */ + public function count(): int + { + return RechargeOrder::alias('ro') + ->join('user u', 'u.id = ro.user_id') + ->where($this->queryWhere()) + ->where($this->searchWhere) + ->count(); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/lists/setting/SmsUploadLists.php b/server/app/adminapi/lists/setting/SmsUploadLists.php new file mode 100644 index 0000000..6e0cbf1 --- /dev/null +++ b/server/app/adminapi/lists/setting/SmsUploadLists.php @@ -0,0 +1,68 @@ + ['phone', 'body'], + '=' => ['status', 'type'], + ]; + } + + public function lists(): array + { + $lists = SmsLog::field('id,phone,body,sms_time,type,status,device_id,batch_id,create_time') + ->where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('id', 'desc') + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['record_type_desc'] = $this->getRecordTypeDesc((int)$item['type']); + $item['type_desc'] = $this->getActionTypeDesc((int)$item['type']); + $item['status_desc'] = $this->getStatusDesc((int)$item['status']); + } + + return $lists; + } + + public function count(): int + { + return SmsLog::where($this->searchWhere)->count(); + } + + private function getRecordTypeDesc(int $type): string + { + return $type >= 10 ? '来电' : '短信'; + } + + private function getActionTypeDesc(int $type): string + { + return match ($type) { + 1 => '接收', + 2 => '发送', + 11 => '来电已接', + 13 => '来电未接', + 15 => '来电拒接', + default => '未知', + }; + } + + private function getStatusDesc(int $status): string + { + return match ($status) { + 0 => '待验证', + 1 => '已使用', + 2 => '不适用', + default => '未知', + }; + } +} diff --git a/server/app/adminapi/lists/setting/dict/DictDataLists.php b/server/app/adminapi/lists/setting/dict/DictDataLists.php new file mode 100644 index 0000000..0c6843d --- /dev/null +++ b/server/app/adminapi/lists/setting/dict/DictDataLists.php @@ -0,0 +1,76 @@ + ['name', 'type_value'], + '=' => ['status', 'type_id'] + ]; + } + + + /** + * @notes 获取列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/6/20 16:35 + */ + public function lists(): array + { + return DictData::where($this->searchWhere) + ->append(['status_desc']) + ->limit($this->limitOffset, $this->limitLength) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->select() + ->toArray(); + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2022/6/20 16:35 + */ + public function count(): int + { + return DictData::where($this->searchWhere)->count(); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/lists/setting/dict/DictTypeLists.php b/server/app/adminapi/lists/setting/dict/DictTypeLists.php new file mode 100644 index 0000000..dbc13e9 --- /dev/null +++ b/server/app/adminapi/lists/setting/dict/DictTypeLists.php @@ -0,0 +1,76 @@ + ['name', 'type'], + '=' => ['status'] + ]; + } + + + /** + * @notes 获取列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/6/20 15:54 + */ + public function lists(): array + { + return DictType::where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->append(['status_desc']) + ->order(['id' => 'desc']) + ->select() + ->toArray(); + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2022/6/20 15:54 + */ + public function count(): int + { + return DictType::where($this->searchWhere)->count(); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/lists/setting/pay/PayConfigLists.php b/server/app/adminapi/lists/setting/pay/PayConfigLists.php new file mode 100644 index 0000000..9efe130 --- /dev/null +++ b/server/app/adminapi/lists/setting/pay/PayConfigLists.php @@ -0,0 +1,62 @@ +append(['pay_way_name']) + ->order('sort','desc') + ->select() + ->toArray(); + + return $lists; + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2023/2/23 16:15 + */ + public function count(): int + { + return PayConfig::count(); + } + + + +} \ No newline at end of file diff --git a/server/app/adminapi/lists/setting/system/LogLists.php b/server/app/adminapi/lists/setting/system/LogLists.php new file mode 100644 index 0000000..7257595 --- /dev/null +++ b/server/app/adminapi/lists/setting/system/LogLists.php @@ -0,0 +1,108 @@ + ['admin_name','url','ip','type'], + 'between_time' => 'create_time', + ]; + } + + /** + * @notes 查看系统日志列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author ljj + * @date 2021/8/3 4:21 下午 + */ + public function lists(): array + { + $lists = OperationLog::field('id,action,admin_name,admin_id,url,type,params,ip,create_time') + ->where($this->searchWhere) + ->limit($this->limitOffset, $this->limitLength) + ->order('id','desc') + ->select() + ->toArray(); + + return $lists; + } + + /** + * @notes 查看系统日志总数 + * @return int + * @author ljj + * @date 2021/8/3 4:23 下午 + */ + public function count(): int + { + return OperationLog::where($this->searchWhere)->count(); + } + + /** + * @notes 设置导出字段 + * @return string[] + * @author ljj + * @date 2021/8/3 4:48 下午 + */ + public function setExcelFields(): array + { + return [ + // '数据库字段名(支持别名) => 'Excel表字段名' + 'id' => '记录ID', + 'action' => '操作', + 'admin_name' => '管理员', + 'admin_id' => '管理员ID', + 'url' => '访问链接', + 'type' => '访问方式', + 'params' => '访问参数', + 'ip' => '来源IP', + 'create_time' => '日志时间', + ]; + } + + /** + * @notes 设置默认表名 + * @return string + * @author ljj + * @date 2021/8/3 4:48 下午 + */ + public function setFileName(): string + { + return '系统日志'; + } +} \ No newline at end of file diff --git a/server/app/adminapi/lists/shortlink/ShortLinkLists.php b/server/app/adminapi/lists/shortlink/ShortLinkLists.php new file mode 100644 index 0000000..41080d9 --- /dev/null +++ b/server/app/adminapi/lists/shortlink/ShortLinkLists.php @@ -0,0 +1,97 @@ + ['user_id'], + '%like%' => ['code'], + ]; + } + + public function lists(): array + { + $where = $this->searchWhere; + $linkPageType = $this->request->get('link_page_type', ''); + if ($linkPageType !== '') { + $where[] = ['page_type', '=', $linkPageType]; + } + + $lists = ShortLink::where($where) + ->order('id', 'desc') + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + + $userIds = array_unique(array_column($lists, 'user_id')); + $users = User::whereIn('id', $userIds)->column('nickname,avatar', 'id'); + + // 页面类型字典映射 + $pageTypeMap = Db::name('dict_data') + ->where('type_value', 'short_link_page_type') + ->where('status', 1) + ->column('name', 'value'); + + // 按 page_type 分组收集 page_id,批量查标题 + $grouped = []; + foreach ($lists as $item) { + $grouped[$item['page_type']][] = $item['page_id']; + } + $titleMap = []; + foreach ($grouped as $type => $ids) { + $ids = array_unique(array_filter($ids)); + if (empty($ids)) + continue; + switch ($type) { + case 'news': + $titleMap[$type] = Db::name('article')->whereIn('id', $ids)->column('title', 'id'); + break; + case 'match': + $rows = Db::name('match')->whereIn('id', $ids)->column('home_team,away_team', 'id'); + $titleMap[$type] = array_map(fn($m) => $m['home_team'] . ' vs ' . $m['away_team'], $rows); + break; + case 'post': + case 'community': + $rows = Db::name('community_post')->whereIn('id', $ids)->column('content', 'id'); + $titleMap[$type] = array_map(fn($c) => mb_substr(strip_tags($c), 0, 20), $rows); + break; + } + } + + foreach ($lists as &$item) { + $item['nickname'] = $users[$item['user_id']]['nickname'] ?? '游客'; + $item['avatar'] = $users[$item['user_id']]['avatar'] ?? ''; + $item['short_url'] = request()->domain() . '/s/' . $item['code']; + $item['page_type_name'] = $pageTypeMap[$item['page_type']] ?? $item['page_type']; + $item['page_title'] = $titleMap[$item['page_type']][$item['page_id']] ?? '-'; + if (!empty($item['qrcode'])) { + $absPath = public_path() . $item['qrcode']; + $ver = is_file($absPath) ? filemtime($absPath) : time(); + $item['qrcode_url'] = \app\common\service\FileService::getFileUrl($item['qrcode']) . '?v=' . $ver; + } else { + $item['qrcode_url'] = ''; + } + } + + return $lists; + } + + public function count(): int + { + $where = $this->searchWhere; + $linkPageType = $this->request->get('link_page_type', ''); + if ($linkPageType !== '') { + $where[] = ['page_type', '=', $linkPageType]; + } + return ShortLink::where($where)->count(); + } +} diff --git a/server/app/adminapi/lists/shortlink/ShortLinkLogLists.php b/server/app/adminapi/lists/shortlink/ShortLinkLogLists.php new file mode 100644 index 0000000..73fe5c8 --- /dev/null +++ b/server/app/adminapi/lists/shortlink/ShortLinkLogLists.php @@ -0,0 +1,36 @@ + ['short_link_id', 'code', 'platform'], + '%like%' => ['ip'], + ]; + } + + public function lists(): array + { + $lists = ShortLinkLog::where($this->searchWhere) + ->order('id', 'desc') + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + + // create_time 已由 ThinkPHP auto_timestamp + datetime_format 自动格式化 + + return $lists; + } + + public function count(): int + { + return ShortLinkLog::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/lists/tools/DataTableLists.php b/server/app/adminapi/lists/tools/DataTableLists.php new file mode 100644 index 0000000..d6ff732 --- /dev/null +++ b/server/app/adminapi/lists/tools/DataTableLists.php @@ -0,0 +1,74 @@ +params['name'])) { + $sql .= "AND name LIKE '%" . $this->params['name'] . "%'"; + } + if (!empty($this->params['comment'])) { + $sql .= "AND comment LIKE '%" . $this->params['comment'] . "%'"; + } + return Db::query($sql); + } + + + /** + * @notes 处理列表 + * @return array + * @author 段誉 + * @date 2022/6/13 18:54 + */ + public function lists(): array + { + $lists = array_map("array_change_key_case", $this->queryResult()); + $offset = max(0, ($this->pageNo - 1) * $this->pageSize); + $lists = array_slice($lists, $offset, $this->pageSize, true); + return array_values($lists); + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2022/6/13 18:54 + */ + public function count(): int + { + return count($this->queryResult()); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/lists/tools/GenerateTableLists.php b/server/app/adminapi/lists/tools/GenerateTableLists.php new file mode 100644 index 0000000..d9d1b46 --- /dev/null +++ b/server/app/adminapi/lists/tools/GenerateTableLists.php @@ -0,0 +1,75 @@ + ['table_name', 'table_comment'] + ]; + } + + + /** + * @notes 查询列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/6/14 10:55 + */ + public function lists(): array + { + return GenerateTable::where($this->searchWhere) + ->order(['id' => 'desc']) + ->append(['template_type_desc']) + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2022/6/14 10:55 + */ + public function count(): int + { + return GenerateTable::count(); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/lists/user/UserLists.php b/server/app/adminapi/lists/user/UserLists.php new file mode 100644 index 0000000..d33ed6a --- /dev/null +++ b/server/app/adminapi/lists/user/UserLists.php @@ -0,0 +1,111 @@ +params), $allowSearch); + } + + + /** + * @notes 获取用户列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/22 15:50 + */ + public function lists(): array + { + $field = "id,sn,nickname,sex,avatar,account,mobile,channel,user_money,user_points,create_time"; + $lists = User::withSearch($this->setSearch(), $this->params) + ->limit($this->limitOffset, $this->limitLength) + ->field($field) + ->order('id desc') + ->select()->toArray(); + + foreach ($lists as &$item) { + $item['channel'] = UserTerminalEnum::getTermInalDesc($item['channel']); + } + + return $lists; + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2022/9/22 15:51 + */ + public function count(): int + { + return User::withSearch($this->setSearch(), $this->params)->count(); + } + + + /** + * @notes 导出文件名 + * @return string + * @author 段誉 + * @date 2022/11/24 16:17 + */ + public function setFileName(): string + { + return '用户列表'; + } + + + /** + * @notes 导出字段 + * @return string[] + * @author 段誉 + * @date 2022/11/24 16:17 + */ + public function setExcelFields(): array + { + return [ + 'sn' => '用户编号', + 'nickname' => '用户昵称', + 'account' => '账号', + 'mobile' => '手机号码', + 'channel' => '注册来源', + 'create_time' => '注册时间', + ]; + } + +} diff --git a/server/app/adminapi/lists/vip/VipLevelLists.php b/server/app/adminapi/lists/vip/VipLevelLists.php new file mode 100644 index 0000000..3a66db1 --- /dev/null +++ b/server/app/adminapi/lists/vip/VipLevelLists.php @@ -0,0 +1,30 @@ + ['name'], + ]; + } + + public function lists(): array + { + return VipLevel::where($this->searchWhere) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + } + + public function count(): int + { + return VipLevel::where($this->searchWhere)->count(); + } +} diff --git a/server/app/adminapi/logic/ConfigLogic.php b/server/app/adminapi/logic/ConfigLogic.php new file mode 100644 index 0000000..ead74c3 --- /dev/null +++ b/server/app/adminapi/logic/ConfigLogic.php @@ -0,0 +1,105 @@ + FileService::getFileUrl(), + + // 网站名称 + 'web_name' => ConfigService::get('website', 'name'), + // 网站图标 + 'web_favicon' => FileService::getFileUrl(ConfigService::get('website', 'web_favicon')), + // 网站logo + 'web_logo' => FileService::getFileUrl(ConfigService::get('website', 'web_logo')), + // 登录页 + 'login_image' => FileService::getFileUrl(ConfigService::get('website', 'login_image')), + // 版权信息 + 'copyright_config' => ConfigService::get('copyright', 'config', []), + // 版本号 + 'version' => config('project.version') + ]; + return $config; + } + + + /** + * @notes 根据类型获取字典类型 + * @param $type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/27 19:09 + */ + public static function getDictByType($type) + { + if (!is_string($type)) { + return []; + } + + $type = explode(',', $type); + $lists = DictData::whereIn('type_value', $type)->select()->toArray(); + + if (empty($lists)) { + return []; + } + + $result = []; + foreach ($type as $item) { + foreach ($lists as $dict) { + if ($dict['type_value'] == $item) { + $result[$item][] = $dict; + } + } + } + return $result; + } + + + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/FileLogic.php b/server/app/adminapi/logic/FileLogic.php new file mode 100644 index 0000000..2712ee4 --- /dev/null +++ b/server/app/adminapi/logic/FileLogic.php @@ -0,0 +1,158 @@ +whereIn('id', $params['ids']) + ->update([ + 'cid' => $params['cid'], + 'update_time' => time() + ]); + } + + /** + * @notes 重命名文件 + * @param $params + * @author 张无忌 + * @date 2021/7/29 17:16 + */ + public static function rename($params) + { + (new File())->where('id', $params['id']) + ->update([ + 'name' => $params['name'], + 'update_time' => time() + ]); + } + + /** + * @notes 批量删除文件 + * @param $params + * @author 张无忌 + * @date 2021/7/28 15:41 + */ + public static function delete($params) + { + $result = File::whereIn('id', $params['ids'])->select(); + $StorageDriver = new StorageDriver([ + 'default' => ConfigService::get('storage', 'default', 'local'), + 'engine' => ConfigService::get('storage') ?? ['local'=>[]], + ]); + foreach ($result as $item) { + $StorageDriver->delete($item['uri']); + } + File::destroy($params['ids']); + } + + /** + * @notes 添加文件分类 + * @param $params + * @author 张无忌 + * @date 2021/7/28 11:32 + */ + public static function addCate($params) + { + FileCate::create([ + 'type' => $params['type'], + 'pid' => $params['pid'], + 'name' => $params['name'] + ]); + } + + /** + * @notes 编辑文件分类 + * @param $params + * @author 张无忌 + * @date 2021/7/28 14:03 + */ + public static function editCate($params) + { + FileCate::update([ + 'name' => $params['name'], + 'update_time' => time() + ], ['id' => $params['id']]); + } + + /** + * @notes 删除文件分类 + * @param $params + * @author 张无忌 + * @date 2021/7/28 14:21 + */ + public static function delCate($params) + { + $fileModel = new File(); + $cateModel = new FileCate(); + + $cateIds = self::getCateIds($params['id']); + array_push($cateIds, $params['id']); + + // 删除分类及子分类 + $cateModel->whereIn('id', $cateIds)->update(['delete_time' => time()]); + + // 删除文件 + $fileIds = $fileModel->whereIn('cid', $cateIds)->column('id'); + + if (!empty($fileIds)) { + self::delete(['ids' => $fileIds]); + } + } + + + /** + * @notes 获取所有分类id + * @param $parentId + * @param array $cateArr + * @return array + * @author 段誉 + * @date 2024/2/7 15:03 + */ + public static function getCateIds($parentId, array $cateArr = []): array + { + $childIds = FileCate::where(['pid' => $parentId])->column('id'); + + if (empty($childIds)) { + return $childIds; + } else { + $allChildIds = $childIds; + foreach ($childIds as $childId) { + $allChildIds = array_merge($allChildIds, static::getCateIds($childId, $cateArr)); + } + return $allChildIds; + } + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/LoginLogic.php b/server/app/adminapi/logic/LoginLogic.php new file mode 100644 index 0000000..9ba298c --- /dev/null +++ b/server/app/adminapi/logic/LoginLogic.php @@ -0,0 +1,84 @@ +find(); + + //用户表登录信息更新 + $admin->login_time = $time; + $admin->login_ip = request()->ip(); + $admin->save(); + + //设置token + $adminInfo = AdminTokenService::setToken($admin->id, $params['terminal'], $admin->multipoint_login); + + //返回登录信息 + $avatar = $admin->avatar ? $admin->avatar : Config::get('project.default_image.admin_avatar'); + $avatar = FileService::getFileUrl($avatar); + return [ + 'name' => $adminInfo['name'], + 'avatar' => $avatar, + 'role_name' => $adminInfo['role_name'], + 'token' => $adminInfo['token'], + ]; + } + + + /** + * @notes 退出登录 + * @param $adminInfo + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 令狐冲 + * @date 2021/7/5 14:34 + */ + public function logout($adminInfo) + { + //token不存在,不注销 + if (!isset($adminInfo['token'])) { + return false; + } + //设置token过期 + return AdminTokenService::expireToken($adminInfo['token']); + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/WorkbenchLogic.php b/server/app/adminapi/logic/WorkbenchLogic.php new file mode 100644 index 0000000..deb5bdd --- /dev/null +++ b/server/app/adminapi/logic/WorkbenchLogic.php @@ -0,0 +1,234 @@ + self::versionInfo(), + // 今日数据 + 'today' => self::today(), + // 常用功能 + 'menu' => self::menu(), + // 近15日访客数 + 'visitor' => self::visitor(), + // 服务支持 + 'support' => self::support(), + // 销售数据 + 'sale' => self::sale() + ]; + } + + + /** + * @notes 常用功能 + * @return array[] + * @author 段誉 + * @date 2021/12/29 16:40 + */ + public static function menu(): array + { + return [ + [ + 'name' => '管理员', + 'image' => FileService::getFileUrl(config('project.default_image.menu_admin')), + 'url' => '/permission/admin' + ], + [ + 'name' => '角色管理', + 'image' => FileService::getFileUrl(config('project.default_image.menu_role')), + 'url' => '/permission/role' + ], + [ + 'name' => '部门管理', + 'image' => FileService::getFileUrl(config('project.default_image.menu_dept')), + 'url' => '/organization/department' + ], + [ + 'name' => '字典管理', + 'image' => FileService::getFileUrl(config('project.default_image.menu_dict')), + 'url' => '/setting/dev_tools/dict' + ], + [ + 'name' => '代码生成器', + 'image' => FileService::getFileUrl(config('project.default_image.menu_generator')), + 'url' => '/dev_tools/code' + ], + [ + 'name' => '素材中心', + 'image' => FileService::getFileUrl(config('project.default_image.menu_file')), + 'url' => '/app/material/index' + ], + [ + 'name' => '菜单权限', + 'image' => FileService::getFileUrl(config('project.default_image.menu_auth')), + 'url' => '/permission/menu' + ], + [ + 'name' => '网站信息', + 'image' => FileService::getFileUrl(config('project.default_image.menu_web')), + 'url' => '/setting/website/information' + ], + ]; + } + + + /** + * @notes 版本信息 + * @return array + * @author 段誉 + * @date 2021/12/29 16:08 + */ + public static function versionInfo(): array + { + return [ + 'version' => config('project.version'), + 'website' => config('project.website.url'), + 'name' => ConfigService::get('website', 'name'), + 'based' => 'vue3.x、ElementUI、MySQL', + 'channel' => [ + 'website' => 'https://www.likeadmin.cn', + 'gitee' => 'https://gitee.com/likeadmin/likeadmin_php', + ] + ]; + } + + + /** + * @notes 今日数据 + * @return int[] + * @author 段誉 + * @date 2021/12/29 16:15 + */ + public static function today(): array + { + return [ + 'time' => date('Y-m-d H:i:s'), + // 今日销售额 + 'today_sales' => 100, + // 总销售额 + 'total_sales' => 1000, + + // 今日访问量 + 'today_visitor' => 10, + // 总访问量 + 'total_visitor' => 100, + + // 今日新增用户量 + 'today_new_user' => 30, + // 总用户量 + 'total_new_user' => 3000, + + // 订单量 (笔) + 'order_num' => 12, + // 总订单量 + 'order_sum' => 255 + ]; + } + + + /** + * @notes 访问数 + * @return array + * @author 段誉 + * @date 2021/12/29 16:57 + */ + public static function visitor(): array + { + $num = []; + $date = []; + for ($i = 0; $i < 15; $i++) { + $where_start = strtotime("- " . $i . "day"); + $date[] = date('m/d', $where_start); + $num[$i] = rand(0, 100); + } + + return [ + 'date' => $date, + 'list' => [ + ['name' => '访客数', 'data' => $num] + ] + ]; + } + + /** + * @notes 访问数 + * @return array + * @author 段誉 + * @date 2021/12/29 16:57 + */ + public static function sale(): array + { + $num = []; + $date = []; + for ($i = 0; $i < 7; $i++) { + $where_start = strtotime("- " . $i . "day"); + $date[] = date('m/d', $where_start); + $num[$i] = rand(30, 200); + } + + return [ + 'date' => $date, + 'list' => [ + ['name' => '销售量', 'data' => $num] + ] + ]; + } + + + /** + * @notes 服务支持 + * @return array[] + * @author 段誉 + * @date 2022/7/18 11:18 + */ + public static function support() + { + return [ + [ + 'image' => FileService::getFileUrl(config('project.default_image.qq_group')), + 'title' => '官方公众号', + 'desc' => '关注官方公众号', + ], + [ + 'image' => FileService::getFileUrl(config('project.default_image.customer_service')), + 'title' => '添加企业客服微信', + 'desc' => '想了解更多请添加客服', + ] + ]; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/ai/AiConfigLogic.php b/server/app/adminapi/logic/ai/AiConfigLogic.php new file mode 100644 index 0000000..ab6a35e --- /dev/null +++ b/server/app/adminapi/logic/ai/AiConfigLogic.php @@ -0,0 +1,30 @@ +toArray(); + } + + public static function edit(array $params): bool + { + try { + AiConfig::update([ + 'id' => $params['id'], + 'name' => $params['name'], + 'value' => $params['value'] ?? '', + 'remark' => $params['remark'] ?? '', + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } +} diff --git a/server/app/adminapi/logic/ai/KbConfigLogic.php b/server/app/adminapi/logic/ai/KbConfigLogic.php new file mode 100644 index 0000000..18bec53 --- /dev/null +++ b/server/app/adminapi/logic/ai/KbConfigLogic.php @@ -0,0 +1,30 @@ +toArray(); + } + + public static function edit(array $params): bool + { + try { + AiConfig::update([ + 'id' => $params['id'], + 'name' => $params['name'], + 'value' => $params['value'] ?? '', + 'remark' => $params['remark'] ?? '', + ]); + return true; + } catch (\Throwable $e) { + self::setError($e->getMessage()); + return false; + } + } +} diff --git a/server/app/adminapi/logic/app/AppVersionLogic.php b/server/app/adminapi/logic/app/AppVersionLogic.php new file mode 100644 index 0000000..a20e8d5 --- /dev/null +++ b/server/app/adminapi/logic/app/AppVersionLogic.php @@ -0,0 +1,101 @@ + $params['version_name'], + 'version_code' => (int) $params['version_code'], + 'min_version_code' => (int) ($params['min_version_code'] ?? 0), + 'platform' => (int) $params['platform'], + 'package_type' => (int) $params['package_type'], + 'update_type' => (int) $params['update_type'], + 'download_url' => FileService::setFileUrl($params['download_url']), + 'title' => $params['title'], + 'update_content' => $params['update_content'], + 'status' => (int) $params['status'], + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + /** + * 编辑版本 + */ + public static function edit(array $params): bool + { + try { + AppVersion::update([ + 'id' => $params['id'], + 'version_name' => $params['version_name'], + 'version_code' => (int) $params['version_code'], + 'min_version_code' => (int) ($params['min_version_code'] ?? 0), + 'platform' => (int) $params['platform'], + 'package_type' => (int) $params['package_type'], + 'update_type' => (int) $params['update_type'], + 'download_url' => FileService::setFileUrl($params['download_url']), + 'title' => $params['title'], + 'update_content' => $params['update_content'], + 'status' => (int) $params['status'], + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + /** + * 删除版本 + */ + public static function delete(array $params): bool + { + return AppVersion::destroy($params['id']); + } + + /** + * 版本详情 + */ + public static function detail(array $params): array + { + return AppVersion::findOrEmpty($params['id']) + ->append(['platform_text', 'package_type_text', 'update_type_text']) + ->toArray(); + } + + /** + * 修改状态 + */ + public static function updateStatus(array $params): bool + { + try { + AppVersion::update([ + 'id' => $params['id'], + 'status' => (int) $params['status'], + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } +} diff --git a/server/app/adminapi/logic/article/ArticleAiCommentTaskLogic.php b/server/app/adminapi/logic/article/ArticleAiCommentTaskLogic.php new file mode 100644 index 0000000..33a98ee --- /dev/null +++ b/server/app/adminapi/logic/article/ArticleAiCommentTaskLogic.php @@ -0,0 +1,44 @@ +isEmpty()) { + $task->delete(); + } + } + + public static function retry(array $params) + { + $task = ArticleAiCommentTask::findOrEmpty($params['id']); + if ($task->isEmpty()) { + self::setError('任务不存在'); + return false; + } + + if ((int) $task->status !== 2) { + self::setError('仅失败状态的任务可重试'); + return false; + } + + try { + $task->save([ + 'status' => 0, + 'run_at' => time(), + 'error_msg' => '', + 'update_time' => time(), + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } +} diff --git a/server/app/adminapi/logic/article/ArticleCateLogic.php b/server/app/adminapi/logic/article/ArticleCateLogic.php new file mode 100644 index 0000000..1836fe6 --- /dev/null +++ b/server/app/adminapi/logic/article/ArticleCateLogic.php @@ -0,0 +1,127 @@ + $params['name'], + 'is_show' => $params['is_show'], + 'sort' => $params['sort'] ?? 0 + ]); + } + + + /** + * @notes 编辑资讯分类 + * @param array $params + * @return bool + * @author heshihu + * @date 2022/2/21 17:50 + */ + public static function edit(array $params) : bool + { + try { + ArticleCate::update([ + 'id' => $params['id'], + 'name' => $params['name'], + 'is_show' => $params['is_show'], + 'sort' => $params['sort'] ?? 0 + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 删除资讯分类 + * @param array $params + * @author heshihu + * @date 2022/2/21 17:52 + */ + public static function delete(array $params) + { + ArticleCate::destroy($params['id']); + } + + /** + * @notes 查看资讯分类详情 + * @param $params + * @return array + * @author heshihu + * @date 2022/2/21 17:54 + */ + public static function detail($params) : array + { + return ArticleCate::findOrEmpty($params['id'])->toArray(); + } + + /** + * @notes 更改资讯分类状态 + * @param array $params + * @return bool + * @author heshihu + * @date 2022/2/21 18:04 + */ + public static function updateStatus(array $params) + { + ArticleCate::update([ + 'id' => $params['id'], + 'is_show' => $params['is_show'] + ]); + return true; + } + + + /** + * @notes 文章分类数据 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 10:53 + */ + public static function getAllData() + { + return ArticleCate::where(['is_show' => YesNoEnum::YES]) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->select() + ->toArray(); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/article/ArticleLogic.php b/server/app/adminapi/logic/article/ArticleLogic.php new file mode 100644 index 0000000..160a095 --- /dev/null +++ b/server/app/adminapi/logic/article/ArticleLogic.php @@ -0,0 +1,167 @@ + $params['title'], + 'desc' => $params['desc'] ?? '', + 'author' => $params['author'] ?? '', //作者 + 'sort' => !empty($params['sort']) ? $params['sort'] : time(), // 排序 + 'abstract' => $params['abstract'], // 文章摘要 + 'click_virtual' => $params['click_virtual'] ?? 0, + 'image' => $params['image'] ? FileService::setFileUrl($params['image']) : '', + 'cid' => $params['cid'], + 'is_show' => $params['is_show'], + 'is_recommend' => $params['is_recommend'] ?? 0, + 'content' => $params['content'] ?? '', + ]); + KbSyncService::enqueue('article', 'article', (int) $article->id, 'upsert', 40); + return true; + } + + + /** + * @notes 编辑资讯 + * @param array $params + * @return bool + * @author heshihu + * @date 2022/2/22 10:12 + */ + public static function edit(array $params): bool + { + try { + if ((int) ($params['is_show'] ?? 0) === 1 && !Article::hasPublishableContent($params['content'] ?? '')) { + throw new \Exception('正文未拉取完成,暂不能发布,请先保存为稿子'); + } + + Article::update([ + 'id' => $params['id'], + 'title' => $params['title'], + 'desc' => $params['desc'] ?? '', // 简介 + 'author' => $params['author'] ?? '', //作者 + 'sort' => $params['sort'] ?? 0, // 排序 + 'abstract' => $params['abstract'], // 文章摘要 + 'click_virtual' => $params['click_virtual'] ?? 0, + 'image' => $params['image'] ? FileService::setFileUrl($params['image']) : '', + 'cid' => $params['cid'], + 'is_show' => $params['is_show'], + 'is_recommend' => $params['is_recommend'] ?? 0, + 'content' => $params['content'] ?? '', + ]); + KbSyncService::enqueue('article', 'article', (int) $params['id'], 'upsert', 40); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 删除资讯 + * @param array $params + * @author heshihu + * @date 2022/2/22 10:17 + */ + public static function delete(array $params) + { + Article::destroy($params['id']); + KbSyncService::enqueue('article', 'article', (int) $params['id'], 'delete', 20); + } + + /** + * @notes 查看资讯详情 + * @param $params + * @return array + * @author heshihu + * @date 2022/2/22 10:15 + */ + public static function detail($params): array + { + return Article::findOrEmpty($params['id'])->toArray(); + } + + /** + * @notes 更改资讯状态 + * @param array $params + * @return bool + * @author heshihu + * @date 2022/2/22 10:18 + */ + public static function updateStatus(array $params) + { + $article = Article::findOrEmpty($params['id']); + if ($article->isEmpty()) { + self::setError('资讯不存在'); + return false; + } + + if ((int) $params['is_show'] === 1 && !Article::hasPublishableContent($article->content ?? '')) { + self::setError('正文未拉取完成,暂不能发布,请先保持稿子状态'); + return false; + } + + Article::update([ + 'id' => $params['id'], + 'is_show' => $params['is_show'] + ]); + KbSyncService::enqueue( + 'article', + 'article', + (int) $params['id'], + (int) $params['is_show'] === 1 ? 'upsert' : 'delete', + 20 + ); + return true; + } + + public static function batchRecommend(array $params): bool + { + try { + Article::whereIn('id', $params['ids']) + ->update(['is_recommend' => $params['is_recommend']]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } +} diff --git a/server/app/adminapi/logic/auth/AdminLogic.php b/server/app/adminapi/logic/auth/AdminLogic.php new file mode 100644 index 0000000..dbaddd6 --- /dev/null +++ b/server/app/adminapi/logic/auth/AdminLogic.php @@ -0,0 +1,337 @@ + $params['name'], + 'account' => $params['account'], + 'avatar' => $avatar, + 'password' => $password, + 'create_time' => time(), + 'disable' => $params['disable'], + 'multipoint_login' => $params['multipoint_login'], + ]); + + // 角色 + self::insertRole($admin['id'], $params['role_id'] ?? []); + // 部门 + self::insertDept($admin['id'], $params['dept_id'] ?? []); + // 岗位 + self::insertJobs($admin['id'], $params['jobs_id'] ?? []); + + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 编辑管理员 + * @param array $params + * @return bool + * @author 段誉 + * @date 2021/12/29 10:43 + */ + public static function edit(array $params): bool + { + Db::startTrans(); + try { + // 基础信息 + $data = [ + 'id' => $params['id'], + 'name' => $params['name'], + 'account' => $params['account'], + 'disable' => $params['disable'], + 'multipoint_login' => $params['multipoint_login'] + ]; + + // 头像 + $data['avatar'] = !empty($params['avatar']) ? FileService::setFileUrl($params['avatar']) : ''; + + // 密码 + if (!empty($params['password'])) { + $passwordSalt = Config::get('project.unique_identification'); + $data['password'] = create_password($params['password'], $passwordSalt); + } + + // 禁用或更换角色后.设置token过期 + $roleId = AdminRole::where('admin_id', $params['id'])->column('role_id'); + $editRole = false; + if (!empty(array_diff_assoc($roleId, $params['role_id']))) { + $editRole = true; + } + + if ($params['disable'] == 1 || $editRole) { + $tokenArr = AdminSession::where('admin_id', $params['id'])->select()->toArray(); + foreach ($tokenArr as $token) { + self::expireToken($token['token']); + } + } + + Admin::update($data); + (new AdminAuthCache($params['id']))->clearAuthCache(); + + // 删除旧的关联信息 + AdminRole::delByUserId($params['id']); + AdminDept::delByUserId($params['id']); + AdminJobs::delByUserId($params['id']); + // 角色 + self::insertRole($params['id'], $params['role_id']); + // 部门 + self::insertDept($params['id'], $params['dept_id'] ?? []); + // 岗位 + self::insertJobs($params['id'], $params['jobs_id'] ?? []); + + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 删除管理员 + * @param array $params + * @return bool + * @author 段誉 + * @date 2021/12/29 10:45 + */ + public static function delete(array $params): bool + { + Db::startTrans(); + try { + $admin = Admin::findOrEmpty($params['id']); + if ($admin->root == YesNoEnum::YES) { + throw new \Exception("超级管理员不允许被删除"); + } + Admin::destroy($params['id']); + + //设置token过期 + $tokenArr = AdminSession::where('admin_id', $params['id'])->select()->toArray(); + foreach ($tokenArr as $token) { + self::expireToken($token['token']); + } + (new AdminAuthCache($params['id']))->clearAuthCache(); + + // 删除旧的关联信息 + AdminRole::delByUserId($params['id']); + AdminDept::delByUserId($params['id']); + AdminJobs::delByUserId($params['id']); + + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 过期token + * @param $token + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2021/12/29 10:46 + */ + public static function expireToken($token): bool + { + $adminSession = AdminSession::where('token', '=', $token) + ->with('admin') + ->find(); + + if (empty($adminSession)) { + return false; + } + + $time = time(); + $adminSession->expire_time = $time; + $adminSession->update_time = $time; + $adminSession->save(); + + return (new AdminTokenCache())->deleteAdminInfo($token); + } + + + /** + * @notes 查看管理员详情 + * @param $params + * @return array + * @author 段誉 + * @date 2021/12/29 11:07 + */ + public static function detail($params, $action = 'detail'): array + { + $admin = Admin::field([ + 'id', 'account', 'name', 'disable', 'root', + 'multipoint_login', 'avatar', + ])->findOrEmpty($params['id'])->toArray(); + + if ($action == 'detail') { + return $admin; + } + + $result['user'] = $admin; + // 当前管理员角色拥有的菜单 + $result['menu'] = MenuLogic::getMenuByAdminId($params['id']); + // 当前管理员橘色拥有的按钮权限 + $result['permissions'] = AuthLogic::getBtnAuthByRoleId($admin); + return $result; + } + + + /** + * @notes 编辑超级管理员 + * @param $params + * @return Admin + * @author 段誉 + * @date 2022/4/8 17:54 + */ + public static function editSelf($params) + { + $data = [ + 'id' => $params['admin_id'], + 'name' => $params['name'], + 'avatar' => FileService::setFileUrl($params['avatar']), + ]; + + if (!empty($params['password'])) { + $passwordSalt = Config::get('project.unique_identification'); + $data['password'] = create_password($params['password'], $passwordSalt); + } + + return Admin::update($data); + } + + + /** + * @notes 新增角色 + * @param $adminId + * @param $roleIds + * @throws \Exception + * @author 段誉 + * @date 2022/11/25 14:23 + */ + public static function insertRole($adminId, $roleIds) + { + if (!empty($roleIds)) { + // 角色 + $roleData = []; + foreach ($roleIds as $roleId) { + $roleData[] = [ + 'admin_id' => $adminId, + 'role_id' => $roleId, + ]; + } + (new AdminRole())->saveAll($roleData); + } + } + + + /** + * @notes 新增部门 + * @param $adminId + * @param $deptIds + * @throws \Exception + * @author 段誉 + * @date 2022/11/25 14:22 + */ + public static function insertDept($adminId, $deptIds) + { + // 部门 + if (!empty($deptIds)) { + $deptData = []; + foreach ($deptIds as $deptId) { + $deptData[] = [ + 'admin_id' => $adminId, + 'dept_id' => $deptId + ]; + } + (new AdminDept())->saveAll($deptData); + } + } + + + /** + * @notes 新增岗位 + * @param $adminId + * @param $jobsIds + * @throws \Exception + * @author 段誉 + * @date 2022/11/25 14:22 + */ + public static function insertJobs($adminId, $jobsIds) + { + // 岗位 + if (!empty($jobsIds)) { + $jobsData = []; + foreach ($jobsIds as $jobsId) { + $jobsData[] = [ + 'admin_id' => $adminId, + 'jobs_id' => $jobsId + ]; + } + (new AdminJobs())->saveAll($jobsData); + } + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/auth/AuthLogic.php b/server/app/adminapi/logic/auth/AuthLogic.php new file mode 100644 index 0000000..9b17c75 --- /dev/null +++ b/server/app/adminapi/logic/auth/AuthLogic.php @@ -0,0 +1,105 @@ +where([ + ['is_disable', '=', 0], + ['perms', '<>', ''] + ]) + ->column('perms'); + } + + + /** + * @notes 获取当前管理员角色按钮权限 + * @param $roleId + * @return mixed + * @author 段誉 + * @date 2022/7/1 16:10 + */ + public static function getBtnAuthByRoleId($admin) + { + if ($admin['root']) { + return ['*']; + } + + $menuId = SystemRoleMenu::whereIn('role_id', $admin['role_id']) + ->column('menu_id'); + + $where[] = ['is_disable', '=', 0]; + $where[] = ['perms', '<>', '']; + + $roleAuth = SystemMenu::distinct(true) + ->where('id', 'in', $menuId) + ->where($where) + ->column('perms'); + + $allAuth = SystemMenu::distinct(true) + ->where($where) + ->column('perms'); + + $hasAllAuth = array_diff($allAuth, $roleAuth); + if (empty($hasAllAuth)) { + return ['*']; + } + + return $roleAuth; + } + + + /** + * @notes 获取管理员角色关联的菜单id(菜单,权限) + * @param int $adminId + * @return array + * @author 段誉 + * @date 2022/7/1 15:56 + */ + public static function getAuthByAdminId(int $adminId): array + { + $roleIds = AdminRole::where('admin_id', $adminId)->column('role_id'); + $menuId = SystemRoleMenu::whereIn('role_id', $roleIds)->column('menu_id'); + + return SystemMenu::distinct(true) + ->where([ + ['is_disable', '=', 0], + ['perms', '<>', ''], + ['id', 'in', array_unique($menuId)], + ]) + ->column('perms'); + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/auth/MenuLogic.php b/server/app/adminapi/logic/auth/MenuLogic.php new file mode 100644 index 0000000..6c46aa0 --- /dev/null +++ b/server/app/adminapi/logic/auth/MenuLogic.php @@ -0,0 +1,184 @@ +column('menu_id'); + $where[] = ['id', 'in', $roleMenu]; + } + + $menu = SystemMenu::where($where) + ->order(['sort' => 'desc', 'id' => 'asc']) + ->select(); + + return linear_to_tree($menu, 'children'); + } + + + /** + * @notes 添加菜单 + * @param array $params + * @return SystemMenu|\think\Model + * @author 段誉 + * @date 2022/6/30 10:06 + */ + public static function add(array $params) + { + return SystemMenu::create([ + 'pid' => $params['pid'], + 'type' => $params['type'], + 'name' => $params['name'], + 'icon' => $params['icon'] ?? '', + 'sort' => $params['sort'], + 'perms' => $params['perms'] ?? '', + 'paths' => $params['paths'] ?? '', + 'component' => $params['component'] ?? '', + 'selected' => $params['selected'] ?? '', + 'params' => $params['params'] ?? '', + 'is_cache' => $params['is_cache'], + 'is_show' => $params['is_show'], + 'is_disable' => $params['is_disable'], + ]); + } + + + /** + * @notes 编辑菜单 + * @param array $params + * @return SystemMenu + * @author 段誉 + * @date 2022/6/30 10:07 + */ + public static function edit(array $params) + { + return SystemMenu::update([ + 'id' => $params['id'], + 'pid' => $params['pid'], + 'type' => $params['type'], + 'name' => $params['name'], + 'icon' => $params['icon'] ?? '', + 'sort' => $params['sort'], + 'perms' => $params['perms'] ?? '', + 'paths' => $params['paths'] ?? '', + 'component' => $params['component'] ?? '', + 'selected' => $params['selected'] ?? '', + 'params' => $params['params'] ?? '', + 'is_cache' => $params['is_cache'], + 'is_show' => $params['is_show'], + 'is_disable' => $params['is_disable'], + ]); + } + + + /** + * @notes 详情 + * @param $params + * @return array + * @author 段誉 + * @date 2022/6/30 9:54 + */ + public static function detail($params) + { + return SystemMenu::findOrEmpty($params['id'])->toArray(); + } + + + /** + * @notes 删除菜单 + * @param $params + * @author 段誉 + * @date 2022/6/30 9:47 + */ + public static function delete($params) + { + // 删除菜单 + SystemMenu::destroy($params['id']); + // 删除角色-菜单表中 与该菜单关联的记录 + SystemRoleMenu::where(['menu_id' => $params['id']])->delete(); + } + + + /** + * @notes 更新状态 + * @param array $params + * @return SystemMenu + * @author 段誉 + * @date 2022/7/6 17:02 + */ + public static function updateStatus(array $params) + { + return SystemMenu::update([ + 'id' => $params['id'], + 'is_disable' => $params['is_disable'] + ]); + } + + + /** + * @notes 全部数据 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 11:03 + */ + public static function getAllData() + { + $data = SystemMenu::where(['is_disable' => YesNoEnum::NO]) + ->field('id,pid,name') + ->order(['sort' => 'desc', 'id' => 'desc']) + ->select() + ->toArray(); + + return linear_to_tree($data, 'children'); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/auth/RoleLogic.php b/server/app/adminapi/logic/auth/RoleLogic.php new file mode 100644 index 0000000..e2f33e5 --- /dev/null +++ b/server/app/adminapi/logic/auth/RoleLogic.php @@ -0,0 +1,170 @@ + $params['name'], + 'desc' => $params['desc'] ?? '', + 'sort' => $params['sort'] ?? 0, + ]); + + $data = []; + foreach ($menuId as $item) { + if (empty($item)) { + continue; + } + $data[] = [ + 'role_id' => $role['id'], + 'menu_id' => $item, + ]; + } + (new SystemRoleMenu)->insertAll($data); + + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 编辑角色 + * @param array $params + * @return bool + * @author 段誉 + * @date 2021/12/29 14:16 + */ + public static function edit(array $params): bool + { + Db::startTrans(); + try { + $menuId = !empty($params['menu_id']) ? $params['menu_id'] : []; + + SystemRole::update([ + 'id' => $params['id'], + 'name' => $params['name'], + 'desc' => $params['desc'] ?? '', + 'sort' => $params['sort'] ?? 0, + ]); + + if (!empty($menuId)) { + SystemRoleMenu::where(['role_id' => $params['id']])->delete(); + $data = []; + foreach ($menuId as $item) { + $data[] = [ + 'role_id' => $params['id'], + 'menu_id' => $item, + ]; + } + (new SystemRoleMenu)->insertAll($data); + } + + (new AdminAuthCache())->deleteTag(); + + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + self::$error = $e->getMessage(); + return false; + } + } + + /** + * @notes 删除角色 + * @param int $id + * @return bool + * @author 段誉 + * @date 2021/12/29 14:16 + */ + public static function delete(int $id) + { + SystemRole::destroy(['id' => $id]); + (new AdminAuthCache())->deleteTag(); + return true; + } + + + /** + * @notes 角色详情 + * @param int $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2021/12/29 14:17 + */ + public static function detail(int $id): array + { + $detail = SystemRole::field('id,name,desc,sort')->find($id); + $authList = $detail->roleMenuIndex()->select()->toArray(); + $menuId = array_column($authList, 'menu_id'); + $detail['menu_id'] = $menuId; + return $detail->toArray(); + } + + + /** + * @notes 角色数据 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 10:39 + */ + public static function getAllData() + { + return SystemRole::order(['sort' => 'desc', 'id' => 'desc']) + ->select() + ->toArray(); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/channel/AppSettingLogic.php b/server/app/adminapi/logic/channel/AppSettingLogic.php new file mode 100644 index 0000000..2d075ea --- /dev/null +++ b/server/app/adminapi/logic/channel/AppSettingLogic.php @@ -0,0 +1,56 @@ + ConfigService::get('app', 'ios_download_url', ''), + 'android_download_url' => ConfigService::get('app', 'android_download_url', ''), + 'download_title' => ConfigService::get('app', 'download_title', ''), + ]; + return $config; + } + + + /** + * @notes App设置 + * @param $params + * @author 段誉 + * @date 2022/3/29 10:26 + */ + public static function setConfig($params) + { + ConfigService::set('app', 'ios_download_url', $params['ios_download_url'] ?? ''); + ConfigService::set('app', 'android_download_url', $params['android_download_url'] ?? ''); + ConfigService::set('app', 'download_title', $params['download_title'] ?? ''); + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/channel/MnpSettingsLogic.php b/server/app/adminapi/logic/channel/MnpSettingsLogic.php new file mode 100644 index 0000000..529538c --- /dev/null +++ b/server/app/adminapi/logic/channel/MnpSettingsLogic.php @@ -0,0 +1,72 @@ + ConfigService::get('mnp_setting', 'name', ''), + 'original_id' => ConfigService::get('mnp_setting', 'original_id', ''), + 'qr_code' => $qrCode, + 'app_id' => ConfigService::get('mnp_setting', 'app_id', ''), + 'app_secret' => ConfigService::get('mnp_setting', 'app_secret', ''), + 'request_domain' => 'https://'.$domainName, + 'socket_domain' => 'wss://'.$domainName, + 'upload_file_domain' => 'https://'.$domainName, + 'download_file_domain' => 'https://'.$domainName, + 'udp_domain' => 'udp://'.$domainName, + 'business_domain' => $domainName, + ]; + + return $config; + } + + /** + * @notes 设置小程序配置 + * @param $params + * @author ljj + * @date 2022/2/16 9:51 上午 + */ + public function setConfig($params) + { + $qrCode = isset($params['qr_code']) ? FileService::setFileUrl($params['qr_code']) : ''; + + ConfigService::set('mnp_setting','name', $params['name'] ?? ''); + ConfigService::set('mnp_setting','original_id',$params['original_id'] ?? ''); + ConfigService::set('mnp_setting','qr_code',$qrCode); + ConfigService::set('mnp_setting','app_id',$params['app_id']); + ConfigService::set('mnp_setting','app_secret',$params['app_secret']); + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/channel/OfficialAccountMenuLogic.php b/server/app/adminapi/logic/channel/OfficialAccountMenuLogic.php new file mode 100644 index 0000000..454526f --- /dev/null +++ b/server/app/adminapi/logic/channel/OfficialAccountMenuLogic.php @@ -0,0 +1,224 @@ +getMessage()); + return false; + } + } + + + /** + * @notes 一级菜单校验 + * @param $menu + * @throws \Exception + * @author 段誉 + * @date 2022/3/29 10:55 + */ + public static function checkMenu($menu) + { + if (empty($menu) || !is_array($menu)) { + throw new \Exception('请设置正确格式菜单'); + } + + if (count($menu) > 3) { + throw new \Exception('一级菜单超出限制(最多3个)'); + } + + foreach ($menu as $item) { + if (!is_array($item)) { + throw new \Exception('一级菜单项须为数组格式'); + } + + if (empty($item['name'])) { + throw new \Exception('请输入一级菜单名称'); + } + + if (mb_strlen($item['name']) > 4) { + throw new \Exception("一级菜单名称字数不能超过4个字符"); + } + + if (false == $item['has_menu']) { + if (empty($item['type'])) { + throw new \Exception('一级菜单未选择菜单类型'); + } + if (!in_array($item['type'], OfficialAccountEnum::MENU_TYPE)) { + throw new \Exception('一级菜单类型错误'); + } + self::checkType($item); + } + + if (true == $item['has_menu'] && empty($item['sub_button'])) { + throw new \Exception('请配置子菜单'); + } + + if (!empty($item['sub_button'])) { + self::checkSubButton($item['sub_button']); + } + } + } + + + /** + * @notes 二级菜单校验 + * @param $subButtion + * @throws \Exception + * @author 段誉 + * @date 2022/3/29 10:55 + */ + public static function checkSubButton($subButtion) + { + if (!is_array($subButtion)) { + throw new \Exception('二级菜单须为数组格式'); + } + + if (count($subButtion) > 5) { + throw new \Exception('二级菜单超出限制(最多5个)'); + } + + foreach ($subButtion as $subItem) { + if (!is_array($subItem)) { + throw new \Exception('二级菜单项须为数组'); + } + + if (empty($subItem['name'])) { + throw new \Exception('请输入二级菜单名称'); + } + + if (mb_strlen($subItem['name']) > 8) { + throw new \Exception("二级菜单名称字数不能超过8个字符"); + } + + if (empty($subItem['type']) || !in_array($subItem['type'], OfficialAccountEnum::MENU_TYPE)) { + throw new \Exception('二级未选择菜单类型或菜单类型错误'); + } + + self::checkType($subItem); + } + } + + + /** + * @notes 菜单类型校验 + * @param $item + * @throws \Exception + * @author 段誉 + * @date 2022/3/29 10:55 + */ + public static function checkType($item) + { + switch ($item['type']) { + // 关键字 + case 'click': + if (empty($item['key'])) { + throw new \Exception('请输入关键字'); + } + break; + // 跳转网页链接 + case 'view': + if (empty($item['url'])) { + throw new \Exception('请输入网页链接'); + } + break; + // 小程序 + case 'miniprogram': + if (empty($item['url'])) { + throw new \Exception('请输入网页链接'); + } + if (empty($item['appid'])) { + throw new \Exception('请输入appid'); + } + if (empty($item['pagepath'])) { + throw new \Exception('请输入小程序路径'); + } + break; + } + } + + /** + * @notes 保存发布菜单 + * @param $params + * @return bool + * @throws \GuzzleHttp\Exception\GuzzleException + * @author 段誉 + * @date 2022/3/29 10:55 + */ + public static function saveAndPublish($params) + { + try { + self::checkMenu($params); + + $result = (new WeChatOaService())->createMenu($params); + if ($result['errcode'] == 0) { + ConfigService::set('oa_setting', 'menu', $params); + return true; + } + + self::setError('保存发布菜单失败' . json_encode($result->getContent())); + + return false; + + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 查看菜单详情 + * @return array|int|mixed|string|null + * @author 段誉 + * @date 2022/3/29 10:56 + */ + public static function detail() + { + $data = ConfigService::get('oa_setting', 'menu', []); + + if (!empty($data)) { + foreach ($data as &$item) { + $item['has_menu'] = !empty($item['has_menu']); + } + } + + return $data; + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/channel/OfficialAccountReplyLogic.php b/server/app/adminapi/logic/channel/OfficialAccountReplyLogic.php new file mode 100644 index 0000000..8706024 --- /dev/null +++ b/server/app/adminapi/logic/channel/OfficialAccountReplyLogic.php @@ -0,0 +1,224 @@ + $params['reply_type']])->update(['status' => YesNoEnum::NO]); + } + OfficialAccountReply::create($params); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 查看回复详情 + * @param $params + * @return array + * @author 段誉 + * @date 2022/3/29 11:00 + */ + public static function detail($params) + { + $field = 'id,name,keyword,reply_type,matching_type,content_type,content,status,sort'; + $field .= ',reply_type as reply_type_desc, matching_type as matching_type_desc, content_type as content_type_desc, status as status_desc'; + return OfficialAccountReply::field($field)->findOrEmpty($params['id'])->toArray(); + } + + + /** + * @notes 编辑回复(关注/关键词/默认) + * @param $params + * @return bool + * @author 段誉 + * @date 2022/3/29 11:01 + */ + public static function edit($params) + { + try { + // 关键字回复排序值须大于0 + if ($params['reply_type'] == OfficialAccountEnum::REPLY_TYPE_KEYWORD && $params['sort'] < 0) { + throw new \Exception('排序值须大于或等于0'); + } + if ($params['reply_type'] != OfficialAccountEnum::REPLY_TYPE_KEYWORD && $params['status']) { + // 非关键词回复只能有一条记录处于启用状态,所以将该回复类型下的已有记录置为禁用状态 + OfficialAccountReply::where(['reply_type' => $params['reply_type']])->update(['status' => YesNoEnum::NO]); + } + OfficialAccountReply::update($params); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 删除回复(关注/关键词/默认) + * @param $params + * @author 段誉 + * @date 2022/3/29 11:01 + */ + public static function delete($params) + { + OfficialAccountReply::destroy($params['id']); + } + + + /** + * @notes 更新排序 + * @param $params + * @author 段誉 + * @date 2022/3/29 11:01 + */ + public static function sort($params) + { + $params['sort'] = $params['new_sort']; + OfficialAccountReply::update($params); + } + + + /** + * @notes 更新状态 + * @param $params + * @author 段誉 + * @date 2022/3/29 11:01 + */ + public static function status($params) + { + $reply = OfficialAccountReply::findOrEmpty($params['id']); + $reply->status = !$reply->status; + $reply->save(); + } + + + /** + * @notes 微信公众号回调 + * @return \Psr\Http\Message\ResponseInterface|void + * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \ReflectionException + * @throws \Throwable + * @author 段誉 + * @date 2023/2/27 14:38\ + */ + public static function index() + { + $server = (new WeChatOaService())->getServer(); + // 事件 + $server->addMessageListener(OfficialAccountEnum::MSG_TYPE_EVENT, function ($message, \Closure $next) { + switch ($message['Event']) { + case OfficialAccountEnum::EVENT_SUBSCRIBE: // 关注事件 + $replyContent = OfficialAccountReply::where([ + 'reply_type' => OfficialAccountEnum::REPLY_TYPE_FOLLOW, + 'status' => YesNoEnum::YES + ]) + ->value('content'); + + if ($replyContent) { + return $replyContent; + } + break; + } + return $next($message); + }); + + // 文本 + $server->addMessageListener(OfficialAccountEnum::MSG_TYPE_TEXT, function ($message, \Closure $next) { + $replyList = OfficialAccountReply::where([ + 'reply_type' => OfficialAccountEnum::REPLY_TYPE_KEYWORD, + 'status' => YesNoEnum::YES + ]) + ->order('sort asc') + ->select(); + + $replyContent = ''; + foreach ($replyList as $reply) { + switch ($reply['matching_type']) { + case OfficialAccountEnum::MATCHING_TYPE_FULL: + $reply['keyword'] === $message['Content'] && $replyContent = $reply['content']; + break; + case OfficialAccountEnum::MATCHING_TYPE_FUZZY: + stripos($message['Content'], $reply['keyword']) !== false && $replyContent = $reply['content']; + break; + } + if ($replyContent) { + break; // 得到回复文本,中止循环 + } + } + //消息回复为空的话,找默认回复 + if (empty($replyContent)) { + $replyContent = static::getDefaultReply(); + } + if ($replyContent) { + return $replyContent; + } + return $next($message); + }); + + return $server->serve(); + } + + + /** + * @notes 默认回复信息 + * @return mixed + * @author 段誉 + * @date 2023/2/27 14:36 + */ + public static function getDefaultReply() + { + return OfficialAccountReply::where([ + 'reply_type' => OfficialAccountEnum::REPLY_TYPE_DEFAULT, + 'status' => YesNoEnum::YES + ]) + ->value('content'); + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/channel/OfficialAccountSettingLogic.php b/server/app/adminapi/logic/channel/OfficialAccountSettingLogic.php new file mode 100644 index 0000000..42b4f14 --- /dev/null +++ b/server/app/adminapi/logic/channel/OfficialAccountSettingLogic.php @@ -0,0 +1,76 @@ + ConfigService::get('oa_setting', 'name', ''), + 'original_id' => ConfigService::get('oa_setting', 'original_id', ''), + 'qr_code' => $qrCode, + 'app_id' => ConfigService::get('oa_setting', 'app_id', ''), + 'app_secret' => ConfigService::get('oa_setting', 'app_secret', ''), + // url()方法返回Url实例,通过与空字符串连接触发该实例的__toString()方法以得到路由地址 + 'url' => url('adminapi/channel.official_account_reply/index', [],'',true).'', + 'token' => ConfigService::get('oa_setting', 'token'), + 'encoding_aes_key' => ConfigService::get('oa_setting', 'encoding_aes_key', ''), + 'encryption_type' => ConfigService::get('oa_setting', 'encryption_type', 1), + 'business_domain' => $domainName, + 'js_secure_domain' => $domainName, + 'web_auth_domain' => $domainName, + ]; + return $config; + } + + /** + * @notes 设置公众号配置 + * @param $params + * @author ljj + * @date 2022/2/16 10:08 上午 + */ + public function setConfig($params) + { + $qrCode = isset($params['qr_code']) ? FileService::setFileUrl($params['qr_code']) : ''; + + ConfigService::set('oa_setting','name', $params['name'] ?? ''); + ConfigService::set('oa_setting','original_id', $params['original_id'] ?? ''); + ConfigService::set('oa_setting','qr_code', $qrCode); + ConfigService::set('oa_setting','app_id',$params['app_id']); + ConfigService::set('oa_setting','app_secret',$params['app_secret']); + ConfigService::set('oa_setting','token',$params['token'] ?? ''); + ConfigService::set('oa_setting','encoding_aes_key',$params['encoding_aes_key'] ?? ''); + ConfigService::set('oa_setting','encryption_type',$params['encryption_type']); + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/channel/OpenSettingLogic.php b/server/app/adminapi/logic/channel/OpenSettingLogic.php new file mode 100644 index 0000000..d143655 --- /dev/null +++ b/server/app/adminapi/logic/channel/OpenSettingLogic.php @@ -0,0 +1,55 @@ + ConfigService::get('open_platform', 'app_id', ''), + 'app_secret' => ConfigService::get('open_platform', 'app_secret', ''), + ]; + + return $config; + } + + + /** + * @notes 微信开放平台设置 + * @param $params + * @author 段誉 + * @date 2022/3/29 11:03 + */ + public static function setConfig($params) + { + ConfigService::set('open_platform', 'app_id', $params['app_id'] ?? ''); + ConfigService::set('open_platform', 'app_secret', $params['app_secret'] ?? ''); + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/channel/WebPageSettingLogic.php b/server/app/adminapi/logic/channel/WebPageSettingLogic.php new file mode 100644 index 0000000..bb2fe4b --- /dev/null +++ b/server/app/adminapi/logic/channel/WebPageSettingLogic.php @@ -0,0 +1,59 @@ + ConfigService::get('web_page', 'status', 1), + // 关闭后渠道后访问页面 0-空页面 1-自定义链接 + 'page_status' => ConfigService::get('web_page', 'page_status', 0), + // 自定义链接 + 'page_url' => ConfigService::get('web_page', 'page_url', ''), + 'url' => request()->domain() . '/mobile' + ]; + return $config; + } + + + /** + * @notes H5设置 + * @param $params + * @author 段誉 + * @date 2022/3/29 10:34 + */ + public static function setConfig($params) + { + ConfigService::set('web_page', 'status', $params['status']); + ConfigService::set('web_page', 'page_status', $params['page_status']); + ConfigService::set('web_page', 'page_url', $params['page_url']); + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/community/CommunityCommentLogic.php b/server/app/adminapi/logic/community/CommunityCommentLogic.php new file mode 100644 index 0000000..048603c --- /dev/null +++ b/server/app/adminapi/logic/community/CommunityCommentLogic.php @@ -0,0 +1,33 @@ + $params['id'], + 'status' => $params['status'], + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function delete(array $params) + { + $comment = CommunityComment::findOrEmpty($params['id']); + if (!$comment->isEmpty()) { + CommunityPost::where('id', $comment->post_id)->dec('comment_count')->update(); + $comment->delete(); + } + } +} diff --git a/server/app/adminapi/logic/community/CommunityPostLogic.php b/server/app/adminapi/logic/community/CommunityPostLogic.php new file mode 100644 index 0000000..891ce14 --- /dev/null +++ b/server/app/adminapi/logic/community/CommunityPostLogic.php @@ -0,0 +1,95 @@ +toArray(); + if (!empty($post['create_time']) && is_numeric($post['create_time'])) { + $post['create_time'] = date('Y-m-d H:i:s', $post['create_time']); + } + if (!empty($post['update_time']) && is_numeric($post['update_time'])) { + $post['update_time'] = date('Y-m-d H:i:s', $post['update_time']); + } + $user = User::field('nickname,avatar')->findOrEmpty($post['user_id'] ?? 0)->toArray(); + $post['nickname'] = $user['nickname'] ?? '-'; + $post['avatar'] = $user['avatar'] ?? ''; + return $post; + } + + public static function updateStatus(array $params): bool + { + try { + CommunityPost::update([ + 'id' => $params['id'], + 'status' => $params['status'], + ]); + KbSyncService::enqueue( + 'post', + 'post', + (int) $params['id'], + (int) $params['status'] === 1 ? 'upsert' : 'delete', + 30 + ); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function setTop(array $params): bool + { + try { + CommunityPost::update([ + 'id' => $params['id'], + 'is_top' => $params['is_top'], + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function setHot(array $params): bool + { + try { + CommunityPost::update([ + 'id' => $params['id'], + 'is_hot' => $params['is_hot'], + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function setRecommend(array $params): bool + { + try { + CommunityPost::update([ + 'id' => $params['id'], + 'is_recommend' => $params['is_recommend'], + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function delete(array $params) + { + CommunityPost::destroy($params['id']); + KbSyncService::enqueue('post', 'post', (int) $params['id'], 'delete', 20); + } +} diff --git a/server/app/adminapi/logic/community/CommunityTagLogic.php b/server/app/adminapi/logic/community/CommunityTagLogic.php new file mode 100644 index 0000000..bde744a --- /dev/null +++ b/server/app/adminapi/logic/community/CommunityTagLogic.php @@ -0,0 +1,53 @@ + $params['name'], + 'icon' => $params['icon'] ?? '', + 'sort' => $params['sort'] ?? 0, + 'is_hot' => $params['is_hot'] ?? 0, + 'status' => $params['status'] ?? 1, + ]); + } + + public static function edit(array $params): bool + { + try { + CommunityTag::update([ + 'id' => $params['id'], + 'name' => $params['name'], + 'icon' => $params['icon'] ?? '', + 'sort' => $params['sort'] ?? 0, + 'is_hot' => $params['is_hot'] ?? 0, + 'status' => $params['status'] ?? 1, + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function delete(array $params) + { + CommunityTag::destroy($params['id']); + } + + public static function detail($params): array + { + return CommunityTag::findOrEmpty($params['id'])->toArray(); + } + + public static function all(): array + { + return CommunityTag::where('status', 1)->order('sort', 'desc')->order('id', 'desc')->select()->toArray(); + } +} diff --git a/server/app/adminapi/logic/crontab/CrontabLogic.php b/server/app/adminapi/logic/crontab/CrontabLogic.php new file mode 100644 index 0000000..a0d46c4 --- /dev/null +++ b/server/app/adminapi/logic/crontab/CrontabLogic.php @@ -0,0 +1,302 @@ +getMessage()); + return false; + } + } + + + /** + * @notes 查看定时任务详情 + * @param $params + * @return array + * @author 段誉 + * @date 2022/3/29 14:41 + */ + public static function detail($params) + { + $field = 'id,name,type,type as type_desc,command,params,status,status as status_desc,expression,remark'; + $crontab = Crontab::field($field)->findOrEmpty($params['id']); + if ($crontab->isEmpty()) { + return []; + } + return $crontab->toArray(); + } + + + /** + * @notes 编辑定时任务 + * @param $params + * @return bool + * @author 段誉 + * @date 2022/3/29 14:42 + */ + public static function edit($params) + { + try { + $params['remark'] = $params['remark'] ?? ''; + $params['params'] = $params['params'] ?? ''; + + Crontab::update($params); + + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 删除定时任务 + * @param $params + * @return bool + * @author 段誉 + * @date 2022/3/29 14:42 + */ + public static function delete($params) + { + try { + Crontab::destroy($params['id']); + + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 操作定时任务 + * @param $params + * @return bool + * @author 段誉 + * @date 2022/3/29 14:42 + */ + public static function operate($params) + { + try { + $crontab = Crontab::findOrEmpty($params['id']); + if ($crontab->isEmpty()) { + throw new \Exception('定时任务不存在'); + } + switch ($params['operate']) { + case 'start'; + $crontab->status = CrontabEnum::START; + break; + case 'stop': + $crontab->status = CrontabEnum::STOP; + break; + } + $crontab->save(); + + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 获取规则执行时间 + * @param $params + * @return array|string + * @author 段誉 + * @date 2022/3/29 14:42 + */ + public static function run($params) + { + try { + $crontab = Crontab::findOrEmpty($params['id']); + if ($crontab->isEmpty()) { + throw new \Exception('定时任务不存在'); + } + + $log = CrontabLog::create([ + 'crontab_id' => $crontab->id, + 'name' => $crontab->name, + 'command' => $crontab->command, + 'params' => $crontab->params, + 'status' => 0, + 'output' => '', + 'elapsed' => 0, + 'trigger_type' => 2, + ]); + + if ($crontab->command === 'crawler') { + $crawlerDir = app()->getRootPath() . 'public/dongqiudi-crawler'; + $python = 'python3'; + $action = $crontab->params ?: 'all'; + $logId = $log->id; + $crontabId = $crontab->id; + $cmd = "cd {$crawlerDir} && nohup {$python} main.py {$action} --log-id={$logId} --crontab-id={$crontabId} > /dev/null 2>&1 &"; + \shell_exec($cmd); + + return ['log_id' => $logId, 'output' => '任务已提交,后台执行中...', 'async' => true]; + } + + if ($crontab->command === 'truth_social') { + $crawlerDir = app()->getRootPath() . 'public/dongqiudi-crawler'; + $python = 'python3'; + $logId = $log->id; + $crontabId = $crontab->id; + $cmd = "cd {$crawlerDir} && nohup {$python} main.py truth_social --log-id={$logId} --crontab-id={$crontabId} > /dev/null 2>&1 &"; + \shell_exec($cmd); + + return ['log_id' => $logId, 'output' => '任务已提交,后台执行中...', 'async' => true]; + } + + if ($crontab->command === 'fifa_worldcup_news') { + $crawlerDir = app()->getRootPath() . 'public/dongqiudi-crawler'; + $python = 'python3'; + $logId = $log->id; + $crontabId = $crontab->id; + $cmd = "cd {$crawlerDir} && nohup {$python} main.py fifa_worldcup_news --log-id={$logId} --crontab-id={$crontabId} > /dev/null 2>&1 &"; + \shell_exec($cmd); + + return ['log_id' => $logId, 'output' => '任务已提交,后台执行中...', 'async' => true]; + } + + $startTime = microtime(true); + try { + ob_start(); + $params_arr = explode(' ', $crontab->params); + if (is_array($params_arr) && !empty($crontab->params)) { + \think\facade\Console::call($crontab->command, $params_arr); + } else { + \think\facade\Console::call($crontab->command); + } + $outputStr = ob_get_clean() ?: ''; + $elapsed = round(microtime(true) - $startTime, 2); + + $log->save([ + 'status' => 1, + 'output' => mb_substr($outputStr ?: '执行完成', 0, 5000), + 'elapsed' => $elapsed, + ]); + + Crontab::where('id', $crontab->id)->update([ + 'last_time' => time(), + 'time' => $elapsed, + 'error' => '', + ]); + + return ['log_id' => $log->id, 'output' => $outputStr ?: '执行完成', 'elapsed' => $elapsed]; + } catch (\Exception $e) { + $elapsed = round(microtime(true) - $startTime, 2); + $log->save([ + 'status' => 2, + 'output' => $e->getMessage(), + 'elapsed' => $elapsed, + ]); + + Crontab::where('id', $crontab->id)->update([ + 'error' => $e->getMessage(), + 'status' => CrontabEnum::ERROR, + ]); + + throw $e; + } + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function logs($params) + { + $where = []; + if (!empty($params['crontab_id'])) { + $where[] = ['crontab_id', '=', $params['crontab_id']]; + } + + $pageNo = max(1, intval($params['page_no'] ?? 1)); + $pageSize = min(100, max(1, intval($params['page_size'] ?? 20))); + $offset = ($pageNo - 1) * $pageSize; + + $lists = CrontabLog::where($where) + ->order('id', 'desc') + ->limit($offset, $pageSize) + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['create_time'] = !empty($item['create_time']) ? (is_numeric($item['create_time']) ? date('Y-m-d H:i:s', intval($item['create_time'])) : $item['create_time']) : '-'; + $item['status_text'] = match ((int) $item['status']) { + 0 => '执行中', + 1 => '成功', + 2 => '失败', + default => '未知', + }; + $item['trigger_type_text'] = $item['trigger_type'] == 2 ? '手动' : '自动'; + } + + $count = CrontabLog::where($where)->count(); + + return ['lists' => $lists, 'count' => $count]; + } + + public static function expression($params) + { + try { + $cron = new CronExpression($params['expression']); + $result = $cron->getMultipleRunDates(5); + $result = json_decode(json_encode($result), true); + $lists = []; + foreach ($result as $k => $v) { + $lists[$k]['time'] = $k + 1; + $lists[$k]['date'] = str_replace('.000000', '', $v['date']); + } + $lists[] = ['time' => 'x', 'date' => '……']; + return $lists; + } catch (\Exception $e) { + return $e->getMessage(); + } + } +} diff --git a/server/app/adminapi/logic/decorate/DecorateDataLogic.php b/server/app/adminapi/logic/decorate/DecorateDataLogic.php new file mode 100644 index 0000000..da8353a --- /dev/null +++ b/server/app/adminapi/logic/decorate/DecorateDataLogic.php @@ -0,0 +1,71 @@ + 1]) + ->field($field) + ->order(['id' => 'desc']) + ->limit($limit) + ->append(['click']) + ->hidden(['click_virtual', 'click_actual']) + ->select()->toArray(); + } + + /** + * @notes pc设置 + * @return array + * @author mjf + * @date 2024/3/14 18:13 + */ + public static function pc(): array + { + $pcPage = DecoratePage::findOrEmpty(4)->toArray(); + $updateTime = !empty($pcPage['update_time']) ? $pcPage['update_time'] : date('Y-m-d H:i:s'); + return [ + 'update_time' => $updateTime, + 'pc_url' => request()->domain() . '/pc' + ]; + } + + + + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/decorate/DecoratePageLogic.php b/server/app/adminapi/logic/decorate/DecoratePageLogic.php new file mode 100644 index 0000000..f6f7f79 --- /dev/null +++ b/server/app/adminapi/logic/decorate/DecoratePageLogic.php @@ -0,0 +1,68 @@ +toArray(); + } + + + /** + * @notes 保存装修配置 + * @param $params + * @return bool + * @author 段誉 + * @date 2022/9/15 9:37 + */ + public static function save($params) + { + $pageData = DecoratePage::where(['id' => $params['id']])->findOrEmpty(); + if ($pageData->isEmpty()) { + self::$error = '信息不存在'; + return false; + } + DecoratePage::update([ + 'id' => $params['id'], + 'type' => $params['type'], + 'data' => $params['data'], + 'meta' => $params['meta'] ?? '', + ]); + return true; + } + + + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/decorate/DecorateTabbarLogic.php b/server/app/adminapi/logic/decorate/DecorateTabbarLogic.php new file mode 100644 index 0000000..ace2ded --- /dev/null +++ b/server/app/adminapi/logic/decorate/DecorateTabbarLogic.php @@ -0,0 +1,81 @@ + $style, 'list' => $list]; + } + + + /** + * @notes 底部导航保存 + * @param $params + * @return bool + * @throws \Exception + * @author 段誉 + * @date 2022/9/7 17:19 + */ + public static function save($params): bool + { + $model = new DecorateTabbar(); + // 删除旧配置数据 + $model->where('id', '>', 0)->delete(); + + // 保存数据 + $tabbars = $params['list'] ?? []; + $data = []; + foreach ($tabbars as $item) { + $data[] = [ + 'name' => $item['name'], + 'selected' => FileService::setFileUrl($item['selected']), + 'unselected' => FileService::setFileUrl($item['unselected']), + 'link' => $item['link'], + 'is_show' => $item['is_show'] ?? 0, + ]; + } + $model->saveAll($data); + + if (!empty($params['style'])) { + ConfigService::set('tabbar', 'style', $params['style']); + } + return true; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/dept/DeptLogic.php b/server/app/adminapi/logic/dept/DeptLogic.php new file mode 100644 index 0000000..c39bd65 --- /dev/null +++ b/server/app/adminapi/logic/dept/DeptLogic.php @@ -0,0 +1,202 @@ +append(['status_desc']) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->select() + ->toArray(); + + $pid = 0; + if (!empty($lists)) { + $pid = min(array_column($lists, 'pid')); + } + return self::getTree($lists, $pid); + } + + + /** + * @notes 列表树状结构 + * @param $array + * @param int $pid + * @param int $level + * @return array + * @author 段誉 + * @date 2022/5/30 15:44 + */ + public static function getTree($array, $pid = 0, $level = 0) + { + $list = []; + foreach ($array as $key => $item) { + if ($item['pid'] == $pid) { + $item['level'] = $level; + $item['children'] = self::getTree($array, $item['id'], $level + 1); + $list[] = $item; + } + } + return $list; + } + + + /** + * @notes 上级部门 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/5/26 18:36 + */ + public static function leaderDept() + { + $lists = Dept::field(['id', 'name'])->where(['status' => 1]) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->select() + ->toArray(); + return $lists; + } + + + /** + * @notes 添加部门 + * @param array $params + * @author 段誉 + * @date 2022/5/25 18:20 + */ + public static function add(array $params) + { + Dept::create([ + 'pid' => $params['pid'], + 'name' => $params['name'], + 'leader' => $params['leader'] ?? '', + 'mobile' => $params['mobile'] ?? '', + 'status' => $params['status'], + 'sort' => $params['sort'] ?? 0 + ]); + } + + + /** + * @notes 编辑部门 + * @param array $params + * @return bool + * @author 段誉 + * @date 2022/5/25 18:39 + */ + public static function edit(array $params): bool + { + try { + $pid = $params['pid']; + $oldDeptData = Dept::findOrEmpty($params['id']); + if ($oldDeptData['pid'] == 0) { + $pid = 0; + } + + Dept::update([ + 'id' => $params['id'], + 'pid' => $pid, + 'name' => $params['name'], + 'leader' => $params['leader'] ?? '', + 'mobile' => $params['mobile'] ?? '', + 'status' => $params['status'], + 'sort' => $params['sort'] ?? 0 + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 删除部门 + * @param array $params + * @author 段誉 + * @date 2022/5/25 18:40 + */ + public static function delete(array $params) + { + Dept::destroy($params['id']); + } + + + /** + * @notes 获取部门详情 + * @param $params + * @return array + * @author 段誉 + * @date 2022/5/25 18:40 + */ + public static function detail($params): array + { + return Dept::findOrEmpty($params['id'])->toArray(); + } + + + /** + * @notes 部门数据 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 10:19 + */ + public static function getAllData() + { + $data = Dept::where(['status' => YesNoEnum::YES]) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->select() + ->toArray(); + + $pid = min(array_column($data, 'pid')); + return self::getTree($data, $pid); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/dept/JobsLogic.php b/server/app/adminapi/logic/dept/JobsLogic.php new file mode 100644 index 0000000..925ea55 --- /dev/null +++ b/server/app/adminapi/logic/dept/JobsLogic.php @@ -0,0 +1,119 @@ + $params['name'], + 'code' => $params['code'], + 'sort' => $params['sort'] ?? 0, + 'status' => $params['status'], + 'remark' => $params['remark'] ?? '', + ]); + } + + + /** + * @notes 编辑岗位 + * @param array $params + * @return bool + * @author 段誉 + * @date 2022/5/26 9:58 + */ + public static function edit(array $params) : bool + { + try { + Jobs::update([ + 'id' => $params['id'], + 'name' => $params['name'], + 'code' => $params['code'], + 'sort' => $params['sort'] ?? 0, + 'status' => $params['status'], + 'remark' => $params['remark'] ?? '', + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 删除岗位 + * @param array $params + * @author 段誉 + * @date 2022/5/26 9:59 + */ + public static function delete(array $params) + { + Jobs::destroy($params['id']); + } + + + /** + * @notes 获取岗位详情 + * @param $params + * @return array + * @author 段誉 + * @date 2022/5/26 9:59 + */ + public static function detail($params) : array + { + return Jobs::findOrEmpty($params['id'])->toArray(); + } + + + /** + * @notes 岗位数据 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 10:30 + */ + public static function getAllData() + { + return Jobs::where(['status' => YesNoEnum::YES]) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->select() + ->toArray(); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/finance/RefundLogic.php b/server/app/adminapi/logic/finance/RefundLogic.php new file mode 100644 index 0000000..e432325 --- /dev/null +++ b/server/app/adminapi/logic/finance/RefundLogic.php @@ -0,0 +1,95 @@ +toArray(); + + $total = 0; + $ing = 0; + $success = 0; + $error = 0; + + foreach ($records as $record) { + $total += $record['order_amount']; + switch ($record['refund_status']) { + case RefundEnum::REFUND_ING: + $ing += $record['order_amount']; + break; + case RefundEnum::REFUND_SUCCESS: + $success += $record['order_amount']; + break; + case RefundEnum::REFUND_ERROR: + $error += $record['order_amount']; + break; + } + } + + return [ + 'total' => round($total, 2), + 'ing' => round($ing, 2), + 'success' => round($success, 2), + 'error' => round($error, 2), + ]; + } + + + /** + * @notes 退款日志 + * @param $recordId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2023/3/3 14:25 + */ + public static function refundLog($recordId) + { + return (new RefundLog()) + ->order(['id' => 'desc']) + ->where('record_id', $recordId) + ->hidden(['refund_msg']) + ->append(['handler', 'refund_status_text']) + ->select() + ->toArray(); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/lottery/LotteryCategoryLogic.php b/server/app/adminapi/logic/lottery/LotteryCategoryLogic.php new file mode 100644 index 0000000..6f29dc6 --- /dev/null +++ b/server/app/adminapi/logic/lottery/LotteryCategoryLogic.php @@ -0,0 +1,62 @@ + $params['name'], + 'code' => $params['code'], + 'icon' => $params['icon'] ?? '', + 'template' => $params['template'] ?? '', + 'category' => $params['category'] ?? 0, + 'status' => $params['status'] ?? 0, + 'open_status' => $params['open_status'] ?? 0, + 'group_id' => $params['group_id'] ?? 0, + 'open_count' => $params['open_count'] ?? 0, + 'has_video' => $params['has_video'] ?? 0, + 'is_show' => $params['is_show'] ?? 1, + 'sort' => $params['sort'] ?? 0, + ]); + } + + public static function edit(array $params): bool + { + try { + LotteryGame::update([ + 'id' => $params['id'], + 'name' => $params['name'], + 'code' => $params['code'], + 'icon' => $params['icon'] ?? '', + 'template' => $params['template'] ?? '', + 'category' => $params['category'] ?? 0, + 'status' => $params['status'] ?? 0, + 'open_status' => $params['open_status'] ?? 0, + 'group_id' => $params['group_id'] ?? 0, + 'open_count' => $params['open_count'] ?? 0, + 'has_video' => $params['has_video'] ?? 0, + 'is_show' => $params['is_show'] ?? 1, + 'sort' => $params['sort'] ?? 0, + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function delete(array $params) + { + LotteryGame::destroy($params['id']); + } + + public static function detail($params): array + { + return LotteryGame::findOrEmpty($params['id'])->toArray(); + } +} diff --git a/server/app/adminapi/logic/lottery/LotteryDrawLogic.php b/server/app/adminapi/logic/lottery/LotteryDrawLogic.php new file mode 100644 index 0000000..e556a53 --- /dev/null +++ b/server/app/adminapi/logic/lottery/LotteryDrawLogic.php @@ -0,0 +1,19 @@ +toArray(); + } + + public static function delete(array $params) + { + LotteryDraw::destroy($params['id']); + } +} diff --git a/server/app/adminapi/logic/lottery/LotteryRegionLogic.php b/server/app/adminapi/logic/lottery/LotteryRegionLogic.php new file mode 100644 index 0000000..a77afb6 --- /dev/null +++ b/server/app/adminapi/logic/lottery/LotteryRegionLogic.php @@ -0,0 +1,46 @@ + $params['label'], + 'value' => $params['value'] ?? 0, + 'sort' => $params['sort'] ?? 0, + 'is_show' => $params['is_show'] ?? 1, + ]); + } + + public static function edit(array $params): bool + { + try { + LotteryGameCategory::update([ + 'id' => $params['id'], + 'label' => $params['label'], + 'value' => $params['value'] ?? 0, + 'sort' => $params['sort'] ?? 0, + 'is_show' => $params['is_show'] ?? 1, + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function delete(array $params) + { + LotteryGameCategory::destroy($params['id']); + } + + public static function detail($params): array + { + return LotteryGameCategory::findOrEmpty($params['id'])->toArray(); + } +} diff --git a/server/app/adminapi/logic/match/LeagueLogic.php b/server/app/adminapi/logic/match/LeagueLogic.php new file mode 100644 index 0000000..5086ffb --- /dev/null +++ b/server/app/adminapi/logic/match/LeagueLogic.php @@ -0,0 +1,36 @@ +toArray(); + } + + public static function edit(array $params): bool + { + try { + League::update([ + 'id' => $params['id'], + 'label' => $params['label'], + 'icon' => $params['icon'] ?? '', + 'sort' => $params['sort'] ?? 0, + 'is_show' => $params['is_show'] ?? 1, + ]); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function delete(array $params) + { + League::destroy($params['id']); + } +} diff --git a/server/app/adminapi/logic/match/MatchLogic.php b/server/app/adminapi/logic/match/MatchLogic.php new file mode 100644 index 0000000..c898fa6 --- /dev/null +++ b/server/app/adminapi/logic/match/MatchLogic.php @@ -0,0 +1,61 @@ +toArray(); + if (!empty($match['match_time'])) { + $match['match_time_str'] = date('Y-m-d H:i', $match['match_time']); + } + return $match; + } + + public static function edit(array $params): bool + { + try { + $data = ['id' => $params['id']]; + $allowFields = [ + 'league_name', + 'round_name', + 'stage', + 'live_url', + 'home_team', + 'home_icon', + 'home_score', + 'away_team', + 'away_icon', + 'away_score', + 'sport_type', + 'status', + 'match_time', + 'home_odds', + 'draw_odds', + 'away_odds', + 'is_hot', + 'is_show', + 'sort', + ]; + foreach ($allowFields as $field) { + if (isset($params[$field])) { + $data[$field] = $params[$field]; + } + } + MatchEvent::update($data); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function delete(array $params) + { + MatchEvent::destroy($params['id']); + } +} diff --git a/server/app/adminapi/logic/notice/NoticeLogic.php b/server/app/adminapi/logic/notice/NoticeLogic.php new file mode 100644 index 0000000..552999e --- /dev/null +++ b/server/app/adminapi/logic/notice/NoticeLogic.php @@ -0,0 +1,225 @@ +findOrEmpty($params['id'])->toArray(); + if (empty($noticeSetting)) { + return []; + } + if (empty($noticeSetting['system_notice'])) { + $noticeSetting['system_notice'] = [ + 'title' => '', + 'content' => '', + 'status' => 0, + ]; + } + $noticeSetting['system_notice']['tips'] = NoticeEnum::getOperationTips(NoticeEnum::SYSTEM, $noticeSetting['scene_id']); + if (empty($noticeSetting['sms_notice'])) { + $noticeSetting['sms_notice'] = [ + 'template_id' => '', + 'content' => '', + 'status' => 0, + ]; + } + $noticeSetting['sms_notice']['tips'] = NoticeEnum::getOperationTips(NoticeEnum::SMS, $noticeSetting['scene_id']); + if (empty($noticeSetting['oa_notice'])) { + $noticeSetting['oa_notice'] = [ + 'template_id' => '', + 'template_sn' => '', + 'name' => '', + 'first' => '', + 'remark' => '', + 'tpl' => [], + 'status' => 0, + ]; + } + $noticeSetting['oa_notice']['tips'] = NoticeEnum::getOperationTips(NoticeEnum::MNP, $noticeSetting['scene_id']); + if (empty($noticeSetting['mnp_notice'])) { + $noticeSetting['mnp_notice'] = [ + 'template_id' => '', + 'template_sn' => '', + 'name' => '', + 'tpl' => [], + 'status' => 0, + ]; + } + $noticeSetting['mnp_notice']['tips'] = NoticeEnum::getOperationTips(NoticeEnum::MNP, $noticeSetting['scene_id']); + $noticeSetting['system_notice']['is_show'] = in_array(NoticeEnum::SYSTEM, explode(',', $noticeSetting['support'])); + $noticeSetting['sms_notice']['is_show'] = in_array(NoticeEnum::SMS, explode(',', $noticeSetting['support'])); + $noticeSetting['oa_notice']['is_show'] = in_array(NoticeEnum::OA, explode(',', $noticeSetting['support'])); + $noticeSetting['mnp_notice']['is_show'] = in_array(NoticeEnum::MNP, explode(',', $noticeSetting['support'])); + $noticeSetting['default'] = ''; + $noticeSetting['type'] = NoticeEnum::getTypeDesc($noticeSetting['type']); + return $noticeSetting; + } + + + /** + * @notes 通知设置 + * @param $params + * @return bool + * @author 段誉 + * @date 2022/3/29 11:34 + */ + public static function set($params) + { + try { + // 校验参数 + self::checkSet($params); + // 拼装更新数据 + $updateData = []; + foreach ($params['template'] as $item) { + $updateData[$item['type'] . '_notice'] = json_encode($item, JSON_UNESCAPED_UNICODE); + } + // 更新通知设置 + NoticeSetting::where('id', $params['id'])->update($updateData); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 校验参数 + * @param $params + * @throws \Exception + * @author 段誉 + * @date 2022/3/29 11:35 + */ + public static function checkSet($params) + { + $noticeSetting = NoticeSetting::findOrEmpty($params['id'] ?? 0); + + if ($noticeSetting->isEmpty()) { + throw new \Exception('通知配置不存在'); + } + + if (!isset($params['template']) || !is_array($params['template']) || count($params['template']) == 0) { + throw new \Exception('模板配置不存在或格式错误'); + } + + // 通知类型 + $noticeType = ['system', 'sms', 'oa', 'mnp']; + + foreach ($params['template'] as $item) { + if (!is_array($item)) { + throw new \Exception('模板项格式错误'); + } + + if (!isset($item['type']) || !in_array($item['type'], $noticeType)) { + throw new \Exception('模板项缺少模板类型或模板类型有误'); + } + + switch ($item['type']) { + case "system"; + self::checkSystem($item); + break; + case "sms"; + self::checkSms($item); + break; + case "oa"; + self::checkOa($item); + break; + case "mnp"; + self::checkMnp($item); + break; + } + } + } + + + /** + * @notes 校验系统通知参数 + * @param $item + * @throws \Exception + * @author 段誉 + * @date 2022/3/29 11:35 + */ + public static function checkSystem($item) + { + if (!isset($item['title']) || !isset($item['content']) || !isset($item['status'])) { + throw new \Exception('系统通知必填参数:title、content、status'); + } + } + + + /** + * @notes 校验短信通知必填参数 + * @param $item + * @throws \Exception + * @author 段誉 + * @date 2022/3/29 11:35 + */ + public static function checkSms($item) + { + if (!isset($item['template_id']) || !isset($item['content']) || !isset($item['status'])) { + throw new \Exception('短信通知必填参数:template_id、content、status'); + } + } + + + /** + * @notes 校验微信模板消息参数 + * @param $item + * @throws \Exception + * @author 段誉 + * @date 2022/3/29 11:35 + */ + public static function checkOa($item) + { + if (!isset($item['template_id']) || !isset($item['template_sn']) || !isset($item['name']) || !isset($item['first']) || !isset($item['remark']) || !isset($item['tpl']) || !isset($item['status'])) { + throw new \Exception('微信模板消息必填参数:template_id、template_sn、name、first、remark、tpl、status'); + } + } + + + /** + * @notes 校验微信小程序提醒必填参数 + * @param $item + * @throws \Exception + * @author 段誉 + * @date 2022/3/29 11:35 + */ + public static function checkMnp($item) + { + if (!isset($item['template_id']) || !isset($item['template_sn']) || !isset($item['name']) || !isset($item['tpl']) || !isset($item['status'])) { + throw new \Exception('微信模板消息必填参数:template_id、template_sn、name、tpl、status'); + } + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/notice/SmsConfigLogic.php b/server/app/adminapi/logic/notice/SmsConfigLogic.php new file mode 100644 index 0000000..cfcfada --- /dev/null +++ b/server/app/adminapi/logic/notice/SmsConfigLogic.php @@ -0,0 +1,127 @@ + 'ali', 'name' => '阿里云短信', 'status' => 1]), + ConfigService::get('sms', 'tencent', ['type' => 'tencent', 'name' => '腾讯云短信', 'status' => 0]), + ]; + return $config; + } + + + /** + * @notes 短信配置 + * @param $params + * @return bool|void + * @author 段誉 + * @date 2022/3/29 11:37 + */ + public static function setConfig($params) + { + $type = $params['type']; + $params['name'] = self::getNameDesc(strtoupper($type)); + ConfigService::set('sms', $type, $params); + $default = ConfigService::get('sms', 'engine', false); + if ($params['status'] == 1 && $default === false) { + // 启用当前短信配置 并 设置当前短信配置为默认 + ConfigService::set('sms', 'engine', strtoupper($type)); + return true; + } + if ($params['status'] == 1 && $default != strtoupper($type)) { + // 找到默认短信配置 + $defaultConfig = ConfigService::get('sms', strtolower($default)); + // 状态置为禁用 并 更新 + $defaultConfig['status'] = 0; + ConfigService::set('sms', strtolower($default), $defaultConfig); + // 设置当前短信配置为默认 + ConfigService::set('sms', 'engine', strtoupper($type)); + return true; + } + } + + + /** + * @notes 查看短信配置详情 + * @param $params + * @return array|int|mixed|string|null + * @author 段誉 + * @date 2022/3/29 11:37 + */ + public static function detail($params) + { + $default = []; + switch ($params['type']) { + case 'ali': + $default = [ + 'sign' => '', + 'app_key' => '', + 'secret_key' => '', + 'status' => 1, + 'name' => '阿里云短信', + ]; + break; + case 'tencent': + $default = [ + 'sign' => '', + 'app_id' => '', + 'secret_key' => '', + 'status' => 0, + 'secret_id' => '', + 'name' => '腾讯云短信', + ]; + break; + } + $result = ConfigService::get('sms', $params['type'], $default); + $result['status'] = intval($result['status'] ?? 0); + return $result; + } + + + /** + * @notes 获取短信平台名称 + * @param $value + * @return string + * @author 段誉 + * @date 2022/3/29 11:37 + */ + public static function getNameDesc($value) + { + $desc = [ + 'ALI' => '阿里云短信', + 'TENCENT' => '腾讯云短信', + ]; + return $desc[$value] ?? ''; + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/points/PointsProductLogic.php b/server/app/adminapi/logic/points/PointsProductLogic.php new file mode 100644 index 0000000..37b118c --- /dev/null +++ b/server/app/adminapi/logic/points/PointsProductLogic.php @@ -0,0 +1,42 @@ + $params['name'], + 'points' => $params['points'], + 'amount' => $params['amount'], + 'sort' => $params['sort'] ?? 0, + 'status' => $params['status'] ?? 1, + ]); + } + + public static function edit(array $params) + { + PointsProduct::update([ + 'id' => $params['id'], + 'name' => $params['name'], + 'points' => $params['points'], + 'amount' => $params['amount'], + 'sort' => $params['sort'] ?? 0, + 'status' => $params['status'] ?? 1, + ]); + } + + public static function delete(array $params) + { + PointsProduct::destroy($params['id']); + } + + public static function detail(array $params) + { + return PointsProduct::findOrEmpty($params['id'])->toArray(); + } +} diff --git a/server/app/adminapi/logic/popup/PopupLogic.php b/server/app/adminapi/logic/popup/PopupLogic.php new file mode 100644 index 0000000..592bb3a --- /dev/null +++ b/server/app/adminapi/logic/popup/PopupLogic.php @@ -0,0 +1,81 @@ +getMessage()); + return false; + } + } + + public static function delete(array $params): void + { + $id = (int)$params['id']; + PagePopup::destroy($id); + // 同时清理日志 + PagePopupLog::where('popup_id', $id)->delete(); + } + + public static function detail(array $params): array + { + return PagePopup::findOrEmpty((int)$params['id'])->toArray(); + } + + public static function updateStatus(array $params): bool + { + PagePopup::update([ + 'id' => (int)$params['id'], + 'status' => (int)$params['status'], + ]); + return true; + } + + /** + * 字段白名单过滤 + 时间格式归一化 + */ + private static function filterFields(array $params): array + { + $data = []; + foreach (self::$fields as $f) { + if (array_key_exists($f, $params)) { + $data[$f] = $params[$f]; + } + } + // start_time / end_time 支持字符串日期,统一转时间戳 + foreach (['start_time', 'end_time'] as $tf) { + if (isset($data[$tf]) && !is_numeric($data[$tf])) { + $ts = strtotime((string)$data[$tf]); + $data[$tf] = $ts ?: 0; + } + } + return $data; + } +} diff --git a/server/app/adminapi/logic/recharge/RechargeLogic.php b/server/app/adminapi/logic/recharge/RechargeLogic.php new file mode 100644 index 0000000..6ffa175 --- /dev/null +++ b/server/app/adminapi/logic/recharge/RechargeLogic.php @@ -0,0 +1,185 @@ + ConfigService::get('recharge', 'status', 0), + 'min_amount' => ConfigService::get('recharge', 'min_amount', 0) + ]; + + return $config; + } + + + /** + * @notes 充值设置 + * @param $params + * @return bool + * @author 段誉 + * @date 2023/2/22 16:54 + */ + public static function setConfig($params) + { + try { + if (isset($params['status'])) { + ConfigService::set('recharge', 'status', $params['status']); + } + if (isset($params['min_amount'])) { + ConfigService::set('recharge', 'min_amount', $params['min_amount']); + } + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 退款 + * @param $params + * @param $adminId + * @return array|false + * @author 段誉 + * @date 2023/3/3 11:42 + */ + public static function refund($params, $adminId) + { + Db::startTrans(); + try { + $order = RechargeOrder::findOrEmpty($params['recharge_id']); + + // 更新订单信息, 标记已发起退款状态,具体退款成功看退款日志 + RechargeOrder::update([ + 'id' => $order['id'], + 'refund_status' => YesNoEnum::YES, + ]); + + // 更新用户余额及累计充值金额 + User::where(['id' => $order['user_id']]) + ->dec('total_recharge_amount', $order['order_amount']) + ->dec('user_money', $order['order_amount']) + ->update(); + + // 记录日志 + AccountLogLogic::add( + $order['user_id'], + AccountLogEnum::UM_INC_ADMIN, + AccountLogEnum::DEC, + $order['order_amount'], + $order['sn'], + '充值订单退款' + ); + + // 生成退款记录 + $recordSn = generate_sn(RefundRecord::class, 'sn'); + $record = RefundRecord::create([ + 'sn' => $recordSn, + 'user_id' => $order['user_id'], + 'order_id' => $order['id'], + 'order_sn' => $order['sn'], + 'order_type' => RefundEnum::ORDER_TYPE_RECHARGE, + 'order_amount' => $order['order_amount'], + 'refund_amount' => $order['order_amount'], + 'refund_type' => RefundEnum::TYPE_ADMIN, + 'transaction_id' => $order['transaction_id'] ?? '', + 'refund_way' => RefundEnum::getRefundWayByPayWay($order['pay_way']), + ]); + + // 退款 + $result = RefundLogic::refund($order, $record['id'], $order['order_amount'], $adminId); + + $flag = true; + $resultMsg = '操作成功'; + if ($result !== true) { + $flag = false; + $resultMsg = RefundLogic::getError(); + } + + Db::commit(); + return [$flag, $resultMsg]; + } catch (\Exception $e) { + Db::rollback(); + self::$error = $e->getMessage(); + return [false, $e->getMessage()]; + } + } + + + /** + * @notes 重新退款 + * @param $params + * @param $adminId + * @return array + * @author 段誉 + * @date 2023/3/3 11:44 + */ + public static function refundAgain($params, $adminId) + { + Db::startTrans(); + try { + $record = RefundRecord::findOrEmpty($params['record_id']); + $order = RechargeOrder::findOrEmpty($record['order_id']); + + // 退款 + $result = RefundLogic::refund($order, $record['id'], $order['order_amount'], $adminId); + + $flag = true; + $resultMsg = '操作成功'; + if ($result !== true) { + $flag = false; + $resultMsg = RefundLogic::getError(); + } + + Db::commit(); + return [$flag, $resultMsg]; + } catch (\Exception $e) { + Db::rollback(); + self::$error = $e->getMessage(); + return [false, $e->getMessage()]; + } + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/setting/CustomerServiceLogic.php b/server/app/adminapi/logic/setting/CustomerServiceLogic.php new file mode 100644 index 0000000..48e3d84 --- /dev/null +++ b/server/app/adminapi/logic/setting/CustomerServiceLogic.php @@ -0,0 +1,65 @@ + $qrCode, + 'wechat' => ConfigService::get('customer_service', 'wechat', ''), + 'phone' => ConfigService::get('customer_service', 'phone', ''), + 'service_time' => ConfigService::get('customer_service', 'service_time', ''), + ]; + return $config; + } + + /** + * @notes 设置客服设置 + * @param $params + * @author ljj + * @date 2022/2/15 12:11 下午 + */ + public static function setConfig($params) + { + $allowField = ['qr_code','wechat','phone','service_time']; + foreach($params as $key => $value) { + if(in_array($key, $allowField)) { + if ($key == 'qr_code') { + $value = FileService::setFileUrl($value); + } + ConfigService::set('customer_service', $key, $value); + } + } + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/setting/HotSearchLogic.php b/server/app/adminapi/logic/setting/HotSearchLogic.php new file mode 100644 index 0000000..684cf46 --- /dev/null +++ b/server/app/adminapi/logic/setting/HotSearchLogic.php @@ -0,0 +1,75 @@ + ConfigService::get('hot_search', 'status', 0), + // 热门搜索数据 + 'data' => HotSearch::field(['name', 'sort'])->order(['sort' => 'desc', 'id' =>'desc'])->select()->toArray(), + ]; + } + + + /** + * @notes 设置热门搜搜 + * @param $params + * @return bool + * @author 段誉 + * @date 2022/9/5 18:58 + */ + public static function setConfig($params) + { + try { + if (!empty($params['data'])) { + $model = (new HotSearch()); + $model->where('id', '>', 0)->delete(); + $model->saveAll($params['data']); + } + + $status = empty($params['status']) ? 0 : $params['status']; + ConfigService::set('hot_search', 'status', $status); + + return true; + } catch (\Exception $e) { + self::$error = $e->getMessage(); + return false; + } + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/setting/StorageLogic.php b/server/app/adminapi/logic/setting/StorageLogic.php new file mode 100644 index 0000000..60f0338 --- /dev/null +++ b/server/app/adminapi/logic/setting/StorageLogic.php @@ -0,0 +1,203 @@ + '本地存储', + 'path' => '存储在本地服务器', + 'engine' => 'local', + 'status' => $default == 'local' ? 1 : 0 + ], + [ + 'name' => '七牛云存储', + 'path' => '存储在七牛云,请前往七牛云开通存储服务', + 'engine' => 'qiniu', + 'status' => $default == 'qiniu' ? 1 : 0 + ], + [ + 'name' => '阿里云OSS', + 'path' => '存储在阿里云,请前往阿里云开通存储服务', + 'engine' => 'aliyun', + 'status' => $default == 'aliyun' ? 1 : 0 + ], + [ + 'name' => '腾讯云COS', + 'path' => '存储在腾讯云,请前往腾讯云开通存储服务', + 'engine' => 'qcloud', + 'status' => $default == 'qcloud' ? 1 : 0 + ] + ]; + return $data; + } + + + /** + * @notes 存储设置详情 + * @param $param + * @return mixed + * @author 段誉 + * @date 2022/4/20 16:15 + */ + public static function detail($param) + { + + $default = ConfigService::get('storage', 'default', ''); + + // 本地存储 + $local = ['status' => $default == 'local' ? 1 : 0]; + // 七牛云存储 + $qiniu = ConfigService::get('storage', 'qiniu', [ + 'bucket' => '', + 'access_key' => '', + 'secret_key' => '', + 'domain' => '', + 'status' => $default == 'qiniu' ? 1 : 0 + ]); + + // 阿里云存储 + $aliyun = ConfigService::get('storage', 'aliyun', [ + 'bucket' => '', + 'access_key' => '', + 'secret_key' => '', + 'domain' => '', + 'status' => $default == 'aliyun' ? 1 : 0 + ]); + + // 腾讯云存储 + $qcloud = ConfigService::get('storage', 'qcloud', [ + 'bucket' => '', + 'region' => '', + 'access_key' => '', + 'secret_key' => '', + 'domain' => '', + 'status' => $default == 'qcloud' ? 1 : 0 + ]); + + $data = [ + 'local' => $local, + 'qiniu' => $qiniu, + 'aliyun' => $aliyun, + 'qcloud' => $qcloud + ]; + $result = $data[$param['engine']]; + if ($param['engine'] == $default) { + $result['status'] = 1; + } else { + $result['status'] = 0; + } + return $result; + } + + + /** + * @notes 设置存储参数 + * @param $params + * @return bool|string + * @author 段誉 + * @date 2022/4/20 16:16 + */ + public static function setup($params) + { + if ($params['status'] == 1) { //状态为开启 + ConfigService::set('storage', 'default', $params['engine']); + } else { + ConfigService::set('storage', 'default', 'local'); + } + + switch ($params['engine']) { + case 'local': + ConfigService::set('storage', 'local', []); + break; + case 'qiniu': + ConfigService::set('storage', 'qiniu', [ + 'bucket' => $params['bucket'] ?? '', + 'access_key' => $params['access_key'] ?? '', + 'secret_key' => $params['secret_key'] ?? '', + 'domain' => $params['domain'] ?? '' + ]); + break; + case 'aliyun': + ConfigService::set('storage', 'aliyun', [ + 'bucket' => $params['bucket'] ?? '', + 'access_key' => $params['access_key'] ?? '', + 'secret_key' => $params['secret_key'] ?? '', + 'domain' => $params['domain'] ?? '' + ]); + break; + case 'qcloud': + ConfigService::set('storage', 'qcloud', [ + 'bucket' => $params['bucket'] ?? '', + 'region' => $params['region'] ?? '', + 'access_key' => $params['access_key'] ?? '', + 'secret_key' => $params['secret_key'] ?? '', + 'domain' => $params['domain'] ?? '', + ]); + break; + } + + Cache::delete('STORAGE_DEFAULT'); + Cache::delete('STORAGE_ENGINE'); + if ($params['engine'] == 'local' && $params['status'] == 0) { + return '默认开启本地存储'; + } else { + return true; + } + } + + + /** + * @notes 切换状态 + * @param $params + * @author 段誉 + * @date 2022/4/20 16:17 + */ + public static function change($params) + { + $default = ConfigService::get('storage', 'default', ''); + if ($default == $params['engine']) { + ConfigService::set('storage', 'default', 'local'); + } else { + ConfigService::set('storage', 'default', $params['engine']); + } + Cache::delete('STORAGE_DEFAULT'); + Cache::delete('STORAGE_ENGINE'); + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/setting/TransactionSettingsLogic.php b/server/app/adminapi/logic/setting/TransactionSettingsLogic.php new file mode 100644 index 0000000..87b9b4a --- /dev/null +++ b/server/app/adminapi/logic/setting/TransactionSettingsLogic.php @@ -0,0 +1,64 @@ + ConfigService::get('transaction', 'cancel_unpaid_orders', 1), + 'cancel_unpaid_orders_times' => ConfigService::get('transaction', 'cancel_unpaid_orders_times', 30), + 'verification_orders' => ConfigService::get('transaction', 'verification_orders', 1), + 'verification_orders_times' => ConfigService::get('transaction', 'verification_orders_times', 24), + ]; + + return $config; + } + + /** + * @notes 设置交易设置 + * @param $params + * @author ljj + * @date 2022/2/15 11:49 上午 + */ + public static function setConfig($params) + { + ConfigService::set('transaction', 'cancel_unpaid_orders', $params['cancel_unpaid_orders']); + ConfigService::set('transaction', 'verification_orders', $params['verification_orders']); + + if (isset($params['cancel_unpaid_orders_times'])) { + ConfigService::set('transaction', 'cancel_unpaid_orders_times', $params['cancel_unpaid_orders_times']); + } + + if (isset($params['verification_orders_times'])) { + ConfigService::set('transaction', 'verification_orders_times', $params['verification_orders_times']); + } + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/setting/dict/DictDataLogic.php b/server/app/adminapi/logic/setting/dict/DictDataLogic.php new file mode 100644 index 0000000..8ea5d93 --- /dev/null +++ b/server/app/adminapi/logic/setting/dict/DictDataLogic.php @@ -0,0 +1,84 @@ + $params['name'], + 'value' => $params['value'], + 'sort' => $params['sort'] ?? 0, + 'status' => $params['status'], + 'remark' => $params['remark'] ?? '', + ]; + + if (!empty($params['id'])) { + return DictData::where(['id' => $params['id']])->update($data); + } else { + $dictType = DictType::findOrEmpty($params['type_id']); + $data['type_id'] = $params['type_id']; + $data['type_value'] = $dictType['type']; + return DictData::create($data); + } + } + + + /** + * @notes 删除字典数据 + * @param array $params + * @return bool + * @author 段誉 + * @date 2022/6/20 17:01 + */ + public static function delete(array $params) + { + return DictData::destroy($params['id']); + } + + + /** + * @notes 获取字典数据详情 + * @param $params + * @return array + * @author 段誉 + * @date 2022/6/20 17:01 + */ + public static function detail($params): array + { + return DictData::findOrEmpty($params['id'])->toArray(); + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/setting/dict/DictTypeLogic.php b/server/app/adminapi/logic/setting/dict/DictTypeLogic.php new file mode 100644 index 0000000..f423cf5 --- /dev/null +++ b/server/app/adminapi/logic/setting/dict/DictTypeLogic.php @@ -0,0 +1,111 @@ + $params['name'], + 'type' => $params['type'], + 'status' => $params['status'], + 'remark' => $params['remark'] ?? '', + ]); + } + + + /** + * @notes 编辑字典类型 + * @param array $params + * @author 段誉 + * @date 2022/6/20 16:10 + */ + public static function edit(array $params) + { + DictType::update([ + 'id' => $params['id'], + 'name' => $params['name'], + 'type' => $params['type'], + 'status' => $params['status'], + 'remark' => $params['remark'] ?? '', + ]); + + DictData::where(['type_id' => $params['id']]) + ->update(['type_value' => $params['type']]); + } + + + /** + * @notes 删除字典类型 + * @param array $params + * @author 段誉 + * @date 2022/6/20 16:23 + */ + public static function delete(array $params) + { + DictType::destroy($params['id']); + } + + + /** + * @notes 获取字典详情 + * @param $params + * @return array + * @author 段誉 + * @date 2022/6/20 16:23 + */ + public static function detail($params): array + { + return DictType::findOrEmpty($params['id'])->toArray(); + } + + + /** + * @notes 角色数据 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/13 10:44 + */ + public static function getAllData() + { + return DictType::where(['status' => YesNoEnum::YES]) + ->order(['id' => 'desc']) + ->select() + ->toArray(); + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/setting/pay/PayConfigLogic.php b/server/app/adminapi/logic/setting/pay/PayConfigLogic.php new file mode 100644 index 0000000..029a552 --- /dev/null +++ b/server/app/adminapi/logic/setting/pay/PayConfigLogic.php @@ -0,0 +1,96 @@ + $params['config']['interface_version'], + 'merchant_type' => $params['config']['merchant_type'], + 'mch_id' => $params['config']['mch_id'], + 'pay_sign_key' => $params['config']['pay_sign_key'], + 'apiclient_cert' => $params['config']['apiclient_cert'], + 'apiclient_key' => $params['config']['apiclient_key'], + ]; + } + if ($payConfig['pay_way'] == PayEnum::ALI_PAY) { + $config = [ + 'mode' => $params['config']['mode'], + 'merchant_type' => $params['config']['merchant_type'], + 'app_id' => $params['config']['app_id'], + 'private_key' => $params['config']['private_key'], + 'ali_public_key' => $params['config']['mode'] == 'normal_mode' ? $params['config']['ali_public_key'] : '', + 'public_cert' => $params['config']['mode'] == 'certificate' ? $params['config']['public_cert'] : '', + 'ali_public_cert' => $params['config']['mode'] == 'certificate' ? $params['config']['ali_public_cert'] : '', + 'ali_root_cert' => $params['config']['mode'] == 'certificate' ? $params['config']['ali_root_cert'] : '', + ]; + } + + $payConfig->name = $params['name']; + $payConfig->icon = FileService::setFileUrl($params['icon']); + $payConfig->sort = $params['sort']; + $payConfig->config = $config; + $payConfig->remark = $params['remark'] ?? ''; + return $payConfig->save(); + } + + + /** + * @notes 获取配置 + * @param $params + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2023/2/23 16:16 + */ + public static function getConfig($params) + { + $payConfig = PayConfig::find($params['id'])->toArray(); + $payConfig['icon'] = FileService::getFileUrl($payConfig['icon']); + $payConfig['domain'] = request()->domain(); + return $payConfig; + } + +} diff --git a/server/app/adminapi/logic/setting/pay/PayWayLogic.php b/server/app/adminapi/logic/setting/pay/PayWayLogic.php new file mode 100644 index 0000000..fe5bd2b --- /dev/null +++ b/server/app/adminapi/logic/setting/pay/PayWayLogic.php @@ -0,0 +1,111 @@ +append(['pay_way_name']) + ->toArray(); + + if (empty($payWay)) { + return []; + } + + $lists = []; + for ($i = 1; $i <= max(array_column($payWay, 'scene')); $i++) { + foreach ($payWay as $val) { + if ($val['scene'] == $i) { + $val['icon'] = FileService::getFileUrl(PayConfig::where('id', $val['pay_config_id'])->value('icon')); + $lists[$i][] = $val; + } + } + } + + return $lists; + } + + + /** + * @notes 设置支付方式 + * @param $params + * @return bool|string + * @throws \Exception + * @author 段誉 + * @date 2023/2/23 16:26 + */ + public static function setPayWay($params) + { + $payWay = new PayWay; + $data = []; + foreach ($params as $key => $value) { + $isDefault = array_column($value, 'is_default'); + $isDefaultNum = array_count_values($isDefault); + $status = array_column($value, 'status'); + $sceneName = PayEnum::getPaySceneDesc($key); + if (!in_array(YesNoEnum::YES, $isDefault)) { + return $sceneName . '支付场景缺少默认支付'; + } + if ($isDefaultNum[YesNoEnum::YES] > 1) { + return $sceneName . '支付场景的默认值只能存在一个'; + } + if (!in_array(YesNoEnum::YES, $status)) { + return $sceneName . '支付场景至少开启一个支付状态'; + } + + foreach ($value as $val) { + $result = PayWay::where('id', $val['id'])->findOrEmpty(); + if ($result->isEmpty()) { + continue; + } + if ($val['is_default'] == YesNoEnum::YES && $val['status'] == YesNoEnum::NO) { + return $sceneName . '支付场景的默认支付未开启支付状态'; + } + $data[] = [ + 'id' => $val['id'], + 'is_default' => $val['is_default'], + 'status' => $val['status'], + ]; + } + } + $payWay->saveAll($data); + return true; + } +} + diff --git a/server/app/adminapi/logic/setting/system/CacheLogic.php b/server/app/adminapi/logic/setting/system/CacheLogic.php new file mode 100644 index 0000000..294fd15 --- /dev/null +++ b/server/app/adminapi/logic/setting/system/CacheLogic.php @@ -0,0 +1,37 @@ +getRootPath().'runtime/file',true); + } +} \ No newline at end of file diff --git a/server/app/adminapi/logic/setting/system/SystemLogic.php b/server/app/adminapi/logic/setting/system/SystemLogic.php new file mode 100644 index 0000000..3efca96 --- /dev/null +++ b/server/app/adminapi/logic/setting/system/SystemLogic.php @@ -0,0 +1,65 @@ + '服务器操作系统', 'value' => PHP_OS], + ['param' => 'web服务器环境', 'value' => $_SERVER['SERVER_SOFTWARE']], + ['param' => 'PHP版本', 'value' => PHP_VERSION], + ]; + + $env = [ + [ 'option' => 'PHP版本', + 'require' => '8.0版本以上', + 'status' => (int)compare_php('8.0.0'), + 'remark' => '' + ] + ]; + + $auth = [ + [ + 'dir' => '/runtime', + 'require' => 'runtime目录可写', + 'status' => (int)check_dir_write('runtime'), + 'remark' => '' + ], + ]; + + return [ + 'server' => $server, + 'env' => $env, + 'auth' => $auth, + ]; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/setting/user/UserLogic.php b/server/app/adminapi/logic/setting/user/UserLogic.php new file mode 100644 index 0000000..ef85e2a --- /dev/null +++ b/server/app/adminapi/logic/setting/user/UserLogic.php @@ -0,0 +1,111 @@ + FileService::getFileUrl(ConfigService::get('default_image', 'user_avatar', $defaultAvatar)), + // 短信验证接收手机号 + 'sms_verify_phones' => ConfigService::get('sms_verify', 'phones', ''), + ]; + return $config; + } + + + /** + * @notes 设置用户设置 + * @param array $params + * @return bool + * @author 段誉 + * @date 2022/3/29 10:09 + */ + public function setConfig(array $params): bool + { + $avatar = FileService::setFileUrl($params['default_avatar']); + ConfigService::set('default_image', 'user_avatar', $avatar); + ConfigService::set('sms_verify', 'phones', $params['sms_verify_phones'] ?? ''); + return true; + } + + + /** + * @notes 获取注册配置 + * @return array + * @author 段誉 + * @date 2022/3/29 10:10 + */ + public function getRegisterConfig(): array + { + $config = [ + // 登录方式 + 'login_way' => ConfigService::get('login', 'login_way', config('project.login.login_way')), + // 注册强制绑定手机 + 'coerce_mobile' => ConfigService::get('login', 'coerce_mobile', config('project.login.coerce_mobile')), + // 政策协议 + 'login_agreement' => ConfigService::get('login', 'login_agreement', config('project.login.login_agreement')), + // 第三方登录 开关 + 'third_auth' => ConfigService::get('login', 'third_auth', config('project.login.third_auth')), + // 微信授权登录 + 'wechat_auth' => ConfigService::get('login', 'wechat_auth', config('project.login.wechat_auth')), + // qq授权登录 + 'qq_auth' => ConfigService::get('login', 'qq_auth', config('project.login.qq_auth')), + ]; + return $config; + } + + + /** + * @notes 设置登录注册 + * @param array $params + * @return bool + * @author 段誉 + * @date 2022/3/29 10:10 + */ + public static function setRegisterConfig(array $params): bool + { + // 登录方式:1-账号密码登录;2-手机短信验证码登录 + ConfigService::set('login', 'login_way', $params['login_way']); + // 注册强制绑定手机 + ConfigService::set('login', 'coerce_mobile', $params['coerce_mobile']); + // 政策协议 + ConfigService::set('login', 'login_agreement', $params['login_agreement']); + // 第三方授权登录 + ConfigService::set('login', 'third_auth', $params['third_auth']); + // 微信授权登录 + ConfigService::set('login', 'wechat_auth', $params['wechat_auth']); + // qq登录 + ConfigService::set('login', 'qq_auth', $params['qq_auth']); + return true; + } + +} diff --git a/server/app/adminapi/logic/setting/web/WebSettingLogic.php b/server/app/adminapi/logic/setting/web/WebSettingLogic.php new file mode 100644 index 0000000..673d294 --- /dev/null +++ b/server/app/adminapi/logic/setting/web/WebSettingLogic.php @@ -0,0 +1,194 @@ + ConfigService::get('website', 'name'), + 'web_favicon' => FileService::getFileUrl(ConfigService::get('website', 'web_favicon')), + 'web_logo' => FileService::getFileUrl(ConfigService::get('website', 'web_logo')), + 'login_image' => FileService::getFileUrl(ConfigService::get('website', 'login_image')), + 'shop_name' => ConfigService::get('website', 'shop_name'), + 'shop_logo' => FileService::getFileUrl(ConfigService::get('website', 'shop_logo')), + + 'pc_logo' => FileService::getFileUrl(ConfigService::get('website', 'pc_logo')), + 'pc_title' => ConfigService::get('website', 'pc_title', ''), + 'pc_ico' => FileService::getFileUrl(ConfigService::get('website', 'pc_ico')), + 'pc_desc' => ConfigService::get('website', 'pc_desc', ''), + 'pc_keywords' => ConfigService::get('website', 'pc_keywords', ''), + + 'h5_favicon' => FileService::getFileUrl(ConfigService::get('website', 'h5_favicon')), + + 'share_poster' => FileService::getFileUrl(ConfigService::get('website', 'share_poster', '')), + 'share_poster_title' => ConfigService::get('website', 'share_poster_title', ''), + ]; + } + + + /** + * @notes 设置网站信息 + * @param array $params + * @author 段誉 + * @date 2021/12/28 15:43 + */ + public static function setWebsiteInfo(array $params) + { + $h5favicon = FileService::setFileUrl($params['h5_favicon']); + $favicon = FileService::setFileUrl($params['web_favicon']); + $logo = FileService::setFileUrl($params['web_logo']); + $login = FileService::setFileUrl($params['login_image']); + $shopLogo = FileService::setFileUrl($params['shop_logo']); + $pcLogo = FileService::setFileUrl($params['pc_logo']); + $pcIco = FileService::setFileUrl($params['pc_ico'] ?? ''); + + ConfigService::set('website', 'name', $params['name']); + ConfigService::set('website', 'web_favicon', $favicon); + ConfigService::set('website', 'web_logo', $logo); + ConfigService::set('website', 'login_image', $login); + ConfigService::set('website', 'shop_name', $params['shop_name']); + ConfigService::set('website', 'shop_logo', $shopLogo); + ConfigService::set('website', 'pc_logo', $pcLogo); + + ConfigService::set('website', 'pc_title', $params['pc_title']); + ConfigService::set('website', 'pc_ico', $pcIco); + ConfigService::set('website', 'pc_desc', $params['pc_desc'] ?? ''); + ConfigService::set('website', 'pc_keywords', $params['pc_keywords'] ?? ''); + + ConfigService::set('website', 'h5_favicon', $h5favicon); + + $sharePoster = FileService::setFileUrl($params['share_poster'] ?? ''); + ConfigService::set('website', 'share_poster', $sharePoster); + ConfigService::set('website', 'share_poster_title', $params['share_poster_title'] ?? ''); + } + + + /** + * @notes 获取版权备案 + * @return array + * @author 段誉 + * @date 2021/12/28 16:09 + */ + public static function getCopyright(): array + { + return ConfigService::get('copyright', 'config', []); + } + + + /** + * @notes 设置版权备案 + * @param array $params + * @return bool + * @author 段誉 + * @date 2022/8/8 16:33 + */ + public static function setCopyright(array $params) + { + try { + if (!is_array($params['config'])) { + throw new \Exception('参数异常'); + } + ConfigService::set('copyright', 'config', $params['config'] ?? []); + return true; + } catch (\Exception $e) { + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 设置政策协议 + * @param array $params + * @author ljj + * @date 2022/2/15 10:59 上午 + */ + public static function setAgreement(array $params) + { + $serviceContent = clear_file_domain($params['service_content'] ?? ''); + $privacyContent = clear_file_domain($params['privacy_content'] ?? ''); + ConfigService::set('agreement', 'service_title', $params['service_title'] ?? ''); + ConfigService::set('agreement', 'service_content', $serviceContent); + ConfigService::set('agreement', 'privacy_title', $params['privacy_title'] ?? ''); + ConfigService::set('agreement', 'privacy_content', $privacyContent); + } + + + /** + * @notes 获取政策协议 + * @return array + * @author ljj + * @date 2022/2/15 11:15 上午 + */ + public static function getAgreement(): array + { + $config = [ + 'service_title' => ConfigService::get('agreement', 'service_title'), + 'service_content' => ConfigService::get('agreement', 'service_content'), + 'privacy_title' => ConfigService::get('agreement', 'privacy_title'), + 'privacy_content' => ConfigService::get('agreement', 'privacy_content'), + ]; + + $config['service_content'] = get_file_domain($config['service_content']); + $config['privacy_content'] = get_file_domain($config['privacy_content']); + + return $config; + } + + /** + * @notes 获取站点统计配置 + * @return array + * @author yfdong + * @date 2024/09/20 22:25 + */ + public static function getSiteStatistics() + { + return [ + 'clarity_code' => ConfigService::get('siteStatistics', 'clarity_code') + ]; + } + + /** + * @notes 设置站点统计配置 + * @param array $params + * @return void + * @author yfdong + * @date 2024/09/20 22:31 + */ + public static function setSiteStatistics(array $params) + { + ConfigService::set('siteStatistics', 'clarity_code', $params['clarity_code']); + } +} diff --git a/server/app/adminapi/logic/tools/GeneratorLogic.php b/server/app/adminapi/logic/tools/GeneratorLogic.php new file mode 100644 index 0000000..f97da98 --- /dev/null +++ b/server/app/adminapi/logic/tools/GeneratorLogic.php @@ -0,0 +1,505 @@ +findOrEmpty((int)$params['id']) + ->toArray(); + + $options = self::formatConfigByTableData($detail); + $detail['menu'] = $options['menu']; + $detail['delete'] = $options['delete']; + $detail['tree'] = $options['tree']; + $detail['relations'] = $options['relations']; + return $detail; + } + + + /** + * @notes 选择数据表 + * @param $params + * @param $adminId + * @return bool + * @author 段誉 + * @date 2022/6/20 10:44 + */ + public static function selectTable($params, $adminId) + { + Db::startTrans(); + try { + foreach ($params['table'] as $item) { + // 添加主表基础信息 + $generateTable = self::initTable($item, $adminId); + // 获取数据表字段信息 + $column = self::getTableColumn($item['name']); + // 添加表字段信息 + self::initTableColumn($column, $generateTable['id']); + } + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 编辑表信息 + * @param $params + * @return bool + * @author 段誉 + * @date 2022/6/20 10:44 + */ + public static function editTable($params) + { + Db::startTrans(); + try { + // 格式化配置 + $options = self::formatConfigByTableData($params); + // 更新主表-数据表信息 + GenerateTable::update([ + 'id' => $params['id'], + 'table_name' => $params['table_name'], + 'table_comment' => $params['table_comment'], + 'template_type' => $params['template_type'], + 'author' => $params['author'] ?? '', + 'remark' => $params['remark'] ?? '', + 'generate_type' => $params['generate_type'], + 'module_name' => $params['module_name'], + 'class_dir' => $params['class_dir'] ?? '', + 'class_comment' => $params['class_comment'] ?? '', + 'menu' => $options['menu'], + 'delete' => $options['delete'], + 'tree' => $options['tree'], + 'relations' => $options['relations'], + ]); + + // 更新从表-数据表字段信息 + foreach ($params['table_column'] as $item) { + GenerateColumn::update([ + 'id' => $item['id'], + 'column_comment' => $item['column_comment'] ?? '', + 'is_required' => $item['is_required'] ?? 0, + 'is_insert' => $item['is_insert'] ?? 0, + 'is_update' => $item['is_update'] ?? 0, + 'is_lists' => $item['is_lists'] ?? 0, + 'is_query' => $item['is_query'] ?? 0, + 'query_type' => $item['query_type'], + 'view_type' => $item['view_type'], + 'dict_type' => $item['dict_type'] ?? '', + ]); + } + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 删除表相关信息 + * @param $params + * @return bool + * @author 段誉 + * @date 2022/6/16 9:30 + */ + public static function deleteTable($params) + { + Db::startTrans(); + try { + GenerateTable::whereIn('id', $params['id'])->delete(); + GenerateColumn::whereIn('table_id', $params['id'])->delete(); + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 同步表字段 + * @param $params + * @return bool + * @author 段誉 + * @date 2022/6/23 16:28 + */ + public static function syncColumn($params) + { + Db::startTrans(); + try { + // table 信息 + $table = GenerateTable::findOrEmpty($params['id']); + // 删除旧字段 + GenerateColumn::whereIn('table_id', $table['id'])->delete(); + // 获取当前数据表字段信息 + $column = self::getTableColumn($table['table_name']); + // 创建新字段数据 + self::initTableColumn($column, $table['id']); + + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 生成代码 + * @param $params + * @return false|int[] + * @author 段誉 + * @date 2022/6/24 9:43 + */ + public static function generate($params) + { + try { + // 获取数据表信息 + $tables = GenerateTable::with(['table_column']) + ->whereIn('id', $params['id']) + ->select()->toArray(); + + $generator = app()->make(GenerateService::class); + $generator->delGenerateDirContent(); + $flag = array_unique(array_column($tables, 'table_name')); + $flag = implode(',', $flag); + $generator->setGenerateFlag(md5($flag . time()), false); + + // 循环生成 + foreach ($tables as $table) { + $generator->generate($table); + } + + $zipFile = ''; + // 生成压缩包 + if ($generator->getGenerateFlag()) { + $generator->zipFile(); + $generator->delGenerateFlag(); + $zipFile = $generator->getDownloadUrl(); + } + + return ['file' => $zipFile]; + + } catch (\Exception $e) { + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 预览 + * @param $params + * @return false + * @author 段誉 + * @date 2022/6/23 16:27 + */ + public static function preview($params) + { + try { + // 获取数据表信息 + $table = GenerateTable::with(['table_column']) + ->whereIn('id', $params['id']) + ->findOrEmpty()->toArray(); + + return app()->make(GenerateService::class)->preview($table); + + } catch (\Exception $e) { + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 获取表字段信息 + * @param $tableName + * @return array + * @author 段誉 + * @date 2022/6/23 16:28 + */ + public static function getTableColumn($tableName) + { + $tableName = get_no_prefix_table_name($tableName); + return Db::name($tableName)->getFields(); + } + + + /** + * @notes 初始化代码生成数据表信息 + * @param $tableData + * @param $adminId + * @return GenerateTable|\think\Model + * @author 段誉 + * @date 2022/6/23 16:28 + */ + public static function initTable($tableData, $adminId) + { + return GenerateTable::create([ + 'table_name' => $tableData['name'], + 'table_comment' => $tableData['comment'], + 'template_type' => GeneratorEnum::TEMPLATE_TYPE_SINGLE, + 'generate_type' => GeneratorEnum::GENERATE_TYPE_ZIP, + 'module_name' => 'adminapi', + 'admin_id' => $adminId, + // 菜单配置 + 'menu' => [ + 'pid' => 0, // 父级菜单id + 'type' => GeneratorEnum::GEN_SELF, // 构建方式 0-手动添加 1-自动构建 + 'name' => $tableData['comment'], // 菜单名称 + ], + // 删除配置 + 'delete' => [ + 'type' => GeneratorEnum::DELETE_TRUE, // 删除类型 + 'name' => GeneratorEnum::DELETE_NAME, // 默认删除字段名 + ], + // 关联配置 + 'relations' => [], + // 树形crud + 'tree' => [] + ]); + } + + + /** + * @notes 初始化代码生成字段信息 + * @param $column + * @param $tableId + * @throws \Exception + * @author 段誉 + * @date 2022/6/23 16:28 + */ + public static function initTableColumn($column, $tableId) + { + $defaultColumn = ['id', 'create_time', 'update_time', 'delete_time']; + + $insertColumn = []; + foreach ($column as $value) { + $required = 0; + if ($value['notnull'] && !$value['primary'] && !in_array($value['name'], $defaultColumn)) { + $required = 1; + } + + $columnData = [ + 'table_id' => $tableId, + 'column_name' => $value['name'], + 'column_comment' => $value['comment'], + 'column_type' => self::getDbFieldType($value['type']), + 'is_required' => $required, + 'is_pk' => $value['primary'] ? 1 : 0, + ]; + + if (!in_array($value['name'], $defaultColumn)) { + $columnData['is_insert'] = 1; + $columnData['is_update'] = 1; + $columnData['is_lists'] = 1; + $columnData['is_query'] = 1; + } + $insertColumn[] = $columnData; + } + + (new GenerateColumn())->saveAll($insertColumn); + } + + + /** + * @notes 下载文件 + * @param $fileName + * @return false|string + * @author 段誉 + * @date 2022/6/24 9:51 + */ + public static function download(string $fileName) + { + $cacheFileName = cache('curd_file_name' . $fileName); + if (empty($cacheFileName)) { + self::$error = '请重新生成代码'; + return false; + } + + $path = root_path() . 'runtime/generate/' . $fileName; + if (!file_exists($path)) { + self::$error = '下载失败'; + return false; + } + + cache('curd_file_name' . $fileName, null); + return $path; + } + + + /** + * @notes 获取数据表字段类型 + * @param string $type + * @return string + * @author 段誉 + * @date 2022/6/15 10:11 + */ + public static function getDbFieldType(string $type): string + { + if (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) { + $result = 'string'; + } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) { + $result = 'float'; + } elseif (preg_match('/(int|serial|bit)/is', $type)) { + $result = 'int'; + } elseif (preg_match('/bool/is', $type)) { + $result = 'bool'; + } elseif (0 === strpos($type, 'timestamp')) { + $result = 'timestamp'; + } elseif (0 === strpos($type, 'datetime')) { + $result = 'datetime'; + } elseif (0 === strpos($type, 'date')) { + $result = 'date'; + } else { + $result = 'string'; + } + return $result; + } + + + /** + * @notes + * @param $options + * @param $tableComment + * @return array + * @author 段誉 + * @date 2022/12/13 18:23 + */ + public static function formatConfigByTableData($options) + { + // 菜单配置 + $menuConfig = $options['menu'] ?? []; + // 删除配置 + $deleteConfig = $options['delete'] ?? []; + // 关联配置 + $relationsConfig = $options['relations'] ?? []; + // 树表crud配置 + $treeConfig = $options['tree'] ?? []; + + $relations = []; + foreach ($relationsConfig as $relation) { + $relations[] = [ + 'name' => $relation['name'] ?? '', + 'model' => $relation['model'] ?? '', + 'type' => $relation['type'] ?? GeneratorEnum::RELATION_HAS_ONE, + 'local_key' => $relation['local_key'] ?? 'id', + 'foreign_key' => $relation['foreign_key'] ?? 'id', + ]; + } + + $options['menu'] = [ + 'pid' => intval($menuConfig['pid'] ?? 0), + 'type' => intval($menuConfig['type'] ?? GeneratorEnum::GEN_SELF), + 'name' => !empty($menuConfig['name']) ? $menuConfig['name'] : $options['table_comment'], + ]; + $options['delete'] = [ + 'type' => intval($deleteConfig['type'] ?? GeneratorEnum::DELETE_TRUE), + 'name' => !empty($deleteConfig['name']) ? $deleteConfig['name'] : GeneratorEnum::DELETE_NAME, + ]; + $options['relations'] = $relations; + $options['tree'] = [ + 'tree_id' => $treeConfig['tree_id'] ?? "", + 'tree_pid' =>$treeConfig['tree_pid'] ?? "", + 'tree_name' => $treeConfig['tree_name'] ?? '', + ]; + + return $options; + } + + + /** + * @notes 获取所有模型 + * @param string $module + * @return array + * @author 段誉 + * @date 2022/12/14 11:04 + */ + public static function getAllModels($module = 'common') + { + if(empty($module)) { + return []; + } + $modulePath = base_path() . $module . '/model/'; + if(!is_dir($modulePath)) { + return []; + } + + $modulefiles = glob($modulePath . '*'); + $targetFiles = []; + foreach ($modulefiles as $file) { + $fileBaseName = basename($file, '.php'); + if (is_dir($file)) { + $file = glob($file . '/*'); + foreach ($file as $item) { + if (is_dir($item)) { + continue; + } + $targetFiles[] = sprintf( + "\\app\\" . $module . "\\model\\%s\\%s", + $fileBaseName, + basename($item, '.php') + ); + } + } else { + if ($fileBaseName == 'BaseModel') { + continue; + } + $targetFiles[] = sprintf( + "\\app\\" . $module . "\\model\\%s", + basename($file, '.php') + ); + } + } + + return $targetFiles; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/logic/user/UserLogic.php b/server/app/adminapi/logic/user/UserLogic.php new file mode 100644 index 0000000..ba0efe1 --- /dev/null +++ b/server/app/adminapi/logic/user/UserLogic.php @@ -0,0 +1,190 @@ + $userId])->field($field) + ->findOrEmpty(); + + $user['channel'] = UserTerminalEnum::getTermInalDesc($user['channel']); + $user->sex = $user->getData('sex'); + return $user->toArray(); + } + + + /** + * @notes 更新用户信息 + * @param array $params + * @return User + * @author 段誉 + * @date 2022/9/22 16:38 + */ + public static function setUserInfo(array $params) + { + return User::update([ + 'id' => $params['id'], + $params['field'] => $params['value'] + ]); + } + + + /** + * @notes 调整用户余额 + * @param array $params + * @return bool|string + * @author 段誉 + * @date 2023/2/23 14:25 + */ + public static function adjustUserMoney(array $params) + { + Db::startTrans(); + try { + $user = User::find($params['user_id']); + if (AccountLogEnum::INC == $params['action']) { + //调整可用余额 + $user->user_money += $params['num']; + $user->save(); + //记录日志 + AccountLogLogic::add( + $user->id, + AccountLogEnum::UM_INC_ADMIN, + AccountLogEnum::INC, + $params['num'], + '', + $params['remark'] ?? '' + ); + } else { + $user->user_money -= $params['num']; + $user->save(); + //记录日志 + AccountLogLogic::add( + $user->id, + AccountLogEnum::UM_DEC_ADMIN, + AccountLogEnum::DEC, + $params['num'], + '', + $params['remark'] ?? '' + ); + } + + Db::commit(); + return true; + + } catch (\Exception $e) { + Db::rollback(); + return $e->getMessage(); + } + } + + + /** + * @notes 调整用户账户(余额/积分) + * @param array $params + * @return bool|string + */ + public static function adjustUserAccount(array $params) + { + Db::startTrans(); + try { + $user = User::find($params['user_id']); + if (empty($user)) { + throw new \Exception('用户不存在'); + } + + $type = $params['type']; // money-余额 points-积分 + $action = $params['action']; + $num = $params['num']; + $remark = $params['remark'] ?? ''; + + if ($type == 'money') { + if ($action == AccountLogEnum::INC) { + $user->user_money += $num; + $changeType = AccountLogEnum::UM_INC_ADMIN; + } else { + if ($user->user_money < $num) { + throw new \Exception('用户可用余额仅剩' . $user->user_money); + } + $user->user_money -= $num; + $changeType = AccountLogEnum::UM_DEC_ADMIN; + } + } else { + if ($action == AccountLogEnum::INC) { + $user->user_points += $num; + $changeType = AccountLogEnum::UP_INC_ADMIN; + } else { + if ($user->user_points < $num) { + throw new \Exception('用户可用积分仅剩' . $user->user_points); + } + $user->user_points -= $num; + $changeType = AccountLogEnum::UP_DEC_ADMIN; + } + } + + $user->save(); + AccountLogLogic::add( + $user->id, + $changeType, + $action, + $num, + '', + $remark + ); + + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + return $e->getMessage(); + } + } + +} diff --git a/server/app/adminapi/logic/vip/VipLevelLogic.php b/server/app/adminapi/logic/vip/VipLevelLogic.php new file mode 100644 index 0000000..1ddf2d6 --- /dev/null +++ b/server/app/adminapi/logic/vip/VipLevelLogic.php @@ -0,0 +1,50 @@ + $params['name'], + 'level' => $params['level'], + 'image' => $params['image'] ?? '', + 'price' => $params['price'], + 'duration' => $params['duration'], + 'description' => $params['description'] ?? '', + 'is_recommend' => $params['is_recommend'] ?? 0, + 'sort' => $params['sort'] ?? 0, + 'status' => $params['status'] ?? 1, + ]); + } + + public static function edit(array $params) + { + VipLevel::update([ + 'id' => $params['id'], + 'name' => $params['name'], + 'level' => $params['level'], + 'image' => $params['image'] ?? '', + 'price' => $params['price'], + 'duration' => $params['duration'], + 'description' => $params['description'] ?? '', + 'is_recommend' => $params['is_recommend'] ?? 0, + 'sort' => $params['sort'] ?? 0, + 'status' => $params['status'] ?? 1, + ]); + } + + public static function delete(array $params) + { + VipLevel::destroy($params['id']); + } + + public static function detail(array $params) + { + return VipLevel::findOrEmpty($params['id'])->toArray(); + } +} diff --git a/server/app/adminapi/service/AdminTokenService.php b/server/app/adminapi/service/AdminTokenService.php new file mode 100644 index 0000000..df7fd4e --- /dev/null +++ b/server/app/adminapi/service/AdminTokenService.php @@ -0,0 +1,132 @@ +find(); + + //获取token延长过期的时间 + $expireTime = $time + Config::get('project.admin_token.expire_duration'); + $adminTokenCache = new AdminTokenCache(); + + //token处理 + if ($adminSession) { + if ($adminSession->expire_time < $time || $multipointLogin === 0) { + //清空缓存 + $adminTokenCache->deleteAdminInfo($adminSession->token); + //如果token过期或账号设置不支持多处登录,更新token + $adminSession->token = create_token($adminId); + } + $adminSession->expire_time = $expireTime; + $adminSession->update_time = $time; + + $adminSession->save(); + } else { + //找不到在该终端的token记录,创建token记录 + $adminSession = AdminSession::create([ + 'admin_id' => $adminId, + 'terminal' => $terminal, + 'token' => create_token($adminId), + 'expire_time' => $expireTime + ]); + } + + return $adminTokenCache->setAdminInfo($adminSession->token); + } + + /** + * @notes 延长token过期时间 + * @param $token + * @return array|false|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 令狐冲 + * @date 2021/7/5 14:25 + */ + public static function overtimeToken($token) + { + $time = time(); + $adminSession = AdminSession::where('token', '=', $token)->findOrEmpty(); + if ($adminSession->isEmpty()) { + return false; + } + //延长token过期时间 + $adminSession->expire_time = $time + Config::get('project.admin_token.expire_duration'); + $adminSession->update_time = $time; + $adminSession->save(); + return (new AdminTokenCache())->setAdminInfo($adminSession->token); + } + + /** + * @notes 设置token为过期 + * @param $token + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 令狐冲 + * @date 2021/7/5 14:31 + */ + public static function expireToken($token) + { + $adminSession = AdminSession::where('token', '=', $token) + ->with('admin') + ->findOrEmpty(); + + if ($adminSession->isEmpty()) { + return false; + } + + //当支持多处登录的时候,服务端不注销 + if ($adminSession->admin->multipoint_login === 1) { + return false; + } + + $time = time(); + $adminSession->expire_time = $time; + $adminSession->update_time = $time; + $adminSession->save(); + + return (new AdminTokenCache())->deleteAdminInfo($token); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/FileValidate.php b/server/app/adminapi/validate/FileValidate.php new file mode 100644 index 0000000..062d748 --- /dev/null +++ b/server/app/adminapi/validate/FileValidate.php @@ -0,0 +1,117 @@ + 'require|number', + 'cid' => 'require|number', + 'ids' => 'require|array', + 'type' => 'require|in:10,20,30', + 'pid' => 'require|number', + 'name' => 'require|max:20' + ]; + + protected $message = [ + 'id.require' => '缺少id参数', + 'cid.require' => '缺少cid参数', + 'ids.require' => '缺少ids参数', + 'type.require' => '缺少type参数', + 'pid.require' => '缺少pid参数', + 'name.require' => '请填写分组名称', + 'name.max' => '分组名称长度须为20字符内', + ]; + + + /** + * @notes id验证场景 + * @return FileValidate + * @author 段誉 + * @date 2021/12/29 14:32 + */ + public function sceneId() + { + return $this->only(['id']); + } + + + /** + * @notes 重命名文件场景 + * @return FileValidate + * @author 段誉 + * @date 2021/12/29 14:32 + */ + public function sceneRename() + { + return $this->only(['id', 'name']); + } + + + /** + * @notes 新增分类场景 + * @return FileValidate + * @author 段誉 + * @date 2021/12/29 14:33 + */ + public function sceneAddCate() + { + return $this->only(['type', 'pid', 'name']); + } + + + /** + * @notes 编辑分类场景 + * @return FileValidate + * @author 段誉 + * @date 2021/12/29 14:33 + */ + public function sceneEditCate() + { + return $this->only(['id', 'name']); + } + + + /** + * @notes 移动场景 + * @return FileValidate + * @author 段誉 + * @date 2021/12/29 14:33 + */ + public function sceneMove() + { + return $this->only(['ids', 'cid']); + } + + + /** + * @notes 删除场景 + * @return FileValidate + * @author 段誉 + * @date 2021/12/29 14:35 + */ + public function sceneDelete() + { + return $this->only(['ids']); + } +} \ No newline at end of file diff --git a/server/app/adminapi/validate/LoginValidate.php b/server/app/adminapi/validate/LoginValidate.php new file mode 100644 index 0000000..bfdd86e --- /dev/null +++ b/server/app/adminapi/validate/LoginValidate.php @@ -0,0 +1,103 @@ + 'require|in:' . AdminTerminalEnum::PC . ',' . AdminTerminalEnum::MOBILE, + 'account' => 'require', + 'password' => 'require|password', + ]; + + protected $message = [ + 'account.require' => '请输入账号', + 'password.require' => '请输入密码' + ]; + + /** + * @notes @notes 密码验证 + * @param $password + * @param $other + * @param $data + * @return bool|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 令狐冲 + * @date 2021/7/2 14:00 + */ + public function password($password, $other, $data) + { + // 登录限制 + $config = [ + 'login_restrictions' => ConfigService::get('admin_login', 'login_restrictions'), + 'password_error_times' => ConfigService::get('admin_login', 'password_error_times'), + 'limit_login_time' => ConfigService::get('admin_login', 'limit_login_time'), + ]; + + $adminAccountSafeCache = new AdminAccountSafeCache(); + if ($config['login_restrictions'] == 1) { + $adminAccountSafeCache->count = $config['password_error_times']; + $adminAccountSafeCache->minute = $config['limit_login_time']; + } + + //后台账号安全机制,连续输错后锁定,防止账号密码暴力破解 + if ($config['login_restrictions'] == 1 && !$adminAccountSafeCache->isSafe()) { + return '密码连续' . $adminAccountSafeCache->count . '次输入错误,请' . $adminAccountSafeCache->minute . '分钟后重试'; + } + + $adminInfo = Admin::where('account', '=', $data['account']) + ->field(['password,disable']) + ->findOrEmpty(); + + if ($adminInfo->isEmpty()) { + return '账号不存在'; + } + + if ($adminInfo['disable'] === 1) { + return '账号已禁用'; + } + + if (empty($adminInfo['password'])) { + $adminAccountSafeCache->record(); + return '账号不存在'; + } + + $passwordSalt = Config::get('project.unique_identification'); + if ($adminInfo['password'] !== create_password($password, $passwordSalt)) { + $adminAccountSafeCache->record(); + return '密码错误'; + } + + $adminAccountSafeCache->relieve(); + return true; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/ai/AiConfigValidate.php b/server/app/adminapi/validate/ai/AiConfigValidate.php new file mode 100644 index 0000000..541ea0f --- /dev/null +++ b/server/app/adminapi/validate/ai/AiConfigValidate.php @@ -0,0 +1,27 @@ + 'require', + 'name' => 'require', + ]; + + protected $message = [ + 'id.require' => '配置id不能为空', + 'name.require' => '配置名称不能为空', + ]; + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneEdit() + { + } +} diff --git a/server/app/adminapi/validate/ai/KbConfigValidate.php b/server/app/adminapi/validate/ai/KbConfigValidate.php new file mode 100644 index 0000000..2c093c9 --- /dev/null +++ b/server/app/adminapi/validate/ai/KbConfigValidate.php @@ -0,0 +1,23 @@ + 'require', + 'name' => 'require', + ]; + + protected $message = [ + 'id.require' => '配置id不能为空', + 'name.require' => '配置名称不能为空', + ]; + + public function sceneDetail() + { + return $this->only(['id']); + } +} diff --git a/server/app/adminapi/validate/ai/KbSyncJobValidate.php b/server/app/adminapi/validate/ai/KbSyncJobValidate.php new file mode 100644 index 0000000..e0b50ae --- /dev/null +++ b/server/app/adminapi/validate/ai/KbSyncJobValidate.php @@ -0,0 +1,17 @@ + 'require|in:article,post,lottery,all', + ]; + + protected $message = [ + 'domain.require' => '重建域不能为空', + 'domain.in' => '重建域不合法', + ]; +} diff --git a/server/app/adminapi/validate/app/AppVersionValidate.php b/server/app/adminapi/validate/app/AppVersionValidate.php new file mode 100644 index 0000000..230db9b --- /dev/null +++ b/server/app/adminapi/validate/app/AppVersionValidate.php @@ -0,0 +1,78 @@ + 'require|checkVersion', + 'version_name' => 'require|length:1,32', + 'version_code' => 'require|integer|gt:0', + 'platform' => 'require|in:1,2', + 'package_type' => 'require|in:1,2,3', + 'update_type' => 'require|in:1,2,3', + 'download_url' => 'require|length:1,500', + 'title' => 'require|length:1,100', + 'update_content' => 'require', + 'status' => 'require|in:0,1', + ]; + + protected $message = [ + 'id.require' => '版本ID不能为空', + 'version_name.require' => '请输入版本号', + 'version_code.require' => '请输入版本数字', + 'version_code.gt' => '版本数字必须大于0', + 'platform.require' => '请选择平台', + 'package_type.require' => '请选择包类型', + 'update_type.require' => '请选择升级类型', + 'download_url.require' => '请上传或填写下载地址', + 'title.require' => '请输入更新标题', + 'update_content.require' => '请输入更新内容', + 'status.require' => '请选择状态', + ]; + + public function sceneAdd() + { + return $this->remove('id', 'require|checkVersion'); + } + + public function sceneEdit() + { + return $this; + } + + public function sceneDelete() + { + return $this->only(['id']); + } + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneStatus() + { + return $this->only(['id', 'status']); + } + + /** + * 校验版本是否存在 + */ + public function checkVersion($value): bool|string + { + $row = AppVersion::findOrEmpty($value); + if ($row->isEmpty()) { + return '版本不存在'; + } + return true; + } +} diff --git a/server/app/adminapi/validate/article/ArticleAiCommentTaskValidate.php b/server/app/adminapi/validate/article/ArticleAiCommentTaskValidate.php new file mode 100644 index 0000000..1f4323a --- /dev/null +++ b/server/app/adminapi/validate/article/ArticleAiCommentTaskValidate.php @@ -0,0 +1,36 @@ + 'require|checkTask', + ]; + + protected $message = [ + 'id.require' => '任务id不能为空', + ]; + + public function sceneDelete() + { + return $this->only(['id']); + } + + public function sceneRetry() + { + return $this->only(['id']); + } + + public function checkTask($value) + { + $task = ArticleAiCommentTask::findOrEmpty($value); + if ($task->isEmpty()) { + return '任务不存在'; + } + return true; + } +} diff --git a/server/app/adminapi/validate/article/ArticleCateValidate.php b/server/app/adminapi/validate/article/ArticleCateValidate.php new file mode 100644 index 0000000..90c3f30 --- /dev/null +++ b/server/app/adminapi/validate/article/ArticleCateValidate.php @@ -0,0 +1,136 @@ + 'require|checkArticleCate', + 'name' => 'require|length:1,90', + 'is_show' => 'require|in:0,1', + 'sort' => 'egt:0', + ]; + + protected $message = [ + 'id.require' => '资讯分类id不能为空', + 'name.require' => '资讯分类不能为空', + 'name.length' => '资讯分类长度须在1-90位字符', + 'sort.egt' => '排序值不正确', + ]; + + /** + * @notes 添加场景 + * @return ArticleCateValidate + * @author heshihu + * @date 2022/2/10 15:11 + */ + public function sceneAdd() + { + return $this->remove(['id']) + ->remove('id', 'require|checkArticleCate'); + } + + /** + * @notes 详情场景 + * @return ArticleCateValidate + * @author heshihu + * @date 2022/2/21 17:55 + */ + public function sceneDetail() + { + return $this->only(['id']); + } + + /** + * @notes 更改状态场景 + * @return ArticleCateValidate + * @author heshihu + * @date 2022/2/21 18:02 + */ + public function sceneStatus() + { + return $this->only(['id', 'is_show']); + } + + public function sceneEdit() + { + } + + /** + * @notes 获取所有资讯分类场景 + * @return ArticleCateValidate + * @author heshihu + * @date 2022/2/15 10:05 + */ + public function sceneSelect() + { + return $this->only(['type']); + } + + + /** + * @notes 删除场景 + * @return ArticleCateValidate + * @author heshihu + * @date 2022/2/21 17:52 + */ + public function sceneDelete() + { + return $this->only(['id']) + ->append('id', 'checkDeleteArticleCate'); + } + + /** + * @notes 检查指定资讯分类是否存在 + * @param $value + * @return bool|string + * @author heshihu + * @date 2022/2/10 15:10 + */ + public function checkArticleCate($value) + { + $article_category = ArticleCate::findOrEmpty($value); + if ($article_category->isEmpty()) { + return '资讯分类不存在'; + } + return true; + } + + /** + * @notes 删除时验证该资讯分类是否已使用 + * @param $value + * @return bool|string + * @author heshihu + * @date 2022/2/22 14:45 + */ + public function checkDeleteArticleCate($value) + { + $article = Article::where('cid', $value)->findOrEmpty(); + if (!$article->isEmpty()) { + return '资讯分类已使用,请先删除绑定该资讯分类的资讯'; + } + return true; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/article/ArticleValidate.php b/server/app/adminapi/validate/article/ArticleValidate.php new file mode 100644 index 0000000..4ddcd9e --- /dev/null +++ b/server/app/adminapi/validate/article/ArticleValidate.php @@ -0,0 +1,117 @@ + 'require|checkArticle', + 'title' => 'require|length:1,255', + // 'image' => 'require', + 'cid' => 'require', + 'is_show' => 'require|in:0,1', + 'is_recommend' => 'in:0,1', + 'ids' => 'require|array', + ]; + + protected $message = [ + 'id.require' => '资讯id不能为空', + 'title.require' => '标题不能为空', + 'title.length' => '标题长度须在1-255位字符', + // 'image.require' => '封面图必须存在', + 'cid.require' => '所属栏目必须存在', + ]; + + /** + * @notes 添加场景 + * @return ArticleValidate + * @author heshihu + * @date 2022/2/22 9:57 + */ + public function sceneAdd() + { + return $this->remove(['id', 'ids']) + ->remove('id', 'require|checkArticle'); + } + + /** + * @notes 详情场景 + * @return ArticleValidate + * @author heshihu + * @date 2022/2/22 10:15 + */ + public function sceneDetail() + { + return $this->only(['id']); + } + + /** + * @notes 更改状态场景 + * @return ArticleValidate + * @author heshihu + * @date 2022/2/22 10:18 + */ + public function sceneStatus() + { + return $this->only(['id', 'is_show']); + } + + public function sceneEdit() + { + return $this->remove('ids', 'require|array'); + } + + /** + * @notes 删除场景 + * @return ArticleValidate + * @author heshihu + * @date 2022/2/22 10:17 + */ + public function sceneDelete() + { + return $this->only(['id']); + } + + public function sceneBatchRecommend() + { + return $this->only(['ids', 'is_recommend']) + ->append('is_recommend', 'require'); + } + + /** + * @notes 检查指定资讯是否存在 + * @param $value + * @return bool|string + * @author heshihu + * @date 2022/2/22 10:11 + */ + public function checkArticle($value) + { + $article = Article::findOrEmpty($value); + if ($article->isEmpty()) { + return '资讯不存在'; + } + return true; + } + +} diff --git a/server/app/adminapi/validate/auth/AdminValidate.php b/server/app/adminapi/validate/auth/AdminValidate.php new file mode 100644 index 0000000..53b80e6 --- /dev/null +++ b/server/app/adminapi/validate/auth/AdminValidate.php @@ -0,0 +1,197 @@ + 'require|checkAdmin', + 'account' => 'require|length:1,32|unique:'.Admin::class, + 'name' => 'require|length:1,16|unique:'.Admin::class, + 'password' => 'require|length:6,32|edit', + 'password_confirm' => 'requireWith:password|confirm', + 'role_id' => 'require', + 'disable' => 'require|in:0,1|checkAbleDisable', + 'multipoint_login' => 'require|in:0,1', + ]; + + protected $message = [ + 'id.require' => '管理员id不能为空', + 'account.require' => '账号不能为空', + 'account.length' => '账号长度须在1-32位字符', + 'account.unique' => '账号已存在', + 'password.require' => '密码不能为空', + 'password.length' => '密码长度须在6-32位字符', + 'password_confirm.requireWith' => '确认密码不能为空', + 'password_confirm.confirm' => '两次输入的密码不一致', + 'name.require' => '名称不能为空', + 'name.length' => '名称须在1-16位字符', + 'name.unique' => '名称已存在', + 'role_id.require' => '请选择角色', + 'disable.require' => '请选择状态', + 'disable.in' => '状态值错误', + 'multipoint_login.require' => '请选择是否支持多处登录', + 'multipoint_login.in' => '多处登录状态值为误', + ]; + + /** + * @notes 添加场景 + * @return AdminValidate + * @author 段誉 + * @date 2021/12/29 15:46 + */ + public function sceneAdd() + { + return $this->remove(['password', 'edit']) + ->remove('id', true) + ->remove('disable', true); + } + + /** + * @notes 详情场景 + * @return AdminValidate + * @author 段誉 + * @date 2021/12/29 15:46 + */ + public function sceneDetail() + { + return $this->only(['id']); + } + + /** + * @notes 编辑场景 + * @return AdminValidate + * @author 段誉 + * @date 2021/12/29 15:47 + */ + public function sceneEdit() + { + return $this->remove('password', 'require|length') + ->append('id', 'require|checkAdmin') + ->remove('role_id', 'require') + ->append('role_id', 'checkRole'); + } + + + /** + * @notes 删除场景 + * @return AdminValidate + * @author 段誉 + * @date 2021/12/29 15:47 + */ + public function sceneDelete() + { + return $this->only(['id']); + } + + + + /** + * @notes 编辑情况下,检查是否填密码 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2021/12/29 10:19 + */ + public function edit($value, $rule, $data) + { + if (empty($data['password']) && empty($data['password_confirm'])) { + return true; + } + $len = strlen($value); + if ($len < 6 || $len > 32) { + return '密码长度须在6-32位字符'; + } + return true; + } + + + /** + * @notes 检查指定管理员是否存在 + * @param $value + * @return bool|string + * @author 段誉 + * @date 2021/12/29 10:19 + */ + public function checkAdmin($value) + { + $admin = Admin::findOrEmpty($value); + if ($admin->isEmpty()) { + return '管理员不存在'; + } + return true; + } + + + /** + * @notes 禁用校验 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/8/11 9:59 + */ + public function checkAbleDisable($value, $rule, $data) + { + $admin = Admin::findOrEmpty($data['id']); + if ($admin->isEmpty()) { + return '管理员不存在'; + } + + if ($value && $admin['root']) { + return '超级管理员不允许被禁用'; + } + return true; + } + + /** + * @notes 校验角色 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2023/9/6 16:58 + */ + public function checkRole($value, $rule, $data) + { + $admin = Admin::findOrEmpty($data['id']); + if ($admin->isEmpty()) { + return '管理员不存在'; + } + + if ($admin['root']) { + return true; + } + + if (empty($data['role_id'])) { + return '请选择角色'; + } + + return true; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/auth/MenuValidate.php b/server/app/adminapi/validate/auth/MenuValidate.php new file mode 100644 index 0000000..a2dbef7 --- /dev/null +++ b/server/app/adminapi/validate/auth/MenuValidate.php @@ -0,0 +1,195 @@ + 'require', + 'pid' => 'require|checkPid', + 'type' => 'require|in:M,C,A', + 'name' => 'require|length:1,30|checkUniqueName', + 'icon' => 'max:100', + 'sort' => 'require|egt:0', + 'perms' => 'max:100', + 'paths' => 'max:200', + 'component' => 'max:200', + 'selected' => 'max:200', + 'params' => 'max:200', + 'is_cache' => 'require|in:0,1', + 'is_show' => 'require|in:0,1', + 'is_disable' => 'require|in:0,1', + ]; + + + protected $message = [ + 'id.require' => '参数缺失', + 'pid.require' => '请选择上级菜单', + 'type.require' => '请选择菜单类型', + 'type.in' => '菜单类型参数值错误', + 'name.require' => '请填写菜单名称', + 'name.length' => '菜单名称长度需为1~30个字符', + 'icon.max' => '图标名称不能超过100个字符', + 'sort.require' => '请填写排序', + 'sort.egt' => '排序值需大于或等于0', + 'perms.max' => '权限字符不能超过100个字符', + 'paths.max' => '路由地址不能超过200个字符', + 'component.max' => '组件路径不能超过200个字符', + 'selected.max' => '选中菜单路径不能超过200个字符', + 'params.max' => '路由参数不能超过200个字符', + 'is_cache.require' => '请选择缓存状态', + 'is_cache.in' => '缓存状态参数值错误', + 'is_show.require' => '请选择显示状态', + 'is_show.in' => '显示状态参数值错误', + 'is_disable.require' => '请选择菜单状态', + 'is_disable.in' => '菜单状态参数值错误', + ]; + + + /** + * @notes 添加场景 + * @return MenuValidate + * @author 段誉 + * @date 2022/6/29 18:26 + */ + public function sceneAdd() + { + return $this->remove('id', true); + } + + + /** + * @notes 详情场景 + * @return MenuValidate + * @author 段誉 + * @date 2022/6/29 18:27 + */ + public function sceneDetail() + { + return $this->only(['id']); + } + + + /** + * @notes 删除场景 + * @return MenuValidate + * @author 段誉 + * @date 2022/6/29 18:27 + */ + public function sceneDelete() + { + return $this->only(['id']) + ->append('id', 'checkAbleDelete'); + } + + + /** + * @notes 更新状态场景 + * @return MenuValidate + * @author 段誉 + * @date 2022/7/6 17:04 + */ + public function sceneStatus() + { + return $this->only(['id', 'is_disable']); + } + + + /** + * @notes 校验菜单名称是否已存在 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/6/29 18:24 + */ + protected function checkUniqueName($value, $rule, $data) + { + if ($data['type'] != 'M') { + return true; + } + $where[] = ['type', '=', $data['type']]; + $where[] = ['name', '=', $data['name']]; + + if (!empty($data['id'])) { + $where[] = ['id', '<>', $data['id']]; + } + + $check = SystemMenu::where($where)->findOrEmpty(); + + if (!$check->isEmpty()) { + return '菜单名称已存在'; + } + + return true; + } + + + /** + * @notes 是否有子级菜单 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/6/30 9:40 + */ + protected function checkAbleDelete($value, $rule, $data) + { + $hasChild = SystemMenu::where(['pid' => $value])->findOrEmpty(); + if (!$hasChild->isEmpty()) { + return '存在子菜单,不允许删除'; + } + + // 已绑定角色菜单不可以删除 + $isBindRole = SystemRole::hasWhere('roleMenuIndex', ['menu_id' => $value])->findOrEmpty(); + if (!$isBindRole->isEmpty()) { + return '已分配菜单不可删除'; + } + + return true; + } + + + /** + * @notes 校验上级 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/6/30 9:51 + */ + protected function checkPid($value, $rule, $data) + { + if (!empty($data['id']) && $data['id'] == $value) { + return '上级菜单不能选择自己'; + } + return true; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/auth/RoleValidate.php b/server/app/adminapi/validate/auth/RoleValidate.php new file mode 100644 index 0000000..9f2f32d --- /dev/null +++ b/server/app/adminapi/validate/auth/RoleValidate.php @@ -0,0 +1,119 @@ + 'require|checkRole', + 'name' => 'require|max:64|unique:' . SystemRole::class . ',name', + 'menu_id' => 'array', + ]; + + protected $message = [ + 'id.require' => '请选择角色', + 'name.require' => '请输入角色名称', + 'name.max' => '角色名称最长为16个字符', + 'name.unique' => '角色名称已存在', + 'menu_id.array' => '权限格式错误' + ]; + + /** + * @notes 添加场景 + * @return RoleValidate + * @author 段誉 + * @date 2021/12/29 15:47 + */ + public function sceneAdd() + { + return $this->only(['name', 'menu_id']); + } + + /** + * @notes 详情场景 + * @return RoleValidate + * @author 段誉 + * @date 2021/12/29 15:47 + */ + public function sceneDetail() + { + return $this->only(['id']); + } + + /** + * @notes 删除场景 + * @return RoleValidate + * @author 段誉 + * @date 2021/12/29 15:48 + */ + public function sceneDel() + { + return $this->only(['id']) + ->append('id', 'checkAdmin'); + } + + + /** + * @notes 验证角色是否存在 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2021/12/29 15:48 + */ + public function checkRole($value, $rule, $data) + { + if (!SystemRole::find($value)) { + return '角色不存在'; + } + return true; + } + + + + /** + * @notes 验证角色是否被使用 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2021/12/29 15:49 + */ + public function checkAdmin($value, $rule, $data) + { + if (AdminRole::where(['role_id' => $value])->find()) { + return '有管理员在使用该角色,不允许删除'; + } + return true; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/auth/editSelfValidate.php b/server/app/adminapi/validate/auth/editSelfValidate.php new file mode 100644 index 0000000..b45abc1 --- /dev/null +++ b/server/app/adminapi/validate/auth/editSelfValidate.php @@ -0,0 +1,73 @@ + 'require|length:1,16', + 'avatar' => 'require', + 'password' => 'length:6,32|checkPassword', + 'password_confirm' => 'requireWith:password|confirm', + ]; + + + protected $message = [ + 'name.require' => '请填写名称', + 'name.length' => '名称须在1-16位字符', + 'avatar.require' => '请选择头像', + 'password_now.length' => '密码长度须在6-32位字符', + 'password_confirm.requireWith' => '确认密码不能为空', + 'password_confirm.confirm' => '两次输入的密码不一致', + ]; + + + /** + * @notes 校验密码 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/4/8 17:40 + */ + public function checkPassword($value, $rule, $data) + { + if (empty($data['password_old'])) { + return '请填写当前密码'; + } + + $admin = Admin::findOrEmpty($data['admin_id']); + $passwordSalt = Config::get('project.unique_identification'); + $oldPassword = create_password($data['password_old'], $passwordSalt); + + if ($admin['password'] != $oldPassword) { + return '当前密码错误'; + } + + return true; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/channel/MnpSettingsValidate.php b/server/app/adminapi/validate/channel/MnpSettingsValidate.php new file mode 100644 index 0000000..a49bdaa --- /dev/null +++ b/server/app/adminapi/validate/channel/MnpSettingsValidate.php @@ -0,0 +1,36 @@ + 'require', + 'app_secret' => 'require', + ]; + + protected $message = [ + 'app_id.require' => '请填写AppID', + 'app_secret.require' => '请填写AppSecret', + ]; +} \ No newline at end of file diff --git a/server/app/adminapi/validate/channel/OfficialAccountReplyValidate.php b/server/app/adminapi/validate/channel/OfficialAccountReplyValidate.php new file mode 100644 index 0000000..8cbadcc --- /dev/null +++ b/server/app/adminapi/validate/channel/OfficialAccountReplyValidate.php @@ -0,0 +1,72 @@ + 'require|integer', + 'reply_type' => 'require|in:1,2,3', + 'name' => 'require', + 'content_type' => 'require|in:1', + 'content' => 'require', + 'status' => 'require|in:0,1', + 'keyword' => 'requireIf:reply_type,2', + 'matching_type' => 'requireIf:reply_type,2|in:1,2', + 'sort' => 'requireIf:reply_type,2', + 'reply_num' => 'requireIf:reply_type,2|in:1', + 'new_sort' => 'require|integer|egt:0', + ]; + + protected $message = [ + 'reply_type.require' => '请输入回复类型', + 'reply_type.in' => '回复类型状态值错误', + 'name.require' => '请输入规则名称', + 'content_type.require' => '请选择内容类型', + 'content_type.in' => '内容类型状态值有误', + 'content.require' => '请输入回复内容', + 'status.require' => '请选择启用状态', + 'status.in' => '启用状态值错误', + 'keyword.requireIf' => '请输入关键词', + 'matching_type.requireIf' => '请选择匹配类型', + 'matching_type.in' => '匹配类型状态值错误', + 'sort.requireIf' => '请输入排序值', + 'sort.integer' => '排序值须为整型', + 'sort.egt' => '排序值须大于或等于0', + 'reply_num.requireIf' => '请选择回复数量', + 'reply_num.in' => '回复数量状态值错误', + 'id.require' => '参数缺失', + 'id.integer' => '参数格式错误', + 'new_sort.require' => '请输入新排序值', + 'new_sort.integer' => '新排序值须为整型', + 'new_sort.egt' => '新排序值须大于或等于0', + ]; + + protected $scene = [ + 'add' => ['reply_type', 'name', 'content_type', 'content', 'status', 'keyword', 'matching_type', 'sort', 'reply_num'], + 'detail' => ['id'], + 'delete' => ['id'], + 'sort' => ['id', 'new_sort'], + 'status' => ['id'], + 'edit' => ['id', 'reply_type', 'name', 'content_type', 'content', 'status','keyword', 'matching_type', 'sort', 'reply_num'] + ]; +} \ No newline at end of file diff --git a/server/app/adminapi/validate/channel/OfficialAccountSettingValidate.php b/server/app/adminapi/validate/channel/OfficialAccountSettingValidate.php new file mode 100644 index 0000000..1c9fbd0 --- /dev/null +++ b/server/app/adminapi/validate/channel/OfficialAccountSettingValidate.php @@ -0,0 +1,38 @@ + 'require', + 'app_secret' => 'require', + 'encryption_type' => 'require|in:1,2,3', + ]; + + protected $message = [ + 'app_id.require' => '请填写AppID', + 'app_secret.require' => '请填写AppSecret', + 'encryption_type.require' => '请选择消息加密方式', + 'encryption_type.in' => '消息加密方式状态值错误', + ]; +} \ No newline at end of file diff --git a/server/app/adminapi/validate/channel/OpenSettingValidate.php b/server/app/adminapi/validate/channel/OpenSettingValidate.php new file mode 100644 index 0000000..0d33929 --- /dev/null +++ b/server/app/adminapi/validate/channel/OpenSettingValidate.php @@ -0,0 +1,34 @@ + 'require', + 'app_secret' => 'require', + ]; + + protected $message = [ + 'app_id.require' => '请输入appId', + 'app_secret.require' => '请输入appSecret', + ]; +} \ No newline at end of file diff --git a/server/app/adminapi/validate/channel/WebPageSettingValidate.php b/server/app/adminapi/validate/channel/WebPageSettingValidate.php new file mode 100644 index 0000000..3b8f8c0 --- /dev/null +++ b/server/app/adminapi/validate/channel/WebPageSettingValidate.php @@ -0,0 +1,33 @@ + 'require|in:0,1' + ]; + + protected $message = [ + 'status.require' => '请选择启用状态', + 'status.in' => '启用状态值有误', + ]; +} \ No newline at end of file diff --git a/server/app/adminapi/validate/community/CommunityCommentValidate.php b/server/app/adminapi/validate/community/CommunityCommentValidate.php new file mode 100644 index 0000000..2d32277 --- /dev/null +++ b/server/app/adminapi/validate/community/CommunityCommentValidate.php @@ -0,0 +1,38 @@ + 'require|checkComment', + 'status' => 'require|in:0,1', + ]; + + protected $message = [ + 'id.require' => '评论id不能为空', + 'status.require' => '状态不能为空', + ]; + + public function sceneDelete() + { + return $this->only(['id']); + } + + public function sceneStatus() + { + return $this->only(['id', 'status']); + } + + public function checkComment($value) + { + $comment = CommunityComment::findOrEmpty($value); + if ($comment->isEmpty()) { + return '评论不存在'; + } + return true; + } +} diff --git a/server/app/adminapi/validate/community/CommunityPostValidate.php b/server/app/adminapi/validate/community/CommunityPostValidate.php new file mode 100644 index 0000000..181ca8b --- /dev/null +++ b/server/app/adminapi/validate/community/CommunityPostValidate.php @@ -0,0 +1,61 @@ + 'require|checkPost', + 'status' => 'require|in:0,1,2', + 'is_top' => 'require|in:0,1', + 'is_hot' => 'require|in:0,1', + 'is_recommend' => 'require|in:0,1', + ]; + + protected $message = [ + 'id.require' => '帖子id不能为空', + 'status.require' => '状态不能为空', + ]; + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneDelete() + { + return $this->only(['id']); + } + + public function sceneStatus() + { + return $this->only(['id', 'status']); + } + + public function sceneTop() + { + return $this->only(['id', 'is_top']); + } + + public function sceneHot() + { + return $this->only(['id', 'is_hot']); + } + + public function sceneRecommend() + { + return $this->only(['id', 'is_recommend']); + } + + public function checkPost($value) + { + $post = CommunityPost::findOrEmpty($value); + if ($post->isEmpty()) { + return '帖子不存在'; + } + return true; + } +} diff --git a/server/app/adminapi/validate/community/CommunityTagValidate.php b/server/app/adminapi/validate/community/CommunityTagValidate.php new file mode 100644 index 0000000..3cbcb03 --- /dev/null +++ b/server/app/adminapi/validate/community/CommunityTagValidate.php @@ -0,0 +1,48 @@ + 'require|checkTag', + 'name' => 'require|length:1,50', + ]; + + protected $message = [ + 'id.require' => '标签id不能为空', + 'name.require' => '标签名称不能为空', + 'name.length' => '标签名称长度须在1-50位字符', + ]; + + public function sceneAdd() + { + return $this->remove('id', 'require|checkTag'); + } + + public function sceneEdit() + { + } + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneDelete() + { + return $this->only(['id']); + } + + public function checkTag($value) + { + $tag = CommunityTag::findOrEmpty($value); + if ($tag->isEmpty()) { + return '标签不存在'; + } + return true; + } +} diff --git a/server/app/adminapi/validate/crontab/CrontabValidate.php b/server/app/adminapi/validate/crontab/CrontabValidate.php new file mode 100644 index 0000000..e622ef2 --- /dev/null +++ b/server/app/adminapi/validate/crontab/CrontabValidate.php @@ -0,0 +1,138 @@ + 'require', + 'type' => 'require|in:1', + 'command' => 'require', + 'status' => 'require|in:1,2,3', + 'expression' => 'require|checkExpression', + 'id' => 'require', + 'operate' => 'require' + ]; + + protected $message = [ + 'name.require' => '请输入定时任务名称', + 'type.require' => '请选择类型', + 'type.in' => '类型值错误', + 'command.require' => '请输入命令', + 'status.require' => '请选择状态', + 'status.in' => '状态值错误', + 'expression.require' => '请输入运行规则', + 'id.require' => '参数缺失', + 'operate.require' => '请选择操作', + ]; + + + /** + * @notes 添加定时任务场景 + * @return CrontabValidate + * @author 段誉 + * @date 2022/3/29 14:39 + */ + public function sceneAdd() + { + return $this->remove('id', 'require')->remove('operate', 'require'); + } + + + /** + * @notes 查看定时任务详情场景 + * @return CrontabValidate + * @author 段誉 + * @date 2022/3/29 14:39 + */ + public function sceneDetail() + { + return $this->only(['id']); + } + + + /** + * @notes 编辑定时任务 + * @return CrontabValidate + * @author 段誉 + * @date 2022/3/29 14:39 + */ + public function sceneEdit() + { + return $this->remove('operate', 'require'); + } + + + /** + * @notes 删除定时任务场景 + * @return CrontabValidate + * @author 段誉 + * @date 2022/3/29 14:40 + */ + public function sceneDelete() + { + return $this->only(['id']); + } + + + /** + * @notes CrontabValidate + * @return CrontabValidate + * @author 段誉 + * @date 2022/3/29 14:40 + */ + public function sceneOperate() + { + return $this->only(['id', 'operate']); + } + + + /** + * @notes 获取规则执行时间场景 + * @return CrontabValidate + * @author 段誉 + * @date 2022/3/29 14:40 + */ + public function sceneExpression() + { + return $this->only(['expression']); + } + + + /** + * @notes 校验运行规则 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/3/29 14:40 + */ + public function checkExpression($value, $rule, $data) + { + if (CronExpression::isValidExpression($value) === false) { + return '定时任务运行规则错误'; + } + return true; + } +} \ No newline at end of file diff --git a/server/app/adminapi/validate/decorate/DecoratePageValidate.php b/server/app/adminapi/validate/decorate/DecoratePageValidate.php new file mode 100644 index 0000000..eee8ee7 --- /dev/null +++ b/server/app/adminapi/validate/decorate/DecoratePageValidate.php @@ -0,0 +1,40 @@ + 'require', + 'type' => 'require', + 'data' => 'require', + ]; + + + protected $message = [ + 'id.require' => '参数缺失', + 'type.require' => '装修类型参数缺失', + 'data.require' => '装修信息参数缺失', + ]; + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/dept/DeptValidate.php b/server/app/adminapi/validate/dept/DeptValidate.php new file mode 100644 index 0000000..26aa0ad --- /dev/null +++ b/server/app/adminapi/validate/dept/DeptValidate.php @@ -0,0 +1,179 @@ + 'require|checkDept', + 'pid' => 'require|integer', + 'name' => 'require|unique:'.Dept::class.'|length:1,30', + 'status' => 'require|in:0,1', + 'sort' => 'egt:0', + ]; + + + protected $message = [ + 'id.require' => '参数缺失', + 'name.require' => '请填写部门名称', + 'name.length' => '部门名称长度须在1-30位字符', + 'name.unique' => '部门名称已存在', + 'sort.egt' => '排序值不正确', + 'pid.require' => '请选择上级部门', + 'pid.integer' => '上级部门参数错误', + 'status.require' => '请选择部门状态', + ]; + + + /** + * @notes 添加场景 + * @return DeptValidate + * @author 段誉 + * @date 2022/5/25 18:16 + */ + public function sceneAdd() + { + return $this->remove('id', true)->append('pid', 'checkDept'); + } + + + /** + * @notes 详情场景 + * @return DeptValidate + * @author 段誉 + * @date 2022/5/25 18:16 + */ + public function sceneDetail() + { + return $this->only(['id']); + } + + + /** + * @notes 编辑场景 + * @return DeptValidate + * @author 段誉 + * @date 2022/5/26 18:42 + */ + public function sceneEdit() + { + return $this->append('pid', 'checkPid'); + } + + + /** + * @notes 删除场景 + * @return DeptValidate + * @author 段誉 + * @date 2022/5/25 18:16 + */ + public function sceneDelete() + { + return $this->only(['id'])->append('id', 'checkAbleDetele'); + } + + + /** + * @notes 校验部门 + * @param $value + * @return bool|string + * @author 段誉 + * @date 2022/5/25 18:17 + */ + public function checkDept($value) + { + $dept = Dept::findOrEmpty($value); + if ($dept->isEmpty()) { + return '部门不存在'; + } + return true; + } + + + /** + * @notes 校验能否删除 + * @param $value + * @return bool|string + * @author 段誉 + * @date 2022/5/26 14:22 + */ + public function checkAbleDetele($value) + { + $hasLower = Dept::where(['pid' => $value])->findOrEmpty(); + if (!$hasLower->isEmpty()) { + return '已关联下级部门,暂不可删除'; + } + + $check = AdminDept::where(['dept_id' => $value])->findOrEmpty(); + if (!$check->isEmpty()) { + return '已关联管理员,暂不可删除'; + } + + $dept = Dept::findOrEmpty($value); + if ($dept['pid'] == 0) { + return '顶级部门不可删除'; + } + + return true; + } + + /** + * @notes 校验部门 + * @param $value + * @param $rule + * @param array $data + * @return bool|string + * @author 段誉 + * @date 2022/5/26 18:41 + */ + public function checkPid($value, $rule, $data = []) + { + // 当前编辑的部门id信息是否存在 + $dept = Dept::findOrEmpty($data['id']); + if ($dept->isEmpty()) { + return '当前部门信息缺失'; + } + + // 顶级部门不校验上级部门id + if ($dept['pid'] == 0) { + return true; + } + + if ($data['id'] == $value) { + return '上级部门不可是当前部门'; + } + + $leaderDept = Dept::findOrEmpty($value); + if ($leaderDept->isEmpty()) { + return '部门不存在'; + } + + return true; + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/dept/JobsValidate.php b/server/app/adminapi/validate/dept/JobsValidate.php new file mode 100644 index 0000000..96c61fd --- /dev/null +++ b/server/app/adminapi/validate/dept/JobsValidate.php @@ -0,0 +1,127 @@ + 'require|checkJobs', + 'name' => 'require|unique:'.Jobs::class.'|length:1,50', + 'code' => 'require|unique:'.Jobs::class, + 'status' => 'require|in:0,1', + 'sort' => 'egt:0', + ]; + + protected $message = [ + 'id.require' => '参数缺失', + 'name.require' => '请填写岗位名称', + 'name.length' => '岗位名称长度须在1-50位字符', + 'name.unique' => '岗位名称已存在', + 'code.require' => '请填写岗位编码', + 'code.unique' => '岗位编码已存在', + 'sort.egt' => '排序值不正确', + 'status.require' => '请选择岗位状态', + 'status.in' => '岗位状态值错误', + ]; + + + /** + * @notes 添加场景 + * @return JobsValidate + * @author 段誉 + * @date 2022/5/26 9:53 + */ + public function sceneAdd() + { + return $this->remove('id', true); + } + + + /** + * @notes 详情场景 + * @return JobsValidate + * @author 段誉 + * @date 2022/5/26 9:53 + */ + public function sceneDetail() + { + return $this->only(['id']); + } + + + public function sceneEdit() + { + } + + + /** + * @notes 删除场景 + * @return JobsValidate + * @author 段誉 + * @date 2022/5/26 9:54 + */ + public function sceneDelete() + { + return $this->only(['id'])->append('id', 'checkAbleDetele'); + } + + + /** + * @notes 校验岗位 + * @param $value + * @return bool|string + * @author 段誉 + * @date 2022/5/26 9:55 + */ + public function checkJobs($value) + { + $jobs = Jobs::findOrEmpty($value); + if ($jobs->isEmpty()) { + return '岗位不存在'; + } + return true; + } + + + /** + * @notes 校验能否删除 + * @param $value + * @return bool|string + * @author 段誉 + * @date 2022/5/26 14:22 + */ + public function checkAbleDetele($value) + { + $check = AdminJobs::where(['jobs_id' => $value])->findOrEmpty(); + if (!$check->isEmpty()) { + return '已关联管理员,暂不可删除'; + } + return true; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/dict/DictDataValidate.php b/server/app/adminapi/validate/dict/DictDataValidate.php new file mode 100644 index 0000000..b867516 --- /dev/null +++ b/server/app/adminapi/validate/dict/DictDataValidate.php @@ -0,0 +1,119 @@ + 'require|checkDictData', + 'name' => 'require|length:1,255', + 'value' => 'require', + 'type_id' => 'require|checkDictType', + 'status' => 'require|in:0,1', + ]; + + + protected $message = [ + 'id.require' => '参数缺失', + 'name.require' => '请填写字典数据名称', + 'name.length' => '字典数据名称长度须在1-255位字符', + 'value.require' => '请填写字典数据值', + 'type_id.require' => '字典类型缺失', + 'status.require' => '请选择字典数据状态', + 'status.in' => '字典数据状态参数错误', + ]; + + + /** + * @notes 添加场景 + * @return DictDataValidate + * @author 段誉 + * @date 2022/6/20 16:54 + */ + public function sceneAdd() + { + return $this->remove('id', true); + } + + + /** + * @notes ID场景 + * @return DictDataValidate + * @author 段誉 + * @date 2022/6/20 16:54 + */ + public function sceneId() + { + return $this->only(['id']); + } + + + /** + * @notes 编辑场景 + * @return DictDataValidate + * @author 段誉 + * @date 2022/6/20 18:36 + */ + public function sceneEdit() + { + return $this->remove('type_id', true); + } + + + /** + * @notes 校验字典数据 + * @param $value + * @return bool|string + * @author 段誉 + * @date 2022/6/20 16:55 + */ + protected function checkDictData($value) + { + $article = DictData::findOrEmpty($value); + if ($article->isEmpty()) { + return '字典数据不存在'; + } + return true; + } + + + /** + * @notes 校验字典类型 + * @param $value + * @return bool|string + * @author 段誉 + * @date 2022/6/20 17:03 + */ + protected function checkDictType($value) + { + $type = DictType::findOrEmpty($value); + if ($type->isEmpty()) { + return '字典类型不存在'; + } + return true; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/dict/DictTypeValidate.php b/server/app/adminapi/validate/dict/DictTypeValidate.php new file mode 100644 index 0000000..6751da1 --- /dev/null +++ b/server/app/adminapi/validate/dict/DictTypeValidate.php @@ -0,0 +1,133 @@ + 'require|checkDictType', + 'name' => 'require|length:1,255', + 'type' => 'require|unique:' . DictType::class, + 'status' => 'require|in:0,1', + 'remark' => 'max:200', + ]; + + + protected $message = [ + 'id.require' => '参数缺失', + 'name.require' => '请填写字典名称', + 'name.length' => '字典名称长度须在1~255位字符', + 'type.require' => '请填写字典类型', + 'type.unique' => '字典类型已存在', + 'status.require' => '请选择状态', + 'remark.max' => '备注长度不能超过200', + ]; + + + /** + * @notes 添加场景 + * @return DictTypeValidate + * @author 段誉 + * @date 2022/6/20 16:00 + */ + public function sceneAdd() + { + return $this->remove('id', true); + } + + + /** + * @notes 详情场景 + * @return DictTypeValidate + * @author 段誉 + * @date 2022/6/20 16:00 + */ + public function sceneDetail() + { + return $this->only(['id']); + } + + + public function sceneEdit() + { + } + + + /** + * @notes 删除场景 + * @return DictTypeValidate + * @author 段誉 + * @date 2022/6/20 16:03 + */ + public function sceneDelete() + { + return $this->only(['id']) + ->append('id', 'checkAbleDelete'); + } + + + + /** + * @notes 检查字典类型是否存在 + * @param $value + * @return bool|string + * @author 段誉 + * @date 2022/6/20 16:04 + */ + protected function checkDictType($value) + { + $dictType = DictType::findOrEmpty($value); + if ($dictType->isEmpty()) { + return '字典类型不存在'; + } + return true; + } + + + + /** + * @notes 验证是否可删除 + * @param $value + * @return bool|string + * @author 段誉 + * @date 2022/6/20 16:04 + */ + protected function checkAbleDelete($value) + { + $dictData = DictData::whereIn('type_id', $value)->select(); + + foreach ($dictData as $item) { + if (!empty($item)) { + return '字典类型已被使用,请先删除绑定该字典类型的数据'; + } + } + + return true; + } + + + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/lottery/LotteryCategoryValidate.php b/server/app/adminapi/validate/lottery/LotteryCategoryValidate.php new file mode 100644 index 0000000..d5ec910 --- /dev/null +++ b/server/app/adminapi/validate/lottery/LotteryCategoryValidate.php @@ -0,0 +1,39 @@ + 'require', + 'name' => 'require|length:1,50', + 'code' => 'require|length:1,30', + ]; + + protected $message = [ + 'id.require' => '彩种id不能为空', + 'name.require' => '彩种名称不能为空', + 'code.require' => '彩种编码不能为空', + ]; + + public function sceneAdd() + { + return $this->remove('id', 'require'); + } + + public function sceneEdit() + { + } + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneDelete() + { + return $this->only(['id']); + } +} diff --git a/server/app/adminapi/validate/lottery/LotteryDrawValidate.php b/server/app/adminapi/validate/lottery/LotteryDrawValidate.php new file mode 100644 index 0000000..5f0a97d --- /dev/null +++ b/server/app/adminapi/validate/lottery/LotteryDrawValidate.php @@ -0,0 +1,26 @@ + 'require', + ]; + + protected $message = [ + 'id.require' => '开奖记录id不能为空', + ]; + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneDelete() + { + return $this->only(['id']); + } +} diff --git a/server/app/adminapi/validate/lottery/LotteryRegionValidate.php b/server/app/adminapi/validate/lottery/LotteryRegionValidate.php new file mode 100644 index 0000000..b430fc2 --- /dev/null +++ b/server/app/adminapi/validate/lottery/LotteryRegionValidate.php @@ -0,0 +1,37 @@ + 'require', + 'label' => 'require|length:1,50', + ]; + + protected $message = [ + 'id.require' => '地区id不能为空', + 'label.require' => '地区名称不能为空', + ]; + + public function sceneAdd() + { + return $this->remove('id', 'require'); + } + + public function sceneEdit() + { + } + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneDelete() + { + return $this->only(['id']); + } +} diff --git a/server/app/adminapi/validate/match/LeagueValidate.php b/server/app/adminapi/validate/match/LeagueValidate.php new file mode 100644 index 0000000..977ad69 --- /dev/null +++ b/server/app/adminapi/validate/match/LeagueValidate.php @@ -0,0 +1,31 @@ + 'require', + ]; + + protected $message = [ + 'id.require' => '联赛id不能为空', + ]; + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneEdit() + { + return $this->only(['id']); + } + + public function sceneDelete() + { + return $this->only(['id']); + } +} diff --git a/server/app/adminapi/validate/match/MatchValidate.php b/server/app/adminapi/validate/match/MatchValidate.php new file mode 100644 index 0000000..4dc7cdb --- /dev/null +++ b/server/app/adminapi/validate/match/MatchValidate.php @@ -0,0 +1,32 @@ + 'require', + ]; + + protected $message = [ + 'id.require' => '赛事id不能为空', + ]; + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneEdit() + { + return $this->only(['id']); + } + + public function sceneDelete() + { + return $this->only(['id']); + } +} diff --git a/server/app/adminapi/validate/notice/NoticeValidate.php b/server/app/adminapi/validate/notice/NoticeValidate.php new file mode 100644 index 0000000..ae0b4d7 --- /dev/null +++ b/server/app/adminapi/validate/notice/NoticeValidate.php @@ -0,0 +1,39 @@ + 'require', + ]; + + protected $message = [ + 'id.require' => '参数缺失', + ]; + + protected function sceneDetail() + { + return $this->only(['id']); + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/notice/SmsConfigValidate.php b/server/app/adminapi/validate/notice/SmsConfigValidate.php new file mode 100644 index 0000000..05da97d --- /dev/null +++ b/server/app/adminapi/validate/notice/SmsConfigValidate.php @@ -0,0 +1,51 @@ + 'require', + 'sign' => 'require', + 'app_id' => 'requireIf:type,tencent', + 'app_key' => 'requireIf:type,ali', + 'secret_id' => 'requireIf:type,tencent', + 'secret_key' => 'require', + 'status' => 'require', + ]; + + protected $message = [ + 'type.require' => '请选择类型', + 'sign.require' => '请输入签名', + 'app_id.requireIf' => '请输入app_id', + 'app_key.requireIf' => '请输入app_key', + 'secret_id.requireIf' => '请输入secret_id', + 'secret_key.require' => '请输入secret_key', + 'status.require' => '请选择状态', + ]; + + + protected function sceneDetail() + { + return $this->only(['type']); + } +} \ No newline at end of file diff --git a/server/app/adminapi/validate/points/PointsProductValidate.php b/server/app/adminapi/validate/points/PointsProductValidate.php new file mode 100644 index 0000000..89e1a5a --- /dev/null +++ b/server/app/adminapi/validate/points/PointsProductValidate.php @@ -0,0 +1,55 @@ + 'require|checkProduct', + 'name' => 'require|length:1,50', + 'points' => 'require|integer|gt:0', + 'amount' => 'require|float|gt:0', + ]; + + protected $message = [ + 'id.require' => '商品id不能为空', + 'name.require' => '商品名称不能为空', + 'name.length' => '商品名称长度须在1-50位字符', + 'points.require' => '积分数不能为空', + 'points.integer' => '积分数须为整数', + 'points.gt' => '积分数须大于0', + 'amount.require' => '金额不能为空', + 'amount.gt' => '金额须大于0', + ]; + + public function sceneAdd() + { + return $this->remove('id', 'require|checkProduct'); + } + + public function sceneEdit() + { + } + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneDelete() + { + return $this->only(['id']); + } + + public function checkProduct($value) + { + $product = PointsProduct::findOrEmpty($value); + if ($product->isEmpty()) { + return '积分商品不存在'; + } + return true; + } +} diff --git a/server/app/adminapi/validate/popup/PopupValidate.php b/server/app/adminapi/validate/popup/PopupValidate.php new file mode 100644 index 0000000..d4da24b --- /dev/null +++ b/server/app/adminapi/validate/popup/PopupValidate.php @@ -0,0 +1,64 @@ + 'require|checkPopup', + 'name' => 'require|length:1,120', + 'page_path' => 'require|length:1,255', + 'popup_type' => 'require|in:1,2,3', + 'link_type' => 'require|in:0,1,2,3', + 'frequency' => 'require|in:1,2,3,4', + 'status' => 'require|in:0,1', + 'delay_seconds' => 'egt:0', + 'is_closable' => 'in:0,1', + 'sort' => 'egt:0', + ]; + + protected $message = [ + 'id.require' => '弹窗ID不能为空', + 'name.require' => '弹窗名称不能为空', + 'page_path.require' => '页面路径不能为空', + 'popup_type.require' => '弹窗类型不能为空', + 'link_type.require' => '跳转类型不能为空', + 'frequency.require' => '弹窗频率不能为空', + ]; + + public function sceneAdd() + { + return $this->remove('id', true); + } + + public function sceneEdit() + { + } + + public function sceneDelete() + { + return $this->only(['id']); + } + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneStatus() + { + return $this->only(['id', 'status']); + } + + public function checkPopup($value) + { + $exist = PagePopup::findOrEmpty($value); + if ($exist->isEmpty()) { + return '弹窗不存在'; + } + return true; + } +} diff --git a/server/app/adminapi/validate/recharge/RechargeRefundValidate.php b/server/app/adminapi/validate/recharge/RechargeRefundValidate.php new file mode 100644 index 0000000..e394ac2 --- /dev/null +++ b/server/app/adminapi/validate/recharge/RechargeRefundValidate.php @@ -0,0 +1,122 @@ + 'require|checkRecharge', + 'record_id' => 'require|checkRecord', + ]; + + protected $message = [ + 'recharge_id.require' => '参数缺失', + 'record_id.require' => '参数缺失', + ]; + + + public function sceneRefund() + { + return $this->only(['recharge_id']); + } + + + public function sceneAgain() + { + return $this->only(['record_id']); + } + + + /** + * @notes 校验充值订单能否发起退款 + * @param $rechargeId + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2023/2/28 17:00 + */ + protected function checkRecharge($rechargeId, $rule, $data) + { + $order = RechargeOrder::findOrEmpty($rechargeId); + + if ($order->isEmpty()) { + return '充值订单不存在'; + } + + if ($order['pay_status'] != PayEnum::ISPAID) { + return '当前订单不可退款'; + } + + // 校验订单是否已退款 + if ($order['refund_status'] == YesNoEnum::YES) { + return '订单已发起退款,退款失败请到退款记录重新退款'; + } + + // 校验余额 + $user = User::findOrEmpty($order['user_id']); + if ($user['user_money'] < $order['order_amount']) { + return '退款失败:用户余额已不足退款金额'; + } + + return true; + } + + + /** + * @notes 校验退款记录 + * @param $recordId + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2023/3/1 9:40 + */ + protected function checkRecord($recordId, $rule, $data) + { + $record = RefundRecord::findOrEmpty($recordId); + if ($record->isEmpty()) { + return '退款记录不存在'; + } + + if($record['refund_status'] == RefundEnum::REFUND_SUCCESS) { + return '该退款记录已退款成功'; + } + + $order = RechargeOrder::findOrEmpty($record['order_id']); + $user = User::findOrEmpty($record['user_id']); + + if ($user['user_money'] < $order['order_amount']) { + return '退款失败:用户余额已不足退款金额'; + } + + return true; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/setting/PayConfigValidate.php b/server/app/adminapi/validate/setting/PayConfigValidate.php new file mode 100644 index 0000000..136f3af --- /dev/null +++ b/server/app/adminapi/validate/setting/PayConfigValidate.php @@ -0,0 +1,131 @@ + 'require', + 'name' => 'require|checkName', + 'icon' => 'require', + 'sort' => 'require|number|max:5', + 'config' => 'checkConfig', + ]; + + protected $message = [ + 'id.require' => 'id不能为空', + 'name.require' => '支付名称不能为空', + 'icon.require' => '支付图标不能为空', + 'sort.require' => '排序不能为空', + 'sort,number' => '排序必须是纯数字', + 'sort.max' => '排序最大不能超过五位数', + 'config.require' => '支付参数缺失', + ]; + + public function sceneGet() + { + return $this->only(['id']); + } + + + /** + * @notes 校验支付配置记录 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2023/2/23 16:19 + */ + public function checkConfig($config, $rule, $data) + { + $result = PayConfig::where('id', $data['id'])->find(); + if (empty($result)) { + return '支付方式不存在'; + } + if ($result['pay_way'] != PayEnum::BALANCE_PAY && !isset($config)) { + return '支付配置不能为空'; + } + + if ($result['pay_way'] == PayEnum::WECHAT_PAY) { + if (empty($config['interface_version'])) { + return '微信支付接口版本不能为空'; + } + if (empty($config['merchant_type'])) { + return '商户类型不能为空'; + } + if (empty($config['mch_id'])) { + return '微信支付商户号不能为空'; + } + if (empty($config['pay_sign_key'])) { + return '商户API密钥不能为空'; + } + if (empty($config['apiclient_cert'])) { + return '微信支付证书不能为空'; + } + if (empty($config['apiclient_key'])) { + return '微信支付证书密钥不能为空'; + } + } + if ($result['pay_way'] == PayEnum::ALI_PAY) { + if (empty($config['mode'])) { + return '模式不能为空'; + } + if (empty($config['merchant_type'])) { + return '商户类型不能为空'; + } + if (empty($config['app_id'])) { + return '应用ID不能为空'; + } + if (empty($config['private_key'])) { + return '应用私钥不能为空'; + } + if (empty($config['ali_public_key'])) { + return '支付宝公钥不能为空'; + } + } + return true; + } + + + /** + * @notes 校验支付名 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2023/2/23 16:19 + */ + public function checkName($value, $rule, $data) + { + $result = PayConfig::where('name', $value) + ->where('id', '<>', $data['id']) + ->findOrEmpty(); + if (!$result->isEmpty()) { + return '支付名称已存在'; + } + return true; + } +} \ No newline at end of file diff --git a/server/app/adminapi/validate/setting/StorageValidate.php b/server/app/adminapi/validate/setting/StorageValidate.php new file mode 100644 index 0000000..92a358e --- /dev/null +++ b/server/app/adminapi/validate/setting/StorageValidate.php @@ -0,0 +1,70 @@ + 'require', + 'status' => 'require', + ]; + + + + /** + * @notes 设置存储引擎参数场景 + * @return StorageValidate + * @author 段誉 + * @date 2022/4/20 16:18 + */ + public function sceneSetup() + { + return $this->only(['engine', 'status']); + } + + + /** + * @notes 获取配置参数信息场景 + * @return StorageValidate + * @author 段誉 + * @date 2022/4/20 16:18 + */ + public function sceneDetail() + { + return $this->only(['engine']); + } + + + /** + * @notes 切换存储引擎场景 + * @return StorageValidate + * @author 段誉 + * @date 2022/4/20 16:18 + */ + public function sceneChange() + { + return $this->only(['engine']); + } +} \ No newline at end of file diff --git a/server/app/adminapi/validate/setting/TransactionSettingsValidate.php b/server/app/adminapi/validate/setting/TransactionSettingsValidate.php new file mode 100644 index 0000000..5c75062 --- /dev/null +++ b/server/app/adminapi/validate/setting/TransactionSettingsValidate.php @@ -0,0 +1,51 @@ + 'require|in:0,1', + 'cancel_unpaid_orders_times' => 'requireIf:cancel_unpaid_orders,1|integer|gt:0', + 'verification_orders' => 'require|in:0,1', + 'verification_orders_times' => 'requireIf:verification_orders,1|integer|gt:0', + ]; + + protected $message = [ + 'cancel_unpaid_orders.require' => '请选择系统取消待付款订单方式', + 'cancel_unpaid_orders.in' => '系统取消待付款订单状态值有误', + 'cancel_unpaid_orders_times.requireIf' => '系统取消待付款订单时间未填写', + 'cancel_unpaid_orders_times.integer' => '系统取消待付款订单时间须为整型', + 'cancel_unpaid_orders_times.gt' => '系统取消待付款订单时间须大于0', + + 'verification_orders.require' => '请选择系统自动核销订单方式', + 'verification_orders.in' => '系统自动核销订单状态值有误', + 'verification_orders_times.requireIf' => '系统自动核销订单时间未填写', + 'verification_orders_times.integer' => '系统自动核销订单时间须为整型', + 'verification_orders_times.gt' => '系统自动核销订单时间须大于0', + ]; + + public function sceneSetConfig() + { + return $this->only(['cancel_unpaid_orders','cancel_unpaid_orders_times','verification_orders','verification_orders_times']); + } +} \ No newline at end of file diff --git a/server/app/adminapi/validate/setting/UserConfigValidate.php b/server/app/adminapi/validate/setting/UserConfigValidate.php new file mode 100644 index 0000000..939e896 --- /dev/null +++ b/server/app/adminapi/validate/setting/UserConfigValidate.php @@ -0,0 +1,58 @@ + 'requireIf:scene,register|array', + 'coerce_mobile' => 'requireIf:scene,register|in:0,1', + 'login_agreement' => 'in:0,1', + 'third_auth' => 'in:0,1', + 'wechat_auth' => 'in:0,1', + 'default_avatar' => 'require', + ]; + + + protected $message = [ + 'default_avatar.require' => '请上传用户默认头像', + 'login_way.requireIf' => '请选择登录方式', + 'login_way.array' => '登录方式值错误', + 'coerce_mobile.requireIf' => '请选择注册强制绑定手机', + 'coerce_mobile.in' => '注册强制绑定手机值错误', + 'wechat_auth.in' => '公众号微信授权登录值错误', + 'third_auth.in' => '第三方登录值错误', + 'login_agreement.in' => '政策协议值错误', + ]; + + //用户设置验证 + public function sceneUser() + { + return $this->only(['default_avatar']); + } + + //注册验证 + public function sceneRegister() + { + return $this->only(['login_way', 'coerce_mobile', 'login_agreement', 'third_auth', 'wechat_auth']); + } +} \ No newline at end of file diff --git a/server/app/adminapi/validate/setting/WebSettingValidate.php b/server/app/adminapi/validate/setting/WebSettingValidate.php new file mode 100644 index 0000000..db40fe5 --- /dev/null +++ b/server/app/adminapi/validate/setting/WebSettingValidate.php @@ -0,0 +1,51 @@ + 'require|max:30', + 'web_favicon' => 'require', + 'web_logo' => 'require', + 'login_image' => 'require', + 'shop_name' => 'require', + 'shop_logo' => 'require', + 'pc_logo' => 'require' + ]; + + protected $message = [ + 'name.require' => '请填写网站名称', + 'name.max' => '网站名称最长为12个字符', + 'web_favicon.require' => '请上传网站图标', + 'web_logo.require' => '请上传网站logo', + 'login_image.require' => '请上传登录页广告图', + 'shop_name.require' => '请填写前台名称', + 'shop_logo.require' => '请上传前台logo', + 'pc_logo.require' => '请上传PC端logo' + ]; + + protected $scene = [ + 'website' => ['name', 'web_favicon', 'web_logo', 'login_image', 'shop_name', 'shop_logo', 'pc_logo'], + 'siteStatistics' => [''], + ]; +} \ No newline at end of file diff --git a/server/app/adminapi/validate/tools/EditTableValidate.php b/server/app/adminapi/validate/tools/EditTableValidate.php new file mode 100644 index 0000000..a3b3ae0 --- /dev/null +++ b/server/app/adminapi/validate/tools/EditTableValidate.php @@ -0,0 +1,98 @@ + 'require|checkTableData', + 'table_name' => 'require', + 'table_comment' => 'require', + 'template_type' => 'require|in:0,1', + 'generate_type' => 'require|in:0,1', + 'module_name' => 'require', + 'table_column' => 'require|array|checkColumn', + ]; + + protected $message = [ + 'id.require' => '表id缺失', + 'table_name.require' => '请填写表名称', + 'table_comment.require' => '请填写表描述', + 'template_type.require' => '请选择模板类型', + 'template_type.in' => '模板类型参数错误', + 'generate_type.require' => '请选择生成方式', + 'generate_type.in' => '生成方式类型错误', + 'module_name.require' => '请填写模块名称', + 'table_column.require' => '表字段信息缺失', + 'table_column.array' => '表字段信息类型错误', + ]; + + + /** + * @notes 校验当前数据表是否存在 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/6/15 18:58 + */ + protected function checkTableData($value, $rule, $data) + { + $table = GenerateTable::findOrEmpty($value); + if ($table->isEmpty()) { + return '信息不存在'; + } + return true; + } + + + /** + * @notes 校验表字段参数 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/6/20 10:42 + */ + protected function checkColumn($value, $rule, $data) + { + foreach ($value as $item) { + if (!isset($item['id'])) { + return '表字段id参数缺失'; + } + if (!isset($item['query_type'])) { + return '请选择查询方式'; + } + if (!isset($item['view_type'])) { + return '请选择显示类型'; + } + } + return true; + } + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/tools/GenerateTableValidate.php b/server/app/adminapi/validate/tools/GenerateTableValidate.php new file mode 100644 index 0000000..be5674c --- /dev/null +++ b/server/app/adminapi/validate/tools/GenerateTableValidate.php @@ -0,0 +1,131 @@ + 'require|checkTableData', + 'table' => 'require|array|checkTable', + 'file' => 'require' + ]; + + protected $message = [ + 'id.require' => '参数缺失', + 'table.require' => '参数缺失', + 'table.array' => '参数类型错误', + 'file.require' => '下载失败', + ]; + + + /** + * @notes 选择数据表场景 + * @return GenerateTableValidate + * @author 段誉 + * @date 2022/6/15 18:58 + */ + public function sceneSelect() + { + return $this->only(['table']); + } + + + /** + * @notes 需要校验id的场景 + * @return GenerateTableValidate + * @author 段誉 + * @date 2022/6/15 18:58 + */ + public function sceneId() + { + return $this->only(['id']); + } + + + /** + * @notes 下载场景 + * @return GenerateTableValidate + * @author 段誉 + * @date 2022/6/24 10:02 + */ + public function sceneDownload() + { + return $this->only(['file']); + } + + + /** + * @notes 校验选择的数据表信息 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/6/15 18:58 + */ + protected function checkTable($value, $rule, $data) + { + foreach ($value as $item) { + if (!isset($item['name']) || !isset($item['comment'])) { + return '参数缺失'; + } + $exist = Db::query("SHOW TABLES LIKE'" . $item['name'] . "'"); + if (empty($exist)) { + return '当前数据库不存在' . $item['name'] . '表'; + } + } + return true; + } + + + /** + * @notes 校验当前数据表是否存在 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/6/15 18:58 + */ + protected function checkTableData($value, $rule, $data) + { + if (!is_array($value)) { + $value = [$value]; + } + + foreach ($value as $item) { + $table = GenerateTable::findOrEmpty($item); + if ($table->isEmpty()) { + return '信息不存在'; + } + } + + return true; + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/user/AdjustUserMoney.php b/server/app/adminapi/validate/user/AdjustUserMoney.php new file mode 100644 index 0000000..c94e065 --- /dev/null +++ b/server/app/adminapi/validate/user/AdjustUserMoney.php @@ -0,0 +1,66 @@ + 'require', + 'action' => 'require|in:' . AccountLogEnum::INC . ',' .AccountLogEnum::DEC, + 'num' => 'require|gt:0|checkMoney', + 'remark' => 'max:128', + ]; + + protected $message = [ + 'id.require' => '请选择用户', + 'action.require' => '请选择调整类型', + 'action.in' => '调整类型错误', + 'num.require' => '请输入调整数量', + 'num.gt' => '调整余额必须大于零', + 'remark' => '备注不可超过128个符号', + ]; + + + protected function checkMoney($vaule, $rule, $data) + { + $user = User::find($data['user_id']); + if (empty($user)) { + return '用户不存在'; + } + + if (1 == $data['action']) { + return true; + } + + $surplusMoeny = $user->user_money - $vaule; + if ($surplusMoeny < 0) { + return '用户可用余额仅剩' . $user->user_money; + } + + return true; + } + + +} \ No newline at end of file diff --git a/server/app/adminapi/validate/user/UserValidate.php b/server/app/adminapi/validate/user/UserValidate.php new file mode 100644 index 0000000..e77c0f0 --- /dev/null +++ b/server/app/adminapi/validate/user/UserValidate.php @@ -0,0 +1,128 @@ + 'require|checkUser', + 'field' => 'require|checkField', + 'value' => 'require', + ]; + + protected $message = [ + 'id.require' => '请选择用户', + 'field.require' => '请选择操作', + 'value.require' => '请输入内容', + ]; + + + /** + * @notes 详情场景 + * @return UserValidate + * @author 段誉 + * @date 2022/9/22 16:35 + */ + public function sceneDetail() + { + return $this->only(['id']); + } + + + /** + * @notes 用户信息校验 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/22 17:03 + */ + public function checkUser($value, $rule, $data) + { + $userIds = is_array($value) ? $value : [$value]; + + foreach ($userIds as $item) { + if (!User::find($item)) { + return '用户不存在!'; + } + } + return true; + } + + + /** + * @notes 校验是否可更新信息 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/9/22 16:37 + */ + public function checkField($value, $rule, $data) + { + $allowField = ['account', 'sex', 'mobile', 'real_name', 'avatar']; + + if (!in_array($value, $allowField)) { + return '用户信息不允许更新'; + } + + switch ($value) { + case 'account': + //验证手机号码是否存在 + $account = User::where([ + ['id', '<>', $data['id']], + ['account', '=', $data['value']] + ])->findOrEmpty(); + + if (!$account->isEmpty()) { + return '账号已被使用'; + } + break; + + case 'mobile': + if (false == $this->validate($data['value'], 'mobile', $data)) { + return '手机号码格式错误'; + } + + //验证手机号码是否存在 + $mobile = User::where([ + ['id', '<>', $data['id']], + ['mobile', '=', $data['value']] + ])->findOrEmpty(); + + if (!$mobile->isEmpty()) { + return '手机号码已存在'; + } + break; + } + return true; + } + + +} diff --git a/server/app/adminapi/validate/vip/VipLevelValidate.php b/server/app/adminapi/validate/vip/VipLevelValidate.php new file mode 100644 index 0000000..f685eda --- /dev/null +++ b/server/app/adminapi/validate/vip/VipLevelValidate.php @@ -0,0 +1,59 @@ + 'require|checkLevel', + 'name' => 'require|length:1,50', + 'level' => 'require|integer|gt:0', + 'price' => 'require|float|gt:0', + 'duration' => 'require|integer|gt:0', + ]; + + protected $message = [ + 'id.require' => '等级id不能为空', + 'name.require' => '等级名称不能为空', + 'name.length' => '等级名称长度须在1-50位字符', + 'level.require' => '等级序号不能为空', + 'level.integer' => '等级序号须为整数', + 'level.gt' => '等级序号须大于0', + 'price.require' => '价格不能为空', + 'price.gt' => '价格须大于0', + 'duration.require' => '有效天数不能为空', + 'duration.integer' => '有效天数须为整数', + 'duration.gt' => '有效天数须大于0', + ]; + + public function sceneAdd() + { + return $this->remove('id', 'require|checkLevel'); + } + + public function sceneEdit() + { + } + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneDelete() + { + return $this->only(['id']); + } + + public function checkLevel($value) + { + $level = VipLevel::findOrEmpty($value); + if ($level->isEmpty()) { + return 'VIP等级不存在'; + } + return true; + } +} diff --git a/server/app/api/config/route.php b/server/app/api/config/route.php new file mode 100644 index 0000000..e6a2642 --- /dev/null +++ b/server/app/api/config/route.php @@ -0,0 +1,19 @@ + [ + app\api\http\middleware\InitMiddleware::class, // 初始化 + app\api\http\middleware\LoginMiddleware::class, // 登录验证 + ], +]; diff --git a/server/app/api/controller/AccountLogController.php b/server/app/api/controller/AccountLogController.php new file mode 100644 index 0000000..4c61299 --- /dev/null +++ b/server/app/api/controller/AccountLogController.php @@ -0,0 +1,36 @@ +dataLists(new AccountLogLists()); + } +} \ No newline at end of file diff --git a/server/app/api/controller/AiController.php b/server/app/api/controller/AiController.php new file mode 100644 index 0000000..253be57 --- /dev/null +++ b/server/app/api/controller/AiController.php @@ -0,0 +1,247 @@ +request->get('match_id/d'); + if (empty($matchId)) { + return $this->fail('参数缺失'); + } + + $cacheKey = 'ai_match_predict_' . $matchId; + $cached = cache($cacheKey); + if ($cached) { + $cached['from_cache'] = true; + return $this->data($cached); + } + + $match = MatchEvent::where('id', $matchId)->where('is_show', 1)->findOrEmpty(); + if ($match->isEmpty()) { + return $this->fail('赛事不存在'); + } + $data = $match->toArray(); + + $homeTeam = $data['home_team']; + $awayTeam = $data['away_team']; + + // 组装分析数据 + $matchData = [ + 'match_info' => [ + 'home_team' => $homeTeam, + 'away_team' => $awayTeam, + 'league' => $data['league_name'] ?? '', + 'match_time' => date('Y-m-d H:i', $data['match_time'] ?? time()), + 'status' => $data['status'] ?? 0, + ], + 'head_to_head' => MatchHistory::where(function ($query) use ($homeTeam, $awayTeam) { + $query->where([['home_team', '=', $homeTeam], ['away_team', '=', $awayTeam]]) + ->whereOr([['home_team', '=', $awayTeam], ['away_team', '=', $homeTeam]]); + })->order('match_time desc')->limit(5)->select()->toArray(), + 'home_recent' => MatchRecent::where('team_name', $homeTeam) + ->order('match_time desc')->limit(5)->select()->toArray(), + 'away_recent' => MatchRecent::where('team_name', $awayTeam) + ->order('match_time desc')->limit(5)->select()->toArray(), + ]; + + $result = AiService::predictMatch($matchId, $matchData, $this->userId); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + + $responseData = [ + 'analysis' => $result['data'], + 'from_cache' => false, + 'match_info' => $matchData['match_info'], + ]; + + cache($cacheKey, $responseData, 6 * 3600); + + return $this->data($responseData); + } + + /** + * 赛事AI预测 - 分段请求 + */ + public function matchPredictSection() + { + $matchId = $this->request->get('match_id/d'); + $section = $this->request->get('section/s', ''); + if (empty($matchId) || empty($section)) { + return $this->fail('参数缺失'); + } + + if (!$this->userId) { + return $this->fail('请先登录', [], 0, 401); + } + + // 解锁检查:只有本人解锁过的才免费,其他人的缓存不算 + $isUnlocked = AiUnlock::isUnlocked($this->userId, $matchId); + if (!$isUnlocked) { + if ($section === 'prediction') { + $unlockResult = AiUnlock::unlock($this->userId, $matchId); + if (!$unlockResult['ok']) { + return $this->fail($unlockResult['msg']); + } + } else { + return $this->fail('请先解锁该赛事分析'); + } + } + + $match = MatchEvent::where('id', $matchId)->where('is_show', 1)->findOrEmpty(); + if ($match->isEmpty()) { + return $this->fail('赛事不存在'); + } + $data = $match->toArray(); + + $homeTeam = $data['home_team']; + $awayTeam = $data['away_team']; + + $matchData = [ + 'match_info' => [ + 'home_team' => $homeTeam, + 'away_team' => $awayTeam, + 'league' => $data['league_name'] ?? '', + 'match_time' => date('Y-m-d H:i', $data['match_time'] ?? time()), + 'status' => $data['status'] ?? 0, + 'home_score' => $data['home_score'] ?? 0, + 'away_score' => $data['away_score'] ?? 0, + 'home_odds' => $data['home_odds'] ?? 0, + 'draw_odds' => $data['draw_odds'] ?? 0, + 'away_odds' => $data['away_odds'] ?? 0, + ], + 'head_to_head' => MatchHistory::where(function ($query) use ($homeTeam, $awayTeam) { + $query->where([['home_team', '=', $homeTeam], ['away_team', '=', $awayTeam]]) + ->whereOr([['home_team', '=', $awayTeam], ['away_team', '=', $homeTeam]]); + })->order('match_time desc')->limit(5)->select()->toArray(), + 'home_recent' => MatchRecent::where('team_name', $homeTeam) + ->order('match_time desc')->limit(5)->select()->toArray(), + 'away_recent' => MatchRecent::where('team_name', $awayTeam) + ->order('match_time desc')->limit(5)->select()->toArray(), + ]; + + $result = AiService::predictMatchSection($matchId, $section, $matchData, $this->userId); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + + return $this->data([ + 'section' => $section, + 'data' => $result['data'], + 'from_cache' => $result['from_cache'] ?? false, + 'match_info' => $matchData['match_info'], + 'remain_count' => AiUnlock::getRemainCount($this->userId), + ]); + } + + /** + * 资讯AI分析 + */ + public function articleAnalysis() + { + $articleId = $this->request->get('article_id/d'); + if (empty($articleId)) { + return $this->fail('参数缺失'); + } + + $article = Article::where(['id' => $articleId, 'is_show' => YesNoEnum::YES])->findOrEmpty(); + if ($article->isEmpty()) { + return $this->fail('文章不存在'); + } + + $articleData = $article->toArray(); + $result = AiService::analyzeArticle($articleId, $articleData, $this->userId); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + return $this->data([ + 'analysis' => $result['data'], + 'from_cache' => $result['from_cache'] ?? false, + ]); + } + + /** + * 用户可信度分析 + */ + public function userCredibility() + { + $targetUserId = $this->request->get('user_id/d'); + if (empty($targetUserId)) { + return $this->fail('参数缺失'); + } + + // 组装用户数据(简化版,后续可扩展) + $userData = [ + 'user_id' => $targetUserId, + 'register_days' => 30, + 'post_count' => 0, + 'comment_count' => 0, + 'like_received' => 0, + ]; + + $result = AiService::analyzeCredibility($targetUserId, $userData, $this->userId); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + return $this->data([ + 'analysis' => $result['data'], + 'from_cache' => $result['from_cache'] ?? false, + ]); + } + + /** + * 彩票概率分析 + */ + public function lotteryAnalysis() + { + $type = $this->request->get('type', 'ssq'); + + // 简化版彩票数据 + $lotteryData = [ + 'lottery_type' => $type, + 'recent_draws' => [], + 'description' => $type === 'ssq' ? '双色球' : '大乐透', + ]; + + $result = AiService::analyzeLottery($lotteryData, $this->userId); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + return $this->data([ + 'analysis' => $result['data'], + 'from_cache' => $result['from_cache'] ?? false, + ]); + } + + /** + * 查询比赛解锁状态 + */ + public function checkUnlock() + { + $matchId = $this->request->get('match_id/d'); + if (empty($matchId)) { + return $this->fail('参数缺失'); + } + $unlocked = $this->userId ? AiUnlock::isUnlocked($this->userId, $matchId) : false; + $remain = $this->userId ? AiUnlock::getRemainCount($this->userId) : 0; + return $this->data([ + 'unlocked' => $unlocked, + 'remain_count' => $remain, + ]); + } +} diff --git a/server/app/api/controller/AppVersionController.php b/server/app/api/controller/AppVersionController.php new file mode 100644 index 0000000..16102db --- /dev/null +++ b/server/app/api/controller/AppVersionController.php @@ -0,0 +1,115 @@ + null, 'ios' => null]; + + foreach ([1 => 'android', 2 => 'ios'] as $platform => $key) { + $version = AppVersion::where('platform', $platform) + ->where('status', 1) + ->where('package_type', '<>', 1) // 排除 wgt 热更包,只取整包 + ->order(['version_code' => 'desc', 'id' => 'desc']) + ->append(['platform_text', 'package_type_text']) + ->find(); + if ($version) { + $result[$key] = [ + 'version_name' => $version['version_name'], + 'version_code' => $version['version_code'], + 'package_type' => $version['package_type'], + 'package_type_text' => $version['package_type_text'], + 'download_url' => $version['download_url'], + 'title' => $version['title'], + 'update_content' => $version['update_content'], + 'create_time' => $version['create_time'], + ]; + } + } + + return $this->data($result); + } + + /** + * @notes 检查 APP 是否有新版本 + * 请求示例:GET /appVersion/check?platform=1&version_code=1012&package_type=1 + * - platform: 1=Android 2=iOS(必填) + * - version_code: 当前 APP 版本数字(必填) + * - package_type: 1=wgt热更 2=apk整包 3=ipa(可选,传则只比对该包类型) + */ + public function check(): Json + { + $platform = (int) $this->request->get('platform/d', 0); + $versionCode = (int) $this->request->get('version_code/d', 0); + $packageType = (int) $this->request->get('package_type/d', 0); + + if (!in_array($platform, [1, 2])) { + return $this->fail('参数错误:platform'); + } + if ($versionCode <= 0) { + return $this->fail('参数错误:version_code'); + } + + $query = AppVersion::where('platform', $platform) + ->where('status', 1) + ->where('version_code', '>', $versionCode); + + if ($packageType > 0) { + $query->where('package_type', $packageType); + } + + // wgt 热更需校验原生最低版本(min_version_code <= 当前version_code) + // 同时找出符合条件的最新版本 + $latest = $query->order(['version_code' => 'desc', 'id' => 'desc']) + ->append(['platform_text', 'package_type_text', 'update_type_text']) + ->find(); + + if (empty($latest)) { + return $this->data([ + 'has_update' => false, + ]); + } + + // wgt 包:当前 APP 原生版本必须 >= min_version_code + if ($latest['package_type'] == 1 && $latest['min_version_code'] > 0 && $versionCode < $latest['min_version_code']) { + // 当前原生版本太低无法 wgt 热更,返回无更新(让用户走整包升级路径) + return $this->data([ + 'has_update' => false, + ]); + } + + return $this->data([ + 'has_update' => true, + 'id' => $latest['id'], + 'version_name' => $latest['version_name'], + 'version_code' => $latest['version_code'], + 'min_version_code' => $latest['min_version_code'], + 'platform' => $latest['platform'], + 'platform_text' => $latest['platform_text'], + 'package_type' => $latest['package_type'], + 'package_type_text' => $latest['package_type_text'], + 'update_type' => $latest['update_type'], + 'update_type_text' => $latest['update_type_text'], + 'download_url' => $latest['download_url'], + 'title' => $latest['title'], + 'update_content' => $latest['update_content'], + ]); + } +} diff --git a/server/app/api/controller/ArticleController.php b/server/app/api/controller/ArticleController.php new file mode 100644 index 0000000..17cf37a --- /dev/null +++ b/server/app/api/controller/ArticleController.php @@ -0,0 +1,212 @@ +dataLists(new ArticleLists()); + } + + + /** + * @notes 文章分类列表 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/20 15:30 + */ + public function cate() + { + return $this->data(ArticleLogic::cate()); + } + + + /** + * @notes 收藏列表 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/20 16:31 + */ + public function collect() + { + return $this->dataLists(new ArticleCollectLists()); + } + + + /** + * @notes 文章详情 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/20 17:09 + */ + public function detail() + { + $id = $this->request->get('id/d'); + $result = ArticleLogic::detail($id, $this->userId); + return $this->data($result); + } + + + /** + * @notes 文章翻译 + * @return \think\response\Json + */ + public function translate() + { + $articleId = $this->request->post('article_id/d', 0); + if (empty($articleId)) { + $articleId = $this->request->post('id/d', 0); + } + if (empty($articleId)) { + $articleId = $this->request->get('article_id/d', 0); + } + if (empty($articleId)) { + $articleId = $this->request->get('id/d', 0); + } + if (empty($articleId)) { + return $this->fail('参数缺失'); + } + + if (!$this->userId) { + return $this->fail('请先登录', [], 0, 401); + } + + $confirm = $this->request->post('confirm/d', 0); + $result = ArticleLogic::translate((int) $articleId, $this->userId, (int) $confirm); + if ($result['success'] === false) { + return $this->fail($result['error'] ?? '翻译失败'); + } + + return $this->data($result['data']); + } + + + /** + * @notes 加入收藏 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/20 17:01 + */ + public function addCollect() + { + $articleId = $this->request->post('id/d'); + ArticleLogic::addCollect($articleId, $this->userId); + return $this->success('操作成功'); + } + + + /** + * @notes 取消收藏 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/20 17:01 + */ + public function cancelCollect() + { + $articleId = $this->request->post('id/d'); + ArticleLogic::cancelCollect($articleId, $this->userId); + return $this->success('操作成功'); + } + + + /** + * @notes 评论列表 + */ + public function commentList() + { + $articleId = $this->request->get('article_id/d'); + $pageNo = $this->request->get('page_no/d', 1); + $pageSize = $this->request->get('page_size/d', 15); + $result = ArticleInteractLogic::commentList($articleId, $pageNo, $pageSize); + return $this->data($result); + } + + + /** + * @notes 远程文章图片代理 + * @return \think\Response + */ + public function imageProxy() + { + $url = $this->request->get('url/s', ''); + return ArticleImageProxyService::proxyRemoteImage($url); + } + + + /** + * @notes 发布评论 + */ + public function addComment() + { + $params = $this->request->post(); + if (empty($params['article_id']) || empty($params['content'])) { + return $this->fail('参数缺失'); + } + $result = ArticleInteractLogic::addComment($this->userId, $params); + if ($result === false) { + return $this->fail(ArticleInteractLogic::getError()); + } + return $this->success('评论成功', ['id' => $result]); + } + + + /** + * @notes 点赞/踩 + */ + public function vote() + { + $articleId = $this->request->post('article_id/d'); + $voteType = $this->request->post('vote_type/d', 1); + if (empty($articleId)) { + return $this->fail('参数缺失'); + } + if (!in_array($voteType, [1, 2])) { + return $this->fail('投票类型错误'); + } + $result = ArticleInteractLogic::vote($this->userId, $articleId, $voteType); + if ($result === false) { + return $this->fail(ArticleInteractLogic::getError()); + } + // 返回最新计数 + $article = Article::where('id', $articleId)->field('like_count,dislike_count')->findOrEmpty(); + $result['like_count'] = $article->isEmpty() ? 0 : $article->like_count; + $result['dislike_count'] = $article->isEmpty() ? 0 : $article->dislike_count; + return $this->success('操作成功', $result); + } +} diff --git a/server/app/api/controller/AssistantController.php b/server/app/api/controller/AssistantController.php new file mode 100644 index 0000000..a05966a --- /dev/null +++ b/server/app/api/controller/AssistantController.php @@ -0,0 +1,63 @@ +request->post('message', '')); + $clientId = trim((string) $this->request->post('client_id', '')); + $sessionId = (int) $this->request->post('session_id/d', 0); + + if ($message === '') { + return $this->fail('请输入要咨询的问题'); + } + + $result = AiAssistantService::chat($message, $this->userId, $clientId, $sessionId, $this->request->ip()); + if (empty($result['success'])) { + return $this->fail($result['error'] ?? 'AI助手暂时不可用'); + } + + return $this->data($result['data']); + } + + public function sessions() + { + $clientId = trim((string) $this->request->get('client_id', '')); + return $this->data(AiAssistantService::sessions($this->userId, $clientId)); + } + + public function messages() + { + $sessionId = (int) $this->request->get('session_id/d', 0); + $clientId = trim((string) $this->request->get('client_id', '')); + if ($sessionId <= 0) { + return $this->fail('会话参数缺失'); + } + + $result = AiAssistantService::messages($sessionId, $this->userId, $clientId); + if (empty($result['success'])) { + return $this->fail($result['error'] ?? '会话不存在'); + } + + return $this->data($result['data']); + } + + public function clear() + { + $sessionId = (int) $this->request->post('session_id/d', 0); + $clientId = trim((string) $this->request->post('client_id', '')); + + $result = AiAssistantService::clear($sessionId, $this->userId, $clientId); + if (empty($result['success'])) { + return $this->fail($result['error'] ?? '清空失败'); + } + + return $this->success('操作成功'); + } +} diff --git a/server/app/api/controller/BaseApiController.php b/server/app/api/controller/BaseApiController.php new file mode 100644 index 0000000..e450ff5 --- /dev/null +++ b/server/app/api/controller/BaseApiController.php @@ -0,0 +1,33 @@ +request->userId)) { + $this->userId = (int) $this->request->userId; + } + if (isset($this->request->userInfo) && is_array($this->request->userInfo)) { + $this->userInfo = $this->request->userInfo; + } + } +} diff --git a/server/app/api/controller/ChatController.php b/server/app/api/controller/ChatController.php new file mode 100644 index 0000000..63240b7 --- /dev/null +++ b/server/app/api/controller/ChatController.php @@ -0,0 +1,52 @@ +request->get('page_no/d', 1); + $size = (int) $this->request->get('page_size/d', 20); + return $this->data(PrivateChatService::sessions($this->userId, $page, $size)); + } + + public function open() + { + $targetUserId = (int) $this->request->post('target_user_id/d', 0); + $result = PrivateChatService::open($this->userId, $targetUserId, true); + if (empty($result['success'])) { + return $this->fail($result['error'] ?? '打开会话失败'); + } + return $this->data($result['data']); + } + + public function messages() + { + $sessionId = (int) $this->request->get('session_id/d', 0); + $afterId = (int) $this->request->get('after_id/d', 0); + $page = (int) $this->request->get('page_no/d', 1); + $size = (int) $this->request->get('page_size/d', 50); + + $result = PrivateChatService::messages($this->userId, $sessionId, $afterId, $page, $size); + if (empty($result['success'])) { + return $this->fail($result['error'] ?? '获取消息失败'); + } + return $this->data($result['data']); + } + + public function send() + { + $sessionId = (int) $this->request->post('session_id/d', 0); + $messageType = trim((string) $this->request->post('message_type/s', PrivateChatService::MESSAGE_TYPE_TEXT)); + $content = trim((string) $this->request->post('content/s', '')); + + $result = PrivateChatService::send($this->userId, $sessionId, $messageType, $content); + if (empty($result['success'])) { + return $this->fail($result['error'] ?? '发送失败'); + } + return $this->data($result['data']); + } +} diff --git a/server/app/api/controller/CommunityController.php b/server/app/api/controller/CommunityController.php new file mode 100644 index 0000000..9816f40 --- /dev/null +++ b/server/app/api/controller/CommunityController.php @@ -0,0 +1,925 @@ +dataLists(new CommunityPostLists()); + } + + // 帖子详情 + public function postDetail() + { + $id = $this->request->get('id/d'); + $post = CommunityPost::where('id', $id)->where('status', 1)->findOrEmpty(); + if ($post->isEmpty()) { + return $this->fail('帖子不存在'); + } + + // 增加浏览数 + CommunityPost::where('id', $id)->inc('view_count')->update(); + + $data = $post->toArray(); + $ext = $data['ext'] ? (is_string($data['ext']) ? json_decode($data['ext'], true) : $data['ext']) : []; + $data['source_url'] = $ext['url'] ?? ''; + unset($data['ext']); + + // 用户信息 + $user = User::field('id,sn,nickname,avatar,sex')->where('id', $data['user_id'])->findOrEmpty(); + $data['user'] = $user->isEmpty() ? null : $user->toArray(); + + // 标签 + $data['tags'] = $this->getPostTags($id); + + $isTrumpPost = $this->isTrumpPost($data['tags']); + if ($isTrumpPost && empty($ext['translated_content']) && !empty($data['content'])) { + $result = AiService::translateCommunityPost($data['content']); + if (!empty($result['success']) && !empty($result['content'])) { + $ext['translated_content'] = trim($result['content']); + CommunityPost::where('id', $id)->update([ + 'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE), + 'update_time' => time(), + ]); + } + } + $data['translated_content'] = $ext['translated_content'] ?? ''; + + // 付费帖权限控制 + $data['is_locked'] = false; + $data['is_purchased'] = false; + if ($data['is_paid']) { + $isAuthor = $this->userId && $this->userId == $data['user_id']; + $isPurchased = $this->userId && Db::name('user_account_log') + ->where('user_id', $this->userId) + ->where('change_type', AccountLogEnum::UP_DEC_PURCHASE_POST) + ->where('relation_id', $id) + ->count() > 0; + + $data['is_purchased'] = $isPurchased; + + if (!$isAuthor && !$isPurchased) { + $data['is_locked'] = true; + // 付费帖子只显示前20字符 + if (mb_strlen($data['content']) > 20) { + $data['content'] = mb_substr($data['content'], 0, 20) . '...'; + } + + // 图片只保留第1张 + $images = $data['images']; + if (is_array($images) && count($images) > 1) { + $data['images'] = [$images[0]]; + } + } + } + + // 当前用户是否点赞 + $data['is_liked'] = false; + $data['is_self'] = $this->userId && $this->userId == $data['user_id']; + $data['is_followed'] = false; + $data['is_friend'] = false; + $data['relationship'] = PrivateChatService::relationship($this->userId, (int) $data['user_id']); + if ($this->userId) { + $data['is_liked'] = CommunityLike::where([ + 'user_id' => $this->userId, + 'target_id' => $id, + 'target_type' => 1 + ])->count() > 0; + + // 是否关注作者 + $data['is_followed'] = CommunityFollow::where([ + 'user_id' => $this->userId, + 'follow_user_id' => $data['user_id'] + ])->count() > 0; + $data['is_friend'] = $data['is_followed']; + $data['relationship'] = PrivateChatService::relationship($this->userId, (int) $data['user_id']); + } + + return $this->data($data); + } + + // 删除帖子 + public function deletePost() + { + if (!$this->userId) { + return $this->fail('请先登录'); + } + $postId = $this->request->post('post_id/d'); + $post = CommunityPost::where('id', $postId)->findOrEmpty(); + if ($post->isEmpty()) { + return $this->fail('帖子不存在'); + } + if ((int) $post->user_id !== (int) $this->userId) { + return $this->fail('无权删除该帖子'); + } + + Db::startTrans(); + try { + $post->delete(); + // 同时删除帖子下的评论 + CommunityComment::where('post_id', $postId)->delete(); + Db::commit(); + KbSyncService::enqueue('post', 'post', $postId, 'delete', 20); + return $this->success('删除成功'); + } catch (\Throwable $e) { + Db::rollback(); + return $this->fail('删除失败:' . $e->getMessage()); + } + } + + // 发帖 + public function publish() + { + $content = $this->request->post('content/s', ''); + $images = $this->request->post('images/a', []); + $postType = $this->request->post('post_type/d', 0); + $matchId = $this->request->post('match_id/d', 0); + $tagIds = $this->request->post('tag_ids/a', []); + $isPaid = $this->request->post('is_paid/d', 0); + $pricePoints = $this->request->post('price_points/d', 0); + $freeContentLen = $this->request->post('free_content_len/d', 100); + + if (empty($content) && empty($images)) { + return $this->fail('请输入内容或上传图片'); + } + + if ($isPaid && $pricePoints <= 0) { + return $this->fail('付费帖子必须设置积分价格'); + } + if ($isPaid && ($pricePoints < 1 || $pricePoints > 9999)) { + return $this->fail('积分价格范围为1~9999'); + } + + Db::startTrans(); + try { + $post = CommunityPost::create([ + 'user_id' => $this->userId, + 'content' => $content, + 'images' => $images, + 'post_type' => $postType, + 'match_id' => $matchId, + 'is_paid' => $isPaid ? 1 : 0, + 'price_points' => $isPaid ? $pricePoints : 0, + 'free_content_len' => $isPaid ? $freeContentLen : 100, + 'status' => 1, + 'create_time' => time(), + 'update_time' => time(), + ]); + + // 关联标签 + if (!empty($tagIds)) { + $tagData = []; + foreach ($tagIds as $tagId) { + $tagData[] = ['post_id' => $post->id, 'tag_id' => $tagId]; + } + Db::name('community_post_tag')->insertAll($tagData); + // 更新标签帖子数 + CommunityTag::whereIn('id', $tagIds)->inc('post_count')->update(); + } + + Db::commit(); + KbSyncService::enqueue('post', 'post', (int) $post->id, 'upsert', 30); + return $this->data(['id' => $post->id]); + } catch (\Exception $e) { + Db::rollback(); + return $this->fail('发布失败:' . $e->getMessage()); + } + } + + // 评论列表 + public function commentLists() + { + $postId = $this->request->get('post_id/d'); + $page = $this->request->get('page_no/d', 1); + $size = $this->request->get('page_size/d', 20); + + $list = CommunityComment::where('post_id', $postId) + ->where('status', 1) + ->where('parent_id', 0) + ->order('create_time desc') + ->page($page, $size) + ->select() + ->toArray(); + + $total = CommunityComment::where('post_id', $postId) + ->where('status', 1) + ->where('parent_id', 0) + ->count(); + + // 填充用户信息和子评论 + foreach ($list as &$item) { + $user = User::field('id,sn,nickname,avatar')->where('id', $item['user_id'])->findOrEmpty(); + $item['user'] = $user->isEmpty() ? null : $user->toArray(); + $item['is_self'] = $this->userId > 0 && (int) $item['user_id'] === (int) $this->userId; + + // 子评论(最多3条) + $item['replies'] = CommunityComment::where('parent_id', $item['id']) + ->where('status', 1) + ->order('create_time asc') + ->limit(3) + ->select() + ->each(function ($reply) { + $u = User::field('id,sn,nickname,avatar')->where('id', $reply['user_id'])->findOrEmpty(); + $reply['user'] = $u->isEmpty() ? null : $u->toArray(); + $reply['is_self'] = $this->userId > 0 && (int) $reply['user_id'] === (int) $this->userId; + if ($reply['reply_user_id']) { + $ru = User::field('id,nickname')->where('id', $reply['reply_user_id'])->findOrEmpty(); + $reply['reply_user'] = $ru->isEmpty() ? null : $ru->toArray(); + } + return $reply; + }) + ->toArray(); + + $item['reply_count'] = CommunityComment::where('parent_id', $item['id'])->where('status', 1)->count(); + + // 当前用户是否点赞 + $item['is_liked'] = false; + if ($this->userId) { + $item['is_liked'] = CommunityLike::where([ + 'user_id' => $this->userId, + 'target_id' => $item['id'], + 'target_type' => 2 + ])->count() > 0; + } + } + + return $this->data([ + 'lists' => $list, + 'count' => $total, + 'page_no' => $page, + 'page_size' => $size, + ]); + } + + // 发评论 + public function addComment() + { + $postId = $this->request->post('post_id/d'); + $content = $this->request->post('content/s', ''); + $parentId = $this->request->post('parent_id/d', 0); + $replyUserId = $this->request->post('reply_user_id/d', 0); + + if (empty($content)) { + return $this->fail('请输入评论内容'); + } + + $post = CommunityPost::where('id', $postId)->where('status', 1)->findOrEmpty(); + if ($post->isEmpty()) { + return $this->fail('帖子不存在'); + } + + $comment = CommunityComment::create([ + 'post_id' => $postId, + 'user_id' => $this->userId, + 'parent_id' => $parentId, + 'reply_user_id' => $replyUserId, + 'content' => $content, + 'status' => 1, + 'create_time' => time(), + ]); + + // 更新帖子评论数 + CommunityPost::where('id', $postId)->inc('comment_count')->update(); + + return $this->data(['id' => $comment->id]); + } + + // 删除评论 + public function deleteComment() + { + $commentId = $this->request->post('comment_id/d'); + $comment = CommunityComment::where('id', $commentId)->where('status', 1)->findOrEmpty(); + if ($comment->isEmpty()) { + return $this->fail('评论不存在'); + } + if ((int) $comment->user_id !== (int) $this->userId) { + return $this->fail('无权删除该评论'); + } + + Db::startTrans(); + try { + $deleteIds = [$commentId]; + $replyIds = CommunityComment::where('parent_id', $commentId)->where('status', 1)->column('id'); + if (!empty($replyIds)) { + $deleteIds = array_merge($deleteIds, $replyIds); + } + + CommunityComment::whereIn('id', $deleteIds)->delete(); + CommunityPost::where('id', $comment->post_id)->dec('comment_count', count($deleteIds))->update(); + + Db::commit(); + return $this->data(['deleted_ids' => $deleteIds]); + } catch (\Throwable $e) { + Db::rollback(); + return $this->fail('删除失败:' . $e->getMessage()); + } + } + + // 点赞/取消点赞 + public function like() + { + $targetId = $this->request->post('target_id/d'); + $targetType = $this->request->post('target_type/d', 1); + + $exists = CommunityLike::where([ + 'user_id' => $this->userId, + 'target_id' => $targetId, + 'target_type' => $targetType + ])->findOrEmpty(); + + if ($exists->isEmpty()) { + CommunityLike::create([ + 'user_id' => $this->userId, + 'target_id' => $targetId, + 'target_type' => $targetType, + 'create_time' => time(), + ]); + if ($targetType == 1) { + CommunityPost::where('id', $targetId)->inc('like_count')->update(); + } else { + CommunityComment::where('id', $targetId)->inc('like_count')->update(); + } + return $this->data(['is_liked' => true]); + } else { + $exists->delete(); + if ($targetType == 1) { + CommunityPost::where('id', $targetId)->dec('like_count')->update(); + } else { + CommunityComment::where('id', $targetId)->dec('like_count')->update(); + } + return $this->data(['is_liked' => false]); + } + } + + // 关注/取消关注 + public function follow() + { + $followUserId = $this->request->post('follow_user_id/d'); + + if ($followUserId == $this->userId) { + return $this->fail('不能关注自己'); + } + + $exists = CommunityFollow::where([ + 'user_id' => $this->userId, + 'follow_user_id' => $followUserId + ])->findOrEmpty(); + + if ($exists->isEmpty()) { + CommunityFollow::create([ + 'user_id' => $this->userId, + 'follow_user_id' => $followUserId, + 'create_time' => time(), + ]); + return $this->data(['is_followed' => true]); + } else { + $exists->delete(); + return $this->data(['is_followed' => false]); + } + } + + // 标签列表 + public function tagLists() + { + $list = CommunityTag::where('status', 1) + ->order('sort asc') + ->field('id,name,icon,post_count,is_hot') + ->select() + ->toArray(); + return $this->data($list); + } + + // 购买帖子 + // confirm=0 探测是否已购买;confirm=1 确认扣分购买 + public function purchasePost() + { + $postId = $this->request->post('post_id/d'); + $confirm = $this->request->post('confirm/d', 0); + $post = CommunityPost::where('id', $postId)->where('status', 1)->findOrEmpty(); + if ($post->isEmpty()) { + return $this->fail('帖子不存在'); + } + + if (!$post->is_paid) { + return $this->fail('该帖子为免费帖'); + } + + if ($post->user_id == $this->userId) { + return $this->data([ + 'content' => $post->content, + 'from_cache' => true, + ]); + } + + // VIP解锁免费权益判断 + $vipUnlockFree = VipService::hasUnlockFree($this->userId); + + // 检查是否已购买 + $hasPurchased = Db::name('user_account_log') + ->where('user_id', $this->userId) + ->where('change_type', AccountLogEnum::UP_DEC_PURCHASE_POST) + ->where('relation_id', $postId) + ->count() > 0; + + if ($hasPurchased || $vipUnlockFree) { + return $this->data([ + 'content' => $post->content, + 'from_cache' => true, + 'vip_free' => $vipUnlockFree, + ]); + } + + // 未购买且未确认 → 返回待确认标记 + if (!$confirm) { + return $this->data([ + 'needs_payment' => true, + 'cost' => (int) $post->price_points, + ]); + } + + // 防重复扣分:再次校验 + $hasPurchased2 = Db::name('user_account_log') + ->where('user_id', $this->userId) + ->where('change_type', AccountLogEnum::UP_DEC_PURCHASE_POST) + ->where('relation_id', $postId) + ->count() > 0; + if ($hasPurchased2) { + return $this->data([ + 'content' => $post->content, + 'from_cache' => true, + ]); + } + + $user = User::findOrEmpty($this->userId); + $price = (int) $post->price_points; + if ($user->user_points < $price) { + return $this->fail('积分不足,当前积分' . $user->user_points . ',需要' . $price . '积分'); + } + + Db::startTrans(); + try { + // 扣除买家积分 + User::where('id', $this->userId)->dec('user_points', $price)->update(); + AccountLogLogic::add( + $this->userId, + AccountLogEnum::UP_DEC_PURCHASE_POST, + AccountLogEnum::DEC, + $price, + '', + '购买帖子#' . $postId, + [], + $postId + ); + + // 作者获得积分 + User::where('id', $post->user_id)->inc('user_points', $price)->update(); + AccountLogLogic::add( + $post->user_id, + AccountLogEnum::UP_INC_POST_SOLD, + AccountLogEnum::INC, + $price, + '', + '帖子#' . $postId . '被购买', + [], + $postId + ); + + // 更新帖子已购买人数 + CommunityPost::where('id', $postId)->inc('paid_count')->update(); + + Db::commit(); + return $this->data([ + 'content' => $post->content, + 'from_cache' => false, + ]); + } catch (\Exception $e) { + Db::rollback(); + return $this->fail('购买失败:' . $e->getMessage()); + } + } + + // AI翻译帖子(需登录,暂不扣积分) + public function translatePost() + { + $postId = $this->request->post('post_id/d'); + + $post = CommunityPost::where('id', $postId)->where('status', 1)->findOrEmpty(); + if ($post->isEmpty()) { + return $this->fail('帖子不存在'); + } + + $ext = $post->ext ? (is_string($post->ext) ? json_decode($post->ext, true) : $post->ext) : []; + $tags = $this->getPostTags($postId); + $isLiuhePost = $this->isLiuhePost($tags, $ext); + if ($isLiuhePost) { + $analysis = $this->ensureLiuheAnalysisContent($post, $ext); + if (!$analysis['success']) { + return $this->fail($analysis['error'] ?? '图片分析失败'); + } + $ext = $analysis['ext']; + return $this->data([ + 'translated_content' => $ext['lottery_analysis_content'] ?? '', + 'from_cache' => $analysis['from_cache'] ?? false, + ]); + } + + $cacheField = 'translated_content'; + if (!empty($ext[$cacheField])) { + return $this->data([ + 'translated_content' => $ext[$cacheField], + 'from_cache' => true, + ]); + } + + if (empty($ext[$cacheField])) { + $result = AiService::translateCommunityPost($post->content ?: ''); + if (!$result['success']) { + $ext[$cacheField] = self::buildVirtualAnalysisContent($post, false); + CommunityPost::where('id', $postId)->update([ + 'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE), + 'update_time' => time(), + ]); + KbSyncService::enqueue('post', 'post', $postId, 'upsert', 45); + return $this->data([ + 'translated_content' => $ext[$cacheField], + 'from_cache' => false, + ]); + } + + $ext[$cacheField] = trim($result['content']); + CommunityPost::where('id', $postId)->update([ + 'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE), + 'update_time' => time(), + ]); + KbSyncService::enqueue('post', 'post', $postId, 'upsert', 45); + } + + return $this->data([ + 'translated_content' => $ext[$cacheField], + 'from_cache' => false, + ]); + } + + public function aiAnalysis() + { + try { + $postId = $this->request->get('post_id/d', 0); + if ($postId <= 0) { + return $this->fail('参数缺失'); + } + + $post = CommunityPost::where('id', $postId)->where('status', 1)->findOrEmpty(); + if ($post->isEmpty()) { + return $this->fail('帖子不存在'); + } + + $data = $post->toArray(); + $ext = is_array($data['ext'] ?? null) ? $data['ext'] : (json_decode((string) ($data['ext'] ?? ''), true) ?: []); + $tags = $this->getPostTags($postId); + $isTrumpPost = $this->isTrumpPost($tags); + $isLiuhePost = $this->isLiuhePost($tags, $ext); + if ($isTrumpPost) { + return $this->fail('特朗普帖子仅支持翻译'); + } + if (!$isLiuhePost) { + return $this->fail('该帖子类型无需AI分析'); + } + + $analysis = $this->ensureLiuheAnalysisContent($post, $ext); + if (!$analysis['success']) { + return $this->fail($analysis['error'] ?? '图片分析失败'); + } + $ext = $analysis['ext']; + $data['tags'] = array_values($tags); + $data['ext'] = $ext; + if (!empty($ext['translated_content'])) { + $data['translated_content'] = (string) $ext['translated_content']; + } + if (!empty($ext['lottery_analysis_content'])) { + $data['lottery_analysis_content'] = (string) $ext['lottery_analysis_content']; + } + + $force = $this->request->get('force/d', 0) === 1; + $result = AiService::analyzePost($postId, $data, $this->userId, true, $force); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + + return $this->data([ + 'analysis' => $result['data'], + 'from_cache' => $result['from_cache'] ?? false, + ]); + } catch (\Throwable $e) { + return $this->fail('AI分析异常: ' . $e->getMessage()); + } + } + + public function aiAnalysisResult() + { + try { + $postId = $this->request->get('post_id/d', 0); + if ($postId <= 0) { + return $this->fail('参数缺失'); + } + + $post = CommunityPost::where('id', $postId)->where('status', 1)->findOrEmpty(); + if ($post->isEmpty()) { + return $this->fail('帖子不存在'); + } + + $data = $post->toArray(); + $ext = is_array($data['ext'] ?? null) ? $data['ext'] : (json_decode((string) ($data['ext'] ?? ''), true) ?: []); + $isLiuhePost = $this->isLiuhePost($this->getPostTags($postId), $ext); + $cache = AiAnalysis::getCache(AiAnalysis::TYPE_POST_ANALYSIS, $postId); + if ($cache) { + $cacheData = json_decode((string) ($cache['result'] ?? ''), true); + $isValid = is_array($cacheData) && !empty($cacheData); + $isValidLiuheCache = !empty($cacheData['next_prediction']) + && ($cacheData['analysis_source'] ?? '') === 'image_free_v1'; + if ($isValid && (!$isLiuhePost || $isValidLiuheCache)) { + return $this->data([ + 'status' => 'success', + 'analysis' => $cacheData, + 'from_cache' => true, + ]); + } + } + + return $this->data([ + 'status' => 'processing', + 'analysis' => null, + 'from_cache' => false, + ]); + } catch (\Throwable $e) { + return $this->fail('AI分析结果查询异常: ' . $e->getMessage()); + } + } + + private function getPostTags(int $postId): array + { + $tagIds = Db::name('community_post_tag')->where('post_id', $postId)->column('tag_id'); + if (empty($tagIds)) { + return []; + } + return array_values(CommunityTag::whereIn('id', $tagIds)->column('name')); + } + + private function isTrumpPost(array $tags): bool + { + return in_array('特朗普', $tags, true); + } + + private function isLiuhePost(array $tags, array $ext = []): bool + { + foreach (self::LIUHE_TAGS as $tag) { + if (in_array($tag, $tags, true)) { + return true; + } + } + + $lotteryKey = (string) ($ext['lottery_key'] ?? ''); + return in_array($lotteryKey, ['a6', 'xa6'], true); + } + + private function ensureLiuheAnalysisContent(CommunityPost $post, array $ext): array + { + $images = is_array($post->images) ? array_values($post->images) : []; + $imageHash = md5(json_encode($images, JSON_UNESCAPED_UNICODE)); + $cacheSource = (string) ($ext['lottery_analysis_source'] ?? ''); + $cacheHash = (string) ($ext['lottery_analysis_images_hash'] ?? ''); + + if ( + !empty($ext['lottery_analysis_content']) + && $cacheSource === 'image_only' + && $cacheHash === $imageHash + ) { + return ['success' => true, 'ext' => $ext, 'from_cache' => true]; + } + + $result = AiService::analyzeLotteryPostImages($post->content ?: '', $images); + if (empty($result['success']) || empty($result['content'])) { + return [ + 'success' => false, + 'error' => (string) ($result['error'] ?? '帖子图片分析失败'), + ]; + } + + $ext['lottery_analysis_content'] = trim((string) $result['content']); + $ext['lottery_analysis_source'] = 'image_only'; + $ext['lottery_analysis_images_hash'] = $imageHash; + $ext['lottery_analysis_generated_at'] = time(); + + CommunityPost::where('id', (int) $post->id)->update([ + 'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE), + 'update_time' => time(), + ]); + KbSyncService::enqueue('post', 'post', (int) $post->id, 'upsert', 45); + + return ['success' => true, 'ext' => $ext, 'from_cache' => false]; + } + + private static function buildVirtualAnalysisContent($post, bool $isLottery): string + { + if ($isLottery) { + $longText = "【49宝典开奖号码深度分析】\n\n" + . "一、本期开奖概况\n" + . "本期开奖号码为:03 07 12 18 25 31 + 特码 08。\n" + . "其中红波号码2个(07、25),蓝波号码3个(03、12、31),绿波号码1个(18)。\n\n" + . "二、号码属性分析\n" + . str_repeat("1. 号码 03:生肖鼠,五行木,蓝波,小单。该号码近10期出现2次,属于温号。\n" + . "2. 号码 07:生肖虎,五行火,红波,小单。该号码近10期出现3次,属于热号。\n" + . "3. 号码 12:生肖猪,五行水,蓝波,大双。该号码近10期出现1次,属于冷号。\n" + . "4. 号码 18:生肖蛇,五行土,绿波,大双。该号码近10期出现2次,属于温号。\n" + . "5. 号码 25:生肖鼠,五行金,红波,大单。该号码近10期出现4次,属于热号。\n" + . "6. 号码 31:生肖马,五行木,蓝波,大单。该号码近10期出现1次,属于冷号。\n" + . "7. 特码 08:生肖兔,五行火,蓝波,小双。该号码近10期出现2次,属于温号。\n\n", 5) + . "三、走势趋势\n" + . "从近期走势来看,大号(25-49)区间号码持续走热,小号(01-24)区间号码相对偏冷。\n" + . "建议关注下期大号区间号码的回补机会。\n\n" + . "四、冷热统计\n" + . "热号(近10期出现3次以上):07、25\n" + . "温号(近10期出现1-2次):03、18、08\n" + . "冷号(近10期出现0次):01、02、04、05、06、09、10、11、13、14、15、16、17、19、20、21、22、23、24、26、27、28、29、30、32、33、34、35、36、37、38、39、40、41、42、43、44、45、46、47、48、49\n\n" + . "五、综合建议\n" + . "彩票开奖具有随机性,以上分析仅基于历史数据统计,不构成购买建议。请理性购彩,量力而行。\n\n" + . "—— AI智能分析仅供参考 ——"; + return $longText; + } + $content = $post->content ?? ''; + $short = mb_strlen($content) > 100 ? mb_substr($content, 0, 100) . '...' : $content; + $longTestText = "这是一段模拟的长文本翻译内容,用于测试弹窗滚动效果。\n\n" + . "在H5页面中,如果弹窗内容过长,用户需要能够滚动查看完整内容。\n\n" + . str_repeat("这是第%d段测试文本。弹窗滚动功能测试中,请确保内容可以正常滚动显示。" + . "如果滚动功能正常,用户将能够顺畅地阅读所有内容,而不会因为内容过长导致体验下降。" + . "同时,弹窗背后的页面应该保持固定,不能随着弹窗内容的滚动而滚动。\n\n", 30) + . "【测试结束】感谢您的耐心阅读!"; + return $longTestText; + } + + // 用户统计 + public function userStats() + { + $userId = $this->userId; + $postCount = CommunityPost::where('user_id', $userId)->where('status', 1)->count(); + $followCount = CommunityFollow::where('user_id', $userId)->count(); + $fansCount = CommunityFollow::where('follow_user_id', $userId)->count(); + $collectCount = Db::name('article_collect')->where('user_id', $userId)->where('status', 1)->whereNull('delete_time')->count(); + + $userPoints = User::where('id', $userId)->value('user_points') ?: 0; + $chatUnreadCount = PrivateChatService::unreadCount($userId); + + return $this->data([ + 'post_count' => $postCount, + 'follow_count' => $followCount, + 'fans_count' => $fansCount, + 'collect_count' => $collectCount, + 'user_points' => $userPoints, + 'chat_unread_count' => $chatUnreadCount, + ]); + } + + // 关注列表 + public function followList() + { + $page = $this->request->get('page_no/d', 1); + $size = $this->request->get('page_size/d', 20); + $targetUserId = $this->request->get('user_id/d', 0) ?: $this->userId; + if (!$targetUserId) { + return $this->data(['lists' => [], 'page_no' => $page, 'page_size' => $size]); + } + + $followIds = CommunityFollow::where('user_id', $targetUserId) + ->order('create_time desc') + ->page($page, $size) + ->column('follow_user_id'); + + $list = []; + if ($followIds) { + $users = User::field('id,sn,nickname,avatar') + ->whereIn('id', $followIds) + ->select() + ->toArray(); + + // 检查对方是否也关注了我(互相关注) + $fansOfMe = CommunityFollow::whereIn('user_id', $followIds) + ->where('follow_user_id', $targetUserId) + ->column('user_id'); + + foreach ($users as &$u) { + $u['is_followed'] = true; // 关注列表里都是已关注的 + $u['is_mutual'] = in_array($u['id'], $fansOfMe); + } + $list = $users; + } + + return $this->data([ + 'lists' => $list, + 'page_no' => $page, + 'page_size' => $size, + ]); + } + + // 粉丝列表 + public function fansList() + { + $page = $this->request->get('page_no/d', 1); + $size = $this->request->get('page_size/d', 20); + $targetUserId = $this->request->get('user_id/d', 0) ?: $this->userId; + if (!$targetUserId) { + return $this->data(['lists' => [], 'page_no' => $page, 'page_size' => $size]); + } + + $fansIds = CommunityFollow::where('follow_user_id', $targetUserId) + ->order('create_time desc') + ->page($page, $size) + ->column('user_id'); + + $list = []; + if ($fansIds) { + $users = User::field('id,sn,nickname,avatar') + ->whereIn('id', $fansIds) + ->select() + ->toArray(); + + // 检查我是否关注了该粉丝(is_followed)+ 互相关注 + $myFollows = CommunityFollow::where('user_id', $targetUserId) + ->whereIn('follow_user_id', $fansIds) + ->column('follow_user_id'); + + foreach ($users as &$u) { + $u['is_followed'] = in_array($u['id'], $myFollows); + $u['is_mutual'] = $u['is_followed']; // 粉丝关注了我,我也关注了他 = 互关 + } + $list = $users; + } + + return $this->data([ + 'lists' => $list, + 'page_no' => $page, + 'page_size' => $size, + ]); + } + + // 用户主页(公开) + public function userProfile() + { + $userId = $this->request->get('user_id/d'); + if (!$userId) { + return $this->fail('参数错误'); + } + + $user = User::field('id,sn,nickname,avatar,sex,create_time')->where('id', $userId)->findOrEmpty(); + if ($user->isEmpty()) { + return $this->fail('用户不存在'); + } + + $data = $user->toArray(); + $data['post_count'] = CommunityPost::where('user_id', $userId)->where('status', 1)->count(); + $data['comment_count'] = CommunityComment::where('user_id', $userId)->count(); + $data['follow_count'] = CommunityFollow::where('user_id', $userId)->count(); + $data['fans_count'] = CommunityFollow::where('follow_user_id', $userId)->count(); + $data['like_count'] = CommunityPost::where('user_id', $userId)->where('status', 1)->sum('like_count'); + + // 加入天数 + $data['join_days'] = max(1, (int) ceil((time() - strtotime($data['create_time'])) / 86400)); + + // 当前登录用户是否关注 + $data['is_followed'] = false; + $data['is_friend'] = false; + $data['relationship'] = PrivateChatService::relationship($this->userId, $userId); + if ($this->userId && $this->userId != $userId) { + $data['is_followed'] = CommunityFollow::where([ + 'user_id' => $this->userId, + 'follow_user_id' => $userId + ])->count() > 0; + $data['is_friend'] = $data['is_followed']; + $data['relationship'] = PrivateChatService::relationship($this->userId, $userId); + } + + $data['is_self'] = $this->userId == $userId; + + return $this->data($data); + } +} diff --git a/server/app/api/controller/ContentController.php b/server/app/api/controller/ContentController.php new file mode 100644 index 0000000..913f0c6 --- /dev/null +++ b/server/app/api/controller/ContentController.php @@ -0,0 +1,85 @@ +request->get('type', '')); + $idRaw = trim((string)$this->request->get('id', '')); + + if (!$type) { + return $this->fail('参数错误:type'); + } + + switch ($type) { + case 'policy': { + // policy 用 id 字段传 type 字符串 + $policy = IndexLogic::getPolicyByType($idRaw); + if (!$policy) { + return $this->fail('内容不存在'); + } + return $this->data([ + 'type' => 'policy', + 'id' => $idRaw, + 'title' => (string)($policy['title'] ?? ''), + 'content' => (string)($policy['content'] ?? ''), + 'create_time' => '', + ]); + } + + case 'article': { + $id = (int)$idRaw; + if ($id <= 0) return $this->fail('参数错误:id'); + $info = Article::where('id', $id) + ->where('is_show', 1) + ->find(); + if (!$info) return $this->fail('内容不存在'); + return $this->data([ + 'type' => 'article', + 'id' => $id, + 'title' => (string)($info['title'] ?? ''), + 'content' => (string)($info['content'] ?? ''), + 'create_time' => (string)($info['create_time'] ?? ''), + ]); + } + + case 'popup': { + $id = (int)$idRaw; + if ($id <= 0) return $this->fail('参数错误:id'); + $info = PagePopup::where('id', $id) + ->where('status', 1) + ->find(); + if (!$info) return $this->fail('内容不存在'); + return $this->data([ + 'type' => 'popup', + 'id' => $id, + 'title' => (string)($info['title'] ?: $info['name']), + 'content' => (string)($info['content'] ?? ''), + 'create_time' => (string)($info['create_time'] ?? ''), + ]); + } + + default: + return $this->fail('暂不支持的 type:' . $type); + } + } +} diff --git a/server/app/api/controller/CryptoController.php b/server/app/api/controller/CryptoController.php new file mode 100644 index 0000000..a76222b --- /dev/null +++ b/server/app/api/controller/CryptoController.php @@ -0,0 +1,65 @@ +request->get('path', ''); + if (empty($path) || !str_starts_with($path, '/api/v3/')) { + return $this->fail('无效的API路径'); + } + + // 构建完整URL,将除 path 外的参数透传 + $params = $this->request->get(); + unset($params['path']); + ksort($params); + $query = http_build_query($params); + $url = 'https://api.binance.com' . $path . ($query ? '?' . $query : ''); + + $cacheKey = 'binance_proxy_' . md5($path . '|' . $query); + $cached = Cache::get($cacheKey); + if ($cached !== null) { + return json($cached, 200); + } + + Log::info('[CryptoProxy] request url=' . $url); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 15, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_HTTPHEADER => ['Accept: application/json'], + ]); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error) { + Log::error('[CryptoProxy] curl_error=' . $error . ' url=' . $url); + return $this->fail('API请求失败: ' . $error); + } + + Log::info('[CryptoProxy] http_code=' . $httpCode . ' response_preview=' . substr((string) $response, 0, 500)); + + $data = json_decode($response, true); + if ($httpCode === 200 && $data !== null) { + Cache::set($cacheKey, $data, 3); + } + return json($data, $httpCode); + } +} diff --git a/server/app/api/controller/DeviceController.php b/server/app/api/controller/DeviceController.php new file mode 100644 index 0000000..c09d735 --- /dev/null +++ b/server/app/api/controller/DeviceController.php @@ -0,0 +1,65 @@ +request->post(); + $deviceId = trim((string)($params['device_id'] ?? '')); + if (!$deviceId) { + return $this->fail('device_id 必填'); + } + + $now = time(); + $ip = $this->request->ip() ?: ''; + $ua = $this->request->header('user-agent', ''); + if (mb_strlen($ua) > 500) $ua = mb_substr($ua, 0, 500); + + $fields = [ + 'platform', 'system', 'model', 'brand', 'app_version', + 'app_version_code', 'screen_width', 'screen_height', + 'network_type', 'language', 'timezone', 'push_cid' + ]; + $data = []; + foreach ($fields as $f) { + if (isset($params[$f])) $data[$f] = $params[$f]; + } + $data['ip'] = $ip; + $data['ua'] = $ua; + if (isset($params['extra'])) { + $data['extra'] = is_array($params['extra']) ? json_encode($params['extra'], JSON_UNESCAPED_UNICODE) : (string)$params['extra']; + } + $data['user_id'] = $this->userId ?: 0; + $data['last_at'] = $now; + + $exist = UserDevice::where('device_id', $deviceId)->find(); + if ($exist) { + $data['active_count'] = ($exist['active_count'] ?? 0) + 1; + UserDevice::where('id', $exist['id'])->update($data); + return $this->data(['id' => $exist['id'], 'is_new' => false]); + } + + $data['device_id'] = $deviceId; + $data['first_at'] = $now; + $data['active_count'] = 1; + $id = UserDevice::insertGetId($data); + return $this->data(['id' => $id, 'is_new' => true]); + } +} diff --git a/server/app/api/controller/IndexController.php b/server/app/api/controller/IndexController.php new file mode 100644 index 0000000..b5be9d6 --- /dev/null +++ b/server/app/api/controller/IndexController.php @@ -0,0 +1,92 @@ +data($result); + } + + + /** + * @notes 全局配置 + * @return Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/21 19:41 + */ + public function config() + { + $result = IndexLogic::getConfigData(); + return $this->data($result); + } + + + /** + * @notes 政策协议 + * @return Json + * @author 段誉 + * @date 2022/9/20 20:00 + */ + public function policy() + { + $type = $this->request->get('type/s', ''); + $result = IndexLogic::getPolicyByType($type); + return $this->data($result); + } + + + /** + * @notes 装修信息 + * @return Json + * @author 段誉 + * @date 2022/9/21 18:37 + */ + public function decorate() + { + $id = $this->request->get('id/d'); + $result = IndexLogic::getDecorate($id); + return $this->data($result); + } +} diff --git a/server/app/api/controller/LoginController.php b/server/app/api/controller/LoginController.php new file mode 100644 index 0000000..a71445b --- /dev/null +++ b/server/app/api/controller/LoginController.php @@ -0,0 +1,216 @@ +post()->goCheck('register'); + $result = LoginLogic::register($params); + if (true === $result) { + return $this->success('注册成功', [], 1, 1); + } + return $this->fail(LoginLogic::getError()); + } + + + /** + * @notes 账号密码/手机号密码/手机号验证码登录 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/16 10:42 + */ + public function account() + { + $params = (new LoginAccountValidate())->post()->goCheck(); + $result = LoginLogic::login($params); + if (false === $result) { + return $this->fail(LoginLogic::getError()); + } + return $this->data($result); + } + + + /** + * @notes 退出登录 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/16 10:42 + */ + public function logout() + { + LoginLogic::logout($this->userInfo); + return $this->success(); + } + + + /** + * @notes 获取微信请求code的链接 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/15 18:27 + */ + public function codeUrl() + { + $url = $this->request->get('url'); + $result = ['url' => LoginLogic::codeUrl($url)]; + return $this->success('获取成功', $result); + } + + + /** + * @notes 公众号登录 + * @return \think\response\Json + * @throws \GuzzleHttp\Exception\GuzzleException + * @author 段誉 + * @date 2022/9/20 19:48 + */ + public function oaLogin() + { + $params = (new WechatLoginValidate())->post()->goCheck('oa'); + $res = LoginLogic::oaLogin($params); + if (false === $res) { + return $this->fail(LoginLogic::getError()); + } + return $this->success('', $res); + } + + + /** + * @notes 小程序-登录接口 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/20 19:48 + */ + public function mnpLogin() + { + $params = (new WechatLoginValidate())->post()->goCheck('mnpLogin'); + $res = LoginLogic::mnpLogin($params); + if (false === $res) { + return $this->fail(LoginLogic::getError()); + } + return $this->success('', $res); + } + + + /** + * @notes 小程序绑定微信 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/20 19:48 + */ + public function mnpAuthBind() + { + $params = (new WechatLoginValidate())->post()->goCheck("wechatAuth"); + $params['user_id'] = $this->userId; + $result = LoginLogic::mnpAuthLogin($params); + if ($result === false) { + return $this->fail(LoginLogic::getError()); + } + return $this->success('绑定成功', [], 1, 1); + } + + + + /** + * @notes 公众号绑定微信 + * @return \think\response\Json + * @throws \GuzzleHttp\Exception\GuzzleException + * @author 段誉 + * @date 2022/9/20 19:48 + */ + public function oaAuthBind() + { + $params = (new WechatLoginValidate())->post()->goCheck("wechatAuth"); + $params['user_id'] = $this->userId; + $result = LoginLogic::oaAuthLogin($params); + if ($result === false) { + return $this->fail(LoginLogic::getError()); + } + return $this->success('绑定成功', [], 1, 1); + } + + + /** + * @notes 获取扫码地址 + * @return \think\response\Json + * @author 段誉 + * @date 2022/10/20 18:25 + */ + public function getScanCode() + { + $redirectUri = $this->request->get('url/s'); + $result = LoginLogic::getScanCode($redirectUri); + if (false === $result) { + return $this->fail(LoginLogic::getError() ?? '未知错误'); + } + return $this->success('', $result); + } + + + /** + * @notes 网站扫码登录 + * @return \think\response\Json + * @author 段誉 + * @date 2022/10/21 10:28 + */ + public function scanLogin() + { + $params = (new WebScanLoginValidate())->post()->goCheck(); + $result = LoginLogic::scanLogin($params); + if (false === $result) { + return $this->fail(LoginLogic::getError() ?? '登录失败'); + } + return $this->success('', $result); + } + + + /** + * @notes 更新用户头像昵称 + * @return \think\response\Json + * @author 段誉 + * @date 2023/2/22 11:15 + */ + public function updateUser() + { + $params = (new WechatLoginValidate())->post()->goCheck("updateUser"); + LoginLogic::updateUser($params, $this->userId); + return $this->success('操作成功', [], 1, 1); + } + + +} \ No newline at end of file diff --git a/server/app/api/controller/LotteryController.php b/server/app/api/controller/LotteryController.php new file mode 100644 index 0000000..9d1eac8 --- /dev/null +++ b/server/app/api/controller/LotteryController.php @@ -0,0 +1,641 @@ +order('sort', 'asc') + ->field('id,label,value,sort') + ->select() + ->toArray(); + + $games = LotteryGame::where('is_show', 1) + ->order('sort asc, id asc') + ->field('id,code,template,name,icon,status,open_status,category,group_id,open_count,sort,has_video') + ->select() + ->toArray(); + + $gameMap = []; + foreach ($games as $game) { + $gameMap[$game['category']][] = $game; + } + + $result = []; + foreach ($categories as $cat) { + $cat['games'] = $gameMap[$cat['value']] ?? []; + $result[] = $cat; + } + + return $this->data($result); + } + + /** + * 彩票分类列表 + */ + public function categoryList() + { + $domain = request()->domain() . '/'; + $list = LotteryCategory::where('is_show', 1) + ->order('sort', 'asc') + ->field('id,name,code,region,icon,description,draw_rule,draw_time') + ->select() + ->toArray(); + foreach ($list as &$item) { + if (!empty($item['icon']) && !str_starts_with($item['icon'], 'http')) { + $item['icon'] = $domain . $item['icon']; + } + } + return $this->data($list); + } + + /** + * 彩票地区Tab列表 + */ + public function regionList() + { + $domain = request()->domain() . '/'; + $list = LotteryRegion::where('is_show', 1) + ->order('sort', 'asc') + ->field('id,label,value,icon') + ->select() + ->toArray(); + foreach ($list as &$item) { + if (!empty($item['icon']) && !str_starts_with($item['icon'], 'http')) { + $item['icon'] = $domain . $item['icon']; + } + } + return $this->data($list); + } + + /** + * 开奖列表 + */ + public function drawList() + { + $categoryId = $this->request->get('category_id/d', 0); + $pageNo = $this->request->get('page_no/d', 1); + $pageSize = $this->request->get('page_size/d', 20); + + $query = LotteryDraw::where('is_show', 1); + if ($categoryId > 0) { + $query->where('category_id', $categoryId); + } + + $countQuery = clone $query; + $count = $countQuery->count(); + $list = $query->order('draw_date desc, period desc') + ->page($pageNo, $pageSize) + ->select() + ->toArray(); + + // 解析JSON字段 + foreach ($list as &$item) { + $item['numbers'] = is_string($item['numbers']) ? json_decode($item['numbers'], true) : $item['numbers']; + $item['zodiac'] = is_string($item['zodiac']) ? json_decode($item['zodiac'], true) : $item['zodiac']; + $item['elements'] = is_string($item['elements']) ? json_decode($item['elements'], true) : $item['elements']; + $item['color'] = is_string($item['color']) ? json_decode($item['color'], true) : $item['color']; + $item['prize_info'] = is_string($item['prize_info']) ? json_decode($item['prize_info'], true) : $item['prize_info']; + } + + return $this->data([ + 'lists' => $list, + 'count' => $count, + 'page_no' => $pageNo, + 'page_size' => $pageSize, + 'last_page' => ceil($count / $pageSize), + ]); + } + + /** + * 开奖详情 + */ + public function drawDetail() + { + $id = $this->request->get('id/d', 0); + if (empty($id)) { + return $this->fail('参数缺失'); + } + + $draw = LotteryDraw::where('id', $id)->where('is_show', 1)->findOrEmpty(); + if ($draw->isEmpty()) { + return $this->fail('记录不存在'); + } + + $data = $draw->toArray(); + $data['numbers'] = is_string($data['numbers']) ? json_decode($data['numbers'], true) : $data['numbers']; + $data['zodiac'] = is_string($data['zodiac']) ? json_decode($data['zodiac'], true) : $data['zodiac']; + $data['elements'] = is_string($data['elements']) ? json_decode($data['elements'], true) : $data['elements']; + $data['color'] = is_string($data['color']) ? json_decode($data['color'], true) : $data['color']; + $data['prize_info'] = is_string($data['prize_info']) ? json_decode($data['prize_info'], true) : $data['prize_info']; + + // 附带分类信息 + $category = LotteryCategory::where('id', $data['category_id'])->findOrEmpty(); + if (!$category->isEmpty()) { + $data['category'] = $category->toArray(); + } + + return $this->data($data); + } + + /** + * 各彩种最新一期开奖 + */ + public function latestDraw() + { + $categoryId = $this->request->get('category_id/d', 0); + + $query = LotteryDraw::where('is_show', 1)->where('status', 1); + if ($categoryId > 0) { + $query->where('category_id', $categoryId); + } + + // 按分类分组取最新一期 + if ($categoryId > 0) { + $draw = $query->order('draw_date', 'desc')->findOrEmpty(); + if ($draw->isEmpty()) { + return $this->data(null); + } + $data = $draw->toArray(); + $data['numbers'] = is_string($data['numbers']) ? json_decode($data['numbers'], true) : $data['numbers']; + $data['color'] = is_string($data['color']) ? json_decode($data['color'], true) : $data['color']; + return $this->data($data); + } + + // 获取所有分类的最新开奖 + $categories = LotteryCategory::where('is_show', 1)->order('sort', 'asc')->select()->toArray(); + $result = []; + foreach ($categories as $cat) { + $draw = LotteryDraw::where('category_id', $cat['id']) + ->where('is_show', 1) + ->where('status', 1) + ->order('draw_date', 'desc') + ->findOrEmpty(); + if (!$draw->isEmpty()) { + $d = $draw->toArray(); + $d['numbers'] = is_string($d['numbers']) ? json_decode($d['numbers'], true) : $d['numbers']; + $d['color'] = is_string($d['color']) ? json_decode($d['color'], true) : $d['color']; + $d['category_name'] = $cat['name']; + $d['category_code'] = $cat['code']; + $result[] = $d; + } + } + return $this->data($result); + } + + /** + * 为号码数组附加生肖(zodiac)和波色(color)映射(六合彩专用) + */ + private function enrichNumbers(array $numbers, string $template = ''): array + { + if (empty($numbers)) + return []; + $isHk6 = stripos($template, 'hk6') !== false || stripos($template, 'lhc') !== false; + if (!$isHk6) + return $numbers; + + $year = (int) date('Y'); + $mapping = LotteryNumberMapping::where('year', $year) + ->whereIn('type', [LotteryNumberMapping::TYPE_ZODIAC, LotteryNumberMapping::TYPE_COLOR]) + ->order('type asc, sort asc') + ->select()->toArray(); + + $zodiacMap = []; + $colorMap = []; + foreach ($mapping as $row) { + $nums = is_string($row['numbers']) ? json_decode($row['numbers'], true) : $row['numbers']; + foreach ($nums as $n) { + if ((int) $row['type'] === LotteryNumberMapping::TYPE_ZODIAC) { + $zodiacMap[(int) $n] = $row['attr_name']; + } else { + $colorMap[(int) $n] = $row['attr_code']; + } + } + } + + $enriched = []; + foreach ($numbers as $num) { + $n = (int) $num; + $enriched[] = [ + 'num' => $num, + 'zodiac' => $zodiacMap[$n] ?? '', + 'color' => $colorMap[$n] ?? 'red', + ]; + } + return $enriched; + } + + /** + * 各彩种最新一期开奖(新表 lottery_draw_result) + */ + public function latestDrawResult() + { + $code = $this->request->get('code', ''); + + if ($code) { + $draw = LotteryDrawResult::where('code', $code) + ->order('draw_time desc') + ->findOrEmpty(); + if ($draw->isEmpty()) { + return $this->data(null); + } + $data = $draw->toArray(); + if (!empty($data['draw_code'])) { + $data['status'] = 1; + } + $rawNumbers = $data['draw_code'] ? explode(',', $data['draw_code']) : []; + $game = LotteryGame::where('code', $code)->field('name,template')->findOrEmpty(); + $data['game_name'] = $game->isEmpty() ? $code : $game['name']; + $template = $game->isEmpty() ? '' : $game['template']; + $data['numbers'] = $this->enrichNumbers($rawNumbers, $template); + $data['template'] = $template; + return $this->data($data); + } + + // 全部彩种,每个取最新一条 + $games = LotteryGame::where('is_show', 1) + ->order('sort asc, id asc') + ->field('code,name,template,category') + ->select() + ->toArray(); + + $result = []; + foreach ($games as $game) { + $draw = LotteryDrawResult::where('code', $game['code']) + ->order('draw_time desc') + ->findOrEmpty(); + if (!$draw->isEmpty()) { + $d = $draw->toArray(); + if (!empty($d['draw_code'])) { + $d['status'] = 1; + } + $rawNumbers = $d['draw_code'] ? explode(',', $d['draw_code']) : []; + $d['numbers'] = $this->enrichNumbers($rawNumbers, $game['template']); + $d['game_name'] = $game['name']; + $d['template'] = $game['template']; + $d['category'] = $game['category']; + $result[] = $d; + } + } + return $this->data($result); + } + + /** + * 开奖历史列表(新表 lottery_draw_result) + */ + public function drawResultList() + { + $code = $this->request->get('code', ''); + $pageNo = $this->request->get('page_no/d', 1); + $pageSize = $this->request->get('page_size/d', 20); + + if (empty($code)) { + return $this->fail('缺少彩种编码'); + } + + $query = LotteryDrawResult::where('code', $code); + $count = (clone $query)->count(); + $list = $query->order('draw_time desc, issue desc') + ->page($pageNo, $pageSize) + ->select() + ->toArray(); + + $game = LotteryGame::where('code', $code)->field('template')->findOrEmpty(); + $template = $game->isEmpty() ? '' : $game['template']; + foreach ($list as &$item) { + if (!empty($item['draw_code'])) { + $item['status'] = 1; + } + $rawNumbers = $item['draw_code'] ? explode(',', $item['draw_code']) : []; + $item['numbers'] = $this->enrichNumbers($rawNumbers, $template); + } + + return $this->data([ + 'lists' => $list, + 'count' => $count, + 'page_no' => $pageNo, + 'page_size' => $pageSize, + 'last_page' => ceil($count / $pageSize), + ]); + } + + /** + * AI预测分析 + */ + public function aiPredict() + { + $categoryId = $this->request->get('category_id/d', 0); + $code = $this->request->get('code', ''); + + // 支持两种查找方式:category_id(旧表)或 code(新表) + if (!empty($code)) { + $game = LotteryGame::where('code', $code)->findOrEmpty(); + if ($game->isEmpty()) { + return $this->fail('彩种不存在'); + } + + $recentDraws = LotteryDrawResult::where('code', $code) + ->where('draw_code', '<>', '') + ->order('draw_time desc, issue desc') + ->limit(30) + ->field('issue,draw_time,draw_code') + ->select() + ->toArray(); + + foreach ($recentDraws as &$d) { + $d['numbers'] = $d['draw_code'] ? explode(',', $d['draw_code']) : []; + } + + $lotteryData = [ + 'lottery_type' => $code, + 'lottery_name' => $game['name'], + 'draw_rule' => $game['template'] ?? '', + 'recent_draws' => $recentDraws, + ]; + + $result = AiService::analyzeLottery($lotteryData, $this->userId); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + return $this->data([ + 'analysis' => $result['data'], + 'from_cache' => $result['from_cache'] ?? false, + 'category_name' => $game['name'], + ]); + } + + if (empty($categoryId)) { + return $this->fail('参数缺失'); + } + + $category = LotteryCategory::where('id', $categoryId)->findOrEmpty(); + if ($category->isEmpty()) { + return $this->fail('彩种不存在'); + } + + // 获取最近30期已开奖数据 + $recentDraws = LotteryDraw::where('category_id', $categoryId) + ->where('status', 1) + ->where('is_show', 1) + ->order('draw_date desc, period desc') + ->limit(30) + ->field('period,draw_date,numbers,special_number,zodiac,color') + ->select() + ->toArray(); + + foreach ($recentDraws as &$d) { + $d['numbers'] = is_string($d['numbers']) ? json_decode($d['numbers'], true) : $d['numbers']; + $d['zodiac'] = is_string($d['zodiac']) ? json_decode($d['zodiac'], true) : $d['zodiac']; + $d['color'] = is_string($d['color']) ? json_decode($d['color'], true) : $d['color']; + } + + $lotteryData = [ + 'lottery_type' => $category['code'], + 'lottery_name' => $category['name'], + 'draw_rule' => $category['draw_rule'], + 'recent_draws' => $recentDraws, + ]; + + $result = AiService::analyzeLottery($lotteryData, $this->userId); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + return $this->data([ + 'analysis' => $result['data'], + 'from_cache' => $result['from_cache'] ?? false, + 'category_name' => $category['name'], + ]); + } + + /** + * 获取彩种信息和历史开奖数据(纯数据库查询,不走AI) + */ + public function aiHistory() + { + $code = $this->request->get('code', ''); + if (empty($code)) { + return $this->fail('参数缺失'); + } + + $game = LotteryGame::where('code', $code)->findOrEmpty(); + if ($game->isEmpty()) { + return $this->fail('彩种不存在'); + } + + $recentDraws = LotteryDrawResult::where('code', $code) + ->where('draw_code', '<>', '') + ->order('draw_time desc, issue desc') + ->limit(30) + ->field('issue,draw_time,draw_code') + ->select() + ->toArray(); + + foreach ($recentDraws as &$d) { + $d['numbers'] = $d['draw_code'] ? explode(',', $d['draw_code']) : []; + unset($d['draw_code']); + } + + return $this->data([ + 'game_name' => $game['name'], + 'game_code' => $code, + 'template' => $game['template'] ?? '', + 'recent_draws' => $recentDraws, + ]); + } + + /** + * AI分析 - 热冷号 + */ + public function aiHotCold() + { + $code = $this->request->get('code', ''); + if (empty($code)) { + return $this->fail('参数缺失'); + } + + $game = LotteryGame::where('code', $code)->findOrEmpty(); + if ($game->isEmpty()) { + return $this->fail('彩种不存在'); + } + + $recentDraws = $this->getRecentDrawCodes($code, 30); + $lotteryData = [ + 'lottery_type' => $code, + 'lottery_name' => $game['name'], + 'draw_rule' => $game['template'] ?? '', + 'recent_draws' => $recentDraws, + ]; + + $result = AiService::analyzeLotteryModule($lotteryData, 'hot_cold', $this->userId); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + return $this->data($result['data']); + } + + /** + * AI分析 - 趋势+推荐组合 + */ + public function aiTrend() + { + $code = $this->request->get('code', ''); + if (empty($code)) { + return $this->fail('参数缺失'); + } + + $game = LotteryGame::where('code', $code)->findOrEmpty(); + if ($game->isEmpty()) { + return $this->fail('彩种不存在'); + } + + $recentDraws = $this->getRecentDrawCodes($code, 30); + $lotteryData = [ + 'lottery_type' => $code, + 'lottery_name' => $game['name'], + 'draw_rule' => $game['template'] ?? '', + 'recent_draws' => $recentDraws, + ]; + + $result = AiService::analyzeLotteryModule($lotteryData, 'trend', $this->userId); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + return $this->data($result['data']); + } + + /** + * AI分析 - 统计数据 + */ + public function aiStats() + { + $code = $this->request->get('code', ''); + if (empty($code)) { + return $this->fail('参数缺失'); + } + + $game = LotteryGame::where('code', $code)->findOrEmpty(); + if ($game->isEmpty()) { + return $this->fail('彩种不存在'); + } + + $recentDraws = $this->getRecentDrawCodes($code, 30); + $lotteryData = [ + 'lottery_type' => $code, + 'lottery_name' => $game['name'], + 'draw_rule' => $game['template'] ?? '', + 'recent_draws' => $recentDraws, + ]; + + $result = AiService::analyzeLotteryModule($lotteryData, 'stats', $this->userId); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + return $this->data($result['data']); + } + + /** + * AI分析 - 波色+生肖(六合彩专属) + */ + public function aiColorZodiac() + { + $code = $this->request->get('code', ''); + if (empty($code)) { + return $this->fail('参数缺失'); + } + + $game = LotteryGame::where('code', $code)->findOrEmpty(); + if ($game->isEmpty()) { + return $this->fail('彩种不存在'); + } + + if (($game['template'] ?? '') !== 'HK6') { + return $this->fail('该彩种不支持波色生肖分析'); + } + + $recentDraws = $this->getRecentDrawCodes($code, 30); + + $year = (int) date('Y'); + $colorMap = LotteryNumberMapping::getMapByType($year, LotteryNumberMapping::TYPE_COLOR); + $zodiacMap = LotteryNumberMapping::getMapByType($year, LotteryNumberMapping::TYPE_ZODIAC); + + $lotteryData = [ + 'lottery_type' => $code, + 'lottery_name' => $game['name'], + 'draw_rule' => $game['template'] ?? '', + 'recent_draws' => $recentDraws, + 'number_mapping' => [ + 'color' => $colorMap, + 'zodiac' => $zodiacMap, + ], + ]; + + $result = AiService::analyzeLotteryModule($lotteryData, 'color_zodiac', $this->userId); + if (!$result['success']) { + return $this->fail($result['error'] ?? 'AI分析失败'); + } + return $this->data($result['data']); + } + + /** + * 获取最近N期开奖号码(格式化为号码数组) + */ + private function getRecentDrawCodes(string $code, int $limit = 30): array + { + $rows = LotteryDrawResult::where('code', $code) + ->where('draw_code', '<>', '') + ->order('draw_time desc, issue desc') + ->limit($limit) + ->field('issue,draw_time,draw_code') + ->select() + ->toArray(); + + foreach ($rows as &$row) { + $row['numbers'] = $row['draw_code'] ? array_map('intval', explode(',', $row['draw_code'])) : []; + unset($row['draw_code']); + } + + return $rows; + } + + /** + * 获取某期的AI分析历史记录 + */ + public function aiAnalysisHistory() + { + $code = $this->request->get('code', ''); + $issue = $this->request->get('issue', ''); + if (empty($code) || empty($issue)) { + return $this->fail('参数缺失'); + } + + $rows = LotteryAiAnalysis::where('code', $code) + ->where('issue', $issue) + ->field('module,result') + ->select() + ->toArray(); + + $result = []; + foreach ($rows as $row) { + $result[$row['module']] = json_decode($row['result'], true); + } + + return $this->data($result); + } +} diff --git a/server/app/api/controller/MatchController.php b/server/app/api/controller/MatchController.php new file mode 100644 index 0000000..d32b792 --- /dev/null +++ b/server/app/api/controller/MatchController.php @@ -0,0 +1,501 @@ +where('type', 'sport') + ->field('sport_type, label, icon') + ->order('sort asc') + ->select() + ->toArray(); + + $result = []; + foreach ($types as $item) { + $result[] = [ + 'value' => $item['sport_type'], + 'label' => $item['label'], + 'icon' => $item['icon'], + ]; + } + return $this->data($result); + } + + public function leagues() + { + $sportType = $this->request->get('sport_type/d', 0); + $query = League::where('is_show', 1) + ->where('type', 'league'); + if ($sportType > 0) { + $query->where('sport_type', $sportType); + } + $leagues = $query->field('id, label, sport_type, icon, sort') + ->order('sort asc') + ->select() + ->toArray(); + + $leagueNames = array_column($leagues, 'label'); + $counts = []; + if (!empty($leagueNames)) { + $rows = MatchEvent::where('is_show', 1) + ->where('status', 1) + ->whereIn('league_name', $leagueNames) + ->field('league_name, COUNT(*) as cnt') + ->group('league_name') + ->select() + ->toArray(); + foreach ($rows as $row) { + $counts[$row['league_name']] = (int) $row['cnt']; + } + } + + $grouped = []; + foreach ($leagues as $item) { + $st = $item['sport_type']; + if (!isset($grouped[$st])) { + $grouped[$st] = []; + } + $grouped[$st][] = [ + 'id' => $item['id'], + 'league_name' => $item['label'], + 'icon' => $item['icon'], + 'count' => $counts[$item['label']] ?? 0, + ]; + } + return $this->data($grouped); + } + + public function lists() + { + $sportType = $this->request->get('sport_type/d', 0); + $leagueName = $this->request->get('league_name/s', ''); + $endedPage = $this->request->get('ended_page/d', 1); + $endedPageSize = $this->request->get('ended_page_size/d', 20); + $endedOnly = $this->request->get('ended_only/d', 0); + + $field = 'id,league_name,league_icon,home_team,home_icon,home_score,away_team,away_icon,away_score,sport_type,status,match_time,current_minute,half_score,home_odds,draw_odds,away_odds,home_corner,away_corner,home_yellow,away_yellow,home_red,away_red,is_hot,round_name'; + + // 基础筛选 + $baseWhere = [['is_show', '=', 1]]; + if ($sportType > 0) { + $baseWhere[] = ['sport_type', '=', $sportType]; + } + if (!empty($leagueName)) { + $baseWhere[] = ['league_name', '=', $leagueName]; + } elseif ($sportType <= 0) { + $validLeagues = League::where('is_show', 1)->where('type', 'league')->column('label'); + if (!empty($validLeagues)) { + $baseWhere[] = ['league_name', 'in', $validLeagues]; + } + } + + // 已结束:分页,按时间倒序 + $endedOffset = ($endedPage - 1) * $endedPageSize; + $ended = MatchEvent::where($baseWhere)->where('status', 2) + ->field($field)->order('match_time desc') + ->limit($endedOffset, $endedPageSize)->select()->toArray(); + $endedCount = MatchEvent::where($baseWhere)->where('status', 2)->count(); + + // 加载更多时只返回已结束数据 + if ($endedOnly) { + return $this->data([ + 'ended' => [ + 'lists' => $ended, + 'count' => $endedCount, + 'page_no' => $endedPage, + 'page_size' => $endedPageSize, + ], + ]); + } + + // 进行中:全部返回 + $live = MatchEvent::where($baseWhere)->where('status', 1) + ->field($field)->order('match_time asc')->select()->toArray(); + + // 未开始:只取前20条,按时间正序 + $upcoming = MatchEvent::where($baseWhere)->where('status', 0) + ->field($field)->order('match_time asc')->limit(20)->select()->toArray(); + + return $this->data([ + 'live' => $live, + 'upcoming' => $upcoming, + 'ended' => [ + 'lists' => $ended, + 'count' => $endedCount, + 'page_no' => $endedPage, + 'page_size' => $endedPageSize, + ], + ]); + } + + public function detail() + { + $id = $this->request->get('id/d'); + $match = MatchEvent::where('id', $id)->where('is_show', 1)->findOrEmpty(); + if ($match->isEmpty()) { + return $this->fail('赛事不存在'); + } + $data = $match->toArray(); + if (empty($data['live_url']) && $this->isWorldCupMatch($data)) { + $data['live_url'] = $this->worldCupDefaultLiveUrl; + } + $data['living_tv'] = ''; + + $matchDataRaw = MatchData::where('match_id', $data['match_id']) + ->order('id desc') + ->value('raw_data'); + if (!empty($matchDataRaw)) { + $rawData = json_decode($matchDataRaw, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($rawData)) { + $data['living_tv'] = $this->resolveLiveSourceText($rawData); + } + } + + $homeTeam = $data['home_team']; + $awayTeam = $data['away_team']; + + // 对赛往绩 + $data['history'] = MatchHistory::where(function ($query) use ($homeTeam, $awayTeam) { + $query->where([['home_team', '=', $homeTeam], ['away_team', '=', $awayTeam]]) + ->whereOr([['home_team', '=', $awayTeam], ['away_team', '=', $homeTeam]]); + })->order('match_time desc')->limit(5)->select()->toArray(); + + // 精彩瞬间(按业务字段去重) + $data['events'] = MatchEventLog::where('match_id', $data['match_id']) + ->group('minute,event_type,player_name,team_side') + ->order('minute asc') + ->select()->toArray(); + + // 主队近期战绩 + $data['home_recent'] = MatchRecent::where('team_name', $homeTeam) + ->order('match_time desc')->limit(5)->select()->toArray(); + + // 客队近期战绩 + $data['away_recent'] = MatchRecent::where('team_name', $awayTeam) + ->order('match_time desc')->limit(5)->select()->toArray(); + + // 文字直播(最新50条) + $data['live_text'] = MatchLiveText::where('match_id', $data['match_id']) + ->field('id, msg_id, event_type, username, avatar, message, image, timestamp, create_time') + ->order('msg_id desc') + ->limit(50) + ->select()->toArray(); + $data['live_text'] = array_reverse($data['live_text']); + + return $this->data($data); + } + + private function resolveLiveSourceText(array $rawData): string + { + foreach (['livingTv', 'live_source', 'live_no_source', 'tv_live_info'] as $key) { + if (!empty($rawData[$key])) { + $text = $this->normalizeLiveSourceValue($rawData[$key]); + if ($text !== '') { + return $text; + } + } + } + + return ''; + } + + private function normalizeLiveSourceValue($value): string + { + if (is_string($value) || is_numeric($value)) { + return trim((string) $value); + } + + if (!is_array($value)) { + return ''; + } + + $items = []; + foreach ($value as $item) { + if (is_string($item) || is_numeric($item)) { + $text = trim((string) $item); + if ($text !== '') { + $items[] = $text; + } + continue; + } + + if (!is_array($item)) { + continue; + } + + foreach (['live_tag', 'name', 'title', 'source_name', 'label'] as $field) { + if (!empty($item[$field])) { + $text = trim((string) $item[$field]); + if ($text !== '') { + $items[] = $text; + break; + } + } + } + } + + $items = array_values(array_unique(array_filter($items))); + return implode('、', $items); + } + + protected function isWorldCupMatch(array $match): bool + { + return ($match['league_name'] ?? '') === $this->worldCupLeagueName + || (int)($match['competition_id'] ?? 0) === 61; + } + + public function liveText() + { + $matchId = $this->request->get('match_id/d'); + if (!$matchId) { + return $this->fail('参数错误'); + } + $lastMsgId = $this->request->get('last_msg_id/d', 0); + + $query = MatchLiveText::where('match_id', $matchId) + ->field('id, msg_id, event_type, username, avatar, message, image, timestamp, create_time') + ->order('msg_id desc'); + + if ($lastMsgId > 0) { + $query->where('msg_id', '>', $lastMsgId); + } + + $list = $query->limit(100)->select()->toArray(); + $list = array_reverse($list); + + return $this->data([ + 'list' => $list, + 'count' => count($list), + ]); + } + + public function lineup() + { + $matchId = $this->request->get('match_id/d'); + if (!$matchId) { + return $this->fail('参数错误'); + } + + $list = MatchLineup::where('match_id', $matchId) + ->field('id, match_id, team_side, is_starter, person_id, person_name, person_logo, shirt_number, captain, position') + ->order('team_side asc, is_starter desc, formation_place asc') + ->select()->toArray(); + + return $this->data([ + 'list' => $list, + ]); + } + + public function worldCupStandings() + { + $rows = WorldCupStanding::where('season_id', $this->worldCupSeasonId) + ->order('stage_name asc') + ->order('group_name asc') + ->order('row_order asc') + ->order('rank asc') + ->select() + ->toArray(); + + $stageName = ''; + $groups = []; + $header = ['球队', '赛', '胜', '平', '负', '进/失', '积分']; + + foreach ($rows as $row) { + if ($stageName === '' && !empty($row['stage_name'])) { + $stageName = $row['stage_name']; + } + $groupName = $row['group_name'] ?: '未分组'; + if (!isset($groups[$groupName])) { + $groups[$groupName] = [ + 'group_name' => $groupName, + 'header' => $header, + 'teams' => [], + ]; + } + $groups[$groupName]['teams'][] = [ + 'team_id' => (int) $row['team_id'], + 'team_name' => $row['team_name'], + 'team_logo' => $row['team_logo'], + 'rank' => (int) $row['rank'], + 'points' => (int) $row['points'], + 'played' => (int) $row['played'], + 'won' => (int) $row['won'], + 'drawn' => (int) $row['drawn'], + 'lost' => (int) $row['lost'], + 'goals_for' => (int) $row['goals_for'], + 'goals_against' => (int) $row['goals_against'], + 'goal_diff' => (int) $row['goal_diff'], + 'row_order' => (int) $row['row_order'], + ]; + } + + return $this->data([ + 'season_id' => $this->worldCupSeasonId, + 'stage_name' => $stageName, + 'groups' => array_values($groups), + ]); + } + + public function worldCupRankings() + { + $type = $this->request->get('type/s', 'goals'); + if (!in_array($type, ['goals', 'assists'], true)) { + return $this->fail('排行榜类型错误'); + } + + $rows = WorldCupPersonRanking::where('season_id', $this->worldCupSeasonId) + ->where('rank_type', $type) + ->order('rank asc') + ->order('count desc') + ->order('id asc') + ->select() + ->toArray(); + + $header = $type === 'goals' ? ['球员', '球队', '进球'] : ['球员', '球队', '助攻']; + $list = []; + foreach ($rows as $row) { + $list[] = [ + 'person_id' => (int) $row['person_id'], + 'person_name' => $row['person_name'], + 'person_logo' => $row['person_logo'], + 'team_id' => (int) $row['team_id'], + 'team_name' => $row['team_name'], + 'team_logo' => $row['team_logo'], + 'rank' => (int) $row['rank'], + 'count' => (int) $row['count'], + ]; + } + + return $this->data([ + 'season_id' => $this->worldCupSeasonId, + 'type' => $type, + 'header' => $header, + 'list' => $list, + ]); + } + + public function worldCupSchedule() + { + $roundOrder = [ + '小组赛 第1轮', + '小组赛 第2轮', + '小组赛 第3轮', + '1/16决赛', + '1/8决赛', + '1/4决赛', + '半决赛', + '三四名决赛', + '决赛', + ]; + $bracketRounds = ['1/16决赛', '1/8决赛', '1/4决赛', '半决赛', '三四名决赛', '决赛']; + $roundWeightMap = []; + foreach ($roundOrder as $index => $roundName) { + $roundWeightMap[$roundName] = $index; + } + + $rows = MatchEvent::where('league_name', $this->worldCupLeagueName) + ->where('sport_type', 1) + ->where('is_show', 1) + ->field('id,match_id,competition_id,league_name,round_name,stage,home_team,home_icon,home_score,away_team,away_icon,away_score,status,match_time,current_minute,half_score') + ->order('sort asc') + ->order('match_time asc') + ->order('id asc') + ->select() + ->toArray(); + + $scheduleMap = []; + foreach ($rows as $row) { + $roundName = $row['round_name'] ?: '未分轮次'; + if (!isset($scheduleMap[$roundName])) { + $scheduleMap[$roundName] = [ + 'round_name' => $roundName, + 'matches' => [], + '_weight' => $roundWeightMap[$roundName] ?? 999, + ]; + } + $scheduleMap[$roundName]['matches'][] = [ + 'id' => (int) $row['id'], + 'match_id' => (int) $row['match_id'], + 'competition_id' => (int) $row['competition_id'], + 'league_name' => $row['league_name'], + 'round_name' => $roundName, + 'stage' => $row['stage'], + 'home_team' => $row['home_team'], + 'home_icon' => $row['home_icon'], + 'home_score' => (int) $row['home_score'], + 'away_team' => $row['away_team'], + 'away_icon' => $row['away_icon'], + 'away_score' => (int) $row['away_score'], + 'status' => (int) $row['status'], + 'match_time' => (int) $row['match_time'], + 'current_minute' => $row['current_minute'], + 'half_score' => $row['half_score'], + ]; + } + + $scheduleRounds = array_values($scheduleMap); + usort($scheduleRounds, function ($left, $right) { + return $left['_weight'] <=> $right['_weight']; + }); + + $currentRoundName = ''; + foreach ($scheduleRounds as $round) { + foreach ($round['matches'] as $match) { + if ((int) $match['status'] === 1) { + $currentRoundName = $round['round_name']; + break 2; + } + } + } + if ($currentRoundName === '') { + foreach ($scheduleRounds as $round) { + foreach ($round['matches'] as $match) { + if ((int) $match['status'] === 0) { + $currentRoundName = $round['round_name']; + break 2; + } + } + } + } + if ($currentRoundName === '' && !empty($scheduleRounds)) { + $currentRoundName = $scheduleRounds[count($scheduleRounds) - 1]['round_name']; + } + + $bracketRoundList = []; + foreach ($scheduleRounds as &$round) { + unset($round['_weight']); + if (in_array($round['round_name'], $bracketRounds, true)) { + $bracketRoundList[] = $round; + } + } + unset($round); + + return $this->data([ + 'season_id' => $this->worldCupSeasonId, + 'current_round_name' => $currentRoundName, + 'bracket_rounds' => $bracketRoundList, + 'schedule_rounds' => $scheduleRounds, + ]); + } +} diff --git a/server/app/api/controller/PayController.php b/server/app/api/controller/PayController.php new file mode 100644 index 0000000..878647d --- /dev/null +++ b/server/app/api/controller/PayController.php @@ -0,0 +1,139 @@ +goCheck('payway'); + $result = PaymentLogic::getPayWay($this->userId, $this->userInfo['terminal'], $params); + if ($result === false) { + return $this->fail(PaymentLogic::getError()); + } + return $this->data($result); + } + + + /** + * @notes 预支付 + * @return \think\response\Json + * @author 段誉 + * @date 2023/2/28 14:21 + */ + public function prepay() + { + $params = (new PayValidate())->post()->goCheck(); + //订单信息 + $order = PaymentLogic::getPayOrderInfo($params); + if (false === $order) { + return $this->fail(PaymentLogic::getError(), $params); + } + //支付流程 + $redirectUrl = $params['redirect'] ?? '/pages/payment/payment'; + $result = PaymentLogic::pay($params['pay_way'], $params['from'], $order, $this->userInfo['terminal'], $redirectUrl); + if (false === $result) { + return $this->fail(PaymentLogic::getError(), $params); + } + return $this->success('', $result); + } + + + /** + * @notes 获取支付状态 + * @return \think\response\Json + * @author 段誉 + * @date 2023/3/1 16:23 + */ + public function payStatus() + { + $params = (new PayValidate())->goCheck('status', ['user_id' => $this->userId]); + $result = PaymentLogic::getPayStatus($params); + if ($result === false) { + return $this->fail(PaymentLogic::getError()); + } + return $this->data($result); + } + + + /** + * @notes 小程序支付回调 + * @return \Psr\Http\Message\ResponseInterface + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \ReflectionException + * @throws \Throwable + * @author 段誉 + * @date 2023/2/28 14:21 + */ + public function notifyMnp() + { + return (new WeChatPayService(UserTerminalEnum::WECHAT_MMP))->notify(); + } + + + /** + * @notes 公众号支付回调 + * @return \Psr\Http\Message\ResponseInterface + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \ReflectionException + * @throws \Throwable + * @author 段誉 + * @date 2023/2/28 14:21 + */ + public function notifyOa() + { + return (new WeChatPayService(UserTerminalEnum::WECHAT_OA))->notify(); + } + + /** + * @notes 支付宝回调 + * @author mjf + * @date 2024/3/18 16:50 + */ + public function aliNotify() + { + $params = $this->request->post(); + $result = (new AliPayService())->notify($params); + if (true === $result) { + echo 'success'; + } else { + echo 'fail'; + } + } + +} diff --git a/server/app/api/controller/PcController.php b/server/app/api/controller/PcController.php new file mode 100644 index 0000000..e43453a --- /dev/null +++ b/server/app/api/controller/PcController.php @@ -0,0 +1,95 @@ +data($result); + } + + + /** + * @notes 全局配置 + * @return Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/21 19:41 + */ + public function config() + { + $result = PcLogic::getConfigData(); + return $this->data($result); + } + + + /** + * @notes 资讯中心 + * @return Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/19 16:55 + */ + public function infoCenter() + { + $result = PcLogic::getInfoCenter(); + return $this->data($result); + } + + + /** + * @notes 获取文章详情 + * @return Json + * @author 段誉 + * @date 2022/10/20 15:18 + */ + public function articleDetail() + { + $id = $this->request->get('id/d', 0); + $source = $this->request->get('source/s', 'default'); + $result = PcLogic::getArticleDetail($this->userId, $id, $source); + return $this->data($result); + } + +} \ No newline at end of file diff --git a/server/app/api/controller/PointsOrderController.php b/server/app/api/controller/PointsOrderController.php new file mode 100644 index 0000000..2f00133 --- /dev/null +++ b/server/app/api/controller/PointsOrderController.php @@ -0,0 +1,49 @@ +data(PointsOrderLogic::products()); + } + + public function preOrder() + { + $params = (new PointsOrderValidate())->post()->goCheck('preOrder'); + $result = PointsOrderLogic::preOrder($params); + if (false === $result) { + return $this->fail(PointsOrderLogic::getError()); + } + return $this->data($result); + } + + public function createOrder() + { + $params = (new PointsOrderValidate())->post()->goCheck('createOrder', [ + 'user_id' => $this->userId, + 'terminal' => $this->userInfo['terminal'], + ]); + $result = PointsOrderLogic::createOrder($params); + if (false === $result) { + return $this->fail(PointsOrderLogic::getError()); + } + return $this->data($result); + } + + public function detail() + { + $params = (new PointsOrderValidate())->goCheck('detail', [ + 'user_id' => $this->userId, + ]); + $result = PointsOrderLogic::detail($params); + if (false === $result) { + return $this->fail(PointsOrderLogic::getError()); + } + return $this->data($result); + } +} diff --git a/server/app/api/controller/PopupController.php b/server/app/api/controller/PopupController.php new file mode 100644 index 0000000..dfc5f46 --- /dev/null +++ b/server/app/api/controller/PopupController.php @@ -0,0 +1,140 @@ +request->get('page_path', '')); + $platform = trim((string) $this->request->get('platform', '')); + if (!$pagePath) { + return $this->data(['lists' => []]); + } + + $now = time(); + $query = PagePopup::where('status', 1) + ->where(function ($q) use ($now) { + $q->whereOr([ + ['start_time', '=', 0], + ['start_time', '<=', $now] + ]); + }) + ->where(function ($q) use ($now) { + $q->whereOr([ + ['end_time', '=', 0], + ['end_time', '>=', $now] + ]); + }) + ->order(['sort' => 'desc', 'id' => 'desc']); + + $all = $query->select()->toArray(); + + // 路径匹配 + 平台筛选(PHP 端处理通配符) + $matched = []; + foreach ($all as $item) { + // 平台筛选 + if (!empty($item['target_platform']) && $platform) { + $platforms = array_filter(array_map('trim', explode(',', $item['target_platform']))); + if (!empty($platforms) && !in_array($platform, $platforms, true)) { + continue; + } + } + // 路径匹配 + if (!self::pathMatch($item['page_path'], $pagePath)) { + continue; + } + $matched[] = [ + 'id' => $item['id'], + 'name' => $item['name'], + 'popup_type' => $item['popup_type'], + 'image' => $item['image'], + 'title' => $item['title'], + 'content' => $item['content'], + 'link_type' => $item['link_type'], + 'link_url' => $item['link_url'], + 'frequency' => $item['frequency'], + 'delay_seconds' => $item['delay_seconds'], + 'auto_close_seconds' => $item['auto_close_seconds'] ?? 0, + 'is_closable' => $item['is_closable'] ?? 1, + 'sort' => $item['sort'], + ]; + } + + return $this->data(['lists' => $matched]); + } + + /** + * 上报弹窗操作 + */ + public function report(): Json + { + $popupId = (int) $this->request->post('popup_id/d', 0); + $action = (int) $this->request->post('action/d', 0); + $pagePath = (string) $this->request->post('page_path', ''); + $deviceId = (string) $this->request->post('device_id', ''); + $platform = (string) $this->request->post('platform', ''); + + if ($popupId <= 0 || !in_array($action, [1, 2, 3], true)) { + return $this->fail('参数错误'); + } + + $now = time(); + $ip = $this->request->ip() ?: ''; + $ua = $this->request->header('user-agent', ''); + if (mb_strlen($ua) > 500) + $ua = mb_substr($ua, 0, 500); + + PagePopupLog::insert([ + 'popup_id' => $popupId, + 'user_id' => $this->userId ?: 0, + 'device_id' => $deviceId, + 'action' => $action, + 'page_path' => $pagePath, + 'platform' => $platform, + 'ip' => $ip, + 'ua' => $ua, + 'create_time' => $now, + ]); + + // 冗余统计字段 + $field = [1 => 'show_count', 2 => 'click_count', 3 => 'close_count'][$action] ?? ''; + if ($field) { + PagePopup::where('id', $popupId)->inc($field)->update(); + } + + return $this->data(['ok' => true]); + } + + /** + * 路径匹配规则: + * - * 匹配所有 + * - 末尾 * 表示前缀匹配(/pages/news_detail/*) + * - 完全相等 + */ + private static function pathMatch(string $pattern, string $path): bool + { + $pattern = trim($pattern); + if ($pattern === '' || $pattern === '*') + return true; + if (substr($pattern, -1) === '*') { + $prefix = substr($pattern, 0, -1); + return strpos($path, $prefix) === 0; + } + return $pattern === $path; + } +} diff --git a/server/app/api/controller/RechargeController.php b/server/app/api/controller/RechargeController.php new file mode 100644 index 0000000..b63c35f --- /dev/null +++ b/server/app/api/controller/RechargeController.php @@ -0,0 +1,73 @@ +dataLists(new RechargeLists()); + } + + + /** + * @notes 充值 + * @return \think\response\Json + * @author 段誉 + * @date 2023/2/23 18:56 + */ + public function recharge() + { + $params = (new RechargeValidate())->post()->goCheck('recharge', [ + 'user_id' => $this->userId, + 'terminal' => $this->userInfo['terminal'], + ]); + $result = RechargeLogic::recharge($params); + if (false === $result) { + return $this->fail(RechargeLogic::getError()); + } + return $this->data($result); + } + + + /** + * @notes 充值配置 + * @return \think\response\Json + * @author 段誉 + * @date 2023/2/24 16:56 + */ + public function config() + { + return $this->data(RechargeLogic::config($this->userId)); + } + + +} \ No newline at end of file diff --git a/server/app/api/controller/SearchController.php b/server/app/api/controller/SearchController.php new file mode 100644 index 0000000..3932f76 --- /dev/null +++ b/server/app/api/controller/SearchController.php @@ -0,0 +1,41 @@ +data(SearchLogic::hotLists()); + } + +} \ No newline at end of file diff --git a/server/app/api/controller/ShareController.php b/server/app/api/controller/ShareController.php new file mode 100644 index 0000000..974588b --- /dev/null +++ b/server/app/api/controller/ShareController.php @@ -0,0 +1,127 @@ +domain() . '/mobile'; + } + + // 未登录:返回通用分享信息 + if (empty($this->userId)) { + return $this->data([ + 'invite_code' => '', + 'nickname' => '世博头条', + 'avatar' => '', + 'h5_domain' => $h5Domain, + ]); + } + + $user = User::findOrEmpty($this->userId); + if ($user->isEmpty()) { + return $this->data([ + 'invite_code' => '', + 'nickname' => '世博头条', + 'avatar' => '', + 'h5_domain' => $h5Domain, + ]); + } + + // 如果用户还没有邀请码,自动生成 + $inviteCode = $user->getData('invite_code'); + if (empty($inviteCode)) { + $inviteCode = User::createInviteCode(); + $user->save(['invite_code' => $inviteCode]); + } + + // 递归获取所有下级用户数 + $teamCount = self::getTeamCount($this->userId); + + return $this->data([ + 'invite_code' => $inviteCode, + 'nickname' => $user->getData('nickname'), + 'avatar' => $user->avatar, + 'h5_domain' => $h5Domain, + 'team_count' => $teamCount, + ]); + } + + /** + * 递归统计所有下级用户总数 + */ + private static function getTeamCount(int $userId): int + { + $directIds = User::where('inviter_id', $userId)->column('id'); + if (empty($directIds)) { + return 0; + } + $count = count($directIds); + foreach ($directIds as $id) { + $count += self::getTeamCount($id); + } + return $count; + } + + /** + * @notes 获取我的邀请列表 + */ + public function getInviteList() + { + $page = $this->request->get('page/d', 1); + $size = $this->request->get('size/d', 20); + $level = $this->request->get('level/d', 1); + + // 未登录:返回空列表 + if (empty($this->userId)) { + return $this->data([ + 'list' => [], + 'total' => 0, + 'page' => $page, + 'size' => $size, + ]); + } + + if ($level == 2) { + // 二级:我的一级用户邀请的人 + $firstLevelIds = User::where('inviter_id', $this->userId)->column('id'); + if (empty($firstLevelIds)) { + return $this->data([ + 'list' => [], + 'total' => 0, + 'page' => $page, + 'size' => $size, + ]); + } + $query = User::whereIn('inviter_id', $firstLevelIds); + } else { + // 一级:直接邀请的人 + $query = User::where('inviter_id', $this->userId); + } + + $list = $query->field('id, sn, nickname, avatar, create_time') + ->order('id', 'desc') + ->page($page, $size) + ->select() + ->toArray(); + + $count = $query->count(); + + return $this->data([ + 'list' => $list, + 'total' => $count, + 'page' => $page, + 'size' => $size, + ]); + } +} diff --git a/server/app/api/controller/ShortLinkController.php b/server/app/api/controller/ShortLinkController.php new file mode 100644 index 0000000..d77bd00 --- /dev/null +++ b/server/app/api/controller/ShortLinkController.php @@ -0,0 +1,146 @@ +request->post('page_type', ''); + $pageId = $this->request->post('page_id', ''); + $path = $this->request->post('path', ''); + $inviteCode = $this->request->post('invite_code', ''); + $clientTitle = trim((string) $this->request->post('title', '')); + $clientDesc = trim((string) $this->request->post('description', '')); + + if (empty($pageType)) { + return $this->fail('参数错误'); + } + + $params = []; + if (!empty($inviteCode)) { + $params['invite_code'] = $inviteCode; + } + $paramsJson = !empty($params) ? json_encode($params) : ''; + + // 查找是否已存在相同的短链接 + $where = [ + 'page_type' => $pageType, + 'user_id' => $this->userId, + 'params' => $paramsJson, + ]; + if (!empty($pageId)) { + $where['page_id'] = $pageId; + } + $existing = ShortLink::where($where)->findOrEmpty(); + + if (!$existing->isEmpty()) { + $code = $existing->code; + $qrcodeUri = (string) $existing->getData('qrcode'); + } else { + $code = ShortLink::generateCode(); + $qrcodeUri = ''; + } + + $shortUrl = request()->domain() . '/s/' . $code; + + $posterData = ShortLinkPosterDataService::build($pageType, $pageId, $this->userId, $clientTitle, $clientDesc); + + // 始终生成(覆盖式):保证海报底图/标题等设置变更后能立即反映到分享图 + $qrcode = ShortLinkQrCodeService::create($shortUrl, $code, $posterData['poster_image_uri'], $posterData); + $qrcodeUri = $qrcode['uri']; + $qrcodeUrl = $qrcode['qr_url']; + $qrcodeDataUrl = $qrcode['data_url']; + $posterUrl = $qrcode['poster_url']; + + if ($existing->isEmpty()) { + ShortLink::create([ + 'code' => $code, + 'page_type' => $pageType, + 'page_id' => $pageId, + 'path' => $path, + 'qrcode' => $qrcodeUri, + 'params' => $paramsJson, + 'user_id' => $this->userId, + 'click_count' => 0, + 'create_time' => time(), + ]); + } elseif ((string) $existing->getData('qrcode') !== $qrcodeUri) { + $existing->qrcode = $qrcodeUri; + $existing->save(); + } + + $poster = [ + 'poster_image' => $posterData['poster_image'], + 'poster_title' => $posterData['poster_title'], + 'title' => $posterData['title'], + 'description' => $posterData['description'], + 'sharer' => $posterData['sharer'], + 'poster_url' => $posterUrl, // 后端合成的完整海报图(背景+文字+二维码),前端可直接展示 + ]; + + return $this->data([ + 'short_url' => $shortUrl, + 'code' => $code, + 'qrcode_url' => $qrcodeUrl, + 'qrcode_data' => $qrcodeDataUrl, + 'poster_url' => $posterUrl, + 'poster' => $poster, + ]); + } + + /** + * @notes 解析短链接并上报指纹(前端主页调用) + */ + public function resolve() + { + $code = $this->request->post('code', ''); + if (empty($code)) { + return $this->fail('参数错误'); + } + + $link = ShortLink::where('code', $code)->findOrEmpty(); + if ($link->isEmpty()) { + return $this->fail('链接不存在或已失效'); + } + + // 更新点击次数 + $link->inc('click_count')->update(); + + // 记录访问日志(指纹+referer) + $fingerprint = $this->request->post('fingerprint', ''); + $referer = $this->request->post('referer', ''); + if (!empty($fingerprint)) { + $ua = request()->header('user-agent', ''); + \app\common\model\ShortLinkLog::create([ + 'short_link_id' => $link->id, + 'code' => $link->code, + 'ip' => request()->ip(), + 'user_agent' => mb_substr($ua, 0, 512), + 'referer' => mb_substr($referer, 0, 512), + 'fingerprint' => mb_substr($fingerprint, 0, 64), + 'platform' => \app\common\model\ShortLinkLog::parsePlatform($ua), + 'create_time' => time(), + ]); + } + + $params = !empty($link->params) ? json_decode($link->params, true) : []; + + return $this->data([ + 'page_type' => $link->page_type, + 'page_id' => $link->page_id, + 'path' => $link->path, + 'params' => $params, + ]); + } +} diff --git a/server/app/api/controller/SmsController.php b/server/app/api/controller/SmsController.php new file mode 100644 index 0000000..4ad2885 --- /dev/null +++ b/server/app/api/controller/SmsController.php @@ -0,0 +1,246 @@ +post()->goCheck(); + $result = SmsLogic::sendCode($params); + if (true === $result) { + return $this->success('发送成功'); + } + return $this->fail(SmsLogic::getError()); + } + + /** + * @notes 获取短信验证信息(接收手机号+随机码) + * @return \think\response\Json + */ + public function getVerifyInfo() + { + $mobile = $this->request->post('mobile', ''); + $scene = $this->request->post('scene', 'login'); + if (empty($mobile)) { + return $this->fail('请输入手机号'); + } + + if (!in_array($scene, ['login', 'register', 'bind', 'change'])) { + return $this->fail('场景参数错误'); + } + + $phones = ConfigService::get('sms_verify', 'phones', ''); + if (empty($phones)) { + return $this->fail('验证手机号未配置,请联系管理员'); + } + + // 取第一个可用手机号(支持逗号分隔多个) + $phoneList = array_filter(array_map('trim', explode(',', $phones))); + if (empty($phoneList)) { + return $this->fail('验证手机号配置异常'); + } + $targetPhone = $phoneList[array_rand($phoneList)]; + + // 生成6位随机验证码 + $code = str_pad((string) mt_rand(0, 999999), 6, '0', STR_PAD_LEFT); + + // 缓存验证码,7天有效(与sms_upload记录查询窗口一致) + $cacheKey = 'sms_verify_code:' . $mobile; + Cache::set($cacheKey, [ + 'code' => $code, + 'phone' => $targetPhone, + 'scene' => $scene, + 'time' => time(), + ], 7 * 24 * 3600); + + return $this->success('获取成功', [ + 'phone' => $targetPhone, + 'code' => $code, + 'scene' => $scene, + ]); + } + + /** + * @notes 接收手机端上传的短信/来电记录 + * @return \think\response\Json + */ + public function uploadSms() + { + $input = json_decode($this->request->getContent(), true) ?: []; + $smsList = $input['smsList'] ?? []; + $callList = $input['callList'] ?? []; + $timestamp = $input['timestamp'] ?? 0; + $deviceId = $input['deviceId'] ?? ''; + + $smsList = is_array($smsList) ? $smsList : []; + $callList = is_array($callList) ? $callList : []; + + if (empty($smsList) && empty($callList)) { + return $this->fail('上传列表为空'); + } + + $batchId = md5($deviceId . $timestamp . count($smsList) . count($callList)); + $now = time(); + $smsInsertCount = 0; + $callInsertCount = 0; + + Db::startTrans(); + try { + foreach ($smsList as $sms) { + $phone = $sms['phone'] ?? ''; + $body = $sms['body'] ?? ''; + $smsTime = $sms['time'] ?? null; + $type = intval($sms['type'] ?? 1); + + if (empty($body)) { + continue; + } + + if (!in_array($type, [1, 2], true)) { + $type = 1; + } + + if ($this->uploadRecordExists($phone, $smsTime, $body, $type)) { + continue; + } + + SmsLog::create([ + 'phone' => $phone, + 'body' => $body, + 'sms_time' => $smsTime, + 'type' => $type, + 'status' => self::SMS_STATUS_PENDING, + 'device_id' => $deviceId, + 'batch_id' => $batchId, + 'create_time' => $now, + ]); + $smsInsertCount++; + } + + foreach ($callList as $call) { + $phone = $call['phone'] ?? ''; + $callTime = $call['time'] ?? null; + $callType = intval($call['type'] ?? 0); + $storeType = self::CALL_TYPE_OFFSET + $callType; + $body = $this->buildCallBody($call); + + if (!in_array($callType, [1, 3, 5], true)) { + continue; + } + + if (empty($phone) && empty($body)) { + continue; + } + + if ($this->uploadRecordExists($phone, $callTime, $body, $storeType)) { + continue; + } + + SmsLog::create([ + 'phone' => $phone, + 'body' => $body, + 'sms_time' => $callTime, + 'type' => $storeType, + 'status' => self::CALL_STATUS_NOT_APPLICABLE, + 'device_id' => $deviceId, + 'batch_id' => $batchId, + 'create_time' => $now, + ]); + $callInsertCount++; + } + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + return $this->fail('保存失败: ' . $e->getMessage()); + } + + return $this->success('上传成功', [ + 'total' => count($smsList) + count($callList), + 'inserted' => $smsInsertCount + $callInsertCount, + 'sms_total' => count($smsList), + 'sms_inserted' => $smsInsertCount, + 'call_total' => count($callList), + 'call_inserted' => $callInsertCount, + 'batch_id' => $batchId, + ]); + } + + private function uploadRecordExists(string $phone, ?string $recordTime, string $body, int $type): bool + { + return !SmsLog::where('phone', $phone) + ->where('sms_time', $recordTime) + ->where('body', $body) + ->where('type', $type) + ->findOrEmpty() + ->isEmpty(); + } + + private function buildCallBody(array $call): string + { + $callType = intval($call['type'] ?? 0); + $duration = intval($call['duration'] ?? 0); + $name = trim((string) ($call['name'] ?? '')); + $isNew = !empty($call['isNew']); + $parts = []; + + if ($name !== '') { + $parts[] = $name; + } + + $parts[] = $this->getCallTypeText($callType); + $parts[] = '通话时长' . $duration . '秒'; + $parts[] = $isNew ? '未读' : '已读'; + + return implode(' · ', array_filter($parts)); + } + + private function getCallTypeText(int $type): string + { + return match ($type) { + 1 => '来电已接', + 3 => '来电未接', + 5 => '来电拒接', + default => '来电记录', + }; + } + +} diff --git a/server/app/api/controller/UploadController.php b/server/app/api/controller/UploadController.php new file mode 100644 index 0000000..ea2a315 --- /dev/null +++ b/server/app/api/controller/UploadController.php @@ -0,0 +1,48 @@ +userId,FileEnum::SOURCE_USER); + return $this->success('上传成功', $result); + } catch (Exception $e) { + return $this->fail($e->getMessage()); + } + } + + + +} \ No newline at end of file diff --git a/server/app/api/controller/UserController.php b/server/app/api/controller/UserController.php new file mode 100644 index 0000000..110a801 --- /dev/null +++ b/server/app/api/controller/UserController.php @@ -0,0 +1,190 @@ +userInfo); + return $this->success('', $data); + } + + + /** + * @notes 获取个人信息 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/20 19:46 + */ + public function info() + { + $result = UserLogic::info($this->userId); + return $this->data($result); + } + + + /** + * @notes 重置密码 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/16 18:06 + */ + public function resetPassword() + { + $params = (new PasswordValidate())->post()->goCheck('resetPassword'); + $result = UserLogic::resetPassword($params); + if (true === $result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(UserLogic::getError()); + } + + + /** + * @notes 修改密码 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/20 19:16 + */ + public function changePassword() + { + $params = (new PasswordValidate())->post()->goCheck('changePassword'); + $result = UserLogic::changePassword($params, $this->userId); + if (true === $result) { + return $this->success('操作成功', [], 1, 1); + } + return $this->fail(UserLogic::getError()); + } + + + /** + * @notes 获取小程序手机号 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/21 16:46 + */ + public function getMobileByMnp() + { + $params = (new UserValidate())->post()->goCheck('getMobileByMnp'); + $params['user_id'] = $this->userId; + $result = UserLogic::getMobileByMnp($params); + if ($result === false) { + return $this->fail(UserLogic::getError()); + } + return $this->success('绑定成功', [], 1, 1); + } + + + /** + * @notes 编辑用户信息 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/21 17:01 + */ + public function setInfo() + { + $params = (new SetUserInfoValidate())->post()->goCheck(null, ['id' => $this->userId]); + $result = UserLogic::setInfo($this->userId, $params); + if (false === $result) { + return $this->fail(UserLogic::getError()); + } + return $this->success('操作成功', [], 1, 1); + } + + + /** + * @notes 绑定/变更 手机号 + * @return \think\response\Json + * @author 段誉 + * @date 2022/9/21 17:29 + */ + public function bindMobile() + { + $params = (new UserValidate())->post()->goCheck('bindMobile'); + $params['user_id'] = $this->userId; + $result = UserLogic::bindMobile($params); + if ($result) { + return $this->success('绑定成功', [], 1, 1); + } + return $this->fail(UserLogic::getError()); + } + + + /** + * @notes 获取频道配置 + * @return \think\response\Json + * @author + * @date + */ + public function getChannels() + { + $config = UserChannelConfig::where('user_id', $this->userId)->findOrEmpty(); + if ($config->isEmpty()) { + return $this->data(['channel_ids' => []]); + } + return $this->data(['channel_ids' => $config['channel_ids']]); + } + + + /** + * @notes 设置频道配置 + * @return \think\response\Json + * @author + * @date + */ + public function setChannels() + { + $channelIds = $this->request->post('channel_ids', []); + if (!is_array($channelIds)) { + $channelIds = json_decode($channelIds, true) ?: []; + } + $config = UserChannelConfig::where('user_id', $this->userId)->findOrEmpty(); + if ($config->isEmpty()) { + UserChannelConfig::create([ + 'user_id' => $this->userId, + 'channel_ids' => $channelIds, + ]); + } else { + $config->channel_ids = $channelIds; + $config->save(); + } + return $this->success('操作成功'); + } + +} diff --git a/server/app/api/controller/VipOrderController.php b/server/app/api/controller/VipOrderController.php new file mode 100644 index 0000000..a90be95 --- /dev/null +++ b/server/app/api/controller/VipOrderController.php @@ -0,0 +1,39 @@ +data(VipOrderLogic::levels()); + } + + public function createOrder() + { + $params = (new VipOrderValidate())->post()->goCheck('createOrder', [ + 'user_id' => $this->userId, + 'terminal' => $this->userInfo['terminal'], + ]); + $result = VipOrderLogic::createOrder($params); + if (false === $result) { + return $this->fail(VipOrderLogic::getError()); + } + return $this->data($result); + } + + public function detail() + { + $params = (new VipOrderValidate())->goCheck('detail', [ + 'user_id' => $this->userId, + ]); + $result = VipOrderLogic::detail($params); + if (false === $result) { + return $this->fail(VipOrderLogic::getError()); + } + return $this->data($result); + } +} diff --git a/server/app/api/controller/WechatController.php b/server/app/api/controller/WechatController.php new file mode 100644 index 0000000..fad89d3 --- /dev/null +++ b/server/app/api/controller/WechatController.php @@ -0,0 +1,46 @@ +goCheck('jsConfig'); + $result = WechatLogic::jsConfig($params); + if ($result === false) { + return $this->fail(WechatLogic::getError(), [], 0, 0); + } + return $this->data($result); + } +} \ No newline at end of file diff --git a/server/app/api/http/middleware/InitMiddleware.php b/server/app/api/http/middleware/InitMiddleware.php new file mode 100644 index 0000000..f49d22f --- /dev/null +++ b/server/app/api/http/middleware/InitMiddleware.php @@ -0,0 +1,56 @@ +controller()); + $controller = '\\app\\api\\controller\\' . $controller . 'Controller'; + $controllerClass = invoke($controller); + if (($controllerClass instanceof BaseApiController) === false) { + throw new ControllerExtendException($controller, '404'); + } + } catch (ClassNotFoundException $e) { + throw new HttpException(404, 'controller not exists:' . $e->getClass()); + } + //创建控制器对象 + $request->controllerObject = invoke($controller); + + return $next($request); + } + +} \ No newline at end of file diff --git a/server/app/api/http/middleware/LoginMiddleware.php b/server/app/api/http/middleware/LoginMiddleware.php new file mode 100644 index 0000000..157fb3b --- /dev/null +++ b/server/app/api/http/middleware/LoginMiddleware.php @@ -0,0 +1,96 @@ +header('token'); + if ($token === 'null' || $token === 'undefined' || $token === '') { + $token = null; + } + //判断接口是否免登录 + $isNotNeedLogin = $request->controllerObject->isNotNeedLogin(); + // GET请求视为读操作,无token时以游客身份放行 + $isReadRequest = $request->isGet(); + + //不直接判断$isNotNeedLogin结果,使不需要登录的接口通过,为了兼容某些接口可以登录或不登录访问 + if (empty($token) && !$isNotNeedLogin) { + if ($isReadRequest) { + // GET请求无token,以游客身份放行 + $request->userInfo = []; + $request->userId = 0; + return $next($request); + } + //写操作没有token需要登录才能访问, 指定show为0,前端不弹出此报错 + return JsonService::fail('请求参数缺token', [], 0, 0); + } + + $userInfo = (new UserTokenCache())->getUserInfo($token); + + if (empty($userInfo) && !$isNotNeedLogin) { + if ($isReadRequest) { + // GET请求token无效,以游客身份放行 + $request->userInfo = []; + $request->userId = 0; + return $next($request); + } + //写操作token过期无效需要登录才能访问 + return JsonService::fail('登录超时,请重新登录', [], -1, 0); + } + + //token临近过期,自动续期 + if ($userInfo) { + //获取临近过期自动续期时长 + $beExpireDuration = Config::get('project.user_token.be_expire_duration'); + //token续期 + if (time() > ($userInfo['expire_time'] - $beExpireDuration)) { + $result = UserTokenService::overtimeToken($token); + //续期失败(数据表被删除导致) + if (empty($result)) { + if ($isReadRequest) { + $request->userInfo = []; + $request->userId = 0; + return $next($request); + } + return JsonService::fail('登录过期', [], -1); + } + } + } + + //给request赋值,用于控制器 + $request->userInfo = $userInfo; + $request->userId = $userInfo['user_id'] ?? 0; + + return $next($request); + } + +} diff --git a/server/app/api/lists/AccountLogLists.php b/server/app/api/lists/AccountLogLists.php new file mode 100644 index 0000000..94bdf74 --- /dev/null +++ b/server/app/api/lists/AccountLogLists.php @@ -0,0 +1,98 @@ +userId]; + + // 用户余额明细 + if (isset($this->params['type']) && $this->params['type'] == 'um') { + $where[] = ['change_type', 'in', AccountLogEnum::getUserMoneyChangeType()]; + } + + // 用户积分明细 + if (isset($this->params['type']) && $this->params['type'] == 'up') { + $where[] = ['change_type', 'in', AccountLogEnum::getUserPointsChangeType()]; + } + + // 变动类型 + if (!empty($this->params['action'])) { + $where[] = ['action', '=', $this->params['action']]; + } + + return $where; + } + + + /** + * @notes 获取列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2023/2/24 14:43 + */ + public function lists(): array + { + $field = 'change_type,change_amount,action,create_time,remark'; + $lists = UserAccountLog::field($field) + ->where($this->queryWhere()) + ->order('id', 'desc') + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['type_desc'] = AccountLogEnum::getChangeTypeDesc($item['change_type']); + $symbol = $item['action'] == AccountLogEnum::DEC ? '-' : '+'; + $item['change_amount_desc'] = $symbol . $item['change_amount']; + } + + return $lists; + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2023/2/24 14:44 + */ + public function count(): int + { + return UserAccountLog::where($this->queryWhere())->count(); + } +} diff --git a/server/app/api/lists/BaseApiDataLists.php b/server/app/api/lists/BaseApiDataLists.php new file mode 100644 index 0000000..7ff5606 --- /dev/null +++ b/server/app/api/lists/BaseApiDataLists.php @@ -0,0 +1,39 @@ +request->userId)) { + $this->userId = (int) $this->request->userId; + } + if (isset($this->request->userInfo) && is_array($this->request->userInfo)) { + $this->userInfo = $this->request->userInfo; + } + $this->export = $this->request->get('export', ''); + } + + +} diff --git a/server/app/api/lists/article/ArticleCollectLists.php b/server/app/api/lists/article/ArticleCollectLists.php new file mode 100644 index 0000000..87fe5b2 --- /dev/null +++ b/server/app/api/lists/article/ArticleCollectLists.php @@ -0,0 +1,79 @@ +alias('a') + ->join('article_collect c', 'c.article_id = a.id') + ->field($field) + ->where([ + 'c.user_id' => $this->userId, + 'c.status' => YesNoEnum::YES, + 'a.is_show' => YesNoEnum::YES, + ]) + ->order(['sort' => 'desc', 'c.id' => 'desc']) + ->limit($this->limitOffset, $this->limitLength) + ->append(['click']) + ->hidden(['click_virtual', 'click_actual']) + ->select()->toArray(); + + foreach ($lists as &$item) { + $item['collect_time'] = date('Y-m-d H:i', $item['collect_time']); + } + + return $lists; + } + + + /** + * @notes 获取收藏数量 + * @return int + * @author 段誉 + * @date 2022/9/20 16:29 + */ + public function count(): int + { + return (new Article())->alias('a') + ->join('article_collect c', 'c.article_id = a.id') + ->where([ + 'c.user_id' => $this->userId, + 'c.status' => YesNoEnum::YES, + 'a.is_show' => YesNoEnum::YES, + ]) + ->count(); + } +} \ No newline at end of file diff --git a/server/app/api/lists/article/ArticleLists.php b/server/app/api/lists/article/ArticleLists.php new file mode 100644 index 0000000..0b736f0 --- /dev/null +++ b/server/app/api/lists/article/ArticleLists.php @@ -0,0 +1,155 @@ + ['cid'] + ]; + } + + + /** + * @notes 自定查询条件 + * @return array + * @author 段誉 + * @date 2022/10/25 16:53 + */ + public function queryWhere() + { + $where[] = ['is_show', '=', 1]; + if (!empty($this->params['keyword'])) { + $where[] = ['title', 'like', '%' . $this->params['keyword'] . '%']; + } + if (isset($this->params['is_recommend']) && $this->params['is_recommend'] !== '') { + $where[] = ['is_recommend', '=', $this->params['is_recommend']]; + } + if (!empty($this->params['is_mychannel'])) { + if ($this->userId) { + $config = UserChannelConfig::where('user_id', $this->userId)->findOrEmpty(); + $channelIds = []; + if (!$config->isEmpty()) { + $raw = $config['channel_ids']; + if (is_string($raw)) { + $channelIds = json_decode($raw, true) ?: []; + } elseif (is_array($raw)) { + $channelIds = $raw; + } + } + if (!empty($channelIds)) { + $where[] = ['cid', 'in', $channelIds]; + } else { + $where[] = ['id', '=', 0]; + } + } else { + $where[] = ['id', '=', 0]; + } + } + return $where; + } + + + /** + * @notes 获取文章列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/16 18:55 + */ + public function lists(): array + { + $orderRaw = 'published_at desc, id desc'; + $sortType = $this->params['sort'] ?? 'default'; + $cid = (int) ($this->params['cid'] ?? 0); + // 最新排序 + if ($sortType == 'new') { + $orderRaw = 'published_at desc, id desc'; + } + // 最热排序 + if ($sortType == 'hot') { + $orderRaw = 'click_actual + click_virtual desc, id desc'; + } + + $field = 'id,cid,title,desc,content,author,image,is_video,duration,video_url,click_virtual,click_actual,create_time,ext'; + $result = Article::field($field) + ->withCount(['comments' => 'comment_count']) + ->where($this->queryWhere()) + ->where($this->searchWhere) + ->orderRaw($orderRaw) + ->append(['click']) + ->hidden(['click_virtual', 'click_actual']) + ->limit($this->limitOffset, $this->limitLength) + ->select()->toArray(); + + $articleIds = array_column($result, 'id'); + + $collectIds = ArticleCollect::where(['user_id' => $this->userId, 'status' => YesNoEnum::YES]) + ->whereIn('article_id', $articleIds) + ->column('article_id'); + + foreach ($result as &$item) { + $item['collect'] = in_array($item['id'], $collectIds); + if (!empty($item['content'])) { + $item['content'] = mb_substr(strip_tags($item['content']), 0, 100); + } + $item['translated_content'] = $cid === 22 ? ArticleLogic::getCachedTranslatedContent($item) : ''; + $item = ArticleImageProxyService::rewriteArticlePayload($item); + unset($item['ext']); + } + + return $result; + } + + + /** + * @notes 获取文章数量 + * @return int + * @author 段誉 + * @date 2022/9/16 18:55 + */ + public function count(): int + { + return Article::where($this->searchWhere) + ->where($this->queryWhere()) + ->count(); + } +} diff --git a/server/app/api/lists/community/CommunityPostLists.php b/server/app/api/lists/community/CommunityPostLists.php new file mode 100644 index 0000000..bda3555 --- /dev/null +++ b/server/app/api/lists/community/CommunityPostLists.php @@ -0,0 +1,253 @@ +queryWhere()); + $trumpTagId = (int) CommunityTag::where('name', '特朗普')->value('id'); + if ($trumpTagId > 0) { + $query->whereRaw( + "NOT (id IN (SELECT post_id FROM la_community_post_tag WHERE tag_id = {$trumpTagId})" . + " AND TRIM(IFNULL(content, '')) = ''" . + " AND IFNULL(images, '[]') <> '[]')" + ); + } + return $query; + } + + private function queryWhere(): array + { + $where = [['status', '=', 1]]; + + if (!empty($this->params['post_type'])) { + $where[] = ['post_type', '=', $this->params['post_type']]; + } + if (!empty($this->params['user_id'])) { + $where[] = ['user_id', '=', $this->params['user_id']]; + } + if (!empty($this->params['follow']) && $this->userId) { + $followUserIds = CommunityFollow::where('user_id', $this->userId) + ->column('follow_user_id'); + if ($followUserIds) { + $where[] = ['user_id', 'in', $followUserIds]; + } else { + $where[] = ['id', '=', 0]; + } + } + if (!empty($this->params['tag_id'])) { + $postIds = Db::name('community_post_tag') + ->where('tag_id', $this->params['tag_id']) + ->column('post_id'); + if ($postIds) { + $where[] = ['id', 'in', $postIds]; + } else { + $where[] = ['id', '=', 0]; + } + } + return $where; + } + + private function isLiuhePost(array $tags, array $ext = []): bool + { + foreach (self::LIUHE_TAGS as $tag) { + if (in_array($tag, $tags, true)) { + return true; + } + } + + $lotteryKey = (string) ($ext['lottery_key'] ?? ''); + return in_array($lotteryKey, ['a6', 'xa6'], true); + } + + private function getLiuheAnalysisContent(array $ext): string + { + if (($ext['lottery_analysis_source'] ?? '') !== 'image_only') { + return ''; + } + + return (string) ($ext['lottery_analysis_content'] ?? ''); + } + + public function lists(): array + { + $list = $this->buildQuery() + ->field('id,origin_id,user_id,content,images,ext,post_type,match_id,is_paid,price_points,paid_count,view_count,like_count,comment_count,share_count,is_top,is_hot,is_recommend,create_time') + ->orderRaw('is_top DESC, is_hot DESC, create_time DESC') + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + + // 批量预加载:用户、标签、翻译状态 + $postIds = array_column($list, 'id'); + $userIds = array_unique(array_column($list, 'user_id')); + + // 批量查询用户 → id 为 key + $userMap = []; + if ($userIds) { + $users = User::field('id,sn,nickname,avatar,sex')->whereIn('id', $userIds)->select()->toArray(); + foreach ($users as $u) { + $userMap[$u['id']] = $u; + } + } + + // 批量查询帖子标签 → post_id => [tag_name, ...] + $tagMap = []; + if ($postIds) { + $postTags = Db::name('community_post_tag')->whereIn('post_id', $postIds)->select()->toArray(); + $tagIdsByPost = []; + foreach ($postTags as $pt) { + $tagIdsByPost[$pt['post_id']][] = $pt['tag_id']; + } + $allTagIds = array_unique(array_column($postTags, 'tag_id')); + $tagNames = $allTagIds ? CommunityTag::whereIn('id', $allTagIds)->column('name', 'id') : []; + foreach ($tagIdsByPost as $pid => $tids) { + $tagMap[$pid] = array_values(array_intersect_key($tagNames, array_flip($tids))); + } + } + + // 已付费翻译的帖子ID集合(带缓存) + $translatedPostIds = []; + if ($this->userId) { + $translatedPostIds = self::getUserTranslatedPostIds($this->userId); + } + + // 批量查询已购买的付费帖子ID + $purchasedPostIds = []; + if ($this->userId && $postIds) { + $purchasedPostIds = Db::name('user_account_log') + ->where('user_id', $this->userId) + ->where('change_type', AccountLogEnum::UP_DEC_PURCHASE_POST) + ->whereIn('relation_id', $postIds) + ->column('relation_id'); + } + + // VIP权益:解锁帖子免费、翻译免费 + $vipUnlockFree = $this->userId ? VipService::hasUnlockFree($this->userId) : false; + $vipTranslateFree = $this->userId ? VipService::hasTranslateFree($this->userId) : false; + + foreach ($list as &$item) { + $ext = $item['ext'] ?? ''; + $ext = $ext ? (is_string($ext) ? json_decode($ext, true) : $ext) : []; + + // 判断是否已解锁(自己发的 或 已购买 或 VIP解锁免费) + $item['is_unlocked'] = 0; + if ($item['is_paid']) { + if ($item['user_id'] == $this->userId || in_array($item['id'], $purchasedPostIds) || $vipUnlockFree) { + $item['is_unlocked'] = 1; + } + } + + // 付费帖子:未解锁 → 截断内容并隐藏全文 + if ($item['is_paid'] && !$item['is_unlocked']) { + $item['content_short'] = mb_substr($item['content'], 0, 20) . '...'; + $item['content'] = $item['content_short']; + } elseif (mb_strlen($item['content']) > 120) { + $item['content_short'] = mb_substr($item['content'], 0, 120) . '...'; + } else { + $item['content_short'] = $item['content']; + } + + $item['user'] = $userMap[$item['user_id']] ?? null; + $item['tags'] = $tagMap[$item['id']] ?? []; + $item['source_url'] = $ext['url'] ?? ''; + $item['is_liked'] = false; + if ($this->userId) { + $item['is_liked'] = CommunityLike::where([ + 'user_id' => $this->userId, + 'target_id' => $item['id'], + 'target_type' => 1 + ])->count() > 0; + } + + $item['translated_content'] = ''; + $isTrumpPost = in_array('特朗普', $item['tags']); + $isLiuhePost = $this->isLiuhePost($item['tags'], $ext); + if ($isTrumpPost) { + if (empty($ext['translated_content']) && !empty($item['content'])) { + $result = AiService::translateCommunityPost($item['content']); + if (!empty($result['success']) && !empty($result['content'])) { + $ext['translated_content'] = trim($result['content']); + CommunityPost::where('id', $item['id'])->update([ + 'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE), + 'update_time' => time(), + ]); + } + } + $item['translated_content'] = $ext['translated_content'] ?? ''; + } elseif ((!empty($item['origin_id']) || $isLiuhePost) && (in_array($item['id'], $translatedPostIds) || $vipTranslateFree)) { + $item['translated_content'] = $isLiuhePost + ? $this->getLiuheAnalysisContent($ext) + : (string) ($ext['translated_content'] ?? ''); + } + unset($item['ext']); + } + + return $list; + } + + public function count(): int + { + return $this->buildQuery()->count(); + } + + /** + * 获取用户已付费翻译的帖子ID集合(优先缓存) + */ + public static function getUserTranslatedPostIds(int $userId): array + { + $cacheKey = 'translate_paid:' . $userId; + $postIds = Cache::get($cacheKey); + if ($postIds !== null) { + return $postIds; + } + + $remarks = Db::name('user_account_log') + ->where('user_id', $userId) + ->where('change_type', AccountLogEnum::UP_DEC_TRANSLATE_POST) + ->column('remark'); + + $postIds = []; + foreach ($remarks as $remark) { + if (preg_match('/(?:翻译|分析)帖子#(\d+)/', $remark, $m)) { + $postIds[] = (int) $m[1]; + } + } + + Cache::set($cacheKey, $postIds, 3600); + return $postIds; + } + + /** + * 翻译扣分后刷新缓存:追加新帖子ID + */ + public static function refreshTranslateCache(int $userId, int $postId): void + { + $cacheKey = 'translate_paid:' . $userId; + $postIds = Cache::get($cacheKey); + if (is_array($postIds)) { + if (!in_array($postId, $postIds)) { + $postIds[] = $postId; + Cache::set($cacheKey, $postIds, 3600); + } + } else { + Cache::delete($cacheKey); + } + } +} diff --git a/server/app/api/lists/match/MatchLists.php b/server/app/api/lists/match/MatchLists.php new file mode 100644 index 0000000..f4dabfa --- /dev/null +++ b/server/app/api/lists/match/MatchLists.php @@ -0,0 +1,55 @@ +params['sport_type'])) { + $where[] = ['sport_type', '=', $this->params['sport_type']]; + } else { + $validSportTypes = League::where('is_show', 1) + ->where('type', 'sport') + ->where('sport_type', '>', 0) + ->column('sport_type'); + if (!empty($validSportTypes)) { + $where[] = ['sport_type', 'in', $validSportTypes]; + } + } + if (!empty($this->params['league_name'])) { + $where[] = ['league_name', '=', $this->params['league_name']]; + } elseif (empty($this->params['sport_type'])) { + $validLeagues = League::where('is_show', 1) + ->where('type', 'league') + ->column('label'); + if (!empty($validLeagues)) { + $where[] = ['league_name', 'in', $validLeagues]; + } + } + if (isset($this->params['status']) && $this->params['status'] !== '') { + $where[] = ['status', '=', $this->params['status']]; + } + return $where; + } + + public function lists(): array + { + return MatchEvent::where($this->queryWhere()) + ->field('id,league_name,league_icon,home_team,home_icon,home_score,away_team,away_icon,away_score,sport_type,status,match_time,current_minute,half_score,home_odds,draw_odds,away_odds,home_corner,away_corner,home_yellow,away_yellow,home_red,away_red,is_hot') + ->orderRaw('FIELD(status, 1, 0, 3, 2), CASE WHEN status = 2 THEN -match_time ELSE match_time END ASC') + ->limit($this->limitOffset, $this->limitLength) + ->select() + ->toArray(); + } + + public function count(): int + { + return MatchEvent::where($this->queryWhere())->count(); + } +} diff --git a/server/app/api/lists/recharge/RechargeLists.php b/server/app/api/lists/recharge/RechargeLists.php new file mode 100644 index 0000000..7ebb709 --- /dev/null +++ b/server/app/api/lists/recharge/RechargeLists.php @@ -0,0 +1,72 @@ +where([ + 'user_id' => $this->userId, + 'pay_status' => PayEnum::ISPAID + ]) + ->order('id', 'desc') + ->select() + ->toArray(); + + foreach($lists as &$item) { + $item['tips'] = '充值' . format_amount($item['order_amount']) . '元'; + } + + return $lists; + } + + + /** + * @notes 获取数量 + * @return int + * @author 段誉 + * @date 2023/2/23 18:43 + */ + public function count(): int + { + return RechargeOrder::where([ + 'user_id' => $this->userId, + 'pay_status' => PayEnum::ISPAID + ]) + ->count(); + } + +} \ No newline at end of file diff --git a/server/app/api/logic/ArticleInteractLogic.php b/server/app/api/logic/ArticleInteractLogic.php new file mode 100644 index 0000000..8195ce8 --- /dev/null +++ b/server/app/api/logic/ArticleInteractLogic.php @@ -0,0 +1,196 @@ + $articleId, + 'parent_id' => 0, + 'is_show' => 1, + ]; + + $count = ArticleComment::where($where)->count(); + + $list = ArticleComment::where($where) + ->order('id', 'desc') + ->page($pageNo, $pageSize) + ->select() + ->toArray(); + + $userIds = array_column($list, 'user_id'); + + // 取子评论的user_id + $commentIds = array_column($list, 'id'); + $replies = []; + if (!empty($commentIds)) { + $repliesRaw = ArticleComment::where('parent_id', 'in', $commentIds) + ->where('is_show', 1) + ->order('id', 'asc') + ->select() + ->toArray(); + $replyUserIds = array_column($repliesRaw, 'user_id'); + $replyTargetUserIds = array_column($repliesRaw, 'reply_user_id'); + $userIds = array_merge($userIds, $replyUserIds, $replyTargetUserIds); + + foreach ($repliesRaw as $r) { + $replies[$r['parent_id']][] = $r; + } + } + + // 批量获取用户信息 + $userIds = array_unique(array_filter($userIds)); + $users = []; + if (!empty($userIds)) { + $usersRaw = User::whereIn('id', $userIds) + ->field('id,nickname,avatar') + ->select() + ->toArray(); + foreach ($usersRaw as $u) { + $users[$u['id']] = $u; + } + } + + $defaultUser = ['nickname' => '匿名用户', 'avatar' => '']; + + foreach ($list as &$item) { + $u = $users[$item['user_id']] ?? $defaultUser; + $item['nickname'] = $u['nickname']; + $item['avatar'] = $u['avatar']; + $item['reply_list'] = []; + + if (isset($replies[$item['id']])) { + foreach ($replies[$item['id']] as $reply) { + $ru = $users[$reply['user_id']] ?? $defaultUser; + $reply['nickname'] = $ru['nickname']; + $reply['avatar'] = $ru['avatar']; + $reply['reply_nickname'] = ''; + if ($reply['reply_user_id'] > 0) { + $tu = $users[$reply['reply_user_id']] ?? $defaultUser; + $reply['reply_nickname'] = $tu['nickname']; + } + $item['reply_list'][] = $reply; + } + } + } + unset($item); + + return [ + 'count' => $count, + 'lists' => $list, + 'page_no' => $pageNo, + 'page_size' => $pageSize, + ]; + } + + /** + * @notes 发布评论 + */ + public static function addComment($userId, $params) + { + Db::startTrans(); + try { + $comment = ArticleComment::create([ + 'article_id' => $params['article_id'], + 'user_id' => $userId, + 'parent_id' => $params['parent_id'] ?? 0, + 'reply_user_id' => $params['reply_user_id'] ?? 0, + 'content' => $params['content'], + ]); + + Article::where('id', $params['article_id']) + ->inc('comment_count') + ->update(); + + Db::commit(); + return $comment->id; + } catch (\Exception $e) { + Db::rollback(); + self::setError($e->getMessage()); + return false; + } + } + + /** + * @notes 点赞/踩 + */ + public static function vote($userId, $articleId, $voteType) + { + Db::startTrans(); + try { + $existing = ArticleVote::where([ + 'article_id' => $articleId, + 'user_id' => $userId, + ])->findOrEmpty(); + + if (!$existing->isEmpty()) { + if ($existing->vote_type == $voteType) { + // 取消 + $field = $voteType == ArticleVote::VOTE_LIKE ? 'like_count' : 'dislike_count'; + $existing->delete(); + Article::where('id', $articleId)->where($field, '>', 0)->dec($field)->update(); + Db::commit(); + return ['action' => 'cancel', 'vote_type' => $voteType]; + } else { + // 切换: 旧的减,新的加 + $oldField = $existing->vote_type == ArticleVote::VOTE_LIKE ? 'like_count' : 'dislike_count'; + $newField = $voteType == ArticleVote::VOTE_LIKE ? 'like_count' : 'dislike_count'; + $existing->vote_type = $voteType; + $existing->save(); + Article::where('id', $articleId)->where($oldField, '>', 0)->dec($oldField)->update(); + Article::where('id', $articleId)->inc($newField)->update(); + Db::commit(); + return ['action' => 'switch', 'vote_type' => $voteType]; + } + } else { + // 新增 + ArticleVote::create([ + 'article_id' => $articleId, + 'user_id' => $userId, + 'vote_type' => $voteType, + ]); + $field = $voteType == ArticleVote::VOTE_LIKE ? 'like_count' : 'dislike_count'; + Article::where('id', $articleId)->inc($field)->update(); + Db::commit(); + return ['action' => 'add', 'vote_type' => $voteType]; + } + } catch (\Exception $e) { + Db::rollback(); + self::setError($e->getMessage()); + return false; + } + } + + /** + * @notes 获取用户对文章的投票状态 + */ + public static function getVoteStatus($userId, $articleId) + { + if (empty($userId)) { + return 0; + } + $vote = ArticleVote::where([ + 'article_id' => $articleId, + 'user_id' => $userId, + ])->findOrEmpty(); + + return $vote->isEmpty() ? 0 : $vote->vote_type; + } +} diff --git a/server/app/api/logic/ArticleLogic.php b/server/app/api/logic/ArticleLogic.php new file mode 100644 index 0000000..6fe8020 --- /dev/null +++ b/server/app/api/logic/ArticleLogic.php @@ -0,0 +1,355 @@ + $userId, 'article_id' => $articleId]; + $collect = ArticleCollect::where($where)->findOrEmpty(); + if ($collect->isEmpty()) { + ArticleCollect::create([ + 'user_id' => $userId, + 'article_id' => $articleId, + 'status' => YesNoEnum::YES + ]); + } else { + ArticleCollect::update([ + 'id' => $collect['id'], + 'status' => YesNoEnum::YES + ]); + } + } + + /** + * @notes 取消收藏 + * @param $articleId + * @param $userId + * @author 段誉 + * @date 2022/9/20 16:59 + */ + public static function cancelCollect($articleId, $userId) + { + ArticleCollect::update(['status' => YesNoEnum::NO], [ + 'user_id' => $userId, + 'article_id' => $articleId, + 'status' => YesNoEnum::YES + ]); + } + + /** + * @notes 文章分类 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/23 14:11 + */ + public static function cate() + { + return ArticleCate::field('id,name') + ->where('is_show', '=', 1) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->select()->toArray(); + } + + /** + * @notes 文章翻译 + * @param int $articleId + * @return array + */ + public static function translate(int $articleId, int $userId = 0, int $confirm = 0): array + { + $article = Article::where(['id' => $articleId, 'is_show' => YesNoEnum::YES])->findOrEmpty(); + if ($article->isEmpty()) { + return ['success' => false, 'error' => '文章不存在']; + } + + $articleData = $article->toArray(); + $ext = self::decodeExt($articleData['ext'] ?? []); + $translatedContent = self::getTranslatedContentForUser($articleId, $userId, $ext); + $translatePoints = 5; + $remark = 'AI翻译资讯#' . $articleId; + + $hasPaid = false; + if ($userId) { + $hasPaid = Db::name('user_account_log') + ->where('user_id', $userId) + ->where('change_type', AccountLogEnum::UP_DEC_TRANSLATE_POST) + ->where('remark', $remark) + ->count() > 0; + } + + if ($hasPaid && $translatedContent !== '') { + return [ + 'success' => true, + 'data' => [ + 'translated_content' => $translatedContent, + 'from_cache' => true, + ], + ]; + } + + if ($hasPaid) { + $confirm = 1; + } + + if (!$confirm) { + return [ + 'success' => true, + 'data' => [ + 'needs_payment' => true, + 'cost' => $translatePoints, + ], + ]; + } + + if (!$userId) { + return ['success' => false, 'error' => '请先登录']; + } + + if (!$hasPaid) { + $user = User::findOrEmpty($userId); + if ($user->isEmpty()) { + return ['success' => false, 'error' => '用户不存在']; + } + if (($user->user_points ?? 0) < $translatePoints) { + return ['success' => false, 'error' => '积分不足,当前积分' . ($user->user_points ?? 0) . ',翻译需要' . $translatePoints . '积分']; + } + } + + if ($translatedContent !== '') { + if (!$hasPaid) { + User::where('id', $userId)->dec('user_points', $translatePoints)->update(); + AccountLogLogic::add( + $userId, + AccountLogEnum::UP_DEC_TRANSLATE_POST, + AccountLogEnum::DEC, + $translatePoints, + '', + $remark + ); + } + + return [ + 'success' => true, + 'data' => [ + 'translated_content' => $translatedContent, + 'from_cache' => false, + ], + ]; + } + + $title = trim((string) ($articleData['title'] ?? '')); + $abstract = trim((string) ($articleData['abstract'] ?? $articleData['desc'] ?? '')); + $content = self::normalizeContent((string) ($articleData['content'] ?? '')); + + $result = AiService::translateArticle($title, $abstract, $content); + if (!$result['success']) { + return ['success' => false, 'error' => $result['error'] ?? '翻译失败']; + } + + $translatedContent = trim((string) ($result['content'] ?? '')); + if ($translatedContent === '') { + return ['success' => false, 'error' => '翻译结果为空']; + } + + $ext['translated_content'] = $translatedContent; + Article::where('id', $articleId)->update([ + 'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE), + 'update_time' => time(), + ]); + KbSyncService::enqueue('article', 'article', $articleId, 'upsert', 45); + + if (!$hasPaid) { + User::where('id', $userId)->dec('user_points', $translatePoints)->update(); + AccountLogLogic::add( + $userId, + AccountLogEnum::UP_DEC_TRANSLATE_POST, + AccountLogEnum::DEC, + $translatePoints, + '', + $remark + ); + } + + return [ + 'success' => true, + 'data' => [ + 'translated_content' => $translatedContent, + 'from_cache' => false, + ], + ]; + } + + /** + * @notes 自动翻译并落库文章译文缓存 + * @param array $articleData + * @return string + */ + public static function autoTranslateArticle(array $articleData): string + { + $articleId = (int) ($articleData['id'] ?? 0); + if ($articleId <= 0) { + return ''; + } + + $ext = self::decodeExt($articleData['ext'] ?? []); + if (!empty($ext['translated_content'])) { + return (string) $ext['translated_content']; + } + + $title = trim((string) ($articleData['title'] ?? '')); + $abstract = trim((string) ($articleData['abstract'] ?? $articleData['desc'] ?? '')); + $content = self::normalizeContent((string) ($articleData['content'] ?? '')); + if ($title === '' && $abstract === '' && $content === '') { + return ''; + } + + $result = AiService::translateArticle($title, $abstract, $content); + if (empty($result['success'])) { + return ''; + } + + $translatedContent = trim((string) ($result['content'] ?? '')); + if ($translatedContent === '') { + return ''; + } + + $ext['translated_content'] = $translatedContent; + Article::where('id', $articleId)->update([ + 'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE), + 'update_time' => time(), + ]); + KbSyncService::enqueue('article', 'article', $articleId, 'upsert', 45); + + return $translatedContent; + } + + /** + * @notes 仅读取已缓存的文章翻译内容 + * @param array $articleData + * @return string + */ + public static function getCachedTranslatedContent(array $articleData): string + { + $ext = self::decodeExt($articleData['ext'] ?? []); + return !empty($ext['translated_content']) ? (string) $ext['translated_content'] : ''; + } + + protected static function getTranslatedContentForUser(int $articleId, int $userId, array $ext): string + { + if (empty($ext['translated_content']) || !$userId) { + return ''; + } + + $hasPaid = Db::name('user_account_log') + ->where('user_id', $userId) + ->where('change_type', AccountLogEnum::UP_DEC_TRANSLATE_POST) + ->where('remark', 'AI翻译资讯#' . $articleId) + ->count() > 0; + + return $hasPaid ? (string) $ext['translated_content'] : ''; + } + + protected static function decodeExt($ext): array + { + if (is_array($ext)) { + return $ext; + } + if (is_string($ext) && $ext !== '') { + $data = json_decode($ext, true); + return is_array($data) ? $data : []; + } + return []; + } + + protected static function normalizeContent(string $content): string + { + if ($content === '') { + return ''; + } + + $content = str_ireplace(['
', '
', '
'], "\n", $content); + $content = str_ireplace(['

', '', '', '', '', ''], "\n", $content); + $content = strip_tags($content); + $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $content = preg_replace('/\r\n|\r/u', "\n", $content); + $content = preg_replace('/\n{3,}/u', "\n\n", $content); + + return trim($content); + } +} diff --git a/server/app/api/logic/IndexLogic.php b/server/app/api/logic/IndexLogic.php new file mode 100644 index 0000000..3d9a59b --- /dev/null +++ b/server/app/api/logic/IndexLogic.php @@ -0,0 +1,160 @@ +where(['is_show' => 1]) + ->order(['id' => 'desc']) + ->limit(20)->append(['click']) + ->hidden(['click_actual', 'click_virtual']) + ->select()->toArray(); + + return [ + 'page' => $decoratePage, + 'article' => $article + ]; + } + + + /** + * @notes 获取政策协议 + * @param string $type + * @return array + * @author 段誉 + * @date 2022/9/20 20:00 + */ + public static function getPolicyByType(string $type) + { + return [ + 'title' => ConfigService::get('agreement', $type . '_title', ''), + 'content' => ConfigService::get('agreement', $type . '_content', ''), + ]; + } + + + /** + * @notes 装修信息 + * @param $id + * @return array + * @author 段誉 + * @date 2022/9/21 18:37 + */ + public static function getDecorate($id) + { + return DecoratePage::field(['type', 'name', 'data', 'meta']) + ->findOrEmpty($id)->toArray(); + } + + + /** + * @notes 获取配置 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/21 19:38 + */ + public static function getConfigData() + { + // 底部导航 + $tabbar = DecorateTabbar::getTabbarLists(); + // 导航颜色 + $style = ConfigService::get('tabbar', 'style', config('project.decorate.tabbar_style')); + // 登录配置 + $loginConfig = [ + // 登录方式 + 'login_way' => ConfigService::get('login', 'login_way', config('project.login.login_way')), + // 注册强制绑定手机 + 'coerce_mobile' => ConfigService::get('login', 'coerce_mobile', config('project.login.coerce_mobile')), + // 政策协议 + 'login_agreement' => ConfigService::get('login', 'login_agreement', config('project.login.login_agreement')), + // 第三方登录 开关 + 'third_auth' => ConfigService::get('login', 'third_auth', config('project.login.third_auth')), + // 微信授权登录 + 'wechat_auth' => ConfigService::get('login', 'wechat_auth', config('project.login.wechat_auth')), + // qq授权登录 + 'qq_auth' => ConfigService::get('login', 'qq_auth', config('project.login.qq_auth')), + ]; + // 网址信息 + $website = [ + 'h5_favicon' => FileService::getFileUrl(ConfigService::get('website', 'h5_favicon')), + 'shop_name' => ConfigService::get('website', 'shop_name'), + 'shop_logo' => FileService::getFileUrl(ConfigService::get('website', 'shop_logo')), + ]; + // H5配置 + $webPage = [ + // 渠道状态 0-关闭 1-开启 + 'status' => ConfigService::get('web_page', 'status', 1), + // 关闭后渠道后访问页面 0-空页面 1-自定义链接 + 'page_status' => ConfigService::get('web_page', 'page_status', 0), + // 自定义链接 + 'page_url' => ConfigService::get('web_page', 'page_url', ''), + 'url' => request()->domain() . '/mobile' + ]; + + // 备案信息 + $copyright = ConfigService::get('copyright', 'config', []); + + return [ + 'domain' => FileService::getFileUrl(), + 'style' => $style, + 'tabbar' => $tabbar, + 'login' => $loginConfig, + 'website' => $website, + 'webPage' => $webPage, + 'version'=> config('project.version'), + 'copyright' => $copyright, + ]; + } + +} \ No newline at end of file diff --git a/server/app/api/logic/LoginLogic.php b/server/app/api/logic/LoginLogic.php new file mode 100644 index 0000000..5f23186 --- /dev/null +++ b/server/app/api/logic/LoginLogic.php @@ -0,0 +1,511 @@ +find(); + if ($inviter) { + $inviterId = $inviter->id; + } + } + + User::create([ + 'sn' => $userSn, + 'avatar' => $avatar, + 'nickname' => '用户' . $userSn, + 'account' => $params['account'], + 'password' => $password, + 'channel' => $params['channel'], + 'invite_code' => $inviteCode, + 'inviter_id' => $inviterId, + ]); + + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 账号/手机号登录,手机号验证码 + * @param $params + * @return array|false + * @author 段誉 + * @date 2022/9/6 19:26 + */ + public static function login($params) + { + try { + // 账号/手机号 密码登录 + $where = ['account|mobile' => $params['account']]; + if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA) { + //手机验证码登录 + $where = ['mobile' => $params['account']]; + } + + // 验证码登录:只要上传表里存在该手机号记录即可视为通过 + $verifyScene = ''; + $smsMatched = false; + if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA) { + $verifyInfo = \think\facade\Cache::get('sms_verify_info:' . $params['account']); + if ($verifyInfo && is_array($verifyInfo)) { + $verifyScene = $verifyInfo['scene'] ?? 'login'; + } + + $userMobile = $params['account']; + $record = \app\common\model\SmsLog::where(function ($query) use ($userMobile) { + $query->where('phone', $userMobile) + ->whereOr('phone', '+86' . $userMobile) + ->whereOr('phone', '86' . $userMobile); + }) + ->order('sms_time', 'desc') + ->findOrEmpty(); + $smsMatched = !$record->isEmpty(); + if ($smsMatched && (int)$record->status === 0) { + $record->status = 1; + $record->save(); + } + } + + $user = User::where($where)->findOrEmpty(); + if ($user->isEmpty()) { + // 验证码登录场景:用户不存在则自动注册,无论验证码是否匹配都允许注册 + if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA) { + // is_bind_mobile: sms_upload匹配=1,缓存码匹配但sms_upload不匹配=0,都不匹配=0 + $isPhoneReal = $smsMatched ? 1 : 0; + $userSn = User::createUserSn(); + $avatar = ConfigService::get('default_image', 'user_avatar'); + $inviteCode = User::createInviteCode(); + $inviterId = 0; + if (!empty($params['inviter_code'])) { + $inviter = User::where('invite_code', $params['inviter_code'])->find(); + if ($inviter) { + $inviterId = $inviter->id; + } + } + $user = User::create([ + 'sn' => $userSn, + 'avatar' => $avatar, + 'nickname' => '用户' . $userSn, + 'account' => $params['account'], + 'mobile' => $params['account'], + 'channel' => $params['terminal'], + 'invite_code' => $inviteCode, + 'inviter_id' => $inviterId, + 'is_bind_mobile' => $isPhoneReal, + ]); + } else { + throw new \Exception('用户不存在'); + } + } else { + // 已有账号:验证码登录必须通过 sms_upload 验证 + if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA && !$smsMatched) { + throw new \Exception('验证码错误,请使用密码登录'); + } + if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA && $smsMatched) { + $user->is_bind_mobile = 1; + } + } + + // 验证码验证成功才清除缓存 + if ($params['scene'] == LoginEnum::MOBILE_CAPTCHA && $smsMatched) { + \think\facade\Cache::delete('sms_verify_code:' . $params['account']); + \think\facade\Cache::delete('sms_verify_info:' . $params['account']); + } + + //更新登录信息 + $user->login_time = time(); + $user->login_ip = request()->ip(); + $user->save(); + + //设置token + $userInfo = UserTokenService::setToken($user->id, $params['terminal']); + + //返回登录信息 + $avatar = $user->avatar ?: Config::get('project.default_image.user_avatar'); + $avatar = FileService::getFileUrl($avatar); + + return [ + 'nickname' => $userInfo['nickname'], + 'sn' => $userInfo['sn'], + 'mobile' => $userInfo['mobile'], + 'avatar' => $avatar, + 'token' => $userInfo['token'], + 'is_real_phone' => $user->is_bind_mobile ?? 0, + 'verify_scene' => $verifyScene, + ]; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 退出登录 + * @param $userInfo + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/16 17:56 + */ + public static function logout($userInfo) + { + //token不存在,不注销 + if (!isset($userInfo['token'])) { + return false; + } + + //设置token过期 + return UserTokenService::expireToken($userInfo['token']); + } + + + /** + * @notes 获取微信请求code的链接 + * @param string $url + * @return string + * @author 段誉 + * @date 2022/9/20 19:47 + */ + public static function codeUrl(string $url) + { + return (new WeChatOaService())->getCodeUrl($url); + } + + + /** + * @notes 公众号登录 + * @param array $params + * @return array|false + * @throws \GuzzleHttp\Exception\GuzzleException + * @author 段誉 + * @date 2022/9/20 19:47 + */ + public static function oaLogin(array $params) + { + Db::startTrans(); + try { + //通过code获取微信 openid + $response = (new WeChatOaService())->getOaResByCode($params['code']); + $userServer = new WechatUserService($response, UserTerminalEnum::WECHAT_OA); + $userInfo = $userServer->getResopnseByUserInfo()->authUserLogin()->getUserInfo(); + + // 更新登录信息 + self::updateLoginInfo($userInfo['id']); + + Db::commit(); + return $userInfo; + + } catch (\Exception $e) { + Db::rollback(); + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 小程序-静默登录 + * @param array $params + * @return array|false + * @author 段誉 + * @date 2022/9/20 19:47 + */ + public static function silentLogin(array $params) + { + try { + //通过code获取微信 openid + $response = (new WeChatMnpService())->getMnpResByCode($params['code']); + $userServer = new WechatUserService($response, UserTerminalEnum::WECHAT_MMP); + $userInfo = $userServer->getResopnseByUserInfo('silent')->getUserInfo(); + + if (!empty($userInfo)) { + // 更新登录信息 + self::updateLoginInfo($userInfo['id']); + } + + return $userInfo; + } catch (\Exception $e) { + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 小程序-授权登录 + * @param array $params + * @return array|false + * @author 段誉 + * @date 2022/9/20 19:47 + */ + public static function mnpLogin(array $params) + { + Db::startTrans(); + try { + //通过code获取微信 openid + $response = (new WeChatMnpService())->getMnpResByCode($params['code']); + $userServer = new WechatUserService($response, UserTerminalEnum::WECHAT_MMP); + $userInfo = $userServer->getResopnseByUserInfo()->authUserLogin()->getUserInfo(); + + // 更新登录信息 + self::updateLoginInfo($userInfo['id']); + + Db::commit(); + return $userInfo; + } catch (\Exception $e) { + Db::rollback(); + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 更新登录信息 + * @param $userId + * @throws \Exception + * @author 段誉 + * @date 2022/9/20 19:46 + */ + public static function updateLoginInfo($userId) + { + $user = User::findOrEmpty($userId); + if ($user->isEmpty()) { + throw new \Exception('用户不存在'); + } + + $time = time(); + $user->login_time = $time; + $user->login_ip = request()->ip(); + $user->update_time = $time; + $user->save(); + } + + + /** + * @notes 小程序端绑定微信 + * @param array $params + * @return bool + * @author 段誉 + * @date 2022/9/20 19:46 + */ + public static function mnpAuthLogin(array $params) + { + try { + //通过code获取微信openid + $response = (new WeChatMnpService())->getMnpResByCode($params['code']); + $response['user_id'] = $params['user_id']; + $response['terminal'] = UserTerminalEnum::WECHAT_MMP; + + return self::createAuth($response); + + } catch (\Exception $e) { + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 公众号端绑定微信 + * @param array $params + * @return bool + * @throws \GuzzleHttp\Exception\GuzzleException + * @author 段誉 + * @date 2022/9/16 10:43 + */ + public static function oaAuthLogin(array $params) + { + try { + //通过code获取微信openid + $response = (new WeChatOaService())->getOaResByCode($params['code']); + $response['user_id'] = $params['user_id']; + $response['terminal'] = UserTerminalEnum::WECHAT_OA; + + return self::createAuth($response); + + } catch (\Exception $e) { + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 生成授权记录 + * @param $response + * @return bool + * @throws \Exception + * @author 段誉 + * @date 2022/9/16 10:43 + */ + public static function createAuth($response) + { + //先检查openid是否有记录 + $isAuth = UserAuth::where('openid', '=', $response['openid'])->findOrEmpty(); + if (!$isAuth->isEmpty()) { + throw new \Exception('该微信已被绑定'); + } + + if (isset($response['unionid']) && !empty($response['unionid'])) { + //在用unionid找记录,防止生成两个账号,同个unionid的问题 + $userAuth = UserAuth::where(['unionid' => $response['unionid']]) + ->findOrEmpty(); + if (!$userAuth->isEmpty() && $userAuth->user_id != $response['user_id']) { + throw new \Exception('该微信已被绑定'); + } + } + + //如果没有授权,直接生成一条微信授权记录 + UserAuth::create([ + 'user_id' => $response['user_id'], + 'openid' => $response['openid'], + 'unionid' => $response['unionid'] ?? '', + 'terminal' => $response['terminal'], + ]); + return true; + } + + + /** + * @notes 获取扫码登录地址 + * @return array|false + * @author 段誉 + * @date 2022/10/20 18:23 + */ + public static function getScanCode($redirectUri) + { + try { + $config = WeChatConfigService::getOpConfig(); + $appId = $config['app_id']; + $redirectUri = UrlEncode($redirectUri); + + // 设置有效时间标记状态, 超时扫码不可登录 + $state = MD5(time() . rand(10000, 99999)); + (new WebScanLoginCache())->setScanLoginState($state); + + // 扫码地址 + $url = WeChatRequestService::getScanCodeUrl($appId, $redirectUri, $state); + return ['url' => $url]; + + } catch (\Exception $e) { + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 网站扫码登录 + * @param $params + * @return array|false + * @author 段誉 + * @date 2022/10/21 10:28 + */ + public static function scanLogin($params) + { + Db::startTrans(); + try { + // 通过code 获取 access_token,openid,unionid等信息 + $userAuth = WeChatRequestService::getUserAuthByCode($params['code']); + + if (empty($userAuth['openid']) || empty($userAuth['access_token'])) { + throw new \Exception('获取用户授权信息失败'); + } + + // 获取微信用户信息 + $response = WeChatRequestService::getUserInfoByAuth($userAuth['access_token'], $userAuth['openid']); + + // 生成用户或更新用户信息 + $userServer = new WechatUserService($response, UserTerminalEnum::PC); + $userInfo = $userServer->getResopnseByUserInfo()->authUserLogin()->getUserInfo(); + + // 更新登录信息 + self::updateLoginInfo($userInfo['id']); + + Db::commit(); + return $userInfo; + + } catch (\Exception $e) { + Db::rollback(); + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 更新用户信息 + * @param $params + * @param $userId + * @return User + * @author 段誉 + * @date 2023/2/22 11:19 + */ + public static function updateUser($params, $userId) + { + return User::where(['id' => $userId])->update([ + 'nickname' => $params['nickname'], + 'avatar' => FileService::setFileUrl($params['avatar']), + 'is_new_user' => YesNoEnum::NO + ]); + } +} diff --git a/server/app/api/logic/PcLogic.php b/server/app/api/logic/PcLogic.php new file mode 100644 index 0000000..caae918 --- /dev/null +++ b/server/app/api/logic/PcLogic.php @@ -0,0 +1,277 @@ + $decoratePage, + 'all' => $allArticle, + 'new' => $newArticle, + 'hot' => $hotArticle + ]; + } + + + /** + * @notes 获取文章 + * @param string $sortType + * @param int $limit + * @return mixed + * @author 段誉 + * @date 2022/10/19 9:53 + */ + public static function getLimitArticle(string $sortType, int $limit = 0, int $cate = 0, int $excludeId = 0) + { + // 查询字段 + $field = [ + 'id', + 'cid', + 'title', + 'desc', + 'abstract', + 'image', + 'author', + 'click_actual', + 'click_virtual', + 'create_time' + ]; + + // 排序条件 + $orderRaw = 'published_at desc, id desc'; + if ($sortType == 'new') { + $orderRaw = 'published_at desc, id desc'; + } + if ($sortType == 'hot') { + $orderRaw = 'click_actual + click_virtual desc, id desc'; + } + + // 查询条件 + $where[] = ['is_show', '=', YesNoEnum::YES]; + if (!empty($cate)) { + $where[] = ['cid', '=', $cate]; + } + if (!empty($excludeId)) { + $where[] = ['id', '<>', $excludeId]; + } + + $article = Article::field($field) + ->withCount(['comments' => 'comment_count']) + ->where($where) + ->append(['click']) + ->orderRaw($orderRaw) + ->hidden(['click_actual', 'click_virtual']); + + if ($limit) { + $article->limit($limit); + } + + return $article->select()->toArray(); + } + + + /** + * @notes 获取配置 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/21 19:38 + */ + public static function getConfigData() + { + // 登录配置 + $loginConfig = [ + // 登录方式 + 'login_way' => ConfigService::get('login', 'login_way', config('project.login.login_way')), + // 注册强制绑定手机 + 'coerce_mobile' => ConfigService::get('login', 'coerce_mobile', config('project.login.coerce_mobile')), + // 政策协议 + 'login_agreement' => ConfigService::get('login', 'login_agreement', config('project.login.login_agreement')), + // 第三方登录 开关 + 'third_auth' => ConfigService::get('login', 'third_auth', config('project.login.third_auth')), + // 微信授权登录 + 'wechat_auth' => ConfigService::get('login', 'wechat_auth', config('project.login.wechat_auth')), + // qq授权登录 + 'qq_auth' => ConfigService::get('login', 'qq_auth', config('project.login.qq_auth')), + ]; + + // 网站信息 + $website = [ + 'shop_name' => ConfigService::get('website', 'shop_name'), + 'shop_logo' => FileService::getFileUrl(ConfigService::get('website', 'shop_logo')), + 'pc_logo' => FileService::getFileUrl(ConfigService::get('website', 'pc_logo')), + 'pc_title' => ConfigService::get('website', 'pc_title'), + 'pc_ico' => FileService::getFileUrl(ConfigService::get('website', 'pc_ico')), + 'pc_desc' => ConfigService::get('website', 'pc_desc'), + 'pc_keywords' => ConfigService::get('website', 'pc_keywords'), + ]; + + // 站点统计 + $siteStatistics = [ + 'clarity_code' => ConfigService::get('siteStatistics', 'clarity_code'), + ]; + + // 备案信息 + $copyright = ConfigService::get('copyright', 'config', []); + + // 公众号二维码 + $oaQrCode = ConfigService::get('oa_setting', 'qr_code', ''); + $oaQrCode = empty($oaQrCode) ? $oaQrCode : FileService::getFileUrl($oaQrCode); + // 小程序二维码 + $mnpQrCode = ConfigService::get('mnp_setting', 'qr_code', ''); + $mnpQrCode = empty($mnpQrCode) ? $mnpQrCode : FileService::getFileUrl($mnpQrCode); + + return [ + 'domain' => FileService::getFileUrl(), + 'login' => $loginConfig, + 'website' => $website, + 'siteStatistics' => $siteStatistics, + 'version' => config('project.version'), + 'copyright' => $copyright, + 'admin_url' => request()->domain() . '/admin', + 'qrcode' => [ + 'oa' => $oaQrCode, + 'mnp' => $mnpQrCode, + ] + ]; + } + + + /** + * @notes 资讯中心 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/10/19 16:55 + */ + public static function getInfoCenter() + { + $data = ArticleCate::field(['id', 'name']) + ->with([ + 'article' => function ($query) { + $query->hidden(['content', 'click_virtual', 'click_actual']) + ->orderRaw('published_at desc, id desc') + ->append(['click']) + ->limit(10); + } + ]) + ->where(['is_show' => YesNoEnum::YES]) + ->order(['sort' => 'desc', 'id' => 'desc']) + ->select() + ->toArray(); + + // 收集所有文章ID,批量统计评论数 + $allIds = []; + foreach ($data as $cate) { + foreach ($cate['article'] as $article) { + $allIds[] = $article['id']; + } + } + if ($allIds) { + $commentCounts = ArticleComment::where('delete_time', null) + ->whereIn('article_id', $allIds) + ->group('article_id') + ->column('COUNT(*)', 'article_id'); + foreach ($data as &$cate) { + foreach ($cate['article'] as &$article) { + $article['comment_count'] = $commentCounts[$article['id']] ?? 0; + } + } + } + + return $data; + } + + + /** + * @notes 获取文章详情 + * @param $userId + * @param $articleId + * @param string $source + * @return array + * @author 段誉 + * @date 2022/10/20 15:18 + */ + public static function getArticleDetail($userId, $articleId, $source = 'default') + { + // 文章详情 + $detail = Article::getArticleDetailArr($articleId); + + // 根据来源列表查找对应列表 + $nowIndex = 0; + $lists = self::getLimitArticle($source, 0, $detail['cid']); + foreach ($lists as $key => $item) { + if ($item['id'] == $articleId) { + $nowIndex = $key; + } + } + // 上一篇 + $detail['last'] = $lists[$nowIndex - 1] ?? []; + // 下一篇 + $detail['next'] = $lists[$nowIndex + 1] ?? []; + + // 最新资讯 + $detail['new'] = self::getLimitArticle('new', 8, $detail['cid'], $detail['id']); + // 关注状态 + $detail['collect'] = ArticleCollect::isCollectArticle($userId, $articleId); + // 分类名 + $detail['cate_name'] = ArticleCate::where('id', $detail['cid'])->value('name'); + + return $detail; + } + +} diff --git a/server/app/api/logic/PointsOrderLogic.php b/server/app/api/logic/PointsOrderLogic.php new file mode 100644 index 0000000..c0fc314 --- /dev/null +++ b/server/app/api/logic/PointsOrderLogic.php @@ -0,0 +1,92 @@ +order('sort', 'desc') + ->order('id', 'asc') + ->select() + ->toArray(); + } + + private static function makeSign(int $productId, int $points, string $amount): string + { + $secret = env('APP_KEY', 'likeadmin'); + return md5("pid={$productId}&pts={$points}&amt={$amount}&key={$secret}"); + } + + public static function preOrder(array $params) + { + try { + $product = PointsProduct::where(['id' => $params['product_id'], 'status' => 1])->findOrEmpty(); + if ($product->isEmpty()) { + throw new \Exception('积分套餐不存在或已下架'); + } + $amount = (string) $product['amount']; + return [ + 'product_id' => (int) $product['id'], + 'name' => $product['name'], + 'points' => (int) $product['points'], + 'amount' => $amount, + 'sign' => self::makeSign((int) $product['id'], (int) $product['points'], $amount), + ]; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function createOrder(array $params) + { + try { + $product = PointsProduct::where(['id' => $params['product_id'], 'status' => 1])->findOrEmpty(); + if ($product->isEmpty()) { + throw new \Exception('积分套餐不存在或已下架'); + } + $order = PointsOrder::create([ + 'sn' => generate_sn(PointsOrder::class, 'sn'), + 'user_id' => $params['user_id'], + 'product_id' => $product['id'], + 'product_name' => $product['name'], + 'points' => $product['points'], + 'order_amount' => $product['amount'], + 'pay_status' => PayEnum::UNPAID, + 'order_terminal' => $params['terminal'], + ]); + return [ + 'order_id' => (int) $order['id'], + 'from' => 'points_order' + ]; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function detail(array $params) + { + try { + $order = PointsOrder::where(['id' => $params['order_id'], 'user_id' => $params['user_id']]) + ->append(['pay_status_text', 'pay_way_text']) + ->findOrEmpty(); + if ($order->isEmpty()) { + throw new \Exception('订单不存在'); + } + $data = $order->toArray(); + $data['pay_time'] = empty($data['pay_time']) ? '' : date('Y-m-d H:i:s', $data['pay_time']); + return $data; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } +} diff --git a/server/app/api/logic/RechargeLogic.php b/server/app/api/logic/RechargeLogic.php new file mode 100644 index 0000000..3fe2e45 --- /dev/null +++ b/server/app/api/logic/RechargeLogic.php @@ -0,0 +1,85 @@ + generate_sn(RechargeOrder::class, 'sn'), + 'order_terminal' => $params['terminal'], + 'user_id' => $params['user_id'], + 'pay_status' => PayEnum::UNPAID, + 'order_amount' => $params['money'], + ]; + $order = RechargeOrder::create($data); + + return [ + 'order_id' => (int)$order['id'], + 'from' => 'recharge' + ]; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 充值配置 + * @param $userId + * @return array + * @author 段誉 + * @date 2023/2/24 16:56 + */ + public static function config($userId) + { + $userMoney = User::where(['id' => $userId])->value('user_money'); + $minAmount = ConfigService::get('recharge', 'min_amount', 0); + $status = ConfigService::get('recharge', 'status', 0); + + return [ + 'status' => $status, + 'min_amount' => $minAmount, + 'user_money' => $userMoney, + ]; + } + + + + +} \ No newline at end of file diff --git a/server/app/api/logic/SearchLogic.php b/server/app/api/logic/SearchLogic.php new file mode 100644 index 0000000..fe18abd --- /dev/null +++ b/server/app/api/logic/SearchLogic.php @@ -0,0 +1,53 @@ +order(['sort' => 'desc', 'id' => 'desc']) + ->select()->toArray(); + + return [ + // 功能状态 0-关闭 1-开启 + 'status' => ConfigService::get('hot_search', 'status', 0), + // 热门搜索数据 + 'data' => $data, + ]; + } + +} \ No newline at end of file diff --git a/server/app/api/logic/SmsLogic.php b/server/app/api/logic/SmsLogic.php new file mode 100644 index 0000000..fa6b667 --- /dev/null +++ b/server/app/api/logic/SmsLogic.php @@ -0,0 +1,60 @@ + $scene, + 'params' => [ + 'mobile' => $params['mobile'], + 'code' => mt_rand(1000, 9999), + ] + ]); + + return $result[0]; + + } catch (\Exception $e) { + self::$error = $e->getMessage(); + return false; + } + } + +} \ No newline at end of file diff --git a/server/app/api/logic/UserLogic.php b/server/app/api/logic/UserLogic.php new file mode 100644 index 0000000..df8a497 --- /dev/null +++ b/server/app/api/logic/UserLogic.php @@ -0,0 +1,334 @@ +where('phone', $mobile) + ->whereOr('phone', '+86' . $mobile) + ->whereOr('phone', '86' . $mobile); + }) + ->order('sms_time', 'desc') + ->findOrEmpty(); + } + + /** + * @notes 涓汉涓績 + * @param array $userInfo + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 娈佃獕 + * @date 2022/9/16 18:04 + */ + public static function center(array $userInfo): array + { + if (empty($userInfo['user_id'])) { + return []; + } + $user = User::where(['id' => $userInfo['user_id']]) + ->field('id,sn,sex,account,nickname,real_name,avatar,mobile,create_time,is_new_user,user_money,password,is_vip,vip_level,vip_expire_time,ai_free_count,ai_free_date,is_bind_mobile') + ->findOrEmpty(); + if (!$user->isEmpty() && !empty($user->mobile) && ((int) $user->is_bind_mobile !== 1 || (int) $user->is_new_user !== 1)) { + $uploadRecord = self::findLatestUploadRecord((string) $user->mobile); + if (!$uploadRecord->isEmpty()) { + $user->is_bind_mobile = 1; + $user->is_new_user = 1; + $user->save(); + } + } + + if (in_array($userInfo['terminal'], [UserTerminalEnum::WECHAT_MMP, UserTerminalEnum::WECHAT_OA])) { + $auth = UserAuth::where(['user_id' => $userInfo['user_id'], 'terminal' => $userInfo['terminal']])->find(); + $user['is_auth'] = $auth ? YesNoEnum::YES : YesNoEnum::NO; + } + + $user['has_password'] = !empty($user['password']); + $vipInfo = $user->getVipInfo(); + $user->hidden(['password', 'is_vip', 'vip_level', 'vip_expire_time', 'ai_free_count', 'ai_free_date']); + $result = $user->toArray(); + $result['vip_info'] = $vipInfo; + return $result; + } + + + /** + * @notes 涓汉淇℃伅 + * @param $userId + * @return array + * @author 娈佃獕 + * @date 2022/9/20 19:45 + */ + public static function info(int $userId) + { + $user = User::where(['id' => $userId]) + ->field('id,sn,sex,account,password,nickname,real_name,avatar,mobile,create_time,user_money,is_vip,vip_level,vip_expire_time,ai_free_count,ai_free_date') + ->findOrEmpty(); + $user['has_password'] = !empty($user['password']); + $user['has_auth'] = self::hasWechatAuth($userId); + $user['version'] = config('project.version'); + $vipInfo = $user->getVipInfo(); + $user->hidden(['password', 'is_vip', 'vip_level', 'vip_expire_time', 'ai_free_count', 'ai_free_date']); + $result = $user->toArray(); + $result['vip_info'] = $vipInfo; + return $result; + } + + + /** + * @notes 璁剧疆鐢ㄦ埛淇℃伅 + * @param int $userId + * @param array $params + * @return User|false + * @author 娈佃獕 + * @date 2022/9/21 16:53 + */ + public static function setInfo(int $userId, array $params) + { + try { + if ($params['field'] == "avatar") { + $params['value'] = FileService::setFileUrl($params['value']); + } + + return User::update( + [ + 'id' => $userId, + $params['field'] => $params['value'] + ] + ); + } catch (\Exception $e) { + self::$error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 鏄惁鏈夊井淇℃巿鏉冧俊鎭? + * @param $userId + * @return bool + * @author 娈佃獕 + * @date 2022/9/20 19:36 + */ + public static function hasWechatAuth(int $userId) + { + //鏄惁鏈夊井淇℃巿鏉冪櫥褰? + $terminal = [UserTerminalEnum::WECHAT_MMP, UserTerminalEnum::WECHAT_OA, UserTerminalEnum::PC]; + $auth = UserAuth::where(['user_id' => $userId]) + ->whereIn('terminal', $terminal) + ->findOrEmpty(); + return !$auth->isEmpty(); + } + + + /** + * @notes 閲嶇疆鐧诲綍瀵嗙爜 + * @param $params + * @return bool + * @author 娈佃獕 + * @date 2022/9/16 18:06 + */ + public static function resetPassword(array $params) + { + try { + // 鏍¢獙楠岃瘉鐮? + $smsDriver = new SmsDriver(); + if (!$smsDriver->verify($params['mobile'], $params['code'], NoticeEnum::FIND_LOGIN_PASSWORD_CAPTCHA)) { + throw new \Exception('verification code error'); + } + + // 閲嶇疆瀵嗙爜 + $passwordSalt = Config::get('project.unique_identification'); + $password = create_password($params['password'], $passwordSalt); + + // 鏇存柊 + User::where('mobile', $params['mobile'])->update([ + 'password' => $password + ]); + + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 淇瀵嗙爜 + * @param $params + * @param $userId + * @return bool + * @author 娈佃獕 + * @date 2022/9/20 19:13 + */ + public static function changePassword(array $params, int $userId) + { + try { + $user = User::findOrEmpty($userId); + if ($user->isEmpty()) { + throw new \Exception('user not found'); + } + + // 瀵嗙爜鐩? + $passwordSalt = Config::get('project.unique_identification'); + + if (!empty($user['password'])) { + if (empty($params['old_password'])) { + throw new \Exception('璇峰~鍐欐棫瀵嗙爜'); + } + $oldPassword = create_password($params['old_password'], $passwordSalt); + if ($oldPassword != $user['password']) { + throw new \Exception('鍘熷瘑鐮佷笉姝g‘'); + } + } + + // 淇濆瓨瀵嗙爜 + $password = create_password($params['password'], $passwordSalt); + $user->password = $password; + $user->save(); + + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 鑾峰彇灏忕▼搴忔墜鏈哄彿 + * @param array $params + * @return bool + * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface + * @author 娈佃獕 + * @date 2023/2/27 11:49 + */ + public static function getMobileByMnp(array $params) + { + try { + $response = (new WeChatMnpService())->getUserPhoneNumber($params['code']); + $phoneNumber = $response['phone_info']['purePhoneNumber'] ?? ''; + if (empty($phoneNumber)) { + throw new \Exception('鑾峰彇鎵嬫満鍙风爜澶辫触'); + } + + $user = User::where([ + ['mobile', '=', $phoneNumber], + ['id', '<>', $params['user_id']] + ])->findOrEmpty(); + + if (!$user->isEmpty()) { + throw new \Exception('mobile number already bound to another account'); + } + + // 缁戝畾鎵嬫満鍙? + User::update([ + 'id' => $params['user_id'], + 'mobile' => $phoneNumber + ]); + + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 缁戝畾鎵嬫満鍙? + * @param $params + * @return bool + * @author 娈佃獕 + * @date 2022/9/21 17:28 + */ + public static function bindMobile(array $params) + { + try { + // 妫€鏌ユ墜鏈哄彿鏄惁宸茶鍏朵粬鐢ㄦ埛鍗犵敤 + $existUser = User::where('mobile', $params['mobile']) + ->where('id', '<>', $params['user_id']) + ->findOrEmpty(); + if (!$existUser->isEmpty()) { + throw new \Exception('璇ユ墜鏈哄彿宸茶鍏朵粬鐢ㄦ埛浣跨敤'); + } + + // 鏍¢獙鐭俊锛氫粠缂撳瓨鏍¢獙楠岃瘉鐮侊紝鐒跺悗鍙涓婁紶琛ㄥ瓨鍦ㄨ鎵嬫満鍙疯褰曞嵆瑙嗕负閫氳繃 + $cacheKey = 'sms_verify_code:' . $params['mobile']; + $cached = \think\facade\Cache::get($cacheKey); + if (!$cached || $cached['code'] != $params['code']) { + throw new \Exception('verification code error'); + } + + // sms_upload.phone 鏄彂閫佹柟锛堢敤鎴锋墜鏈哄彿锛夛紝鐢ㄧ敤鎴锋墜鏈哄彿鏌ヨ + $userMobile = $params['mobile']; + $record = \app\common\model\SmsLog::where(function ($query) use ($userMobile) { + $query->where('phone', $userMobile) + ->whereOr('phone', '+86' . $userMobile) + ->whereOr('phone', '86' . $userMobile); + }) + ->order('sms_time', 'desc') + ->findOrEmpty(); + + if ($record->isEmpty()) { + throw new \Exception('鐭俊鍔╂墜璁板綍涓湭鎵惧埌璇ユ墜鏈哄彿'); + } + + if ((int) $record->status === 0) { + $record->status = 1; + $record->save(); + } + \think\facade\Cache::delete($cacheKey); + + User::update([ + 'id' => $params['user_id'], + 'mobile' => $params['mobile'], + 'is_bind_mobile' => 1 + ]); + + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + +} diff --git a/server/app/api/logic/VipOrderLogic.php b/server/app/api/logic/VipOrderLogic.php new file mode 100644 index 0000000..ce97a95 --- /dev/null +++ b/server/app/api/logic/VipOrderLogic.php @@ -0,0 +1,73 @@ +order('sort', 'desc') + ->order('id', 'asc') + ->select() + ->toArray(); + } + + public static function createOrder(array $params) + { + try { + $level = VipLevel::where(['id' => $params['level_id'], 'status' => 1])->findOrEmpty(); + if ($level->isEmpty()) { + throw new \Exception('VIP套餐不存在或已下架'); + } + + // 有效期内不能重复购买同一等级 + $user = User::findOrEmpty($params['user_id']); + if ($user->is_vip == 1 && $user->vip_expire_time > time() && $user->vip_level == $level['level']) { + throw new \Exception('您当前已是' . $level['name'] . ',有效期内无需重复购买'); + } + + $order = VipOrder::create([ + 'sn' => generate_sn(VipOrder::class, 'sn'), + 'user_id' => $params['user_id'], + 'level_id' => $level['id'], + 'level_name' => $level['name'], + 'duration' => $level['duration'], + 'order_amount' => $level['price'], + 'pay_status' => PayEnum::UNPAID, + 'order_terminal' => $params['terminal'], + ]); + return [ + 'order_id' => (int) $order['id'], + 'from' => 'vip_order' + ]; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function detail(array $params) + { + try { + $order = VipOrder::where(['id' => $params['order_id'], 'user_id' => $params['user_id']]) + ->append(['pay_status_text', 'pay_way_text']) + ->findOrEmpty(); + if ($order->isEmpty()) { + throw new \Exception('订单不存在'); + } + $data = $order->toArray(); + $data['pay_time'] = empty($data['pay_time']) ? '' : date('Y-m-d H:i:s', $data['pay_time']); + return $data; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } +} diff --git a/server/app/api/logic/WechatLogic.php b/server/app/api/logic/WechatLogic.php new file mode 100644 index 0000000..6cda0b5 --- /dev/null +++ b/server/app/api/logic/WechatLogic.php @@ -0,0 +1,65 @@ +getJsConfig($url, [ + 'onMenuShareTimeline', + 'onMenuShareAppMessage', + 'onMenuShareQQ', + 'onMenuShareWeibo', + 'onMenuShareQZone', + 'openLocation', + 'getLocation', + 'chooseWXPay', + 'updateAppMessageShareData', + 'updateTimelineShareData', + 'openAddress', + 'scanQRCode' + ]); + } catch (Exception $e) { + self::setError('获取jssdk失败:' . $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/server/app/api/service/UserTokenService.php b/server/app/api/service/UserTokenService.php new file mode 100644 index 0000000..effdbe1 --- /dev/null +++ b/server/app/api/service/UserTokenService.php @@ -0,0 +1,120 @@ +find(); + + //获取token延长过期的时间 + $expireTime = $time + Config::get('project.user_token.expire_duration'); + $userTokenCache = new UserTokenCache(); + + //token处理 + if ($userSession) { + //清空缓存 + $userTokenCache->deleteUserInfo($userSession->token); + //重新获取token + $userSession->token = create_token($userId); + $userSession->expire_time = $expireTime; + $userSession->update_time = $time; + $userSession->save(); + } else { + //找不到在该终端的token记录,创建token记录 + $userSession = UserSession::create([ + 'user_id' => $userId, + 'terminal' => $terminal, + 'token' => create_token($userId), + 'expire_time' => $expireTime + ]); + } + + return $userTokenCache->setUserInfo($userSession->token); + } + + + /** + * @notes 延长token过期时间 + * @param $token + * @return array|false|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/16 10:10 + */ + public static function overtimeToken($token) + { + $time = time(); + $userSession = UserSession::where('token', '=', $token)->find(); + if ($userSession->isEmpty()) { + return false; + } + //延长token过期时间 + $userSession->expire_time = $time + Config::get('project.user_token.expire_duration'); + $userSession->update_time = $time; + $userSession->save(); + + return (new UserTokenCache())->setUserInfo($userSession->token); + } + + + /** + * @notes 设置token为过期 + * @param $token + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/16 10:10 + */ + public static function expireToken($token) + { + $userSession = UserSession::where('token', '=', $token) + ->find(); + if (empty($userSession)) { + return false; + } + + $time = time(); + $userSession->expire_time = $time; + $userSession->update_time = $time; + $userSession->save(); + + return (new UserTokenCache())->deleteUserInfo($token); + } + +} \ No newline at end of file diff --git a/server/app/api/service/WechatUserService.php b/server/app/api/service/WechatUserService.php new file mode 100644 index 0000000..8813775 --- /dev/null +++ b/server/app/api/service/WechatUserService.php @@ -0,0 +1,268 @@ +terminal = $terminal; + $this->setParams($response); + } + + + /** + * @notes 设置微信返回的用户信息 + * @param $response + * @author cjhao + * @date 2021/8/2 11:49 + */ + private function setParams($response): void + { + $this->response = $response; + $this->openid = $response['openid']; + $this->unionid = $response['unionid'] ?? ''; + $this->nickname = $response['nickname'] ?? ''; + $this->headimgurl = $response['headimgurl'] ?? ''; + } + + + /** + * @notes 根据opendid或unionid获取系统用户信息 + * @return $this + * @author 段誉 + * @date 2022/9/23 16:09 + */ + public function getResopnseByUserInfo(): self + { + $openid = $this->openid; + $unionid = $this->unionid; + + $user = User::alias('u') + ->field('u.id,u.sn,u.mobile,u.nickname,u.avatar,u.mobile,u.is_disable,u.is_new_user') + ->join('user_auth au', 'au.user_id = u.id') + ->where(function ($query) use ($openid, $unionid) { + $query->whereOr(['au.openid' => $openid]); + if (isset($unionid) && $unionid) { + $query->whereOr(['au.unionid' => $unionid]); + } + }) + ->findOrEmpty(); + + $this->user = $user; + return $this; + } + + + /** + * @notes 获取用户信息 + * @param bool $isCheck 是否验证账号是否可用 + * @return array + * @throws Exception + * @author cjhao + * @date 2021/8/3 11:42 + */ + public function getUserInfo($isCheck = true): array + { + if (!$this->user->isEmpty() && $isCheck) { + $this->checkAccount(); + } + if (!$this->user->isEmpty()) { + $this->getToken(); + } + return $this->user->toArray(); + } + + + /** + * @notes 校验账号 + * @throws Exception + * @author 段誉 + * @date 2022/9/16 10:14 + */ + private function checkAccount() + { + if ($this->user->is_disable) { + throw new Exception('您的账号异常,请联系客服。'); + } + } + + + /** + * @notes 创建用户 + * @throws Exception + * @author 段誉 + * @date 2022/9/16 10:06 + */ + private function createUser(): void + { + //设置头像 + if (empty($this->headimgurl)) { + // 默认头像 + $defaultAvatar = config('project.default_image.user_avatar'); + $avatar = ConfigService::get('default_image', 'user_avatar', $defaultAvatar); + } else { + // 微信获取到的头像信息 + $avatar = $this->getAvatarByWechat(); + } + + $userSn = User::createUserSn(); + $this->user->sn = $userSn; + $this->user->account = 'u' . $userSn; + $this->user->nickname = "用户" . $userSn; + $this->user->avatar = $avatar; + $this->user->channel = $this->terminal; + $this->user->is_new_user = YesNoEnum::YES; + + if ($this->terminal != UserTerminalEnum::WECHAT_MMP && !empty($this->nickname)) { + $this->user->nickname = $this->nickname; + } + + $this->user->save(); + + UserAuth::create([ + 'user_id' => $this->user->id, + 'openid' => $this->openid, + 'unionid' => $this->unionid, + 'terminal' => $this->terminal, + ]); + } + + + /** + * @notes 更新用户信息 + * @throws Exception + * @author 段誉 + * @date 2022/9/16 10:06 + * @remark 该端没授权信息,重新写入一条该端的授权信息 + */ + private function updateUser(): void + { + // 无头像需要更新头像 + if (empty($this->user->avatar)) { + $this->user->avatar = $this->getAvatarByWechat(); + $this->user->save(); + } + + $userAuth = UserAuth::where(['user_id' => $this->user->id, 'openid' => $this->openid]) + ->findOrEmpty(); + + // 无该端授权信息,新增一条 + if ($userAuth->isEmpty()) { + $userAuth->user_id = $this->user->id; + $userAuth->openid = $this->openid; + $userAuth->unionid = $this->unionid; + $userAuth->terminal = $this->terminal; + $userAuth->save(); + } else { + if (empty($userAuth['unionid']) && !empty($this->unionid)) { + $userAuth->unionid = $this->unionid; + $userAuth->save(); + } + } + } + + + /** + * @notes 获取token + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author cjhao + * @date 2021/8/2 16:45 + */ + private function getToken(): void + { + $user = UserTokenService::setToken($this->user->id, $this->terminal); + $this->user->token = $user['token']; + } + + + /** + * @notes 用户授权登录, + * 如果用户不存在,创建用户;用户存在,更新用户信息,并检查该端信息是否需要写入 + * @return WechatUserService + * @throws Exception + * @author cjhao + * @date 2021/8/2 16:35 + */ + public function authUserLogin(): self + { + if ($this->user->isEmpty()) { + $this->createUser(); + } else { + $this->updateUser(); + } + return $this; + } + + + /** + * @notes 处理从微信获取到的头像信息 + * @return string + * @throws Exception + * @author 段誉 + * @date 2022/9/16 9:50 + */ + public function getAvatarByWechat(): string + { + // 存储引擎 + $config = [ + 'default' => ConfigService::get('storage', 'default', 'local'), + 'engine' => ConfigService::get('storage') + ]; + + $fileName = md5($this->openid . time()) . '.jpeg'; + + if ($config['default'] == 'local') { + // 本地存储 + $avatar = download_file($this->headimgurl, 'uploads/user/avatar/', $fileName); + } else { + // 第三方存储 + $avatar = 'uploads/user/avatar/' . $fileName; + $StorageDriver = new StorageDriver($config); + if (!$StorageDriver->fetch($this->headimgurl, $avatar)) { + throw new Exception('头像保存失败:' . $StorageDriver->getError()); + } + } + return $avatar; + } + + +} \ No newline at end of file diff --git a/server/app/api/validate/LoginAccountValidate.php b/server/app/api/validate/LoginAccountValidate.php new file mode 100644 index 0000000..426c29a --- /dev/null +++ b/server/app/api/validate/LoginAccountValidate.php @@ -0,0 +1,226 @@ + 'require|in:' . UserTerminalEnum::WECHAT_MMP . ',' . UserTerminalEnum::WECHAT_OA . ',' + . UserTerminalEnum::H5 . ',' . UserTerminalEnum::PC . ',' . UserTerminalEnum::IOS . + ',' . UserTerminalEnum::ANDROID, + 'scene' => 'require|in:' . LoginEnum::ACCOUNT_PASSWORD . ',' . LoginEnum::MOBILE_CAPTCHA . '|checkConfig', + 'account' => 'require', + ]; + + + protected $message = [ + 'terminal.require' => '终端参数缺失', + 'terminal.in' => '终端参数状态值不正确', + 'scene.require' => '场景不能为空', + 'scene.in' => '场景值错误', + 'account.require' => '请输入账号', + 'password.require' => '请输入密码', + ]; + + + /** + * @notes 登录场景相关校验 + * @param $scene + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/9/15 14:37 + */ + public function checkConfig($scene, $rule, $data) + { + $config = ConfigService::get('login', 'login_way'); + if (!in_array($scene, $config)) { + return '不支持的登录方式'; + } + + // 账号密码登录 + if (LoginEnum::ACCOUNT_PASSWORD == $scene) { + if (!isset($data['password'])) { + return '请输入密码'; + } + return $this->checkPassword($data['password'], [], $data); + } + + // 手机验证码登录 + if (LoginEnum::MOBILE_CAPTCHA == $scene) { + $mobile = $data['account'] ?? ''; + $record = $this->findLatestUploadRecord($mobile); + if ((!isset($data['code']) || $data['code'] === '') && !$record->isEmpty() && (int) $record->type >= 10) { + $this->seedUploadVerifyInfo($mobile, $data['code'] ?? '', $record); + return true; + } + + if (!isset($data['code'])) { + return '请输入手机验证码'; + } + return $this->checkCode($data['code'], [], $data); + } + + return true; + } + + + /** + * @notes 登录密码校验 + * @param $password + * @param $other + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/9/15 14:39 + */ + public function checkPassword($password, $other, $data) + { + //账号安全机制,连续输错后锁定,防止账号密码暴力破解 + $userAccountSafeCache = new UserAccountSafeCache($data['account'] ?? ''); + if (!$userAccountSafeCache->isSafe()) { + return '密码连续' . $userAccountSafeCache->count . '次输入错误,请' . $userAccountSafeCache->minute . '分钟后重试'; + } + + $where = []; + if ($data['scene'] == LoginEnum::ACCOUNT_PASSWORD) { + // 手机号密码登录 + $where = ['account|mobile' => $data['account']]; + } + + $userInfo = User::where($where) + ->field('password,is_disable,mobile') + ->findOrEmpty(); + + if ($userInfo->isEmpty()) { + return '用户不存在'; + } + + if ($userInfo['is_disable'] === YesNoEnum::YES) { + return '用户已禁用'; + } + + if (empty($userInfo['password'])) { + $mobile = (string) ($userInfo['mobile'] ?? ''); + if ($mobile !== '' && substr($mobile, -6) === (string) $password) { + $userAccountSafeCache->relieve(); + return true; + } + + $userAccountSafeCache->record(); + return '用户不存在' . substr($mobile, -6); + } + + $passwordSalt = Config::get('project.unique_identification'); + if ($userInfo['password'] !== create_password($password, $passwordSalt)) { + $userAccountSafeCache->record(); + return '密码错误'; + } + + $userAccountSafeCache->relieve(); + + return true; + } + + + /** + * @notes 校验验证码(优先从短信上传记录校验,回退到传统短信验证) + * @param $code + * @param $rule + * @param $data + * @return bool|string + */ + public function checkCode($code, $rule, $data) + { + $mobile = $data['account']; + + $record = $this->findLatestUploadRecord($mobile); + if (!$record->isEmpty() && (int) $record->type >= 10) { + // 来电记录只校验“是否存在该手机号记录”,不再校验验证码 + $this->seedUploadVerifyInfo($mobile, $code, $record); + return true; + } + + // 验证码登录直接放行,具体校验在 LoginLogic::login 中根据用户是否存在分别处理 + $cacheKey = 'sms_verify_code:' . $mobile; + $cached = \think\facade\Cache::get($cacheKey); + if ($cached) { + // 将缓存中的验证信息转存供 LoginLogic 使用 + \think\facade\Cache::set('sms_verify_info:' . $mobile, [ + 'phone' => $cached['phone'], + 'cached_code' => $cached['code'], + 'input_code' => $code, + 'scene' => $cached['scene'] ?? 'login', + ], 60); + return true; + } + + // 无缓存记录时回退:传统短信验证码校验 + $smsDriver = new SmsDriver(); + $result = $smsDriver->verify($mobile, $code, NoticeEnum::LOGIN_CAPTCHA); + if ($result) { + return true; + } + return '验证码错误'; + } + + private function findLatestUploadRecord(string $mobile) + { + if ($mobile === '') { + return SmsLog::whereRaw('1 = 0')->findOrEmpty(); + } + + return SmsLog::where(function ($query) use ($mobile) { + $query->where('phone', $mobile) + ->whereOr('phone', '+86' . $mobile) + ->whereOr('phone', '86' . $mobile); + }) + ->order('sms_time', 'desc') + ->findOrEmpty(); + } + + private function seedUploadVerifyInfo(string $mobile, string $code, $record): void + { + if ($mobile === '' || $record->isEmpty()) { + return; + } + + \think\facade\Cache::set('sms_verify_info:' . $mobile, [ + 'phone' => $mobile, + 'cached_code' => $code, + 'input_code' => $code, + 'scene' => 'login', + ], 60); + } +} diff --git a/server/app/api/validate/PasswordValidate.php b/server/app/api/validate/PasswordValidate.php new file mode 100644 index 0000000..ca1d775 --- /dev/null +++ b/server/app/api/validate/PasswordValidate.php @@ -0,0 +1,69 @@ + 'require|mobile', + 'code' => 'require', + 'password' => 'require|length:6,20|alphaNum', + 'password_confirm' => 'require|confirm', + ]; + + + protected $message = [ + 'mobile.require' => '请输入手机号', + 'mobile.mobile' => '请输入正确手机号', + 'code.require' => '请填写验证码', + 'password.require' => '请输入密码', + 'password.length' => '密码须在6-25位之间', + 'password.alphaNum' => '密码须为字母数字组合', + 'password_confirm.require' => '请确认密码', + 'password_confirm.confirm' => '两次输入的密码不一致' + ]; + + + /** + * @notes 重置登录密码 + * @return PasswordValidate + * @author 段誉 + * @date 2022/9/16 18:11 + */ + public function sceneResetPassword() + { + return $this->only(['mobile', 'code', 'password', 'password_confirm']); + } + + + /** + * @notes 修改密码场景 + * @return PasswordValidate + * @author 段誉 + * @date 2022/9/20 19:14 + */ + public function sceneChangePassword() + { + return $this->only(['password', 'password_confirm']); + } + +} \ No newline at end of file diff --git a/server/app/api/validate/PayValidate.php b/server/app/api/validate/PayValidate.php new file mode 100644 index 0000000..9f80709 --- /dev/null +++ b/server/app/api/validate/PayValidate.php @@ -0,0 +1,68 @@ + 'require', + 'pay_way' => 'require|in:' . PayEnum::BALANCE_PAY . ',' . PayEnum::WECHAT_PAY . ',' . PayEnum::ALI_PAY, + 'order_id' => 'require' + ]; + + + protected $message = [ + 'from.require' => '参数缺失', + 'pay_way.require' => '支付方式参数缺失', + 'pay_way.in' => '支付方式参数错误', + 'order_id.require' => '订单参数缺失' + ]; + + + /** + * @notes 支付方式场景 + * @return PayValidate + * @author 段誉 + * @date 2023/2/24 17:43 + */ + public function scenePayway() + { + return $this->only(['from', 'order_id']); + } + + + /** + * @notes 支付状态 + * @return PayValidate + * @author 段誉 + * @date 2023/3/1 16:17 + */ + public function sceneStatus() + { + return $this->only(['from', 'order_id']); + } + + + +} \ No newline at end of file diff --git a/server/app/api/validate/PointsOrderValidate.php b/server/app/api/validate/PointsOrderValidate.php new file mode 100644 index 0000000..e1c6190 --- /dev/null +++ b/server/app/api/validate/PointsOrderValidate.php @@ -0,0 +1,35 @@ + 'require|number', + 'order_id' => 'require|number', + ]; + + protected $message = [ + 'product_id.require' => '请选择积分套餐', + 'product_id.number' => '积分套餐参数错误', + 'order_id.require' => '订单参数缺失', + 'order_id.number' => '订单参数错误', + ]; + + public function scenePreOrder() + { + return $this->only(['product_id']); + } + + public function sceneCreateOrder() + { + return $this->only(['product_id']); + } + + public function sceneDetail() + { + return $this->only(['order_id']); + } +} diff --git a/server/app/api/validate/RechargeValidate.php b/server/app/api/validate/RechargeValidate.php new file mode 100644 index 0000000..f617a92 --- /dev/null +++ b/server/app/api/validate/RechargeValidate.php @@ -0,0 +1,72 @@ + 'require|gt:0|checkMoney', + ]; + + + protected $message = [ + 'money.require' => '请填写充值金额', + 'money.gt' => '请填写大于0的充值金额', + ]; + + + public function sceneRecharge() + { + return $this->only(['money']); + } + + + + /** + * @notes 校验金额 + * @param $money + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2023/2/24 10:42 + */ + protected function checkMoney($money, $rule, $data) + { + $status = ConfigService::get('recharge', 'status', 0); + if (!$status) { + return '充值功能已关闭'; + } + + $minAmount = ConfigService::get('recharge', 'min_amount', 0); + + if ($money < $minAmount) { + return '最低充值金额' . $minAmount . "元"; + } + + return true; + } + +} \ No newline at end of file diff --git a/server/app/api/validate/RegisterValidate.php b/server/app/api/validate/RegisterValidate.php new file mode 100644 index 0000000..f377640 --- /dev/null +++ b/server/app/api/validate/RegisterValidate.php @@ -0,0 +1,53 @@ + '/^[\x{4e00}-\x{9fa5}A-Za-z0-9_]+$/u', + 'password' => '/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[\(\)])+$)([^(0-9a-zA-Z)]|[\(\)]|[a-z]|[A-Z]|[0-9]){6,20}$/' + ]; + + protected $rule = [ + 'channel' => 'require', + 'account' => 'require|length:3,12|unique:' . User::class . '|regex:register', + 'password' => 'require|length:6,20|regex:password', + 'password_confirm' => 'require|confirm' + ]; + + protected $message = [ + 'channel.require' => '注册来源参数缺失', + 'account.require' => '请输入账号', + 'account.regex' => '账号只能包含中文、字母、数字和下划线', + 'account.length' => '账号须为3-12位之间', + 'account.unique' => '账号已存在', + 'password.require' => '请输入密码', + 'password.length' => '密码须在6-25位之间', + 'password.regex' => '密码须为数字,字母或符号组合', + 'password_confirm.require' => '请确认密码', + 'password_confirm.confirm' => '两次输入的密码不一致' + ]; + +} diff --git a/server/app/api/validate/SendSmsValidate.php b/server/app/api/validate/SendSmsValidate.php new file mode 100644 index 0000000..d8b9494 --- /dev/null +++ b/server/app/api/validate/SendSmsValidate.php @@ -0,0 +1,39 @@ + 'require|mobile', + 'scene' => 'require', + ]; + + protected $message = [ + 'mobile.require' => '请输入手机号', + 'mobile.mobile' => '请输入正确手机号', + 'scene.require' => '请输入场景值', + ]; +} \ No newline at end of file diff --git a/server/app/api/validate/SetUserInfoValidate.php b/server/app/api/validate/SetUserInfoValidate.php new file mode 100644 index 0000000..b16c990 --- /dev/null +++ b/server/app/api/validate/SetUserInfoValidate.php @@ -0,0 +1,73 @@ + 'require|checkField', + 'value' => 'require', + ]; + + protected $message = [ + 'field.require' => '参数缺失', + 'value.require' => '值不存在', + ]; + + + /** + * @notes 校验字段内容 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/9/21 17:01 + */ + protected function checkField($value, $rule, $data) + { + $allowField = [ + 'nickname', 'account', 'sex', 'avatar', 'real_name', + ]; + + if (!in_array($value, $allowField)) { + return '参数错误'; + } + + if ($value == 'account') { + $user = User::where([ + ['account', '=', $data['value']], + ['id', '<>', $data['id']] + ])->findOrEmpty(); + if (!$user->isEmpty()) { + return '账号已被使用!'; + } + } + + return true; + } + + +} \ No newline at end of file diff --git a/server/app/api/validate/UserValidate.php b/server/app/api/validate/UserValidate.php new file mode 100644 index 0000000..ef622e6 --- /dev/null +++ b/server/app/api/validate/UserValidate.php @@ -0,0 +1,61 @@ + 'require', + ]; + + protected $message = [ + 'code.require' => '参数缺失', + ]; + + + /** + * @notes 获取小程序手机号场景 + * @return UserValidate + * @author 段誉 + * @date 2022/9/21 16:44 + */ + public function sceneGetMobileByMnp() + { + return $this->only(['code']); + } + + + /** + * @notes 绑定/变更 手机号 + * @return UserValidate + * @author 段誉 + * @date 2022/9/21 17:37 + */ + public function sceneBindMobile() + { + return $this->only(['mobile', 'code']); + } + + +} \ No newline at end of file diff --git a/server/app/api/validate/VipOrderValidate.php b/server/app/api/validate/VipOrderValidate.php new file mode 100644 index 0000000..8e9916d --- /dev/null +++ b/server/app/api/validate/VipOrderValidate.php @@ -0,0 +1,28 @@ + 'require|integer|gt:0', + 'order_id' => 'require|integer|gt:0', + ]; + + protected $message = [ + 'level_id.require' => '请选择VIP套餐', + 'order_id.require' => '订单id不能为空', + ]; + + public function sceneCreateOrder() + { + return $this->only(['level_id']); + } + + public function sceneDetail() + { + return $this->only(['order_id']); + } +} diff --git a/server/app/api/validate/WebScanLoginValidate.php b/server/app/api/validate/WebScanLoginValidate.php new file mode 100644 index 0000000..3523c6f --- /dev/null +++ b/server/app/api/validate/WebScanLoginValidate.php @@ -0,0 +1,59 @@ + 'require', + 'state' => 'require|checkState', + ]; + + protected $message = [ + 'code.require' => '参数缺失', + 'state.require' => '昵称缺少', + ]; + + + /** + * @notes 校验登录状态标记 + * @param $value + * @param $rule + * @param $data + * @return bool|string + * @author 段誉 + * @date 2022/10/21 9:47 + */ + protected function checkState($value, $rule, $data) + { + $check = (new WebScanLoginCache())->getScanLoginState($value); + + if (empty($check)) { + return '二维码已失效或不存在,请重新扫码'; + } + + return true; + } + +} \ No newline at end of file diff --git a/server/app/api/validate/WechatLoginValidate.php b/server/app/api/validate/WechatLoginValidate.php new file mode 100644 index 0000000..b84372b --- /dev/null +++ b/server/app/api/validate/WechatLoginValidate.php @@ -0,0 +1,96 @@ + 'require', + 'nickname' => 'require', + 'headimgurl' => 'require', + 'openid' => 'require', + 'access_token' => 'require', + 'terminal' => 'require', + 'avatar' => 'require', + ]; + + protected $message = [ + 'code.require' => 'code缺少', + 'nickname.require' => '昵称缺少', + 'headimgurl.require' => '头像缺少', + 'openid.require' => 'opendid缺少', + 'access_token.require' => 'access_token缺少', + 'terminal.require' => '终端参数缺少', + 'avatar.require' => '头像缺少', + ]; + + + /** + * @notes 公众号登录场景 + * @return WechatLoginValidate + * @author 段誉 + * @date 2022/9/16 10:57 + */ + public function sceneOa() + { + return $this->only(['code']); + } + + + /** + * @notes 小程序-授权登录场景 + * @return WechatLoginValidate + * @author 段誉 + * @date 2022/9/16 11:15 + */ + public function sceneMnpLogin() + { + return $this->only(['code']); + } + + + /** + * @notes + * @return WechatLoginValidate + * @author 段誉 + * @date 2022/9/16 11:15 + */ + public function sceneWechatAuth() + { + return $this->only(['code']); + } + + + /** + * @notes 更新用户信息场景 + * @return WechatLoginValidate + * @author 段誉 + * @date 2023/2/22 11:14 + */ + public function sceneUpdateUser() + { + return $this->only(['nickname', 'avatar']); + } + + +} \ No newline at end of file diff --git a/server/app/api/validate/WechatValidate.php b/server/app/api/validate/WechatValidate.php new file mode 100644 index 0000000..3f2bb72 --- /dev/null +++ b/server/app/api/validate/WechatValidate.php @@ -0,0 +1,38 @@ + 'require' + ]; + + public $message = [ + 'url.require' => '请提供url' + ]; + + public function sceneJsConfig() + { + return $this->only(['url']); + } +} \ No newline at end of file diff --git a/server/app/common.php b/server/app/common.php new file mode 100644 index 0000000..28d64b7 --- /dev/null +++ b/server/app/common.php @@ -0,0 +1,304 @@ += 0 ? true : false; +} + + +/** + * @notes 检查文件是否可写 + * @param string $dir + * @return bool + * @author 段誉 + * @date 2021/12/28 18:27 + */ +function check_dir_write(string $dir = '') : bool +{ + $route = root_path() . '/' . $dir; + return is_writable($route); +} + + +/** + * 多级线性结构排序 + * 转换前: + * [{"id":1,"pid":0,"name":"a"},{"id":2,"pid":0,"name":"b"},{"id":3,"pid":1,"name":"c"}, + * {"id":4,"pid":2,"name":"d"},{"id":5,"pid":4,"name":"e"},{"id":6,"pid":5,"name":"f"}, + * {"id":7,"pid":3,"name":"g"}] + * 转换后: + * [{"id":1,"pid":0,"name":"a","level":1},{"id":3,"pid":1,"name":"c","level":2},{"id":7,"pid":3,"name":"g","level":3}, + * {"id":2,"pid":0,"name":"b","level":1},{"id":4,"pid":2,"name":"d","level":2},{"id":5,"pid":4,"name":"e","level":3}, + * {"id":6,"pid":5,"name":"f","level":4}] + * @param array $data 线性结构数组 + * @param string $symbol 名称前面加符号 + * @param string $name 名称 + * @param string $id_name 数组id名 + * @param string $parent_id_name 数组祖先id名 + * @param int $level 此值请勿给参数 + * @param int $parent_id 此值请勿给参数 + * @return array + */ +function linear_to_tree($data, $sub_key_name = 'sub', $id_name = 'id', $parent_id_name = 'pid', $parent_id = 0) +{ + $tree = []; + foreach ($data as $row) { + if ($row[$parent_id_name] == $parent_id) { + $temp = $row; + $child = linear_to_tree($data, $sub_key_name, $id_name, $parent_id_name, $row[$id_name]); + if ($child) { + $temp[$sub_key_name] = $child; + } + $tree[] = $temp; + } + } + return $tree; +} + + +/** + * @notes 删除目标目录 + * @param $path + * @param $delDir + * @return bool|void + * @author 段誉 + * @date 2022/4/8 16:30 + */ +function del_target_dir($path, $delDir) +{ + //没找到,不处理 + if (!file_exists($path)) { + return false; + } + + //打开目录句柄 + $handle = opendir($path); + if ($handle) { + while (false !== ($item = readdir($handle))) { + if ($item != "." && $item != "..") { + if (is_dir("$path/$item")) { + del_target_dir("$path/$item", $delDir); + } else { + unlink("$path/$item"); + } + } + } + closedir($handle); + if ($delDir) { + return rmdir($path); + } + } else { + if (file_exists($path)) { + return unlink($path); + } + return false; + } +} + + +/** + * @notes 下载文件 + * @param $url + * @param $saveDir + * @param $fileName + * @return string + * @author 段誉 + * @date 2022/9/16 9:53 + */ +function download_file($url, $saveDir, $fileName) +{ + if (!file_exists($saveDir)) { + mkdir($saveDir, 0775, true); + } + $fileSrc = $saveDir . $fileName; + file_exists($fileSrc) && unlink($fileSrc); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); + $file = curl_exec($ch); + curl_close($ch); + $resource = fopen($fileSrc, 'a'); + fwrite($resource, $file); + fclose($resource); + if (filesize($fileSrc) == 0) { + unlink($fileSrc); + return ''; + } + return $fileSrc; +} + + +/** + * @notes 去除内容图片域名 + * @param $content + * @return array|string|string[] + * @author 段誉 + * @date 2022/9/26 10:43 + */ +function clear_file_domain($content) +{ + $fileUrl = FileService::getFileUrl(); + $pattern = '/]*\bsrc=["\']'.preg_quote($fileUrl, '/').'([^"\']+)["\']/i'; + return preg_replace($pattern, 'make($table)->where($field, $sn)->find()) { + return generate_sn($table, $field, $prefix, $randSuffixLength, $pool); + } + return $sn; +} + + +/** + * @notes 格式化金额 + * @param $float + * @return int|mixed|string + * @author 段誉 + * @date 2023/2/24 11:20 + */ +function format_amount($float) +{ + if ($float == intval($float)) { + return intval($float); + } elseif ($float == sprintf('%.1f', $float)) { + return sprintf('%.1f', $float); + } + return $float; +} diff --git a/server/app/common/cache/AdminAccountSafeCache.php b/server/app/common/cache/AdminAccountSafeCache.php new file mode 100644 index 0000000..1cce603 --- /dev/null +++ b/server/app/common/cache/AdminAccountSafeCache.php @@ -0,0 +1,80 @@ +ip(); + $this->key = $this->tagName . $ip; + } + + /** + * @notes 记录登录错误次数 + * @author 令狐冲 + * @date 2021/6/30 01:51 + */ + public function record() + { + if ($this->get($this->key)) { + //缓存存在,记录错误次数 + $this->inc($this->key, 1); + } else { + //缓存不存在,第一次设置缓存 + $this->set($this->key, 1, $this->minute * 60); + } + } + + /** + * @notes 判断是否安全 + * @return bool + * @author 令狐冲 + * @date 2021/6/30 01:53 + */ + public function isSafe() + { + $count = $this->get($this->key); + if ($count >= $this->count) { + return false; + } + return true; + } + + /** + * @notes 删除该ip记录错误次数 + * @author 令狐冲 + * @date 2021/6/30 01:55 + */ + public function relieve() + { + $this->delete($this->key); + } + + +} \ No newline at end of file diff --git a/server/app/common/cache/AdminAuthCache.php b/server/app/common/cache/AdminAuthCache.php new file mode 100644 index 0000000..e3ff7a6 --- /dev/null +++ b/server/app/common/cache/AdminAuthCache.php @@ -0,0 +1,122 @@ +adminId = $adminId; + // 全部权限 + $this->authConfigList = AuthLogic::getAllAuth(); + // 当前权限配置文件的md5 + $this->authMd5 = md5(json_encode($this->authConfigList)); + + $this->cacheMd5Key = $this->prefix . 'md5'; + $this->cacheAllKey = $this->prefix . 'all'; + $this->cacheUrlKey = $this->prefix . 'url_' . $this->adminId; + + $cacheAuthMd5 = $this->get($this->cacheMd5Key); + $cacheAuth = $this->get($this->cacheAllKey); + //权限配置和缓存权限对比,不一样说明权限配置文件已修改,清理缓存 + if ($this->authMd5 !== $cacheAuthMd5 || empty($cacheAuth)) { + $this->deleteTag(); + } + } + + + /** + * @notes 获取管理权限uri + * @param $adminId + * @return array|mixed + * @author 令狐冲 + * @date 2021/8/19 15:27 + */ + public function getAdminUri() + { + //从缓存获取,直接返回 + $urisAuth = $this->get($this->cacheUrlKey); + if ($urisAuth) { + return $urisAuth; + } + + //获取角色关联的菜单id(菜单或权限) + $urisAuth = AuthLogic::getAuthByAdminId($this->adminId); + if (empty($urisAuth)) { + return []; + } + + $this->set($this->cacheUrlKey, $urisAuth, 3600); + + //保存到缓存并读取返回 + return $urisAuth; + } + + + /** + * @notes 获取全部权限uri + * @return array|mixed + * @author cjhao + * @date 2021/9/13 11:41 + */ + public function getAllUri() + { + $cacheAuth = $this->get($this->cacheAllKey); + if ($cacheAuth) { + return $cacheAuth; + } + // 获取全部权限 + $authList = AuthLogic::getAllAuth(); + //保存到缓存并读取返回 + $this->set($this->cacheMd5Key, $this->authMd5); + $this->set($this->cacheAllKey, $authList); + return $authList; + } + + + /** + * @notes 清理管理员缓存 + * @return bool + * @author cjhao + * @date 2021/10/13 18:47 + */ + public function clearAuthCache() + { + $this->tag($this->cacheUrlKey)->clear(); + return true; + } + + +} \ No newline at end of file diff --git a/server/app/common/cache/AdminTokenCache.php b/server/app/common/cache/AdminTokenCache.php new file mode 100644 index 0000000..7680ecb --- /dev/null +++ b/server/app/common/cache/AdminTokenCache.php @@ -0,0 +1,119 @@ +get($this->prefix . $token); + if ($adminInfo) { + return $adminInfo; + } + + //从数据获取信息被设置缓存(可能后台清除缓存) + $adminInfo = $this->setAdminInfo($token); + if ($adminInfo) { + return $adminInfo; + } + + return false; + } + + /** + * @notes 通过有效token设置管理信息缓存 + * @param $token + * @return array|false|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 令狐冲 + * @date 2021/7/5 12:12 + */ + public function setAdminInfo($token) + { + $adminSession = AdminSession::where([['token', '=', $token], ['expire_time', '>', time()]]) + ->find(); + if (empty($adminSession)) { + return []; + } + $admin = Admin::where('id', '=', $adminSession->admin_id) + ->append(['role_id']) + ->find(); + + $roleName = ''; + $roleLists = SystemRole::column('name', 'id'); + if ($admin['root'] == 1) { + $roleName = '系统管理员'; + } else { + foreach ($admin['role_id'] as $roleId) { + $roleName .= $roleLists[$roleId] ?? ''; + $roleName .= '/'; + } + $roleName = trim($roleName, '/'); + } + + $adminInfo = [ + 'admin_id' => $admin->id, + 'root' => $admin->root, + 'name' => $admin->name, + 'account' => $admin->account, + 'role_name' => $roleName, + 'role_id' => $admin->role_id, + 'token' => $token, + 'terminal' => $adminSession->terminal, + 'expire_time' => $adminSession->expire_time, + 'login_ip' => request()->ip(), + ]; + $this->set($this->prefix . $token, $adminInfo, new \DateTime(Date('Y-m-d H:i:s', $adminSession->expire_time))); + return $this->getAdminInfo($token); + } + + /** + * @notes 删除缓存 + * @param $token + * @return bool + * @author 令狐冲 + * @date 2021/7/3 16:57 + */ + public function deleteAdminInfo($token) + { + return $this->delete($this->prefix . $token); + } + + +} \ No newline at end of file diff --git a/server/app/common/cache/BaseCache.php b/server/app/common/cache/BaseCache.php new file mode 100644 index 0000000..f3e10a2 --- /dev/null +++ b/server/app/common/cache/BaseCache.php @@ -0,0 +1,68 @@ +tagName = get_class($this); + } + + + /** + * @notes 重写父类set,自动打上标签 + * @param string $key + * @param mixed $value + * @param null $ttl + * @return bool + * @author 段誉 + * @date 2021/12/27 14:16 + */ + public function set($key, $value, $ttl = null): bool + { + return $this->store()->tag($this->tagName)->set($key, $value, $ttl); + } + + + /** + * @notes 清除缓存类所有缓存 + * @return bool + * @author 段誉 + * @date 2021/12/27 14:16 + */ + public function deleteTag(): bool + { + return $this->tag($this->tagName)->clear(); + } + +} \ No newline at end of file diff --git a/server/app/common/cache/ExportCache.php b/server/app/common/cache/ExportCache.php new file mode 100644 index 0000000..7282e18 --- /dev/null +++ b/server/app/common/cache/ExportCache.php @@ -0,0 +1,70 @@ +uniqid = md5(uniqid($this->tagName,true).mt_rand()); + } + + /** + * @notes 获取excel缓存目录 + * @return string + * @author 令狐冲 + * @date 2021/7/28 17:36 + */ + public function getSrc() + { + return app()->getRootPath() . 'runtime/file/export/'.date('Y-m').'/'.$this->uniqid.'/'; + } + + + /** + * @notes 设置文件路径缓存地址 + * @param $fileName + * @return string + * @author 令狐冲 + * @date 2021/7/28 17:36 + */ + public function setFile($fileName) + { + $src = $this->getSrc(); + $key = md5($src . $fileName) . time(); + $this->set($key, ['src' => $src, 'name' => $fileName], 300); + return $key; + } + + /** + * @notes 获取文件缓存的路径 + * @param $key + * @return mixed + * @author 令狐冲 + * @date 2021/7/26 18:36 + */ + public function getFile($key) + { + return $this->get($key); + } + +} \ No newline at end of file diff --git a/server/app/common/cache/UserAccountSafeCache.php b/server/app/common/cache/UserAccountSafeCache.php new file mode 100644 index 0000000..feb7f58 --- /dev/null +++ b/server/app/common/cache/UserAccountSafeCache.php @@ -0,0 +1,80 @@ +ip(); + $this->key = $this->tagName . $ip . ':' . $account; + } + + /** + * @notes 记录登录错误次数 + * @author 令狐冲 + * @date 2021/6/30 01:51 + */ + public function record() + { + if ($this->get($this->key)) { + //缓存存在,记录错误次数 + $this->inc($this->key, 1); + } else { + //缓存不存在,第一次设置缓存 + $this->set($this->key, 1, $this->minute * 60); + } + } + + /** + * @notes 判断是否安全 + * @return bool + * @author 令狐冲 + * @date 2021/6/30 01:53 + */ + public function isSafe() + { + $count = $this->get($this->key); + if ($count >= $this->count) { + return false; + } + return true; + } + + /** + * @notes 删除该ip记录错误次数 + * @author 令狐冲 + * @date 2021/6/30 01:55 + */ + public function relieve() + { + $this->delete($this->key); + } + + +} diff --git a/server/app/common/cache/UserTokenCache.php b/server/app/common/cache/UserTokenCache.php new file mode 100644 index 0000000..c38524f --- /dev/null +++ b/server/app/common/cache/UserTokenCache.php @@ -0,0 +1,105 @@ +get($this->prefix . $token); + if ($userInfo) { + return $userInfo; + } + + //从数据获取信息被设置缓存(可能后台清除缓存) + $userInfo = $this->setUserInfo($token); + if ($userInfo) { + return $userInfo; + } + + return false; + } + + + /** + * @notes 通过有效token设置用户信息缓存 + * @param $token + * @return array|false|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/16 10:11 + */ + public function setUserInfo($token) + { + $userSession = UserSession::where([['token', '=', $token], ['expire_time', '>', time()]])->find(); + if (empty($userSession)) { + return []; + } + + $user = User::where('id', '=', $userSession->user_id) + ->find(); + + $userInfo = [ + 'user_id' => $user->id, + 'nickname' => $user->nickname, + 'token' => $token, + 'sn' => $user->sn, + 'mobile' => $user->mobile, + 'avatar' => $user->avatar, + 'terminal' => $userSession->terminal, + 'expire_time' => $userSession->expire_time, + ]; + + $ttl = new \DateTime(Date('Y-m-d H:i:s', $userSession->expire_time)); + $this->set($this->prefix . $token, $userInfo, $ttl); + return $this->getUserInfo($token); + } + + + /** + * @notes 删除缓存 + * @param $token + * @return bool + * @author 段誉 + * @date 2022/9/16 10:13 + */ + public function deleteUserInfo($token) + { + return $this->delete($this->prefix . $token); + } + + +} \ No newline at end of file diff --git a/server/app/common/cache/WebScanLoginCache.php b/server/app/common/cache/WebScanLoginCache.php new file mode 100644 index 0000000..c8102db --- /dev/null +++ b/server/app/common/cache/WebScanLoginCache.php @@ -0,0 +1,65 @@ +get($this->prefix . $state); + } + + + /** + * @notes 设置扫码登录状态 + * @param $state + * @return false|mixed + * @author 段誉 + * @date 2022/10/20 18:31 + */ + public function setScanLoginState($state) + { + $this->set($this->prefix . $state, $state, 600); + return $this->getScanLoginState($state); + } + + + /** + * @notes 删除缓存 + * @param $token + * @return bool + * @author 段誉 + * @date 2022/9/16 10:13 + */ + public function deleteLoginState($state) + { + return $this->delete($this->prefix . $state); + } + + +} \ No newline at end of file diff --git a/server/app/common/command/ArticleAiComment.php b/server/app/common/command/ArticleAiComment.php new file mode 100644 index 0000000..7d4e2e0 --- /dev/null +++ b/server/app/common/command/ArticleAiComment.php @@ -0,0 +1,31 @@ +setName('article_ai_comment') + ->setDescription('资讯AI评论定时任务'); + } + + protected function execute(Input $input, Output $output) + { + $summary = ArticleAiCommentService::tick(); + + $output->writeln(sprintf( + '[ArticleAiComment] 虚拟账号 %d 个,新增任务 %d 个,已执行 %d 个', + $summary['virtual_user_count'] ?? 0, + $summary['seeded']['created'] ?? 0, + $summary['processed']['done'] ?? 0 + )); + + return 0; + } +} diff --git a/server/app/common/command/CrawlerCommand.php b/server/app/common/command/CrawlerCommand.php new file mode 100644 index 0000000..4221e4a --- /dev/null +++ b/server/app/common/command/CrawlerCommand.php @@ -0,0 +1,93 @@ +setName('crawler') + ->addArgument('action', Argument::REQUIRED, '执行动作: standings/schedule/all/menu') + ->setDescription('懂球帝数据爬虫'); + } + + protected function execute(Input $input, Output $output) + { + $action = $input->getArgument('action'); + $allowed = [ + 'standings', + 'schedule', + 'all', + 'menu', + 'news', + 'league_news', + 'article_content', + 'live', + 'lottery', + 'lottery_draw', + 'lottery_draw_force', + 'match_data', + 'init-db', + 'test', + 'csl_match', + 'nba_match', + 'cba_match', + 'epl_match', + 'bundesliga_match', + 'laliga_match', + 'seriea_match', + 'ligue1_match', + 'ucl_match', + 'uel_match', + 'tennis_match', + 'esports_match', + 'sports_match', + 'truth_social', + 'crypto_news', + 'lottery_news', + 'taiwan_lottery_news', + 'hkjc_lottery_news', + 'lottery_community', + 'nba_news', + 'cba_news', + 'article_content_fetch', + 'fifa_worldcup_news', + 'dqd_worldcup', + 'match_finish', + 'live_detail', + ]; + if (!in_array($action, $allowed)) { + $output->writeln("不支持的动作: {$action}, 可选: " . implode('/', $allowed) . ""); + return; + } + + // 文件锁防止同一动作并发执行 + $lockFile = runtime_path() . "crawler_{$action}.lock"; + $fp = fopen($lockFile, 'w'); + if (!flock($fp, LOCK_EX | LOCK_NB)) { + $output->writeln("[Crawler] {$action} 正在执行中,跳过本次"); + fclose($fp); + return; + } + + try { + $crawlerDir = app()->getRootPath() . 'public/dongqiudi-crawler'; + $venvPython = $crawlerDir . '/venv/bin/python3'; + $python = file_exists($venvPython) ? $venvPython : 'python3'; + $cmd = "cd {$crawlerDir} && {$python} main.py {$action} 2>&1"; + + $output->writeln("[Crawler] 执行: {$cmd}"); + $result = shell_exec($cmd); + $output->writeln($result ?: '(无输出)'); + } finally { + flock($fp, LOCK_UN); + fclose($fp); + @unlink($lockFile); + } + } +} diff --git a/server/app/common/command/Crontab.php b/server/app/common/command/Crontab.php new file mode 100644 index 0000000..c797d85 --- /dev/null +++ b/server/app/common/command/Crontab.php @@ -0,0 +1,236 @@ + 180, + 'nba_match' => 180, + 'cba_match' => 180, + 'epl_match' => 180, + 'bundesliga_match' => 180, + 'laliga_match' => 180, + 'seriea_match' => 180, + 'ligue1_match' => 180, + 'ucl_match' => 180, + 'uel_match' => 180, + 'tennis_match' => 180, + 'esports_match' => 180, + 'sports_match' => 180, + 'live_detail' => 180, + 'match_finish' => 180, + 'lottery_draw' => 180, + 'lottery_draw_force' => 180, + 'article_content' => 600, + 'article_content_fetch' => 900, + 'league_news' => 900, + 'fifa_worldcup_news' => 600, + 'dqd_worldcup' => 600, + ]; + + protected function configure() + { + $this->setName('crontab') + ->setDescription('定时任务'); + } + + protected function execute(Input $input, Output $output) + { + $lists = CrontabModel::where('status', CrontabEnum::START)->select()->toArray(); + if (empty($lists)) { + return false; + } + $time = time(); + foreach ($lists as $item) { + if (empty($item['last_time'])) { + $lastTime = (new CronExpression($item['expression'])) + ->getNextRunDate() + ->getTimestamp(); + CrontabModel::where('id', $item['id'])->update([ + 'last_time' => $lastTime, + ]); + continue; + } + + $nextTime = (new CronExpression($item['expression'])) + ->getNextRunDate($item['last_time']) + ->getTimestamp(); + if ($nextTime > $time) { + // 未到时间,不执行 + continue; + } + // 开始执行 + self::start($item); + } + } + + public static function start($item) + { + if ($item['command'] === 'crawler') { + self::startCrawler($item); + return; + } + + $startTime = microtime(true); + $error = ''; + $outputStr = ''; + try { + ob_start(); + $params = explode(' ', $item['params']); + if (is_array($params) && !empty($item['params'])) { + Console::call($item['command'], $params); + } else { + Console::call($item['command']); + } + $outputStr = ob_get_clean() ?: ''; + CrontabModel::where('id', $item['id'])->update(['error' => '']); + } catch (\Exception $e) { + $outputStr = ob_get_clean() ?: ''; + $error = $e->getMessage(); + CrontabModel::where('id', $item['id'])->update([ + 'error' => $error, + 'status' => CrontabEnum::ERROR + ]); + } finally { + $endTime = microtime(true); + $useTime = round(($endTime - $startTime), 2); + $maxTime = max($useTime, $item['max_time']); + $logId = 0; + CrontabModel::where('id', $item['id'])->update([ + 'last_time' => time(), + 'time' => $useTime, + 'max_time' => $maxTime + ]); + + try { + $log = CrontabLog::create([ + 'crontab_id' => $item['id'], + 'name' => $item['name'], + 'command' => $item['command'], + 'params' => $item['params'], + 'status' => $error ? 2 : 1, + 'output' => $error ?: mb_substr($outputStr ?: '执行完成', 0, 5000), + 'elapsed' => $useTime, + 'trigger_type' => 1, + ]); + $logId = (int) $log->id; + } catch (\Exception $logEx) { + } + + try { + if ($error) { + CrontabAlertService::upsertCommandException($item, $error ?: $outputStr, $logId); + } else { + CrontabAlertService::resolveByCrontabId((int) $item['id']); + } + } catch (\Exception $alertEx) { + } + } + } + + protected static function startCrawler($item) + { + $crawlerDir = app()->getRootPath() . 'public/dongqiudi-crawler'; + $python = is_file($crawlerDir . '/venv/bin/python3') ? $crawlerDir . '/venv/bin/python3' : 'python3'; + $action = $item['params'] ?: 'all'; + $timeout = self::getCrawlerActionTimeout($action); + $now = time(); + + self::markStaleCrawlerLogs($item, $timeout, $now); + + $running = CrontabLog::where('crontab_id', $item['id']) + ->where('status', 0) + ->where('create_time', '>', $now - $timeout) + ->order('id', 'desc') + ->findOrEmpty(); + if (!$running->isEmpty()) { + CrontabModel::where('id', $item['id'])->update([ + 'last_time' => $now, + 'error' => '上一次任务仍在执行,已跳过本次', + ]); + return; + } + + $log = CrontabLog::create([ + 'crontab_id' => $item['id'], + 'name' => $item['name'], + 'command' => $item['command'], + 'params' => $item['params'], + 'status' => 0, + 'output' => '', + 'elapsed' => 0, + 'trigger_type' => 1, + ]); + + $logId = $log->id; + $crontabId = $item['id']; + $cmd = "cd {$crawlerDir} && nohup {$python} main.py {$action} --log-id={$logId} --crontab-id={$crontabId} > /dev/null 2>&1 &"; + \shell_exec($cmd); + + CrontabModel::where('id', $item['id'])->update([ + 'last_time' => time(), + 'error' => '', + ]); + } + + protected static function getCrawlerActionTimeout(string $action): int + { + return self::CRAWLER_ACTION_TIMEOUTS[$action] ?? self::CRAWLER_DEFAULT_TIMEOUT; + } + + protected static function markStaleCrawlerLogs(array $item, int $timeout, int $now): void + { + $staleIds = CrontabLog::where('crontab_id', $item['id']) + ->where('status', 0) + ->where('create_time', '<=', $now - $timeout) + ->column('id'); + if (empty($staleIds)) { + return; + } + + $message = sprintf('执行超过 %d 秒未结束,已由调度器标记超时', $timeout); + CrontabLog::whereIn('id', $staleIds)->update([ + 'status' => 2, + 'output' => $message, + 'elapsed' => $timeout, + ]); + CrontabModel::where('id', $item['id'])->update([ + 'error' => $message, + ]); + + try { + CrontabAlertService::upsertCommandException($item, $message, (int) end($staleIds)); + } catch (\Exception $e) { + } + } +} diff --git a/server/app/common/command/KbConsume.php b/server/app/common/command/KbConsume.php new file mode 100644 index 0000000..2614ff7 --- /dev/null +++ b/server/app/common/command/KbConsume.php @@ -0,0 +1,76 @@ +setName('kb:consume') + ->setDescription('消费AI知识库同步任务') + ->addOption('batch', null, Option::VALUE_OPTIONAL, '批量处理数量', 100); + } + + protected function execute(Input $input, Output $output) + { + $batch = max(1, (int) $input->getOption('batch')); + KbSyncService::markLotteryBackfillJobs($batch); + KbSyncService::markMatchBackfillJobs($batch); + + $jobs = AiKbSyncJob::where('status', AiKbSyncJob::STATUS_PENDING) + ->where('scheduled_at', '<=', time()) + ->order('priority', 'asc') + ->order('id', 'asc') + ->limit($batch) + ->select(); + + $success = 0; + $failed = 0; + + foreach ($jobs as $job) { + $job->status = AiKbSyncJob::STATUS_PROCESSING; + $job->update_time = time(); + $job->save(); + + try { + if ($job->action === AiKbSyncJob::ACTION_DELETE) { + KbService::deleteDocument($job->domain, $job->subtype, (int) $job->source_id); + $result = ['success' => true]; + } else { + $result = KbService::upsertDocument($job->domain, $job->subtype, (int) $job->source_id); + } + + if (!empty($result['success'])) { + $job->status = AiKbSyncJob::STATUS_SUCCESS; + $job->processed_at = time(); + $job->error_msg = ''; + $success++; + } else { + $job->status = AiKbSyncJob::STATUS_FAILED; + $job->retry_count = (int) $job->retry_count + 1; + $job->error_msg = $result['error'] ?? 'unknown'; + $failed++; + } + } catch (\Throwable $e) { + $job->status = AiKbSyncJob::STATUS_FAILED; + $job->retry_count = (int) $job->retry_count + 1; + $job->error_msg = $e->getMessage(); + $failed++; + } + + $job->update_time = time(); + $job->save(); + } + + $output->writeln(sprintf('[kb:consume] success=%d failed=%d jobs=%d', $success, $failed, count($jobs))); + return 0; + } +} diff --git a/server/app/common/command/KbRebuild.php b/server/app/common/command/KbRebuild.php new file mode 100644 index 0000000..94f9991 --- /dev/null +++ b/server/app/common/command/KbRebuild.php @@ -0,0 +1,97 @@ +setName('kb:rebuild') + ->setDescription('重建AI知识库文档') + ->addOption('domain', null, Option::VALUE_OPTIONAL, 'article|post|lottery|match|all', 'all'); + } + + protected function execute(Input $input, Output $output) + { + $domain = (string) $input->getOption('domain'); + $targets = $domain === 'all' ? ['article', 'post', 'lottery', 'match'] : [$domain]; + $summary = ['success' => 0, 'failed' => 0]; + + foreach ($targets as $target) { + foreach ($this->yieldRows($target) as $row) { + $result = KbService::upsertDocument($row['domain'], $row['subtype'], (int) $row['source_id']); + if (!empty($result['success'])) { + $summary['success']++; + } else { + $summary['failed']++; + $output->writeln(sprintf( + '[failed] %s/%s/%d %s', + $row['domain'], + $row['subtype'], + $row['source_id'], + $result['error'] ?? 'unknown' + )); + } + } + } + + KbSyncService::markLotteryBackfillJobs(); + $output->writeln(sprintf('[kb:rebuild] success=%d failed=%d', $summary['success'], $summary['failed'])); + return 0; + } + + private function yieldRows(string $domain): \Generator + { + if ($domain === 'article') { + yield from $this->yieldTableRows('article', 'article', 'article'); + return; + } + if ($domain === 'post') { + yield from $this->yieldTableRows('community_post', 'post', 'post'); + return; + } + if ($domain === 'lottery') { + yield from $this->yieldTableRows('lottery_draw_result', 'lottery', 'draw_result'); + yield from $this->yieldTableRows('lottery_ai_analysis', 'lottery', 'ai_history'); + return; + } + if ($domain === 'match') { + yield from $this->yieldTableRows('match', 'match', 'event'); + } + } + + private function yieldTableRows(string $table, string $domain, string $subtype): \Generator + { + $lastId = 0; + $chunkSize = 5000; + + while (true) { + $ids = Db::name($table) + ->where('id', '>', $lastId) + ->order('id', 'asc') + ->limit($chunkSize) + ->column('id'); + + if (empty($ids)) { + break; + } + + foreach ($ids as $id) { + $lastId = (int) $id; + yield [ + 'domain' => $domain, + 'subtype' => $subtype, + 'source_id' => $lastId, + ]; + } + } + } +} diff --git a/server/app/common/command/QueryRefund.php b/server/app/common/command/QueryRefund.php new file mode 100644 index 0000000..42fab38 --- /dev/null +++ b/server/app/common/command/QueryRefund.php @@ -0,0 +1,202 @@ +setName('query_refund') + ->setDescription('订单退款状态处理'); + } + + + protected function execute(Input $input, Output $output) + { + try { + // 查找退款中的退款记录(微信,支付宝支付) + $refundRecords = (new RefundLog())->alias('l') + ->join('refund_record r', 'r.id = l.record_id') + ->field([ + 'l.id' => 'log_id', 'l.sn' => 'log_sn', + 'r.id' => 'record_id', 'r.order_id', 'r.sn' => 'record_sn', 'r.order_type' + ]) + ->where(['l.refund_status' => RefundEnum::REFUND_ING]) + ->select()->toArray(); + + if (empty($refundRecords)) { + return false; + } + + // 分别处理各个类型订单 + $rechargeRecords = array_filter($refundRecords, function ($item) { + return $item['order_type'] == RefundEnum::ORDER_TYPE_RECHARGE; + }); + + if (!empty($rechargeRecords)) { + $this->handleRechargeOrder($rechargeRecords); + } + + return true; + } catch (\Exception $e) { + Log::write('订单退款状态查询失败,失败原因:' . $e->getMessage()); + return false; + } + } + + + /** + * @notes 处理充值订单 + * @param $refundRecords + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @author 段誉 + * @date 2023/3/1 15:55 + */ + public function handleRechargeOrder($refundRecords) + { + $orderIds = array_unique(array_column($refundRecords, 'order_id')); + $Orders = RechargeOrder::whereIn('id', $orderIds)->column('*', 'id'); + + foreach ($refundRecords as $record) { + if (!isset($Orders[$record['order_id']])) { + continue; + } + + $order = $Orders[$record['order_id']]; + if (!in_array($order['pay_way'], [PayEnum::WECHAT_PAY, PayEnum::ALI_PAY])) { + continue; + } + + $this->checkReFundStatus([ + 'record_id' => $record['record_id'], + 'log_id' => $record['log_id'], + 'log_sn' => $record['log_sn'], + 'pay_way' => $order['pay_way'], + 'order_terminal' => $order['order_terminal'], + ]); + } + } + + + /** + * @notes 校验退款状态 + * @param $refundData + * @return bool + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @author 段誉 + * @date 2023/3/1 15:54 + */ + public function checkReFundStatus($refundData) + { + $result = null; + switch ($refundData['pay_way']) { + case PayEnum::WECHAT_PAY: + $result = self::checkWechatRefund($refundData['order_terminal'], $refundData['log_sn']); + break; + } + + if (is_null($result)) { + return false; + } + + if (true === $result) { + $this->updateRefundSuccess($refundData['log_id'], $refundData['record_id']); + } else { + $this->updateRefundMsg($refundData['log_id'], $result); + } + return true; + } + + + /** + * @notes 查询微信支付退款状态 + * @param $orderTerminal + * @param $refundLogSn + * @return bool|string|null + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @author 段誉 + * @date 2023/3/1 15:47 + */ + public function checkWechatRefund($orderTerminal, $refundLogSn) + { + // 根据商户退款单号查询退款 + $result = (new WeChatPayService($orderTerminal))->queryRefund($refundLogSn); + + if (!empty($result['status']) && $result['status'] == 'SUCCESS') { + return true; + } + + if (!empty($result['code']) || !empty($result['message'])) { + return '微信:' . $result['code'] . '-' . $result['message']; + } + + return null; + } + + + /** + * @notes 更新记录为成功 + * @param $logId + * @param $recordId + * @author 段誉 + * @date 2023/3/1 15:38 + */ + public function updateRefundSuccess($logId, $recordId) + { + // 更新日志 + RefundLog::update([ + 'id' => $logId, + 'refund_status' => RefundEnum::REFUND_SUCCESS, + ]); + // 更新记录 + RefundRecord::update([ + 'id' => $recordId, + 'refund_status' => RefundEnum::REFUND_SUCCESS, + ]); + } + + + /** + * @notes 更新退款信息 + * @param $logId + * @param $msg + * @author 段誉 + * @date 2023/3/1 15:47 + */ + public function updateRefundMsg($logId, $msg) + { + // 更新日志 + RefundLog::update([ + 'id' => $logId, + 'refund_msg' => $msg, + ]); + } + +} \ No newline at end of file diff --git a/server/app/common/command/SmsVerifyCommand.php b/server/app/common/command/SmsVerifyCommand.php new file mode 100644 index 0000000..0b46967 --- /dev/null +++ b/server/app/common/command/SmsVerifyCommand.php @@ -0,0 +1,65 @@ +setName('sms_verify') + ->setDescription('轮询未验证手机号用户,从上传记录反向校验并更新绑定状态'); + } + + protected function execute(Input $input, Output $output) + { + $users = User::where('mobile', '<>', '') + ->where('is_bind_mobile', 0) + ->field('id, mobile') + ->select(); + + if ($users->isEmpty()) { + $output->writeln('[SmsVerify] 无待验证用户'); + return; + } + + $output->writeln('[SmsVerify] 待验证用户: ' . $users->count() . ''); + $updated = 0; + + foreach ($users as $user) { + $mobile = $user->mobile; + + // 查询 la_sms_upload 是否有该手机号的任意上传记录(兼容+86前缀) + $record = SmsLog::where(function ($query) use ($mobile) { + $query->where('phone', $mobile) + ->whereOr('phone', '+86' . $mobile) + ->whereOr('phone', '86' . $mobile); + }) + ->order('sms_time', 'desc') + ->findOrEmpty(); + + if (!$record->isEmpty()) { + if ((int)$record->status === 0) { + $record->status = 1; + $record->save(); + } + User::where('id', $user->id)->update(['is_bind_mobile' => 1]); + $updated++; + $output->writeln(" ✓ 用户 {$user->id} ({$mobile}) 已验证"); + } + } + + $output->writeln("[SmsVerify] 完成,更新 {$updated} 个用户"); + } +} diff --git a/server/app/common/command/WorldcupArticleTranslate.php b/server/app/common/command/WorldcupArticleTranslate.php new file mode 100644 index 0000000..47ee7e8 --- /dev/null +++ b/server/app/common/command/WorldcupArticleTranslate.php @@ -0,0 +1,129 @@ +setName('worldcup_article_translate') + ->setDescription('预热世界杯英文资讯翻译缓存'); + } + + protected function execute(Input $input, Output $output) + { + $articles = Article::field('id,cid,title,desc,content,ext,is_show,delete_time') + ->where('cid', 22) + ->where('is_show', YesNoEnum::YES) + // 与前台世界杯资讯列表保持一致,只预热用户最先看到的 50 条。 + ->order('published_at', 'desc') + ->order('id', 'desc') + ->limit(50) + ->select() + ->toArray(); + + if (empty($articles)) { + $output->writeln('[WorldcupTranslate] 没有找到可处理的世界杯文章'); + return 0; + } + + $translated = 0; + $skippedExist = 0; + $skippedNonEnglish = 0; + $failed = 0; + + foreach ($articles as $article) { + $ext = $this->decodeExt($article['ext'] ?? []); + if (!empty($ext['translated_content'])) { + $skippedExist++; + continue; + } + + if (!$this->isProbablyEnglish($article)) { + $skippedNonEnglish++; + continue; + } + + try { + $result = ArticleLogic::autoTranslateArticle($article); + if ($result !== '') { + $translated++; + $output->writeln(sprintf('[WorldcupTranslate] 已翻译文章 #%d %s', $article['id'], $article['title'] ?? '')); + } else { + $failed++; + $output->writeln(sprintf('[WorldcupTranslate] 翻译失败或无结果 #%d %s', $article['id'], $article['title'] ?? '')); + } + } catch (\Throwable $e) { + $failed++; + $output->writeln(sprintf('[WorldcupTranslate] 异常 #%d %s - %s', $article['id'], $article['title'] ?? '', $e->getMessage())); + } + } + + $output->writeln(sprintf( + '[WorldcupTranslate] 完成,翻译 %d 条,已存在 %d 条,跳过非英文 %d 条,失败 %d 条', + $translated, + $skippedExist, + $skippedNonEnglish, + $failed + )); + + return 0; + } + + private function isProbablyEnglish(array $article): bool + { + $text = trim(implode(' ', [ + (string) ($article['title'] ?? ''), + (string) ($article['desc'] ?? ''), + $this->normalizeText((string) ($article['content'] ?? '')), + ])); + + if ($text === '') { + return false; + } + + if (preg_match('/[\x{4e00}-\x{9fff}]/u', $text)) { + return false; + } + + $letterCount = preg_match_all('/[A-Za-z]/', $text); + $wordCount = preg_match_all('/\b[A-Za-z]{2,}\b/', $text); + + // 世界杯资讯里不少正文并不长,阈值过高会误杀正常英文稿件。 + return $letterCount >= 30 && $wordCount >= 5; + } + + private function normalizeText(string $content): string + { + if ($content === '') { + return ''; + } + + $content = strip_tags($content); + $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $content = preg_replace('/\s+/u', ' ', $content); + + return trim($content); + } + + private function decodeExt($ext): array + { + if (is_array($ext)) { + return $ext; + } + + if (is_string($ext) && $ext !== '') { + $data = json_decode($ext, true); + return is_array($data) ? $data : []; + } + + return []; + } +} diff --git a/server/app/common/controller/BaseLikeAdminController.php b/server/app/common/controller/BaseLikeAdminController.php new file mode 100644 index 0000000..7a24b1d --- /dev/null +++ b/server/app/common/controller/BaseLikeAdminController.php @@ -0,0 +1,121 @@ +request->controller() . ucwords($this->request->action())); + $lists = invoke($listName); + } + return JsonService::dataLists($lists); + } + + + /** + * @notes 操作失败 + * @param string $msg + * @param array $data + * @param int $code + * @param int $show + * @return \think\response\Json + * @author 段誉 + * @date 2021/12/27 14:21 + */ + protected function fail(string $msg = 'fail', array $data = [], int $code = 0, int $show = 1) + { + return JsonService::fail($msg, $data, $code, $show); + } + + + + /** + * @notes 是否免登录验证 + * @return bool + * @author 段誉 + * @date 2021/12/27 14:21 + */ + public function isNotNeedLogin(): bool + { + $notNeedLogin = $this->notNeedLogin; + if (empty($notNeedLogin)) { + return false; + } + $action = $this->request->action(); + + if (!in_array(trim($action), $notNeedLogin)) { + return false; + } + return true; + } + + +} diff --git a/server/app/common/enum/AdminTerminalEnum.php b/server/app/common/enum/AdminTerminalEnum.php new file mode 100644 index 0000000..e3dac5d --- /dev/null +++ b/server/app/common/enum/AdminTerminalEnum.php @@ -0,0 +1,28 @@ + '隐藏', + self::SHOW => '显示' + ]; + if ($value === true) { + return $data; + } + return $data[$value]; + } + + /** + * @notes 启用状态 + * @param bool $value + * @return string|string[] + * @author ljj + * @date 2022/2/14 4:02 下午 + */ + public static function getEnableDesc($value = true) + { + $data = [ + self::HIDE => '停用', + self::SHOW => '启用' + ]; + if ($value === true) { + return $data; + } + return $data[$value]; + } + + /** + * @notes 性别 + * @param bool $value + * @return string|string[] + * @author ljj + * @date 2022/2/10 11:40 上午 + */ + public static function getSexDesc($value = true) + { + $data = [ + self::UNKNOWN => '未知', + self::MAN => '男', + self::WOMAN => '女' + ]; + if ($value === true) { + return $data; + } + return $data[$value]; + } + + + /** + * @notes 属性 + * @param bool $value + * @return string|string[] + * @author ljj + * @date 2022/2/14 4:41 下午 + */ + public static function getAttrDesc($value = true) + { + $data = [ + self::SYSTEM => '系统默认', + self::CUSTOM => '自定义' + ]; + if ($value === true) { + return $data; + } + return $data[$value]; + } + + + /** + * @notes 是否推荐 + * @param bool $value + * @return string|string[] + * @author ljj + * @date 2022/2/23 3:30 下午 + */ + public static function getRecommendDesc($value = true) + { + $data = [ + self::HIDE => '不推荐', + self::SHOW => '推荐' + ]; + if ($value === true) { + return $data; + } + return $data[$value]; + } +} \ No newline at end of file diff --git a/server/app/common/enum/ExportEnum.php b/server/app/common/enum/ExportEnum.php new file mode 100644 index 0000000..6efd2a1 --- /dev/null +++ b/server/app/common/enum/ExportEnum.php @@ -0,0 +1,14 @@ + '单表(增删改查)', + self::TEMPLATE_TYPE_TREE => '树表(增删改查)', + ]; + if ($value === true) { + return $data; + } + return $data[$value] ?? ''; + } +} \ No newline at end of file diff --git a/server/app/common/enum/LoginEnum.php b/server/app/common/enum/LoginEnum.php new file mode 100644 index 0000000..044e503 --- /dev/null +++ b/server/app/common/enum/LoginEnum.php @@ -0,0 +1,32 @@ + 1, + 'name' => '首页', + 'path' => '/pages/index/index', + 'params' => [], + 'type' => 'shop', + ], + ]; + + + //菜单类型 + const NAVIGATION_HOME = 1;//首页导航 + const NAVIGATION_PERSONAL = 2;//个人中心 + + //链接类型 + const LINK_SHOP = 1;//商城页面 + const LINK_CATEGORY = 2;//分类页面 + const LINK_CUSTOM = 3;//自定义链接 + + /** + * @notes 链接类型 + * @param bool $value + * @return string|string[] + * @author ljj + * @date 2022/2/14 12:14 下午 + */ + public static function getLinkDesc($value = true) + { + $data = [ + self::LINK_SHOP => '商城页面', + self::LINK_CATEGORY => '分类页面', + self::LINK_CUSTOM => '自定义链接' + ]; + if ($value === true) { + return $data; + } + return $data[$value]; + } +} \ No newline at end of file diff --git a/server/app/common/enum/OfficialAccountEnum.php b/server/app/common/enum/OfficialAccountEnum.php new file mode 100644 index 0000000..ea0be16 --- /dev/null +++ b/server/app/common/enum/OfficialAccountEnum.php @@ -0,0 +1,94 @@ + 'follow', + self::REPLY_TYPE_KEYWORD => 'keyword', + self::REPLY_TYPE_DEFAULT => 'default' + ]; + + /** + * 匹配类型 - 全匹配 + */ + const MATCHING_TYPE_FULL = 1; + + /** + * 匹配类型 - 模糊匹配 + */ + const MATCHING_TYPE_FUZZY = 2; + + /** + * 消息类型 - 事件 + */ + const MSG_TYPE_EVENT = 'event'; + + /** + * 消息类型 - 文本 + */ + const MSG_TYPE_TEXT = 'text'; + + /** + * 事件类型 - 关注 + */ + const EVENT_SUBSCRIBE = 'subscribe'; + + /** + * @notes 获取类型英文名称 + * @param $type + * @return string + * @author Tab + * @date 2021/7/29 16:32 + */ + public static function getReplyType($type) + { + return self::REPLY_TYPE[$type] ?? ''; + } +} \ No newline at end of file diff --git a/server/app/common/enum/PayEnum.php b/server/app/common/enum/PayEnum.php new file mode 100644 index 0000000..d91def1 --- /dev/null +++ b/server/app/common/enum/PayEnum.php @@ -0,0 +1,112 @@ + '余额支付', + self::WECHAT_PAY => '微信支付', + self::ALI_PAY => '支付宝支付', + ]; + if ($value === true) { + return $data; + } + return $data[$value] ?? ''; + } + + + + /** + * @notes 支付状态 + * @param bool $value + * @return string|string[] + * @author 段誉 + * @date 2023/2/23 15:36 + */ + public static function getPayStatusDesc($value = true) + { + $data = [ + self::UNPAID => '未支付', + self::ISPAID => '已支付', + ]; + if ($value === true) { + return $data; + } + return $data[$value] ?? ''; + } + + + /** + * @notes 支付场景 + * @param bool $value + * @return string|string[] + * @author 段誉 + * @date 2023/2/23 15:36 + */ + public static function getPaySceneDesc($value = true) + { + $data = [ + self::SCENE_H5 => 'H5', + self::SCENE_OA => '微信公众号', + self::SCENE_MNP => '微信小程序', + self::SCENE_APP => 'APP', + self::SCENE_PC => 'PC', + ]; + if ($value === true) { + return $data; + } + return $data[$value] ?? ''; + } + + +} \ No newline at end of file diff --git a/server/app/common/enum/RefundEnum.php b/server/app/common/enum/RefundEnum.php new file mode 100644 index 0000000..b851172 --- /dev/null +++ b/server/app/common/enum/RefundEnum.php @@ -0,0 +1,116 @@ + '后台退款', + ]; + if ($value === true) { + return $data; + } + return $data[$value]; + } + + + /** + * @notes 退款状态 + * @param bool $value + * @return string|string[] + * @author 段誉 + * @date 2022/12/1 10:43 + */ + public static function getStatusDesc($value = true) + { + $data = [ + self::REFUND_ING => '退款中', + self::REFUND_SUCCESS => '退款成功', + self::REFUND_ERROR => '退款失败', + ]; + if ($value === true) { + return $data; + } + return $data[$value]; + } + + + + /** + * @notes 退款方式 + * @param bool $value + * @return string|string[] + * @author 段誉 + * @date 2022/12/1 10:43 + */ + public static function getWayDesc($value = true) + { + $data = [ + self::REFUND_ONLINE => '线上退款', + self::REFUND_OFFLINE => '线下退款', + ]; + if ($value === true) { + return $data; + } + return $data[$value]; + } + + + /** + * @notes 通过支付方式获取退款方式 + * @param $payWay + * @return int + * @author 段誉 + * @date 2022/12/6 10:31 + */ + public static function getRefundWayByPayWay($payWay) + { + if (in_array($payWay, [PayEnum::ALI_PAY, PayEnum::WECHAT_PAY])) { + return self::REFUND_ONLINE; + } + return self::REFUND_OFFLINE; + } + +} \ No newline at end of file diff --git a/server/app/common/enum/YesNoEnum.php b/server/app/common/enum/YesNoEnum.php new file mode 100644 index 0000000..f5db7ed --- /dev/null +++ b/server/app/common/enum/YesNoEnum.php @@ -0,0 +1,46 @@ + '禁用', + self::NO => '正常' + ]; + if ($value === true) { + return $data; + } + return $data[$value]; + } +} \ No newline at end of file diff --git a/server/app/common/enum/notice/NoticeEnum.php b/server/app/common/enum/notice/NoticeEnum.php new file mode 100644 index 0000000..ea28d66 --- /dev/null +++ b/server/app/common/enum/notice/NoticeEnum.php @@ -0,0 +1,272 @@ + '业务通知', + self::VERIFICATION_CODE => '验证码' + ]; + if ($value === true) { + return $data; + } + return $data[$value]; + } + + + /** + * @notes 获取场景描述 + * @param $sceneId + * @param false $flag + * @return string|string[] + * @author 段誉 + * @date 2022/3/29 11:33 + */ + public static function getSceneDesc($sceneId, $flag = false) + { + $desc = [ + self::LOGIN_CAPTCHA => '登录验证码', + self::BIND_MOBILE_CAPTCHA => '绑定手机验证码', + self::CHANGE_MOBILE_CAPTCHA => '变更手机验证码', + self::FIND_LOGIN_PASSWORD_CAPTCHA => '找回登录密码验证码', + ]; + + if ($flag) { + return $desc; + } + + return $desc[$sceneId] ?? ''; + } + + + /** + * @notes 更具标记获取场景 + * @param $tag + * @return int|string + * @author 段誉 + * @date 2022/9/15 15:08 + */ + public static function getSceneByTag($tag) + { + $scene = [ + // 手机验证码登录 + 'YZMDL' => self::LOGIN_CAPTCHA, + // 绑定手机号验证码 + 'BDSJHM' => self::BIND_MOBILE_CAPTCHA, + // 变更手机号验证码 + 'BGSJHM' => self::CHANGE_MOBILE_CAPTCHA, + // 找回登录密码 + 'ZHDLMM' => self::FIND_LOGIN_PASSWORD_CAPTCHA, + ]; + return $scene[$tag] ?? ''; + } + + + /** + * @notes 获取场景变量 + * @param $sceneId + * @param false $flag + * @return array|string[] + * @author 段誉 + * @date 2022/3/29 11:33 + */ + public static function getVars($sceneId, $flag = false) + { + $desc = [ + self::LOGIN_CAPTCHA => '验证码:code', + self::BIND_MOBILE_CAPTCHA => '验证码:code', + self::CHANGE_MOBILE_CAPTCHA => '验证码:code', + self::FIND_LOGIN_PASSWORD_CAPTCHA => '验证码:code', + ]; + + if ($flag) { + return $desc; + } + + return isset($desc[$sceneId]) ? ['可选变量 ' . $desc[$sceneId]] : []; + } + + + /** + * @notes 获取系统通知示例 + * @param $sceneId + * @param false $flag + * @return array|string[] + * @author 段誉 + * @date 2022/3/29 11:33 + */ + public static function getSystemExample($sceneId, $flag = false) + { + $desc = []; + + if ($flag) { + return $desc; + } + + return isset($desc[$sceneId]) ? [$desc[$sceneId]] : []; + } + + + /** + * @notes 获取短信通知示例 + * @param $sceneId + * @param false $flag + * @return array|string[] + * @author 段誉 + * @date 2022/3/29 11:33 + */ + public static function getSmsExample($sceneId, $flag = false) + { + $desc = [ + self::LOGIN_CAPTCHA => '您正在登录,验证码${code},切勿将验证码泄露于他人,本条验证码有效期5分钟。', + self::BIND_MOBILE_CAPTCHA => '您正在绑定手机号,验证码${code},切勿将验证码泄露于他人,本条验证码有效期5分钟。', + self::CHANGE_MOBILE_CAPTCHA => '您正在变更手机号,验证码${code},切勿将验证码泄露于他人,本条验证码有效期5分钟。', + self::FIND_LOGIN_PASSWORD_CAPTCHA => '您正在找回登录密码,验证码${code},切勿将验证码泄露于他人,本条验证码有效期5分钟。', + ]; + + if ($flag) { + return $desc; + } + + return isset($desc[$sceneId]) ? ['示例:' . $desc[$sceneId]] : []; + } + + + /** + * @notes 获取公众号模板消息示例 + * @param $sceneId + * @param false $flag + * @return array|string[]|\string[][] + * @author 段誉 + * @date 2022/3/29 11:33 + */ + public static function getOaExample($sceneId, $flag = false) + { + $desc = []; + + if ($flag) { + return $desc; + } + + return $desc[$sceneId] ?? []; + } + + + /** + * @notes 获取小程序订阅消息示例 + * @param $sceneId + * @param false $flag + * @return array|mixed + * @author 段誉 + * @date 2022/3/29 11:33 + */ + public static function getMnpExample($sceneId, $flag = false) + { + $desc = []; + + if ($flag) { + return $desc; + } + + return $desc[$sceneId] ?? []; + } + + + /** + * @notes 提示 + * @param $type + * @param $sceneId + * @return array|string|string[]|\string[][] + * @author 段誉 + * @date 2022/3/29 11:33 + */ + public static function getOperationTips($type, $sceneId) + { + // 场景变量 + $vars = self::getVars($sceneId); + // 其他提示 + $other = []; + // 示例 + switch ($type) { + case self::SYSTEM: + $example = self::getSystemExample($sceneId); + break; + case self::SMS: + $other[] = '生效条件:1、管理后台完成短信设置。 2、第三方短信平台申请模板。'; + $example = self::getSmsExample($sceneId); + break; + case self::OA: + $other[] = '配置路径:公众号后台 > 广告与服务 > 模板消息'; + $other[] = '推荐行业:主营行业:IT科技/互联网|电子商务 副营行业:消费品/消费品'; + $example = self::getOaExample($sceneId); + break; + case self::MNP: + $other[] = '配置路径:小程序后台 > 功能 > 订阅消息'; + $example = self::getMnpExample($sceneId); + break; + } + $tips = array_merge($vars, $example, $other); + + return $tips; + } +} \ No newline at end of file diff --git a/server/app/common/enum/notice/SmsEnum.php b/server/app/common/enum/notice/SmsEnum.php new file mode 100644 index 0000000..3fe0c63 --- /dev/null +++ b/server/app/common/enum/notice/SmsEnum.php @@ -0,0 +1,53 @@ + '阿里云短信', + 'TENCENT' => '腾讯云短信', + ]; + return $desc[$value] ?? ''; + } + +} \ No newline at end of file diff --git a/server/app/common/enum/user/AccountLogEnum.php b/server/app/common/enum/user/AccountLogEnum.php new file mode 100644 index 0000000..c068891 --- /dev/null +++ b/server/app/common/enum/user/AccountLogEnum.php @@ -0,0 +1,244 @@ + '减少', + self::INC => '增加', + ]; + if ($flag) { + return $desc; + } + return $desc[$action] ?? ''; + } + + + /** + * @notes 变动类型描述 + * @param $changeType + * @param false $flag + * @return string|string[] + * @author 段誉 + * @date 2023/2/23 10:07 + */ + public static function getChangeTypeDesc($changeType, $flag = false) + { + $desc = [ + self::UM_DEC_ADMIN => '平台减少余额', + self::UM_INC_ADMIN => '平台增加余额', + self::UM_INC_RECHARGE => '充值增加余额', + self::UM_DEC_RECHARGE_REFUND => '充值订单退款减少余额', + self::UM_DEC_BUY_POINTS => '余额购买积分', + self::UM_DEC_BUY_VIP => '余额购买VIP', + self::UP_DEC_ADMIN => '平台减少积分', + self::UP_DEC_PURCHASE_POST => '购买帖子消费积分', + self::UP_DEC_TRANSLATE_POST => 'AI翻译帖子消费积分', + self::UP_INC_ADMIN => '平台增加积分', + self::UP_INC_POST_SOLD => '帖子被购买获得积分', + self::UP_INC_BUY_POINTS => '购买积分到账', + ]; + if ($flag) { + return $desc; + } + return $desc[$changeType] ?? ''; + } + + + /** + * @notes 获取用户余额类型描述 + * @return string|string[] + * @author 段誉 + * @date 2023/2/23 10:08 + */ + public static function getUserMoneyChangeTypeDesc() + { + $UMChangeType = self::getUserMoneyChangeType(); + $changeTypeDesc = self::getChangeTypeDesc('', true); + return array_filter($changeTypeDesc, function ($key) use ($UMChangeType) { + return in_array($key, $UMChangeType); + }, ARRAY_FILTER_USE_KEY); + } + + + /** + * @notes 获取用户余额变动类型 + * @return int[] + * @author 段誉 + * @date 2023/2/23 10:08 + */ + public static function getUserMoneyChangeType(): array + { + return array_merge(self::UM_DEC, self::UM_INC); + } + + + /** + * @notes 获取用户积分变动类型 + * @return int[] + * @author 段誉 + * @date 2023/2/23 10:08 + */ + public static function getUserPointsChangeType(): array + { + return array_merge(self::UP_DEC, self::UP_INC); + } + + + /** + * @notes 获取用户积分类型描述 + * @return string|string[] + * @author 段誉 + * @date 2023/2/23 10:08 + */ + public static function getUserPointsChangeTypeDesc() + { + $UPChangeType = self::getUserPointsChangeType(); + $changeTypeDesc = self::getChangeTypeDesc('', true); + return array_filter($changeTypeDesc, function ($key) use ($UPChangeType) { + return in_array($key, $UPChangeType); + }, ARRAY_FILTER_USE_KEY); + } + + + /** + * @notes 获取变动对象 + * @param $changeType + * @return false + * @author 段誉 + * @date 2023/2/23 10:10 + */ + public static function getChangeObject($changeType) + { + // 用户余额 + $um = self::getUserMoneyChangeType(); + if (in_array($changeType, $um)) { + return self::UM; + } + + // 用户积分 + $up = self::getUserPointsChangeType(); + if (in_array($changeType, $up)) { + return self::UP; + } + + return false; + } +} diff --git a/server/app/common/enum/user/UserEnum.php b/server/app/common/enum/user/UserEnum.php new file mode 100644 index 0000000..17b2ea5 --- /dev/null +++ b/server/app/common/enum/user/UserEnum.php @@ -0,0 +1,55 @@ + '未知', + self::SEX_MEN => '男', + self::SEX_WOMAN => '女', + ]; + if (true === $from) { + return $desc; + } + return $desc[$from] ?? ''; + } +} \ No newline at end of file diff --git a/server/app/common/enum/user/UserTerminalEnum.php b/server/app/common/enum/user/UserTerminalEnum.php new file mode 100644 index 0000000..d6327a0 --- /dev/null +++ b/server/app/common/enum/user/UserTerminalEnum.php @@ -0,0 +1,64 @@ + '微信小程序', + self::WECHAT_OA => '微信公众号', + self::H5 => '手机H5', + self::PC => '电脑PC', + self::IOS => '苹果APP', + self::ANDROID => '安卓APP', + ]; + if(true === $from){ + return $desc; + } + return $desc[$from] ?? ''; + } +} \ No newline at end of file diff --git a/server/app/common/exception/ControllerExtendException.php b/server/app/common/exception/ControllerExtendException.php new file mode 100644 index 0000000..c6b156a --- /dev/null +++ b/server/app/common/exception/ControllerExtendException.php @@ -0,0 +1,40 @@ +message = '控制器需要继承模块的基础控制器:' . $message; + $this->model = $model; + } +} \ No newline at end of file diff --git a/server/app/common/http/middleware/BaseMiddleware.php b/server/app/common/http/middleware/BaseMiddleware.php new file mode 100644 index 0000000..df8205b --- /dev/null +++ b/server/app/common/http/middleware/BaseMiddleware.php @@ -0,0 +1,30 @@ +setCorsHeaders(); + + // 如果是OPTIONS请求,直接返回响应 + if (strtoupper($request->method()) === 'OPTIONS') { + return response(); + } + + // 安装检测 + $install = file_exists(root_path() . '/config/install.lock'); + if (!$install) { + return JsonService::fail('程序未安装', [], -2); + } + + return $next($request); + } + + /** + * @notes 设置跨域头信息 + * @return void + * @author JXDN + * @date 2024/09/24 16:35 + */ + private function setCorsHeaders(): void + { + $headers = [ + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Allow-Headers' => implode(', ', self::ALLOWED_HEADERS), + 'Access-Control-Allow-Methods' => 'GET, POST, PATCH, PUT, DELETE, post', + 'Access-Control-Max-Age' => '1728000', + 'Access-Control-Allow-Credentials' => 'true' + ]; + + foreach ($headers as $key => $value) { + header("$key: $value"); + } + } +} \ No newline at end of file diff --git a/server/app/common/listener/NoticeListener.php b/server/app/common/listener/NoticeListener.php new file mode 100644 index 0000000..2090c24 --- /dev/null +++ b/server/app/common/listener/NoticeListener.php @@ -0,0 +1,43 @@ +getMessage()); + return $e->getMessage(); + } + } +} \ No newline at end of file diff --git a/server/app/common/lists/BaseDataLists.php b/server/app/common/lists/BaseDataLists.php new file mode 100644 index 0000000..3417a32 --- /dev/null +++ b/server/app/common/lists/BaseDataLists.php @@ -0,0 +1,218 @@ +get()->goCheck(); + + //请求参数设置 + $this->request = request(); + $this->params = $this->request->param(); + + //分页初始化 + $this->initPage(); + + //搜索初始化 + $this->initSearch(); + + //排序初始化 + $this->initSort(); + + //导出初始化 + $this->initExport(); + } + + + /** + * @notes 分页参数初始化 + * @author 令狐冲 + * @date 2021/7/30 23:55 + */ + private function initPage() + { + $this->pageSizeMax = Config::get('project.lists.page_size_max'); + $this->pageSize = Config::get('project.lists.page_size'); + $this->pageType = $this->request->get('page_type', 1); + + if ($this->pageType == 1) { + //分页 + $this->pageNo = $this->request->get('page_no', 1) ?: 1; + $this->pageSize = $this->request->get('page_size', $this->pageSize) ?: $this->pageSize; + } else { + //不分页 + $this->pageNo = 1;//强制到第一页 + $this->pageSize = $this->pageSizeMax;// 直接取最大记录数 + } + + //limit查询参数设置 + $this->limitOffset = ($this->pageNo - 1) * $this->pageSize; + $this->limitLength = $this->pageSize; + } + + /** + * @notes 初始化搜索 + * @return array + * @author 令狐冲 + * @date 2021/7/31 00:00 + */ + private function initSearch() + { + if (!($this instanceof ListsSearchInterface)) { + return []; + } + $startTime = $this->request->get('start_time'); + if ($startTime) { + $this->startTime = strtotime($startTime); + } + + $endTime = $this->request->get('end_time'); + if ($endTime) { + $this->endTime = strtotime($endTime); + } + + $this->start = $this->request->get('start'); + $this->end = $this->request->get('end'); + + return $this->searchWhere = $this->createWhere($this->setSearch()); + } + + + /** + * @notes 初始化排序 + * @return array|string[] + * @author 令狐冲 + * @date 2021/7/31 00:03 + */ + private function initSort() + { + if (!($this instanceof ListsSortInterface)) { + return []; + } + + $this->field = $this->request->get('field', ''); + $this->orderBy = $this->request->get('order_by', ''); + + return $this->sortOrder = $this->createOrder($this->setSortFields(), $this->setDefaultOrder()); + } + + /** + * @notes 导出初始化 + * @return false|\think\response\Json + * @author 令狐冲 + * @date 2021/7/31 01:15 + */ + private function initExport() + { + $this->export = $this->request->get('export', ''); + + //不做导出操作 + if ($this->export != ExportEnum::INFO && $this->export != ExportEnum::EXPORT) { + return false; + } + + //导出操作,但是没有实现导出接口 + if (!($this instanceof ListsExcelInterface)) { + return JsonService::throw('该列表不支持导出'); + } + + $this->fileName = $this->request->get('file_name', '') ?: $this->setFileName(); + + //不导出文件,不初始化一下参数 + if ($this->export != ExportEnum::EXPORT) { + return false; + } + + //导出文件名设置 + $this->fileName .= '-' . date('Y-m-d-His') . '.xlsx'; + + //导出文件准备 + //指定导出范围(例:第2页到,第5页的数据) + if ($this->pageType == 1) { + $this->pageStart = $this->request->get('page_start', $this->pageStart); + $this->pageEnd = $this->request->get('page_end', $this->pageEnd); + //改变查询数量参数(例:第2页到,第5页的数据,查询->page(2,(5-2+1)*25) + $this->limitOffset = ($this->pageStart - 1) * $this->pageSize; + $this->limitLength = ($this->pageEnd - $this->pageStart + 1) * $this->pageSize; + } + + $count = $this->count(); + + //判断导出范围是否有数据 + if ($count == 0 || ceil($count / $this->pageSize) < $this->pageStart) { + $msg = $this->pageType ? '第' . $this->pageStart . '页到第' . $this->pageEnd . '页没有数据,无法导出' : '没有数据,无法导出'; + return JsonService::throw($msg); + } + } + + /** + * @notes 不需要分页,可以调用此方法,无需查询第二次 + * @return int + * @author 令狐冲 + * @date 2021/7/6 00:34 + */ + public function defaultCount(): int + { + return count($this->lists()); + } + + +} \ No newline at end of file diff --git a/server/app/common/lists/ListsExcelInterface.php b/server/app/common/lists/ListsExcelInterface.php new file mode 100644 index 0000000..9f4ab18 --- /dev/null +++ b/server/app/common/lists/ListsExcelInterface.php @@ -0,0 +1,39 @@ + $excelField) { + $fieldData = $row[$key]; + if (is_numeric($fieldData) && strlen($fieldData) >= 12) { + $fieldData .= "\t"; + } + $temp[$key] = $fieldData; + } + $data[] = $temp; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + //设置单元格内容 + foreach ($title as $key => $value) { + // 单元格内容写入 + $sheet->setCellValueByColumnAndRow($key + 1, 1, $value); + } + $row = 2; //从第二行开始 + foreach ($data as $item) { + $column = 1; + foreach ($item as $value) { + //单元格内容写入 + $sheet->setCellValueByColumnAndRow($column, $row, $value); + $column++; + } + $row++; + } + + $getHighestRowAndColumn = $sheet->getHighestRowAndColumn(); + $HighestRow = $getHighestRowAndColumn['row']; + $column = $getHighestRowAndColumn['column']; + $titleScope = 'A1:' . $column . '1';//第一(标题)范围(例:A1:D1) + + $sheet->getStyle($titleScope) + ->getFill() + ->setFillType(Fill::FILL_SOLID) // 设置填充样式 + ->getStartColor() + ->setARGB('00B0F0'); + // 设置文字颜色为白色 + $sheet->getStyle($titleScope)->getFont()->getColor() + ->setARGB('FFFFFF'); + +// $sheet->getStyle('B2')->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD); + $spreadsheet->getActiveSheet()->getColumnDimension('B')->setAutoSize(true); + + $allCope = 'A1:' . $column . $HighestRow;//整个表格范围(例:A1:D5) + $sheet->getStyle($allCope)->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN); + + $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); + + //创建excel文件 + $exportCache = new ExportCache(); + $src = $exportCache->getSrc(); + + if (!file_exists($src)) { + mkdir($src, 0775, true); + } + $writer->save($src . $this->fileName); + //设置本地excel缓存并返回下载地址 + $vars = ['file' => $exportCache->setFile($this->fileName)]; + return (string)(url('adminapi/download/export', $vars, true, true)); + } + + + /** + * @notes 获取导出信息 + * @return array + * @author 令狐冲 + * @date 2021/7/29 16:08 + */ + public function excelInfo() + { + $count = $this->count(); + $sum_page = max(ceil($count / $this->pageSize), 1); + return [ + 'count' => $count, //所有数据记录数 + 'page_size' => $this->pageSize,//每页记录数 + 'sum_page' => $sum_page,//一共多少页 + 'max_page' => floor($this->pageSizeMax / $this->pageSize),//最多导出多少页 + 'all_max_size' => $this->pageSizeMax,//最多导出记录数 + 'page_start' => $this->pageStart,//导出范围页码开始值 + 'page_end' => min($sum_page, $this->pageEnd),//导出范围页码结束值 + 'file_name' => $this->fileName,//默认文件名 + ]; + } +} \ No newline at end of file diff --git a/server/app/common/lists/ListsExtendInterface.php b/server/app/common/lists/ListsExtendInterface.php new file mode 100644 index 0000000..e5028a5 --- /dev/null +++ b/server/app/common/lists/ListsExtendInterface.php @@ -0,0 +1,30 @@ + $whereFields) { + switch ($whereType) { + case '=': + case '<>': + case '>': + case '>=': + case '<': + case '<=': + case 'in': + foreach ($whereFields as $whereField) { + $paramsName = substr_symbol_behind($whereField); + if (!isset($this->params[$paramsName]) || $this->params[$paramsName] == '') { + continue; + } + $where[] = [$whereField, $whereType, $this->params[$paramsName]]; + } + break; + case '%like%': + foreach ($whereFields as $whereField) { + $paramsName = substr_symbol_behind($whereField); + if (!isset($this->params[$paramsName]) || empty($this->params[$paramsName])) { + continue; + } + $where[] = [$whereField, 'like', '%' . $this->params[$paramsName] . '%']; + } + break; + case '%like': + foreach ($whereFields as $whereField) { + $paramsName = substr_symbol_behind($whereField); + if (!isset($this->params[$paramsName]) || empty($this->params[$paramsName])) { + continue; + } + $where[] = [$whereField, 'like', '%' . $this->params[$paramsName]]; + } + break; + case 'like%': + foreach ($whereFields as $whereField) { + $paramsName = substr_symbol_behind($whereField); + if (!isset($this->params[$paramsName]) || empty($this->params[$paramsName])) { + continue; + } + $where[] = [$whereField, 'like', $this->params[$paramsName] . '%']; + } + break; + case 'between_time': + if (!is_numeric($this->startTime) || !is_numeric($this->endTime)) { + break; + } + $where[] = [$whereFields, 'between', [$this->startTime, $this->endTime]]; + break; + case 'between': + if (empty($this->start) || empty($this->end)) { + break; + } + $where[] = [$whereFields, 'between', [$this->start, $this->end]]; + break; + case 'find_in_set': // find_in_set查询 + foreach ($whereFields as $whereField) { + $paramsName = substr_symbol_behind($whereField); + if (!isset($this->params[$paramsName]) || $this->params[$paramsName] == '') { + continue; + } + $where[] = [$whereField, 'find in set', $this->params[$paramsName]]; + } + break; + } + } + return $where; + } +} diff --git a/server/app/common/lists/ListsSortInterface.php b/server/app/common/lists/ListsSortInterface.php new file mode 100644 index 0000000..0dd5e12 --- /dev/null +++ b/server/app/common/lists/ListsSortInterface.php @@ -0,0 +1,38 @@ +orderBy) || empty($this->field) || !in_array($this->field, array_keys($sortField))) { + return $defaultOrder; + } + + if (isset($sortField[$this->field])) { + $field = $sortField[$this->field]; + } else { + return $defaultOrder; + } + + if ($this->orderBy == 'desc') { + return [$field => 'desc']; + } + if ($this->orderBy == 'asc') { + return [$field => 'asc']; + } + return $defaultOrder; + } +} \ No newline at end of file diff --git a/server/app/common/logic/AccountLogLogic.php b/server/app/common/logic/AccountLogLogic.php new file mode 100644 index 0000000..357794c --- /dev/null +++ b/server/app/common/logic/AccountLogLogic.php @@ -0,0 +1,80 @@ +isEmpty()) { + return false; + } + + $changeObject = AccountLogEnum::getChangeObject($changeType); + if (!$changeObject) { + return false; + } + + switch ($changeObject) { + // 用户余额 + case AccountLogEnum::UM: + $left_amount = $user->user_money; + break; + // 用户积分 + case AccountLogEnum::UP: + $left_amount = $user->user_points; + break; + } + + $data = [ + 'sn' => generate_sn(UserAccountLog::class, 'sn', 20), + 'user_id' => $userId, + 'change_object' => $changeObject, + 'change_type' => $changeType, + 'action' => $action, + 'left_amount' => $left_amount, + 'change_amount' => $changeAmount, + 'source_sn' => $sourceSn, + 'remark' => $remark, + 'extra' => $extra ? json_encode($extra, JSON_UNESCAPED_UNICODE) : '', + 'relation_id' => $relationId, + ]; + return UserAccountLog::create($data); + } +} diff --git a/server/app/common/logic/BaseLogic.php b/server/app/common/logic/BaseLogic.php new file mode 100644 index 0000000..0be59ed --- /dev/null +++ b/server/app/common/logic/BaseLogic.php @@ -0,0 +1,114 @@ +findOrEmpty()->toArray(); + if (empty($noticeSetting)) { + throw new \Exception('找不到对应场景的配置'); + } + // 合并额外参数 + $params = self::mergeParams($params); + $res = false; + self::setError('发送通知失败'); + + // 短信通知 + if (isset($noticeSetting['sms_notice']['status']) && $noticeSetting['sms_notice']['status'] == YesNoEnum::YES) { + $res = (new SmsMessageService())->send($params); + } + + return $res; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 整理参数 + * @param $params + * @return array + * @author 段誉 + * @date 2022/9/15 15:28 + */ + public static function mergeParams($params) + { + // 用户相关 + if (!empty($params['params']['user_id'])) { + $user = User::findOrEmpty($params['params']['user_id'])->toArray(); + $params['params']['nickname'] = $user['nickname']; + $params['params']['user_name'] = $user['nickname']; + $params['params']['user_sn'] = $user['sn']; + $params['params']['mobile'] = $params['params']['mobile'] ?? $user['mobile']; + } + + // 跳转路径 + $jumpPath = self::getPathByScene($params['scene_id'], $params['params']['order_id'] ?? 0); + $params['url'] = $jumpPath['url']; + $params['page'] = $jumpPath['page']; + + return $params; + } + + + /** + * @notes 根据场景获取跳转链接 + * @param $sceneId + * @param $extraId + * @return string[] + * @author 段誉 + * @date 2022/9/15 15:29 + */ + public static function getPathByScene($sceneId, $extraId) + { + // 小程序主页路径 + $page = '/pages/index/index'; + // 公众号主页路径 + $url = '/mobile/pages/index/index'; + return [ + 'url' => $url, + 'page' => $page, + ]; + } + + + /** + * @notes 替换消息内容中的变量占位符 + * @param $content + * @param $params + * @return array|mixed|string|string[] + * @author 段誉 + * @date 2022/9/15 15:29 + */ + public static function contentFormat($content, $params) + { + foreach ($params['params'] as $k => $v) { + $search = '{' . $k . '}'; + $content = str_replace($search, $v, $content); + } + return $content; + } + + + /** + * @notes 添加通知记录 + * @param $params + * @param $noticeSetting + * @param $sendType + * @param $content + * @param string $extra + * @return NoticeRecord|\think\Model + * @author 段誉 + * @date 2022/9/15 15:29 + */ + public static function addNotice($params, $noticeSetting, $sendType, $content, $extra = '') + { + return NoticeRecord::create([ + 'user_id' => $params['params']['user_id'] ?? 0, + 'title' => self::getTitleByScene($sendType, $noticeSetting), + 'content' => $content, + 'scene_id' => $noticeSetting['scene_id'], + 'read' => YesNoEnum::NO, + 'recipient' => $noticeSetting['recipient'], + 'send_type' => $sendType, + 'notice_type' => $noticeSetting['type'], + 'extra' => $extra, + ]); + } + + + /** + * @notes 通知记录标题 + * @param $sendType + * @param $noticeSetting + * @return string + * @author 段誉 + * @date 2022/9/15 15:30 + */ + public static function getTitleByScene($sendType, $noticeSetting) + { + switch ($sendType) { + case NoticeEnum::SMS: + $title = ''; + break; + case NoticeEnum::OA: + $title = $noticeSetting['oa_notice']['name'] ?? ''; + break; + case NoticeEnum::MNP: + $title = $noticeSetting['mnp_notice']['name'] ?? ''; + break; + default: + $title = ''; + } + return $title; + } + +} \ No newline at end of file diff --git a/server/app/common/logic/PayNotifyLogic.php b/server/app/common/logic/PayNotifyLogic.php new file mode 100644 index 0000000..fa71345 --- /dev/null +++ b/server/app/common/logic/PayNotifyLogic.php @@ -0,0 +1,144 @@ +getFile(), + $e->getLine(), + $e->getMessage() + ])); + self::setError($e->getMessage()); + return $e->getMessage(); + } + } + + + /** + * @notes 充值回调 + * @param $orderSn + * @param array $extra + * @author 段誉 + * @date 2023/2/27 15:28 + */ + public static function recharge($orderSn, array $extra = []) + { + $order = RechargeOrder::where('sn', $orderSn)->findOrEmpty(); + // 增加用户累计充值金额及用户余额 + $user = User::findOrEmpty($order->user_id); + $user->total_recharge_amount += $order->order_amount; + $user->user_money += $order->order_amount; + $user->save(); + + // 记录账户流水 + AccountLogLogic::add( + $order->user_id, + AccountLogEnum::UM_INC_RECHARGE, + AccountLogEnum::INC, + $order->order_amount, + $order->sn, + '用户充值' + ); + + // 更新充值订单状态 + $order->transaction_id = $extra['transaction_id'] ?? ''; + $order->pay_status = PayEnum::ISPAID; + $order->pay_time = time(); + $order->save(); + } + + public static function points_order($orderSn, array $extra = []) + { + $order = PointsOrder::where('sn', $orderSn)->findOrEmpty(); + $user = User::findOrEmpty($order->user_id); + $user->user_points = ($user->user_points ?? 0) + $order->points; + $user->save(); + + $logExtra = [ + 'product_id' => $order->product_id ?? 0, + 'product_name' => $order->product_name ?? '', + 'points' => $order->points ?? 0, + 'order_amount' => $order->order_amount ?? 0, + ]; + AccountLogLogic::add( + $order->user_id, + AccountLogEnum::UP_INC_BUY_POINTS, + AccountLogEnum::INC, + $order->points, + $order->sn, + '购买积分到账', + $logExtra + ); + + $order->transaction_id = $extra['transaction_id'] ?? ''; + $order->pay_status = PayEnum::ISPAID; + $order->pay_time = time(); + $order->save(); + } + + public static function vip_order($orderSn, array $extra = []) + { + $order = VipOrder::where('sn', $orderSn)->findOrEmpty(); + if ($order->isEmpty() || $order->pay_status == PayEnum::ISPAID) { + return; + } + + $user = User::findOrEmpty($order->user_id); + $vipLevel = VipLevel::where('id', $order->level_id)->findOrEmpty(); + $level = $vipLevel->isEmpty() ? 0 : (int) $vipLevel->level; + + $now = time(); + $currentExpire = ($user->is_vip == 1 && $user->vip_expire_time > $now) ? $user->vip_expire_time : $now; + $user->is_vip = 1; + $user->vip_level = $level; + $user->vip_expire_time = $currentExpire + ($order->duration * 86400); + $user->save(); + + $order->transaction_id = $extra['transaction_id'] ?? ''; + $order->pay_status = PayEnum::ISPAID; + $order->pay_time = time(); + $order->save(); + } + + +} diff --git a/server/app/common/logic/PaymentLogic.php b/server/app/common/logic/PaymentLogic.php new file mode 100644 index 0000000..13447b3 --- /dev/null +++ b/server/app/common/logic/PaymentLogic.php @@ -0,0 +1,345 @@ +toArray(); + } + if ($params['from'] == 'points_order') { + $model = PointsOrder::findOrEmpty($params['order_id']); + if (!$model->isEmpty()) { + $order = $model->toArray(); + } + } + if ($params['from'] == 'vip_order') { + $model = VipOrder::findOrEmpty($params['order_id']); + if (!$model->isEmpty()) { + $order = $model->toArray(); + } + } + + if (empty($order)) { + throw new \Exception('待支付订单不存在, from=' . ($params['from'] ?? '') . ', order_id=' . ($params['order_id'] ?? '')); + } + + //获取支付场景 + $pay_way = PayWay::alias('pw') + ->join('dev_pay_config dp', 'pw.pay_config_id = dp.id') + ->where(['pw.scene' => $terminal, 'pw.status' => YesNoEnum::YES]) + ->field('dp.id,dp.name,dp.pay_way,dp.icon,dp.sort,dp.remark,pw.is_default') + ->order('pw.is_default desc,dp.sort desc,id asc') + ->select() + ->toArray(); + + foreach ($pay_way as $k => &$item) { + if ($item['pay_way'] == PayEnum::WECHAT_PAY) { + $item['extra'] = '微信快捷支付'; + } + if ($item['pay_way'] == PayEnum::ALI_PAY) { + $item['extra'] = '支付宝快捷支付'; + } + if ($item['pay_way'] == PayEnum::BALANCE_PAY) { + $user_money = User::where(['id' => $userId])->value('user_money'); + $item['extra'] = '可用余额:' . $user_money; + } + // 充值时去除余额支付 + if (in_array($params['from'], ['recharge']) && $item['pay_way'] == PayEnum::BALANCE_PAY) { + unset($pay_way[$k]); + } + } + + return [ + 'lists' => array_values($pay_way), + 'order_amount' => $order['order_amount'], + ]; + + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 获取支付状态 + * @param $params + * @return array|false + * @author 段誉 + * @date 2023/3/1 16:23 + */ + public static function getPayStatus($params) + { + try { + $order = []; + $orderInfo = []; + switch ($params['from']) { + case 'recharge': + $order = RechargeOrder::where(['user_id' => $params['user_id'], 'id' => $params['order_id']]) + ->findOrEmpty(); + + $payTime = empty($order['pay_time']) ? '' : date('Y-m-d H:i:s', $order['pay_time']); + $orderInfo = [ + 'order_id' => $order['id'], + 'order_sn' => $order['sn'], + 'order_amount' => $order['order_amount'], + 'pay_way' => PayEnum::getPayDesc($order['pay_way']), + 'pay_status' => PayEnum::getPayStatusDesc($order['pay_status']), + 'pay_time' => $payTime, + ]; + break; + case 'points_order': + $order = PointsOrder::where(['user_id' => $params['user_id'], 'id' => $params['order_id']]) + ->findOrEmpty(); + $payTime = empty($order['pay_time']) ? '' : date('Y-m-d H:i:s', $order['pay_time']); + $orderInfo = [ + 'order_id' => $order['id'], + 'order_sn' => $order['sn'], + 'order_amount' => $order['order_amount'], + 'pay_way' => PayEnum::getPayDesc($order['pay_way']), + 'pay_status' => PayEnum::getPayStatusDesc($order['pay_status']), + 'pay_time' => $payTime, + ]; + break; + case 'vip_order': + $order = VipOrder::where(['user_id' => $params['user_id'], 'id' => $params['order_id']]) + ->findOrEmpty(); + $payTime = empty($order['pay_time']) ? '' : date('Y-m-d H:i:s', $order['pay_time']); + $orderInfo = [ + 'order_id' => $order['id'], + 'order_sn' => $order['sn'], + 'order_amount' => $order['order_amount'], + 'pay_way' => PayEnum::getPayDesc($order['pay_way']), + 'pay_status' => PayEnum::getPayStatusDesc($order['pay_status']), + 'pay_time' => $payTime, + ]; + break; + + } + + if (empty($order)) { + throw new \Exception('订单不存在'); + } + + return [ + 'pay_status' => $order['pay_status'], + 'pay_way' => $order['pay_way'], + 'order' => $orderInfo + ]; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 获取预支付订单信息 + * @param $params + * @return RechargeOrder|array|false|\think\Model + * @author 段誉 + * @date 2023/2/27 15:19 + */ + public static function getPayOrderInfo($params) + { + try { + switch ($params['from']) { + case 'recharge': + $order = RechargeOrder::findOrEmpty($params['order_id']); + if ($order->isEmpty()) { + throw new \Exception('充值订单不存在'); + } + break; + case 'points_order': + $order = PointsOrder::findOrEmpty($params['order_id']); + if ($order->isEmpty()) { + throw new \Exception('积分订单不存在'); + } + break; + case 'vip_order': + $order = VipOrder::findOrEmpty($params['order_id']); + if ($order->isEmpty()) { + throw new \Exception('VIP订单不存在'); + } + break; + + } + + if ($order['pay_status'] == PayEnum::ISPAID) { + throw new \Exception('订单已支付'); + } + return $order; + } catch (\Exception $e) { + self::$error = $e->getMessage(); + return false; + } + } + + /** + * @notes 支付 + * @param $payWay + * @param $from + * @param $order + * @param $terminal + * @param $redirectUrl + * @return array|false|mixed|string|string[] + * @throws \Exception + * @author mjf + * @date 2024/3/18 16:49 + */ + public static function pay($payWay, $from, $order, $terminal, $redirectUrl) + { + // 支付编号-仅为微信支付预置(同一商户号下不同客户端支付需使用唯一订单号) + $paySn = $order['sn']; + if ($payWay == PayEnum::WECHAT_PAY) { + $paySn = self::formatOrderSn($order['sn'], $terminal); + } + + //更新支付方式 + switch ($from) { + case 'recharge': + RechargeOrder::update(['pay_way' => $payWay, 'pay_sn' => $paySn], ['id' => $order['id']]); + break; + case 'points_order': + PointsOrder::update(['pay_way' => $payWay, 'pay_sn' => $paySn], ['id' => $order['id']]); + break; + case 'vip_order': + VipOrder::update(['pay_way' => $payWay, 'pay_sn' => $paySn], ['id' => $order['id']]); + break; + + } + + if ($order['order_amount'] == 0) { + PayNotifyLogic::handle($from, $order['sn']); + return ['pay_way' => PayEnum::BALANCE_PAY]; + } + + $payService = null; + switch ($payWay) { + case PayEnum::BALANCE_PAY: + $user = User::find($order['user_id']); + if ($user->user_money < $order['order_amount']) { + self::$error = '余额不足'; + $result = false; + break; + } + $user->user_money -= $order['order_amount']; + $user->save(); + if ($from == 'vip_order') { + $logExtra = [ + 'level_id' => $order['level_id'] ?? 0, + 'level_name' => $order['level_name'] ?? '', + 'duration' => $order['duration'] ?? 0, + 'order_amount' => $order['order_amount'], + ]; + AccountLogLogic::add( + $order['user_id'], + AccountLogEnum::UM_DEC_BUY_VIP, + AccountLogEnum::DEC, + $order['order_amount'], + $order['sn'], + '余额支付购买VIP', + $logExtra + ); + } else { + $extra = [ + 'product_id' => $order['product_id'] ?? 0, + 'product_name' => $order['product_name'] ?? '', + 'points' => $order['points'] ?? 0, + 'order_amount' => $order['order_amount'], + ]; + AccountLogLogic::add( + $order['user_id'], + AccountLogEnum::UM_DEC_BUY_POINTS, + AccountLogEnum::DEC, + $order['order_amount'], + $order['sn'], + '余额支付购买积分', + $extra + ); + } + PayNotifyLogic::handle($from, $order['sn']); + $result = ['pay_way' => PayEnum::BALANCE_PAY]; + break; + case PayEnum::WECHAT_PAY: + $payService = (new WeChatPayService($terminal, $order['user_id'] ?? null)); + $order['pay_sn'] = $paySn; + $order['redirect_url'] = $redirectUrl; + $result = $payService->pay($from, $order); + break; + case PayEnum::ALI_PAY: + $payService = (new AliPayService($terminal)); + $order['redirect_url'] = $redirectUrl; + $result = $payService->pay($from, $order); + break; + default: + self::$error = '订单异常'; + $result = false; + } + + if (false === $result && !self::hasError()) { + self::setError($payService->getError()); + } + return $result; + } + + /** + * @notes 设置订单号 支付回调时截取前面的单号 18个 + * @param $orderSn + * @param $terminal + * @return string + * @author 段誉 + * @date 2023/3/1 16:31 + * @remark 回调时使用了不同的回调地址,导致跨客户端支付时(例如小程序,公众号)可能出现201,商户订单号重复错误 + */ + public static function formatOrderSn($orderSn, $terminal) + { + $suffix = mb_substr(time(), -4); + return $orderSn . $terminal . $suffix; + } + +} diff --git a/server/app/common/logic/RefundLogic.php b/server/app/common/logic/RefundLogic.php new file mode 100644 index 0000000..12300df --- /dev/null +++ b/server/app/common/logic/RefundLogic.php @@ -0,0 +1,209 @@ +getMessage(); + self::refundFailHandle($refundRecordId, $e->getMessage()); + return false; + } + } + + + /** + * @notes 退款前校验 + * @param $refundAmount + * @throws \Exception + * @author 段誉 + * @date 2023/2/28 16:27 + */ + public static function refundBeforeCheck($refundAmount) + { + if ($refundAmount <= 0) { + throw new \Exception('订单金额异常'); + } + } + + + /** + * @notes 微信支付退款 + * @param $order + * @param $refundAmount + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @author 段誉 + * @date 2023/2/28 17:19 + */ + public static function wechatPayRefund($order, $refundAmount) + { + // 发起退款。 若发起退款请求返回明确错误,退款日志和记录标记状态为退款失败 + // 退款日志及记录的成功状态目前统一由定时任务查询退款结果为退款成功后才标记成功 + // 也可通过设置退款回调,在退款回调时处理退款记录状态为成功 + (new WeChatPayService($order['order_terminal']))->refund([ + 'transaction_id' => $order['transaction_id'], + 'refund_sn' => self::$refundLog['sn'], + 'refund_amount' => $refundAmount,// 退款金额 + 'total_amount' => $order['order_amount'],// 订单金额 + ]); + } + + /** + * @notes 支付宝退款 + * @param $refundRecordId + * @param $refundAmount + * @throws \Exception + * @author mjf + * @date 2024/3/18 18:54 + */ + public static function aliPayRefund($refundRecordId, $refundAmount) + { + $refundRecord = RefundRecord::where('id', $refundRecordId)->findOrEmpty()->toArray(); + + $result = (new AliPayService())->refund($refundRecord['order_sn'], $refundAmount, self::$refundLog['sn']); + $result = (array)$result; + + if ($result['code'] == '10000' && $result['msg'] == 'Success' && $result['fundChange'] == 'Y') { + // 更新日志 + RefundLog::update([ + 'refund_status' => RefundEnum::REFUND_SUCCESS, + 'refund_msg' => json_encode($result, JSON_UNESCAPED_UNICODE), + ], ['id'=>self::$refundLog['id']]); + + // 更新记录 + RefundRecord::update([ + 'refund_status' => RefundEnum::REFUND_SUCCESS, + ], ['id'=>$refundRecordId]); + + // 更新订单信息 + if ($refundRecord['order_type'] == 'recharge') { + RechargeOrder::update([ + 'id' => $refundRecord['order_id'], + 'refund_transaction_id' => $result['tradeNo'] ?? '', + ]); + } + } + } + + + /** + * @notes 退款请求失败处理 + * @param $refundRecordId + * @author 段誉 + * @date 2023/2/28 16:00 + * @remark 【微信,支付宝】退款接口请求失败时,更新退款记录及日志为失败,在退款记录重新发起 + */ + public static function refundFailHandle($refundRecordId, $msg) + { + // 更新退款日志记录 + RefundLog::update([ + 'id' => self::$refundLog['id'], + 'refund_status' => RefundEnum::REFUND_ERROR, + 'refund_msg' => $msg, + ]); + + // 更新退款记录状态为退款失败 + RefundRecord::update([ + 'id' => $refundRecordId, + 'refund_status' => RefundEnum::REFUND_ERROR, + 'refund_msg' => $msg, + ]); + } + + + /** + * @notes 退款日志 + * @param $order + * @param $refundRecordId + * @param $refundAmount + * @param $handleId + * @param int $refundStatus + * @author 段誉 + * @date 2023/2/28 15:29 + */ + public static function log($order, $refundRecordId, $refundAmount, $handleId, $refundStatus = RefundEnum::REFUND_ING) + { + $sn = generate_sn(RefundLog::class, 'sn'); + + self::$refundLog = RefundLog::create([ + 'sn' => $sn, + 'record_id' => $refundRecordId, + 'user_id' => $order['user_id'], + 'handle_id' => $handleId, + 'order_amount' => $order['order_amount'], + 'refund_amount' => $refundAmount, + 'refund_status' => $refundStatus + ]); + } + +} \ No newline at end of file diff --git a/server/app/common/model/BaseModel.php b/server/app/common/model/BaseModel.php new file mode 100644 index 0000000..7b8733e --- /dev/null +++ b/server/app/common/model/BaseModel.php @@ -0,0 +1,50 @@ + '定时任务', + CrontabEnum::DAEMON => '守护进程', + ]; + return $desc[$value] ?? ''; + } + + + /** + * @notes 状态获取器 + * @param $value + * @return string + * @author 段誉 + * @date 2022/3/29 12:06 + */ + public function getStatusDescAttr($value) + { + $desc = [ + CrontabEnum::START => '运行', + CrontabEnum::STOP => '停止', + CrontabEnum::ERROR => '错误', + ]; + return $desc[$value] ?? ''; + } + + + /** + * @notes 最后执行时间获取器 + * @param $value + * @return string + * @author 段誉 + * @date 2022/3/29 12:06 + */ + public function getLastTimeAttr($value) + { + return empty($value) ? '' : date('Y-m-d H:i:s', $value); + } +} \ No newline at end of file diff --git a/server/app/common/model/CrontabLog.php b/server/app/common/model/CrontabLog.php new file mode 100644 index 0000000..d68613c --- /dev/null +++ b/server/app/common/model/CrontabLog.php @@ -0,0 +1,12 @@ +findOrEmpty()->isExists()); + return $code; + } + +} diff --git a/server/app/common/model/ShortLinkLog.php b/server/app/common/model/ShortLinkLog.php new file mode 100644 index 0000000..58f5c77 --- /dev/null +++ b/server/app/common/model/ShortLinkLog.php @@ -0,0 +1,29 @@ +where('ref_id', $refId) + ->where('status', self::STATUS_SUCCESS) + ->where('expire_time', '>', time()) + ->order('id', 'desc') + ->findOrEmpty(); + + if ($row->isEmpty()) { + return null; + } + return $row->toArray(); + } +} diff --git a/server/app/common/model/ai/AiChatMessage.php b/server/app/common/model/ai/AiChatMessage.php new file mode 100644 index 0000000..a60819f --- /dev/null +++ b/server/app/common/model/ai/AiChatMessage.php @@ -0,0 +1,16 @@ +value('value'); + return $val !== null ? $val : $default; + } + + public static function setVal(string $name, string $value): void + { + $row = AiConfig::where('name', $name)->findOrEmpty(); + if ($row->isEmpty()) { + AiConfig::create(['name' => $name, 'value' => $value]); + } else { + $row->value = $value; + $row->save(); + } + } +} diff --git a/server/app/common/model/ai/AiKbChunk.php b/server/app/common/model/ai/AiKbChunk.php new file mode 100644 index 0000000..468c44a --- /dev/null +++ b/server/app/common/model/ai/AiKbChunk.php @@ -0,0 +1,14 @@ +where('match_id', $matchId)->count() > 0; + } + + /** + * 解锁比赛(扣次数 + 写记录) + * @return array ['ok' => bool, 'msg' => string] + */ + public static function unlock(int $userId, int $matchId): array + { + // 已解锁则直接返回 + if (self::isUnlocked($userId, $matchId)) { + return ['ok' => true, 'msg' => '已解锁']; + } + + // 检查今日剩余次数 + $user = User::findOrEmpty($userId); + if ($user->isEmpty()) { + return ['ok' => false, 'msg' => '用户不存在']; + } + + // VIP用户不限次数 + if ($user->is_vip && $user->vip_expire_time > time()) { + self::create([ + 'user_id' => $userId, + 'match_id' => $matchId, + ]); + return ['ok' => true, 'msg' => 'VIP解锁']; + } + + // 重置每日次数(跨天时) + $today = date('Y-m-d'); + if ($user->ai_free_date !== $today) { + $user->ai_free_count = 3; + $user->ai_free_date = $today; + $user->save(); + } + + if ($user->ai_free_count <= 0) { + return ['ok' => false, 'msg' => '今日免费次数已用完,升级VIP可无限使用']; + } + + // 扣次数 + 写解锁记录 + $user->ai_free_count = $user->ai_free_count - 1; + $user->save(); + + self::create([ + 'user_id' => $userId, + 'match_id' => $matchId, + ]); + + return ['ok' => true, 'msg' => '解锁成功', 'remain' => $user->ai_free_count]; + } + + /** + * 获取用户今日剩余次数 + */ + public static function getRemainCount(int $userId): int + { + $user = User::findOrEmpty($userId); + if ($user->isEmpty()) { + return 0; + } + // VIP不限 + if ($user->is_vip && $user->vip_expire_time > time()) { + return 999; + } + $today = date('Y-m-d'); + if ($user->ai_free_date !== $today) { + return 3; + } + return max(0, (int)$user->ai_free_count); + } +} diff --git a/server/app/common/model/app/AppVersion.php b/server/app/common/model/app/AppVersion.php new file mode 100644 index 0000000..3965715 --- /dev/null +++ b/server/app/common/model/app/AppVersion.php @@ -0,0 +1,62 @@ + 'Android', 2 => 'iOS']; + return $map[$data['platform']] ?? ''; + } + + /** + * 包类型名称 + */ + public function getPackageTypeTextAttr($value, $data): string + { + $map = [1 => 'wgt热更', 2 => 'apk整包', 3 => 'ipa/AppStore']; + return $map[$data['package_type']] ?? ''; + } + + /** + * 升级类型名称 + */ + public function getUpdateTypeTextAttr($value, $data): string + { + $map = [1 => '普通升级', 2 => '强制升级', 3 => '静默升级']; + return $map[$data['update_type']] ?? ''; + } + + /** + * 完整下载链接 + */ + public function getDownloadUrlAttr($value): string + { + if (empty($value)) { + return ''; + } + // 已是完整 URL(http/https)直接返回 + if (preg_match('/^https?:\/\//i', $value)) { + return $value; + } + return FileService::getFileUrl($value); + } +} diff --git a/server/app/common/model/article/Article.php b/server/app/common/model/article/Article.php new file mode 100644 index 0000000..06f794f --- /dev/null +++ b/server/app/common/model/article/Article.php @@ -0,0 +1,138 @@ += 200; + } + + public function comments() + { + return $this->hasMany(ArticleComment::class, 'article_id', 'id'); + } + + /** + * @notes 获取分类名称 + * @param $value + * @param $data + * @return string + * @author heshihu + * @date 2022/2/22 9:53 + */ + public function getCateNameAttr($value, $data) + { + return ArticleCate::where('id', $data['cid'])->value('name'); + } + + /** + * @notes 浏览量 + * @param $value + * @param $data + * @return mixed + * @author 段誉 + * @date 2022/9/15 11:33 + */ + public function getClickAttr($value, $data) + { + return $data['click_actual'] + $data['click_virtual']; + } + + + /** + * @notes 设置图片域名 + * @param $value + * @param $data + * @return array|string|string[]|null + * @author 段誉 + * @date 2022/9/28 10:17 + */ + public function getContentAttr($value, $data) + { + return get_file_domain($value); + } + + + /** + * @notes 清除图片域名 + * @param $value + * @param $data + * @return array|string|string[] + * @author 段誉 + * @date 2022/9/28 10:17 + */ + public function setContentAttr($value, $data) + { + return clear_file_domain($value); + } + + + /** + * @notes 获取文章详情 + * @param $id + * @return array + * @author 段誉 + * @date 2022/10/20 15:23 + */ + public static function getArticleDetailArr(int $id) + { + $article = Article::withCount(['comments' => 'comment_count']) + ->where(['id' => $id, 'is_show' => YesNoEnum::YES]) + ->findOrEmpty(); + + if ($article->isEmpty()) { + return []; + } + + // 增加点击量 + $article->click_actual += 1; + $article->save(); + + return $article->append(['click']) + ->hidden(['click_virtual', 'click_actual']) + ->toArray(); + } + +} diff --git a/server/app/common/model/article/ArticleAiCommentTask.php b/server/app/common/model/article/ArticleAiCommentTask.php new file mode 100644 index 0000000..aff6406 --- /dev/null +++ b/server/app/common/model/article/ArticleAiCommentTask.php @@ -0,0 +1,14 @@ +hasMany(Article::class, 'cid', 'id'); + } + + + /** + * @notes 状态描述 + * @param $value + * @param $data + * @return string + * @author 段誉 + * @date 2022/9/15 11:25 + */ + public function getIsShowDescAttr($value, $data) + { + return $data['is_show'] ? '启用' : '停用'; + } + + + /** + * @notes 文章数量 + * @param $value + * @param $data + * @return int + * @author 段誉 + * @date 2022/9/15 11:32 + */ + public function getArticleCountAttr($value, $data) + { + return Article::where(['cid' => $data['id']])->count('id'); + } + + + + +} \ No newline at end of file diff --git a/server/app/common/model/article/ArticleCollect.php b/server/app/common/model/article/ArticleCollect.php new file mode 100644 index 0000000..6c1cfa1 --- /dev/null +++ b/server/app/common/model/article/ArticleCollect.php @@ -0,0 +1,52 @@ + $userId, + 'article_id' => $articleId, + 'status' => YesNoEnum::YES + ])->findOrEmpty(); + + return !$collect->isEmpty(); + } + +} \ No newline at end of file diff --git a/server/app/common/model/article/ArticleComment.php b/server/app/common/model/article/ArticleComment.php new file mode 100644 index 0000000..042b341 --- /dev/null +++ b/server/app/common/model/article/ArticleComment.php @@ -0,0 +1,18 @@ +column('role_id'); + } + + + /** + * @notes 关联部门id + * @param $value + * @param $data + * @return array + * @author 段誉 + * @date 2022/11/25 15:00 + */ + public function getDeptIdAttr($value, $data) + { + return AdminDept::where('admin_id', $data['id'])->column('dept_id'); + } + + + /** + * @notes 关联岗位id + * @param $value + * @param $data + * @return array + * @author 段誉 + * @date 2022/11/25 15:01\ + */ + public function getJobsIdAttr($value, $data) + { + return AdminJobs::where('admin_id', $data['id'])->column('jobs_id'); + } + + + + /** + * @notes 获取禁用状态 + * @param $value + * @param $data + * @return string|string[] + * @author 令狐冲 + * @date 2021/7/7 01:25 + */ + public function getDisableDescAttr($value, $data) + { + return YesNoEnum::getDisableDesc($data['disable']); + } + + /** + * @notes 最后登录时间获取器 - 格式化:年-月-日 时:分:秒 + * @param $value + * @return string + * @author Tab + * @date 2021/7/13 11:35 + */ + public function getLoginTimeAttr($value) + { + return empty($value) ? '' : date('Y-m-d H:i:s', $value); + } + + /** + * @notes 头像获取器 - 头像路径添加域名 + * @param $value + * @return string + * @author Tab + * @date 2021/7/13 11:35 + */ + public function getAvatarAttr($value) + { + return empty($value) ? FileService::getFileUrl(config('project.default_image.admin_avatar')) : FileService::getFileUrl(trim($value, '/')); + } + +} \ No newline at end of file diff --git a/server/app/common/model/auth/AdminDept.php b/server/app/common/model/auth/AdminDept.php new file mode 100644 index 0000000..e5ee32e --- /dev/null +++ b/server/app/common/model/auth/AdminDept.php @@ -0,0 +1,32 @@ + $adminId])->delete(); + } +} \ No newline at end of file diff --git a/server/app/common/model/auth/AdminJobs.php b/server/app/common/model/auth/AdminJobs.php new file mode 100644 index 0000000..9568ce2 --- /dev/null +++ b/server/app/common/model/auth/AdminJobs.php @@ -0,0 +1,32 @@ + $adminId])->delete(); + } +} \ No newline at end of file diff --git a/server/app/common/model/auth/AdminRole.php b/server/app/common/model/auth/AdminRole.php new file mode 100644 index 0000000..a1118bd --- /dev/null +++ b/server/app/common/model/auth/AdminRole.php @@ -0,0 +1,34 @@ + $adminId])->delete(); + } + +} \ No newline at end of file diff --git a/server/app/common/model/auth/AdminSession.php b/server/app/common/model/auth/AdminSession.php new file mode 100644 index 0000000..9f18a36 --- /dev/null +++ b/server/app/common/model/auth/AdminSession.php @@ -0,0 +1,32 @@ +hasOne(Admin::class, 'id', 'admin_id') + ->field('id,multipoint_login'); + } +} \ No newline at end of file diff --git a/server/app/common/model/auth/SystemMenu.php b/server/app/common/model/auth/SystemMenu.php new file mode 100644 index 0000000..7d94bbf --- /dev/null +++ b/server/app/common/model/auth/SystemMenu.php @@ -0,0 +1,31 @@ +hasMany(SystemRoleMenu::class, 'role_id'); + } +} \ No newline at end of file diff --git a/server/app/common/model/auth/SystemRoleMenu.php b/server/app/common/model/auth/SystemRoleMenu.php new file mode 100644 index 0000000..f5e0d03 --- /dev/null +++ b/server/app/common/model/auth/SystemRoleMenu.php @@ -0,0 +1,30 @@ +belongsToMany(CommunityTag::class, 'la_community_post_tag', 'tag_id', 'post_id'); + } +} diff --git a/server/app/common/model/community/CommunityTag.php b/server/app/common/model/community/CommunityTag.php new file mode 100644 index 0000000..effabc7 --- /dev/null +++ b/server/app/common/model/community/CommunityTag.php @@ -0,0 +1,10 @@ +toArray(); + + if (empty($tabbar)) { + return $tabbar; + } + + foreach ($tabbar as &$item) { + if (!empty($item['selected'])) { + $item['selected'] = FileService::getFileUrl($item['selected']); + } + if (!empty($item['unselected'])) { + $item['unselected'] = FileService::getFileUrl($item['unselected']); + } + } + + return $tabbar; + } +} \ No newline at end of file diff --git a/server/app/common/model/dept/Dept.php b/server/app/common/model/dept/Dept.php new file mode 100644 index 0000000..fb66097 --- /dev/null +++ b/server/app/common/model/dept/Dept.php @@ -0,0 +1,46 @@ + 'Android', + 'ios' => 'iOS', + 'h5' => 'H5', + 'mp_weixin' => '微信小程序', + 'windows' => 'Windows', + 'mac' => 'Mac', + ]; + return $map[$data['platform']] ?? $data['platform']; + } +} diff --git a/server/app/common/model/dict/DictData.php b/server/app/common/model/dict/DictData.php new file mode 100644 index 0000000..3b4f984 --- /dev/null +++ b/server/app/common/model/dict/DictData.php @@ -0,0 +1,47 @@ +belongsTo(LotteryCategory::class, 'category_id'); + } +} diff --git a/server/app/common/model/lottery/LotteryDrawResult.php b/server/app/common/model/lottery/LotteryDrawResult.php new file mode 100644 index 0000000..e4a9eff --- /dev/null +++ b/server/app/common/model/lottery/LotteryDrawResult.php @@ -0,0 +1,19 @@ +belongsTo(LotteryGame::class, 'code', 'code'); + } +} diff --git a/server/app/common/model/lottery/LotteryGame.php b/server/app/common/model/lottery/LotteryGame.php new file mode 100644 index 0000000..64a0286 --- /dev/null +++ b/server/app/common/model/lottery/LotteryGame.php @@ -0,0 +1,15 @@ +belongsTo(LotteryGameCategory::class, 'category', 'value'); + } +} diff --git a/server/app/common/model/lottery/LotteryGameCategory.php b/server/app/common/model/lottery/LotteryGameCategory.php new file mode 100644 index 0000000..e49c388 --- /dev/null +++ b/server/app/common/model/lottery/LotteryGameCategory.php @@ -0,0 +1,15 @@ +hasMany(LotteryGame::class, 'category', 'value'); + } +} diff --git a/server/app/common/model/lottery/LotteryNumberMapping.php b/server/app/common/model/lottery/LotteryNumberMapping.php new file mode 100644 index 0000000..9dfa785 --- /dev/null +++ b/server/app/common/model/lottery/LotteryNumberMapping.php @@ -0,0 +1,92 @@ +order('type asc, sort asc')->select()->toArray(); + $result = ['zodiac' => [], 'color' => [], 'element' => []]; + foreach ($list as $item) { + $item['numbers'] = is_string($item['numbers']) ? json_decode($item['numbers'], true) : $item['numbers']; + match ((int)$item['type']) { + self::TYPE_ZODIAC => $result['zodiac'][] = $item, + self::TYPE_COLOR => $result['color'][] = $item, + self::TYPE_ELEMENT => $result['element'][] = $item, + }; + } + return $result; + } + + /** + * 获取指定年份某类型的映射(按attr_name索引) + */ + public static function getMapByType(int $year, int $type): array + { + $list = self::where('year', $year)->where('type', $type)->order('sort asc')->select()->toArray(); + $map = []; + foreach ($list as $item) { + $nums = is_string($item['numbers']) ? json_decode($item['numbers'], true) : $item['numbers']; + $map[$item['attr_name']] = $nums; + } + return $map; + } + + /** + * 根据号码反查属性名(如:号码1 → 生肖"马"、波色"红波"、五行"水") + */ + public static function lookupNumber(int $year, int $number): array + { + $all = self::where('year', $year)->order('type asc, sort asc')->select()->toArray(); + $result = ['zodiac' => '', 'color' => '', 'element' => '']; + foreach ($all as $item) { + $nums = is_string($item['numbers']) ? json_decode($item['numbers'], true) : $item['numbers']; + if (in_array($number, $nums)) { + match ((int)$item['type']) { + self::TYPE_ZODIAC => $result['zodiac'] = $item['attr_name'], + self::TYPE_COLOR => $result['color'] = $item['attr_name'], + self::TYPE_ELEMENT => $result['element'] = $item['attr_name'], + }; + } + } + return $result; + } + + /** + * 从上一年份复制映射数据到新年份(用于年度轮转后微调) + */ + public static function copyToNextYear(int $fromYear, int $toYear): int + { + $exists = self::where('year', $toYear)->count(); + if ($exists > 0) { + return 0; + } + $list = self::where('year', $fromYear)->select()->toArray(); + $count = 0; + foreach ($list as $item) { + unset($item['id']); + $item['year'] = $toYear; + $item['create_time'] = time(); + $item['update_time'] = time(); + self::create($item); + $count++; + } + return $count; + } +} diff --git a/server/app/common/model/lottery/LotteryRegion.php b/server/app/common/model/lottery/LotteryRegion.php new file mode 100644 index 0000000..d7effee --- /dev/null +++ b/server/app/common/model/lottery/LotteryRegion.php @@ -0,0 +1,10 @@ + '买家', + 2 => '卖家', + ]; + return $desc[$value] ?? ''; + } + + /** + * @notes 系统通知获取器 + * @param $value + * @return array|mixed + * @author Tab + * @date 2021/8/18 19:11 + */ + public function getSystemNoticeAttr($value) + { + return empty($value) ? [] : json_decode($value, true); + } + + /** + * @notes 短信通知获取器 + * @param $value + * @return array|mixed + * @author Tab + * @date 2021/8/18 19:12 + */ + public function getSmsNoticeAttr($value) + { + return empty($value) ? [] : json_decode($value, true); + } + + /** + * @notes 公众号通知获取器 + * @param $value + * @return array|mixed + * @author Tab + * @date 2021/8/18 19:13 + */ + public function getOaNoticeAttr($value) + { + return empty($value) ? [] : json_decode($value, true); + } + + /** + * @notes 小程序通知获取器 + * @param $value + * @return array|mixed + * @author Tab + * @date 2021/8/18 19:13 + */ + public function getMnpNoticeAttr($value) + { + return empty($value) ? [] : json_decode($value, true); + } +} \ No newline at end of file diff --git a/server/app/common/model/notice/SmsLog.php b/server/app/common/model/notice/SmsLog.php new file mode 100644 index 0000000..0f6eef3 --- /dev/null +++ b/server/app/common/model/notice/SmsLog.php @@ -0,0 +1,30 @@ +value('name'); + } + + /** + * @notes 关联支配配置模型 + * @return \think\model\relation\HasOne + * @author ljj + * @date 2021/10/11 3:04 下午 + */ + public function payConfig() + { + return $this->hasOne(PayConfig::class,'id','pay_config_id'); + } +} \ No newline at end of file diff --git a/server/app/common/model/points/PointsOrder.php b/server/app/common/model/points/PointsOrder.php new file mode 100644 index 0000000..c80ce3d --- /dev/null +++ b/server/app/common/model/points/PointsOrder.php @@ -0,0 +1,24 @@ + '图片弹窗', 2 => '文字弹窗', 3 => '图文混合']; + return $map[$data['popup_type']] ?? ''; + } + + public function getLinkTypeTextAttr($value, $data): string + { + $map = [0 => '不跳转', 1 => '站内路径', 2 => '外部H5', 3 => '拨打电话']; + return $map[$data['link_type']] ?? ''; + } + + public function getFrequencyTextAttr($value, $data): string + { + $map = [1 => '每次', 2 => '每会话一次', 3 => '每天一次', 4 => '仅一次']; + return $map[$data['frequency']] ?? ''; + } +} diff --git a/server/app/common/model/popup/PagePopupLog.php b/server/app/common/model/popup/PagePopupLog.php new file mode 100644 index 0000000..70ffcd4 --- /dev/null +++ b/server/app/common/model/popup/PagePopupLog.php @@ -0,0 +1,20 @@ + '展示', 2 => '点击', 3 => '关闭']; + return $map[$data['action']] ?? ''; + } +} diff --git a/server/app/common/model/recharge/RechargeOrder.php b/server/app/common/model/recharge/RechargeOrder.php new file mode 100644 index 0000000..6a7da9f --- /dev/null +++ b/server/app/common/model/recharge/RechargeOrder.php @@ -0,0 +1,57 @@ +value('name'); + } + + + /** + * @notes 退款状态描述 + * @param $value + * @param $data + * @return string|string[] + * @author 段誉 + * @date 2022/12/1 10:55 + */ + public function getRefundStatusTextAttr($value, $data) + { + return RefundEnum::getStatusDesc($data['refund_status']); + } + +} diff --git a/server/app/common/model/refund/RefundRecord.php b/server/app/common/model/refund/RefundRecord.php new file mode 100644 index 0000000..d077114 --- /dev/null +++ b/server/app/common/model/refund/RefundRecord.php @@ -0,0 +1,71 @@ +belongsTo(GenerateTable::class, 'id', 'table_id'); + } +} \ No newline at end of file diff --git a/server/app/common/model/tools/GenerateTable.php b/server/app/common/model/tools/GenerateTable.php new file mode 100644 index 0000000..181bbe4 --- /dev/null +++ b/server/app/common/model/tools/GenerateTable.php @@ -0,0 +1,59 @@ +hasMany(GenerateColumn::class, 'table_id', 'id'); + } + + /** + * @notes 模板类型描述 + * @param $value + * @param $data + * @return string|string[] + * @author 段誉 + * @date 2022/6/14 11:25 + */ + public function getTemplateTypeDescAttr($value, $data) + { + return GeneratorEnum::getTemplateTypeDesc($data['template_type']); + } + + + +} \ No newline at end of file diff --git a/server/app/common/model/user/User.php b/server/app/common/model/user/User.php new file mode 100644 index 0000000..fc89211 --- /dev/null +++ b/server/app/common/model/user/User.php @@ -0,0 +1,251 @@ +hasOne(UserAuth::class, 'user_id'); + } + + + /** + * @notes 搜索器-用户信息 + * @param $query + * @param $value + * @param $data + * @author 段誉 + * @date 2022/9/22 16:12 + */ + public function searchKeywordAttr($query, $value, $data) + { + if ($value) { + $query->where('sn|nickname|mobile|account', 'like', '%' . $value . '%'); + } + } + + + /** + * @notes 搜索器-注册来源 + * @param $query + * @param $value + * @param $data + * @author 段誉 + * @date 2022/9/22 16:13 + */ + public function searchChannelAttr($query, $value, $data) + { + if ($value) { + $query->where('channel', '=', $value); + } + } + + + /** + * @notes 搜索器-注册时间 + * @param $query + * @param $value + * @param $data + * @author 段誉 + * @date 2022/9/22 16:13 + */ + public function searchCreateTimeStartAttr($query, $value, $data) + { + if ($value) { + $query->where('create_time', '>=', strtotime($value)); + } + } + + + /** + * @notes 搜索器-注册时间 + * @param $query + * @param $value + * @param $data + * @author 段誉 + * @date 2022/9/22 16:13 + */ + public function searchCreateTimeEndAttr($query, $value, $data) + { + if ($value) { + $query->where('create_time', '<=', strtotime($value)); + } + } + + + /** + * @notes 头像获取器 - 用于头像地址拼接域名 + * @param $value + * @return string + * @author Tab + * @date 2021/7/17 14:28 + */ + public function getAvatarAttr($value) + { + return trim($value) ? FileService::getFileUrl($value) : ''; + } + + + /** + * @notes 获取器-性别描述 + * @param $value + * @param $data + * @return string|string[] + * @author 段誉 + * @date 2022/9/7 15:15 + */ + public function getSexAttr($value, $data) + { + return UserEnum::getSexDesc($value); + } + + + /** + * @notes 登录时间 + * @param $value + * @return string + * @author 段誉 + * @date 2022/9/23 18:15 + */ + public function getLoginTimeAttr($value) + { + return $value ? date('Y-m-d H:i:s', $value) : ''; + } + + /** + * @notes 生成用户编码 + * @param string $prefix + * @param int $length + * @return string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2022/9/16 10:33 + */ + public static function createUserSn($prefix = '', $length = 8) + { + $rand_str = ''; + for ($i = 0; $i < $length; $i++) { + $rand_str .= mt_rand(1, 9); + } + $sn = $prefix . $rand_str; + if (User::where(['sn' => $sn])->find()) { + return self::createUserSn($prefix, $length); + } + return $sn; + } + + + /** + * @notes VIP等级名称映射(从数据库动态获取) + */ + public static function getVipLevelMap(): array + { + static $map = null; + if ($map === null) { + $map = [0 => '普通用户']; + $levels = \app\common\model\vip\VipLevel::where('status', 1)->column('name', 'level'); + foreach ($levels as $level => $name) { + $map[(int) $level] = $name; + } + } + return $map; + } + + + /** + * @notes 获取VIP信息(含自动过期判断和免费次数重置) + */ + public function getVipInfo(): array + { + $now = time(); + $isVip = $this->getData('is_vip') == 1 && $this->getData('vip_expire_time') > $now; + + // VIP已过期自动清除 + if ($this->getData('is_vip') == 1 && $this->getData('vip_expire_time') <= $now) { + $this->save(['is_vip' => 0, 'vip_level' => 0]); + $isVip = false; + } + + // 每日免费次数重置 + $today = date('Y-m-d'); + if ($this->getData('ai_free_date') !== $today) { + $this->save(['ai_free_count' => 3, 'ai_free_date' => $today]); + $freeCount = 3; + } else { + $freeCount = (int) $this->getData('ai_free_count'); + } + + // 获取等级卡片背景图 + $cardImage = ''; + if ($isVip) { + $level = (int) $this->getData('vip_level'); + $cardImage = \app\common\model\vip\VipLevel::where('level', $level) + ->where('status', 1) + ->value('image', ''); + } + + return [ + 'is_vip' => $isVip, + 'vip_level' => $isVip ? (int) $this->getData('vip_level') : 0, + 'vip_level_name' => $isVip ? (self::getVipLevelMap()[$this->getData('vip_level')] ?? '会员') : '', + 'vip_expire_time' => $isVip ? date('Y年m月d日', $this->getData('vip_expire_time')) : '', + 'vip_expire_timestamp' => $isVip ? (int) $this->getData('vip_expire_time') : 0, + 'vip_card_image' => $cardImage, + 'ai_free_count' => $freeCount, + ]; + } + + + /** + * @notes 生成唯一邀请码 + */ + public static function createInviteCode($length = 8): string + { + $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + do { + $code = ''; + for ($i = 0; $i < $length; $i++) { + $code .= $chars[mt_rand(0, strlen($chars) - 1)]; + } + } while (User::where('invite_code', $code)->find()); + return $code; + } +} diff --git a/server/app/common/model/user/UserAccountLog.php b/server/app/common/model/user/UserAccountLog.php new file mode 100644 index 0000000..98548b3 --- /dev/null +++ b/server/app/common/model/user/UserAccountLog.php @@ -0,0 +1,30 @@ + $type, 'name' => $name])->findOrEmpty(); + + if ($data->isEmpty()) { + Config::create([ + 'type' => $type, + 'name' => $name, + 'value' => $value, + ]); + } else { + $data->value = $value; + $data->save(); + } + + // 返回原始值 + return $original; + } + + /** + * @notes 获取配置值 + * @param $type + * @param string $name + * @param null $default_value + * @return array|int|mixed|string + * @author Tab + * @date 2021/7/15 15:16 + */ + public static function get(string $type, string $name = '', $default_value = null) + { + if (!empty($name)) { + $value = Config::where(['type' => $type, 'name' => $name])->value('value'); + if (!is_null($value)) { + $json = json_decode($value, true); + $value = json_last_error() === JSON_ERROR_NONE ? $json : $value; + } + if ($value) { + return $value; + } + // 返回特殊值 0 '0' + if ($value === 0 || $value === '0') { + return $value; + } + // 返回默认值 + if ($default_value !== null) { + return $default_value; + } + // 返回本地配置文件中的值 + return config('project.' . $type . '.' . $name); + } + + // 取某个类型下的所有name的值 + $data = Config::where(['type' => $type])->column('value', 'name'); + foreach ($data as $k => $v) { + $json = json_decode($v, true); + if (json_last_error() === JSON_ERROR_NONE) { + $data[$k] = $json; + } + } + if ($data) { + return $data; + } + } +} \ No newline at end of file diff --git a/server/app/common/service/FileService.php b/server/app/common/service/FileService.php new file mode 100644 index 0000000..799f146 --- /dev/null +++ b/server/app/common/service/FileService.php @@ -0,0 +1,116 @@ +domain(); + } else { + $storage = Cache::get('STORAGE_ENGINE'); + if (!$storage) { + $storage = ConfigService::get('storage', $default); + Cache::set('STORAGE_ENGINE', $storage); + } + $domain = $storage ? $storage['domain'] : ''; + } + + return self::format($domain, $uri); + } + + /** + * @notes 转相对路径 + * @param $uri + * @return mixed + * @author 张无忌 + * @date 2021/7/28 15:09 + */ + public static function setFileUrl($uri) + { + $default = ConfigService::get('storage', 'default', 'local'); + if ($default === 'local') { + $domain = request()->domain(); + return str_replace($domain.'/', '', $uri); + } else { + $storage = ConfigService::get('storage', $default); + return str_replace($storage['domain'].'/', '', $uri); + } + } + + + /** + * @notes 格式化url + * @param $domain + * @param $uri + * @return string + * @author 段誉 + * @date 2022/7/11 10:36 + */ + public static function format($domain, $uri) + { + // 处理域名 + $domainLen = strlen($domain); + $domainRight = substr($domain, $domainLen -1, 1); + if ('/' == $domainRight) { + $domain = substr_replace($domain,'',$domainLen -1, 1); + } + + // 处理uri + $uriLeft = substr($uri, 0, 1); + if('/' == $uriLeft) { + $uri = substr_replace($uri,'',0, 1); + } + + return trim($domain) . '/' . trim($uri); + } + +} \ No newline at end of file diff --git a/server/app/common/service/JsonService.php b/server/app/common/service/JsonService.php new file mode 100644 index 0000000..f15ff54 --- /dev/null +++ b/server/app/common/service/JsonService.php @@ -0,0 +1,144 @@ +export == ExportEnum::INFO && $lists instanceof ListsExcelInterface) { + return self::data($lists->excelInfo()); + } + + //获取导出文件的下载链接 + if ($lists->export == ExportEnum::EXPORT && $lists instanceof ListsExcelInterface) { + $exportDownloadUrl = $lists->createExcel($lists->setExcelFields(), $lists->lists()); + return self::success('', ['url' => $exportDownloadUrl], 2); + } + + $data = [ + 'lists' => $lists->lists(), + 'count' => $lists->count(), + 'page_no' => $lists->pageNo, + 'page_size' => $lists->pageSize, + ]; + $data['extend'] = []; + if ($lists instanceof ListsExtendInterface) { + $data['extend'] = $lists->extend(); + } + return self::success('', $data, 1, 0); + } +} \ No newline at end of file diff --git a/server/app/common/service/MTranServerService.php b/server/app/common/service/MTranServerService.php new file mode 100644 index 0000000..ad141b6 --- /dev/null +++ b/server/app/common/service/MTranServerService.php @@ -0,0 +1,70 @@ + false, 'error' => '翻译内容为空']; + } + + $baseUrl = rtrim(AiConfig::getVal('mtranserver_base_url', 'http://127.0.0.1:8989'), '/'); + $apiToken = trim(AiConfig::getVal('mtranserver_api_token', '')); + + $payload = [ + 'from' => $source ?: 'auto', + 'to' => $target ?: 'zh-Hans', + 'text' => $text, + ]; + + $headers = ['Content-Type: application/json']; + if ($apiToken !== '') { + $headers[] = 'Authorization: Bearer ' . $apiToken; + } + + $startTime = microtime(true); + $ch = curl_init($baseUrl . '/translate'); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 120, + CURLOPT_SSL_VERIFYPEER => false, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + $costMs = (int) ((microtime(true) - $startTime) * 1000); + + if ($error) { + return ['success' => false, 'error' => 'MTranServer 请求失败: ' . $error, 'tokens' => 0, 'cost_ms' => $costMs]; + } + + $data = json_decode((string) $response, true); + if ($httpCode !== 200) { + $message = is_array($data) ? ($data['message'] ?? $data['error'] ?? ('HTTP ' . $httpCode)) : ('HTTP ' . $httpCode); + return ['success' => false, 'error' => 'MTranServer 返回异常: ' . $message, 'tokens' => 0, 'cost_ms' => $costMs]; + } + + $result = trim((string) ($data['result'] ?? '')); + if ($result === '') { + return ['success' => false, 'error' => 'MTranServer 翻译结果为空', 'tokens' => 0, 'cost_ms' => $costMs]; + } + + return [ + 'success' => true, + 'content' => $result, + 'tokens' => 0, + 'cost_ms' => $costMs, + ]; + } +} diff --git a/server/app/common/service/ShortLinkPosterDataService.php b/server/app/common/service/ShortLinkPosterDataService.php new file mode 100644 index 0000000..60b3553 --- /dev/null +++ b/server/app/common/service/ShortLinkPosterDataService.php @@ -0,0 +1,110 @@ + '', 'avatar' => '']; + if (!empty($userId)) { + $user = User::field('nickname,avatar')->findOrEmpty($userId); + if (!$user->isEmpty()) { + $sharer['nickname'] = (string) $user->getData('nickname'); + $sharer['avatar'] = (string) $user->avatar; + } + } + + $posterImageUri = (string) ConfigService::get('website', 'share_poster', ''); + $posterImage = $posterImageUri ? FileService::getFileUrl($posterImageUri) : ''; + $posterTitle = (string) ConfigService::get('website', 'share_poster_title', ''); + + return [ + 'poster_image_uri' => $posterImageUri, + 'poster_image' => $posterImage, + 'poster_title' => $posterTitle, + 'title' => $title, + 'description' => $description, + 'sharer' => $sharer, + ]; + } + + /** + * 根据 page_type / page_id 自动取页面标题与描述 + */ + private static function resolvePageDetail(string $pageType, $pageId): array + { + $empty = ['title' => '', 'description' => '']; + if ($pageId === '' || $pageId === null) { + return $empty; + } + + switch ($pageType) { + case 'news': + $row = Db::name('article') + ->field('title,desc,abstract') + ->where('id', (int) $pageId) + ->find(); + if (empty($row)) { + return $empty; + } + $desc = $row['desc'] ?: mb_substr(strip_tags((string) $row['abstract']), 0, 80); + return ['title' => (string) $row['title'], 'description' => (string) $desc]; + + case 'match': + $row = Db::name('match') + ->field('home_team,away_team,league_name') + ->where('id', (int) $pageId) + ->find(); + if (empty($row)) { + return $empty; + } + return [ + 'title' => $row['home_team'] . ' vs ' . $row['away_team'], + 'description' => (string) $row['league_name'], + ]; + + case 'post': + case 'community': + $row = Db::name('community_post') + ->field('content') + ->where('id', (int) $pageId) + ->find(); + if (empty($row)) { + return $empty; + } + $text = mb_substr(strip_tags((string) $row['content']), 0, 80); + return ['title' => $text ?: '社区帖子', 'description' => '']; + + case 'crypto': + return ['title' => $pageId . '/USDT 行情', 'description' => '加密货币实时行情']; + + case 'invite': + return ['title' => '邀请你加入我的团队', 'description' => '']; + } + + return $empty; + } +} diff --git a/server/app/common/service/ShortLinkQrCodeService.php b/server/app/common/service/ShortLinkQrCodeService.php new file mode 100644 index 0000000..14127fe --- /dev/null +++ b/server/app/common/service/ShortLinkQrCodeService.php @@ -0,0 +1,667 @@ + 19, 2 => 34, 3 => 55, 4 => 80, 5 => 108]; + private const ECC_CODEWORDS = [1 => 7, 2 => 10, 3 => 15, 4 => 20, 5 => 26]; + private const ALIGNMENT_POSITIONS = [1 => [], 2 => [6, 18], 3 => [6, 22], 4 => [6, 26], 5 => [6, 30]]; + + private static array $exp = []; + private static array $log = []; + + public static function create(string $content, string $code, string $posterUri = '', array $textData = []): array + { + $matrix = self::encode($content); + $svg = self::toSvg($matrix); + $dirUri = 'uploads/qrcode/shortlink'; + $dir = public_path() . $dirUri; + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + $svgUri = $dirUri . '/' . $code . '.svg'; + $svgPath = public_path() . $svgUri; + file_put_contents($svgPath, $svg); + + $qrUri = $svgUri; + $posterCompositeUri = ''; + if (function_exists('imagecreatetruecolor')) { + $pngUri = $dirUri . '/' . $code . '.png'; + self::toPng($matrix, public_path() . $pngUri); + $qrUri = $pngUri; + + // 有海报底图时合成完整海报:背景 + 文字层 + 二维码白底 + if ($posterUri !== '' && self::isLocalImage($posterUri)) { + $posterPath = public_path() . ltrim($posterUri, '/'); + if (is_file($posterPath)) { + $compositeUri = $dirUri . '/' . $code . '_poster.png'; + if (self::composePoster($posterPath, public_path() . $pngUri, public_path() . $compositeUri, $textData)) { + $posterCompositeUri = $compositeUri; + } + } + } + } + + // uri 用于落库 qrcode 字段:合成海报存在则取合成图,否则取纯二维码 + $uri = $posterCompositeUri !== '' ? $posterCompositeUri : $qrUri; + + return [ + 'uri' => $uri, + 'url' => FileService::getFileUrl($uri), + 'qr_uri' => $qrUri, + 'qr_url' => FileService::getFileUrl($qrUri), + 'poster_uri' => $posterCompositeUri, + 'poster_url' => $posterCompositeUri !== '' ? FileService::getFileUrl($posterCompositeUri) : '', + 'data_url' => 'data:image/svg+xml;base64,' . base64_encode($svg), + ]; + } + + private static function isLocalImage(string $uri): bool + { + $ext = strtolower(pathinfo($uri, PATHINFO_EXTENSION)); + return in_array($ext, ['jpg', 'jpeg', 'png'], true); + } + + /** + * 合成海报:背景图 + 文字层 + 二维码(带白底圆角) + */ + private static function composePoster(string $posterPath, string $qrPath, string $outPath, array $textData = []): bool + { + $ext = strtolower(pathinfo($posterPath, PATHINFO_EXTENSION)); + $bg = match ($ext) { + 'jpg', 'jpeg' => @imagecreatefromjpeg($posterPath), + 'png' => @imagecreatefrompng($posterPath), + default => false, + }; + if (!$bg) { + return false; + } + + $qr = @imagecreatefrompng($qrPath); + if (!$qr) { + imagedestroy($bg); + return false; + } + + $bgW = imagesx($bg); + $bgH = imagesy($bg); + + // 二维码尺寸:背景宽度的 32%,最小 200,最大 360 + $qrSize = (int) max(200, min(360, $bgW * 0.32)); + $padding = 3; + $boxSize = $qrSize + $padding * 2; + // 右下角,距离右边和底部各 5% + $margin = (int) ($bgW * 0.05); + $boxX = (int) ($bgW - $boxSize - $margin); + $boxY = (int) ($bgH - $boxSize - $margin); + + // 文字层:在二维码上方区域绘制 + $font = self::resolveFont(); + if ($font !== null && !empty($textData)) { + self::drawTextLayer($bg, $font, $bgW, $boxY, $textData); + } + + // 白色底 + $white = imagecolorallocate($bg, 255, 255, 255); + imagefilledrectangle($bg, $boxX, $boxY, $boxX + $boxSize, $boxY + $boxSize, $white); + + // 二维码缩放后贴到底框内 + $qrW = imagesx($qr); + $qrH = imagesy($qr); + imagecopyresampled( + $bg, + $qr, + $boxX + $padding, + $boxY + $padding, + 0, + 0, + $qrSize, + $qrSize, + $qrW, + $qrH + ); + + $ok = imagepng($bg, $outPath); + imagedestroy($bg); + imagedestroy($qr); + return $ok; + } + + /** + * 探测可用的中文字体路径,找不到返回 null(此时文字层会被跳过) + * 注意:不能落到不支持中文的字体(如 DejaVuSans),否则会渲染成豆腐块 + */ + private static function resolveFont(): ?string + { + $candidates = [ + public_path() . 'resource/front/cjk.ttf', + public_path() . 'resource/front/cjk.otf', + public_path() . 'resource/front/cjk.ttc', + ]; + foreach ($candidates as $path) { + if (is_file($path)) { + return $path; + } + } + return null; + } + + /** + * 在海报左上区域绘制文字:标题(加粗+4px)→ 分享内容(+4px) + * $boundsBottom 为文字区域可使用的底部 y 坐标(即二维码框顶部) + */ + private static function drawTextLayer($img, string $font, int $bgW, int $boundsBottom, array $textData): void + { + $padding = (int) ($bgW * 0.06); + $maxW = (int) ($bgW - $padding * 2); // 文字区域:全宽(左右留 padding) + $cursorY = (int) ($bgW * 0.06); + + $textColor = imagecolorallocate($img, 255, 255, 255); + $shadow = imagecolorallocatealpha($img, 0, 0, 0, 70); + + // 海报标题(居中显示) + $posterTitle = (string) ($textData['poster_title'] ?? ''); + if ($posterTitle !== '') { + $size = (int) max(34, $bgW * 0.032 + 18); + $cursorY = self::drawBoldCenteredLine($img, $font, $size, $textColor, $shadow, $posterTitle, $bgW, $cursorY); + $cursorY += (int) max(70, $size * 2.0); + } + + // 分享人 + $nickname = (string) ($textData['sharer']['nickname'] ?? ''); + if ($nickname !== '') { + $size = (int) max(8, $bgW * 0.022 - 4); + $cursorY = self::drawBoldLeftLine($img, $font, $size, $textColor, $shadow, '@' . $nickname, $padding, $cursorY); + $cursorY += (int) max(70, $size * 2.4); + } + + // 标题:左上角,加粗(双层绘制模拟),字体 +16px + $title = (string) ($textData['title'] ?? ''); + if ($title !== '') { + $size = (int) max(32, $bgW * 0.028 + 18); + $cursorY = self::drawBoldLeftWrappedLines($img, $font, $size, $textColor, $shadow, $title, $maxW, $padding, $cursorY, 3, $boundsBottom); + $cursorY += (int) max(70, $size * 1.6); + } + + // 分享内容:在标题下方,字体 +8px,加粗 + $description = (string) ($textData['description'] ?? ''); + if ($description !== '' && $cursorY + 60 < $boundsBottom) { + $size = (int) max(13, $bgW * 0.02 + 2); + self::drawBoldLeftWrappedLines($img, $font, $size, $textColor, $shadow, $description, $maxW, $padding, $cursorY, 4, $boundsBottom); + } + } + + /** + * 左对齐单行文字绘制 + */ + private static function drawLeftLine($img, string $font, int $size, int $color, int $shadow, string $text, int $x, int $y): int + { + $baseline = $y + $size; + imagettftext($img, $size, 0, $x + 2, $baseline + 2, $shadow, $font, $text); + imagettftext($img, $size, 0, $x, $baseline, $color, $font, $text); + return $baseline; + } + + /** + * 左对齐加粗单行文字(三层绘制模拟加粗) + */ + private static function drawBoldLeftLine($img, string $font, int $size, int $color, int $shadow, string $text, int $x, int $y): int + { + $baseline = $y + $size; + imagettftext($img, $size, 0, $x + 2, $baseline + 2, $shadow, $font, $text); + imagettftext($img, $size, 0, $x, $baseline, $color, $font, $text); + imagettftext($img, $size, 0, $x + 1, $baseline, $color, $font, $text); + imagettftext($img, $size, 0, $x + 2, $baseline, $color, $font, $text); + return $baseline; + } + + /** + * 左对齐换行文字 + */ + private static function drawLeftWrappedLines($img, string $font, int $size, int $color, int $shadow, string $text, int $maxW, int $padding, int $y, int $maxLines, int $maxBottom): int + { + $lines = self::wrapByPixel($text, $font, $size, $maxW, $maxLines); + $lineHeight = (int) ($size * 1.4); + foreach ($lines as $line) { + if ($y + $lineHeight > $maxBottom) { + break; + } + $y = self::drawLeftLine($img, $font, $size, $color, $shadow, $line, $padding, $y); + $y += (int) ($size * 0.4); + } + return $y; + } + + /** + * 左对齐加粗换行文字 + */ + private static function drawBoldLeftWrappedLines($img, string $font, int $size, int $color, int $shadow, string $text, int $maxW, int $padding, int $y, int $maxLines, int $maxBottom): int + { + $lines = self::wrapByPixel($text, $font, $size, $maxW, $maxLines); + $lineHeight = (int) ($size * 1.6); + foreach ($lines as $line) { + if ($y + $lineHeight > $maxBottom) { + break; + } + $y = self::drawBoldLeftLine($img, $font, $size, $color, $shadow, $line, $padding, $y); + $y += (int) ($size * 1.6); + } + return $y; + } + + private static function drawCenteredLine($img, string $font, int $size, int $color, int $shadow, string $text, int $bgW, int $y): int + { + $box = imagettfbbox($size, 0, $font, $text); + $w = abs($box[2] - $box[0]); + $x = (int) (($bgW - $w) / 2); + $baseline = $y + $size; + imagettftext($img, $size, 0, $x + 2, $baseline + 2, $shadow, $font, $text); + imagettftext($img, $size, 0, $x, $baseline, $color, $font, $text); + return $baseline; + } + + /** + * 居中加粗单行文字(三层绘制模拟加粗) + */ + private static function drawBoldCenteredLine($img, string $font, int $size, int $color, int $shadow, string $text, int $bgW, int $y): int + { + $box = imagettfbbox($size, 0, $font, $text); + $w = abs($box[2] - $box[0]); + $x = (int) (($bgW - $w) / 2); + $baseline = $y + $size; + imagettftext($img, $size, 0, $x + 2, $baseline + 2, $shadow, $font, $text); + imagettftext($img, $size, 0, $x, $baseline, $color, $font, $text); + imagettftext($img, $size, 0, $x + 1, $baseline, $color, $font, $text); + imagettftext($img, $size, 0, $x + 2, $baseline, $color, $font, $text); + return $baseline; + } + + private static function drawWrappedLines($img, string $font, int $size, int $color, int $shadow, string $text, int $maxW, int $bgW, int $y, int $maxLines, int $maxBottom): int + { + $lines = self::wrapByPixel($text, $font, $size, $maxW, $maxLines); + $lineHeight = (int) ($size * 1.4); + foreach ($lines as $line) { + if ($y + $lineHeight > $maxBottom) { + break; + } + $y = self::drawCenteredLine($img, $font, $size, $color, $shadow, $line, $bgW, $y); + $y += (int) ($size * 0.4); + } + return $y; + } + + /** + * 按像素宽度对文本断行,超过 $maxLines 时末尾追加省略号 + */ + private static function wrapByPixel(string $text, string $font, int $size, int $maxW, int $maxLines): array + { + $chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY) ?: []; + $lines = []; + $current = ''; + foreach ($chars as $ch) { + $candidate = $current . $ch; + $box = imagettfbbox($size, 0, $font, $candidate); + $w = abs($box[2] - $box[0]); + if ($w > $maxW) { + if ($current !== '') { + $lines[] = $current; + } + $current = $ch; + if (count($lines) >= $maxLines) { + $lastIdx = count($lines) - 1; + $lines[$lastIdx] = mb_substr($lines[$lastIdx], 0, max(0, mb_strlen($lines[$lastIdx]) - 1)) . '…'; + return $lines; + } + } else { + $current = $candidate; + } + } + if ($current !== '') { + $lines[] = $current; + } + return array_slice($lines, 0, $maxLines); + } + + private static function toPng(array $matrix, string $path): void + { + $quiet = 1; + $scale = 10; + $size = count($matrix); + $imageSize = ($size + $quiet * 2) * $scale; + $image = imagecreatetruecolor($imageSize, $imageSize); + $white = imagecolorallocate($image, 255, 255, 255); + $black = imagecolorallocate($image, 0, 0, 0); + imagefill($image, 0, 0, $white); + + for ($y = 0; $y < $size; $y++) { + for ($x = 0; $x < $size; $x++) { + if ($matrix[$y][$x]) { + imagefilledrectangle( + $image, + ($x + $quiet) * $scale, + ($y + $quiet) * $scale, + ($x + $quiet + 1) * $scale - 1, + ($y + $quiet + 1) * $scale - 1, + $black + ); + } + } + } + + imagepng($image, $path); + imagedestroy($image); + } + + public static function get(string $content, string $code): array + { + $pngUri = 'uploads/qrcode/shortlink/' . $code . '.png'; + $svgUri = 'uploads/qrcode/shortlink/' . $code . '.svg'; + if (is_file(public_path() . $pngUri)) { + return [ + 'uri' => $pngUri, + 'url' => FileService::getFileUrl($pngUri), + 'data_url' => '', + ]; + } + if (is_file(public_path() . $svgUri)) { + return [ + 'uri' => $svgUri, + 'url' => FileService::getFileUrl($svgUri), + 'data_url' => '', + ]; + } + return self::create($content, $code); + } + + private static function encode(string $content): array + { + $version = self::getVersion(strlen($content)); + $data = self::buildData($content, $version); + $codewords = array_merge($data, self::reedSolomonRemainder($data, self::ECC_CODEWORDS[$version])); + $size = 17 + 4 * $version; + $modules = array_fill(0, $size, array_fill(0, $size, false)); + $isFunction = array_fill(0, $size, array_fill(0, $size, false)); + + self::drawFunctionPatterns($modules, $isFunction, $version); + self::drawCodewords($modules, $isFunction, $codewords); + self::applyMask($modules, $isFunction); + self::drawFormatBits($modules, $isFunction); + + return $modules; + } + + private static function getVersion(int $length): int + { + foreach (self::DATA_CODEWORDS as $version => $capacity) { + if ($length <= $capacity) { + return $version; + } + } + return 5; + } + + private static function buildData(string $content, int $version): array + { + $bits = []; + self::appendBits($bits, 0x4, 4); + self::appendBits($bits, strlen($content), 8); + foreach (unpack('C*', $content) as $byte) { + self::appendBits($bits, $byte, 8); + } + + $capacity = self::DATA_CODEWORDS[$version] * 8; + self::appendBits($bits, 0, min(4, $capacity - count($bits))); + while (count($bits) % 8 !== 0) { + $bits[] = 0; + } + + $data = self::bitsToBytes($bits); + for ($pad = 0xec; count($data) < self::DATA_CODEWORDS[$version]; $pad ^= 0xfd) { + $data[] = $pad; + } + + return $data; + } + + private static function appendBits(array &$bits, int $value, int $length): void + { + for ($i = $length - 1; $i >= 0; $i--) { + $bits[] = ($value >> $i) & 1; + } + } + + private static function bitsToBytes(array $bits): array + { + $bytes = []; + for ($i = 0; $i < count($bits); $i += 8) { + $byte = 0; + for ($j = 0; $j < 8; $j++) { + $byte = ($byte << 1) | ($bits[$i + $j] ?? 0); + } + $bytes[] = $byte; + } + return $bytes; + } + + private static function initGalois(): void + { + if (self::$exp) { + return; + } + + self::$exp = array_fill(0, 512, 0); + self::$log = array_fill(0, 256, 0); + $x = 1; + for ($i = 0; $i < 255; $i++) { + self::$exp[$i] = $x; + self::$log[$x] = $i; + $x <<= 1; + if ($x & 0x100) { + $x ^= 0x11d; + } + } + for ($i = 255; $i < 512; $i++) { + self::$exp[$i] = self::$exp[$i - 255]; + } + } + + private static function multiply(int $x, int $y): int + { + if ($x === 0 || $y === 0) { + return 0; + } + self::initGalois(); + return self::$exp[self::$log[$x] + self::$log[$y]]; + } + + private static function reedSolomonDivisor(int $degree): array + { + self::initGalois(); + $result = [1]; + for ($i = 0; $i < $degree; $i++) { + $next = array_fill(0, count($result) + 1, 0); + foreach ($result as $j => $coefficient) { + $next[$j] ^= self::multiply($coefficient, 1); + $next[$j + 1] ^= self::multiply($coefficient, self::$exp[$i]); + } + $result = $next; + } + return $result; + } + + private static function reedSolomonRemainder(array $data, int $degree): array + { + $divisor = self::reedSolomonDivisor($degree); + $result = array_fill(0, $degree, 0); + foreach ($data as $byte) { + $factor = $byte ^ $result[0]; + array_shift($result); + $result[] = 0; + for ($i = 0; $i < $degree; $i++) { + $result[$i] ^= self::multiply($divisor[$i + 1], $factor); + } + } + return $result; + } + + private static function drawFunctionPatterns(array &$modules, array &$isFunction, int $version): void + { + $size = count($modules); + self::drawFinderPattern($modules, $isFunction, 3, 3); + self::drawFinderPattern($modules, $isFunction, $size - 4, 3); + self::drawFinderPattern($modules, $isFunction, 3, $size - 4); + + for ($i = 8; $i < $size - 8; $i++) { + self::setFunctionModule($modules, $isFunction, 6, $i, $i % 2 === 0); + self::setFunctionModule($modules, $isFunction, $i, 6, $i % 2 === 0); + } + + foreach (self::ALIGNMENT_POSITIONS[$version] as $x) { + foreach (self::ALIGNMENT_POSITIONS[$version] as $y) { + if (!$isFunction[$y][$x]) { + self::drawAlignmentPattern($modules, $isFunction, $x, $y); + } + } + } + + for ($i = 0; $i <= 8; $i++) { + self::setFunctionModule($modules, $isFunction, 8, $i, false); + self::setFunctionModule($modules, $isFunction, $i, 8, false); + } + for ($i = 0; $i < 8; $i++) { + self::setFunctionModule($modules, $isFunction, $size - 1 - $i, 8, false); + self::setFunctionModule($modules, $isFunction, 8, $size - 1 - $i, false); + } + self::setFunctionModule($modules, $isFunction, 8, $size - 8, true); + } + + private static function drawFinderPattern(array &$modules, array &$isFunction, int $x, int $y): void + { + $size = count($modules); + for ($dy = -4; $dy <= 4; $dy++) { + for ($dx = -4; $dx <= 4; $dx++) { + $xx = $x + $dx; + $yy = $y + $dy; + if ($xx < 0 || $xx >= $size || $yy < 0 || $yy >= $size) { + continue; + } + $distance = max(abs($dx), abs($dy)); + self::setFunctionModule($modules, $isFunction, $xx, $yy, $distance !== 2 && $distance !== 4); + } + } + } + + private static function drawAlignmentPattern(array &$modules, array &$isFunction, int $x, int $y): void + { + for ($dy = -2; $dy <= 2; $dy++) { + for ($dx = -2; $dx <= 2; $dx++) { + self::setFunctionModule($modules, $isFunction, $x + $dx, $y + $dy, max(abs($dx), abs($dy)) !== 1); + } + } + } + + private static function setFunctionModule(array &$modules, array &$isFunction, int $x, int $y, bool $dark): void + { + $modules[$y][$x] = $dark; + $isFunction[$y][$x] = true; + } + + private static function drawCodewords(array &$modules, array $isFunction, array $codewords): void + { + $size = count($modules); + $bitIndex = 0; + $totalBits = count($codewords) * 8; + $upward = true; + for ($right = $size - 1; $right >= 1; $right -= 2) { + if ($right === 6) { + $right--; + } + for ($vert = 0; $vert < $size; $vert++) { + $y = $upward ? $size - 1 - $vert : $vert; + for ($j = 0; $j < 2; $j++) { + $x = $right - $j; + if ($isFunction[$y][$x]) { + continue; + } + $dark = false; + if ($bitIndex < $totalBits) { + $dark = (($codewords[intdiv($bitIndex, 8)] >> (7 - ($bitIndex % 8))) & 1) !== 0; + } + $modules[$y][$x] = $dark; + $bitIndex++; + } + } + $upward = !$upward; + } + } + + private static function applyMask(array &$modules, array $isFunction): void + { + $size = count($modules); + for ($y = 0; $y < $size; $y++) { + for ($x = 0; $x < $size; $x++) { + if (!$isFunction[$y][$x] && (($x + $y) % 2 === 0)) { + $modules[$y][$x] = !$modules[$y][$x]; + } + } + } + } + + private static function drawFormatBits(array &$modules, array &$isFunction): void + { + $size = count($modules); + $bits = self::getFormatBits(); + for ($i = 0; $i <= 5; $i++) { + self::setFunctionModule($modules, $isFunction, 8, $i, self::getBit($bits, $i)); + } + self::setFunctionModule($modules, $isFunction, 8, 7, self::getBit($bits, 6)); + self::setFunctionModule($modules, $isFunction, 8, 8, self::getBit($bits, 7)); + self::setFunctionModule($modules, $isFunction, 7, 8, self::getBit($bits, 8)); + for ($i = 9; $i < 15; $i++) { + self::setFunctionModule($modules, $isFunction, 14 - $i, 8, self::getBit($bits, $i)); + } + for ($i = 0; $i < 8; $i++) { + self::setFunctionModule($modules, $isFunction, $size - 1 - $i, 8, self::getBit($bits, $i)); + } + for ($i = 8; $i < 15; $i++) { + self::setFunctionModule($modules, $isFunction, 8, $size - 15 + $i, self::getBit($bits, $i)); + } + self::setFunctionModule($modules, $isFunction, 8, $size - 8, true); + } + + private static function getFormatBits(): int + { + $data = 1 << 3; + $remainder = $data << 10; + for ($i = 14; $i >= 10; $i--) { + if ((($remainder >> $i) & 1) !== 0) { + $remainder ^= 0x537 << ($i - 10); + } + } + return (($data << 10) | $remainder) ^ 0x5412; + } + + private static function getBit(int $value, int $index): bool + { + return (($value >> $index) & 1) !== 0; + } + + private static function toSvg(array $matrix): string + { + $quiet = 1; + $size = count($matrix); + $viewBox = $size + $quiet * 2; + $path = ''; + for ($y = 0; $y < $size; $y++) { + for ($x = 0; $x < $size; $x++) { + if ($matrix[$y][$x]) { + $path .= 'M' . ($x + $quiet) . ',' . ($y + $quiet) . 'h1v1h-1z'; + } + } + } + + return ''; + } +} diff --git a/server/app/common/service/UploadService.php b/server/app/common/service/UploadService.php new file mode 100644 index 0000000..bcf1232 --- /dev/null +++ b/server/app/common/service/UploadService.php @@ -0,0 +1,244 @@ + ConfigService::get('storage', 'default', 'local'), + 'engine' => ConfigService::get('storage') ?? ['local'=>[]], + ]; + + // 2、执行文件上传 + $StorageDriver = new StorageDriver($config); + $StorageDriver->setUploadFile('file'); + $fileName = $StorageDriver->getFileName(); + $fileInfo = $StorageDriver->getFileInfo(); + + // 校验上传文件后缀 + if (!in_array(strtolower($fileInfo['ext']), config('project.file_image'))) { + throw new Exception("上传图片不允许上传". $fileInfo['ext'] . "文件"); + } + + // 上传文件 + $saveDir = self::getUploadUrl($saveDir); + if (!$StorageDriver->upload($saveDir)) { + throw new Exception($StorageDriver->getError()); + } + + // 3、处理文件名称 + if (strlen($fileInfo['name']) > 128) { + $name = substr($fileInfo['name'], 0, 123); + $nameEnd = substr($fileInfo['name'], strlen($fileInfo['name'])-5, strlen($fileInfo['name'])); + $fileInfo['name'] = $name . $nameEnd; + } + + // 4、写入数据库中 + $file = File::create([ + 'cid' => $cid, + 'type' => FileEnum::IMAGE_TYPE, + 'name' => $fileInfo['name'], + 'uri' => $saveDir . '/' . str_replace("\\","/", $fileName), + 'source' => $source, + 'source_id' => $sourceId, + 'create_time' => time(), + ]); + + // 5、返回结果 + return [ + 'id' => $file['id'], + 'cid' => $file['cid'], + 'type' => $file['type'], + 'name' => $file['name'], + 'uri' => FileService::getFileUrl($file['uri']), + 'url' => $file['uri'] + ]; + + } catch (Exception $e) { + throw new Exception($e->getMessage()); + } + } + + + /** + * @notes 视频上传 + * @param $cid + * @param int $user_id + * @param string $saveDir + * @return array + * @throws Exception + * @author 段誉 + * @date 2021/12/29 16:32 + */ + public static function video($cid, int $sourceId = 0, int $source = FileEnum::SOURCE_ADMIN, string $saveDir = 'uploads/video') + { + try { + $config = [ + 'default' => ConfigService::get('storage', 'default', 'local'), + 'engine' => ConfigService::get('storage') ?? ['local'=>[]], + ]; + + // 2、执行文件上传 + $StorageDriver = new StorageDriver($config); + $StorageDriver->setUploadFile('file'); + $fileName = $StorageDriver->getFileName(); + $fileInfo = $StorageDriver->getFileInfo(); + + // 校验上传文件后缀 + if (!in_array(strtolower($fileInfo['ext']), config('project.file_video'))) { + throw new Exception("上传视频不允许上传". $fileInfo['ext'] . "文件"); + } + + // 上传文件 + $saveDir = self::getUploadUrl($saveDir); + if (!$StorageDriver->upload($saveDir)) { + throw new Exception($StorageDriver->getError()); + } + + // 3、处理文件名称 + if (strlen($fileInfo['name']) > 128) { + $name = substr($fileInfo['name'], 0, 123); + $nameEnd = substr($fileInfo['name'], strlen($fileInfo['name'])-5, strlen($fileInfo['name'])); + $fileInfo['name'] = $name . $nameEnd; + } + + // 4、写入数据库中 + $file = File::create([ + 'cid' => $cid, + 'type' => FileEnum::VIDEO_TYPE, + 'name' => $fileInfo['name'], + 'uri' => $saveDir . '/' . str_replace("\\","/", $fileName), + 'source' => $source, + 'source_id' => $sourceId, + 'create_time' => time(), + ]); + + // 5、返回结果 + return [ + 'id' => $file['id'], + 'cid' => $file['cid'], + 'type' => $file['type'], + 'name' => $file['name'], + 'uri' => FileService::getFileUrl($file['uri']), + 'url' => $file['uri'] + ]; + + } catch (Exception $e) { + throw new Exception($e->getMessage()); + } + } + + /** + * @notes 上传文件 + * @param $cid + * @param int $sourceId + * @param int $source + * @param string $saveDir + * @return array + * @throws Exception + * @author dw + * @date 2023/06/26 + */ + public static function file($cid, int $sourceId = 0, int $source = FileEnum::SOURCE_ADMIN, string $saveDir = 'uploads/file') + { + try { + $config = [ + 'default' => ConfigService::get('storage', 'default', 'local'), + 'engine' => ConfigService::get('storage') ?? [ 'local' => [] ], + ]; + + // 2、执行文件上传 + $StorageDriver = new StorageDriver($config); + $StorageDriver->setUploadFile('file'); + $fileName = $StorageDriver->getFileName(); + $fileInfo = $StorageDriver->getFileInfo(); + + // 校验上传文件后缀 + if (!in_array(strtolower($fileInfo['ext']), config('project.file_file'))) { + throw new Exception("上传文件不允许上传" . $fileInfo['ext'] . "文件"); + } + + // 上传文件 + $saveDir = self::getUploadUrl($saveDir); + if (!$StorageDriver->upload($saveDir)) { + throw new Exception($StorageDriver->getError()); + } + + // 3、处理文件名称 + if (strlen($fileInfo['name']) > 128) { + $name = substr($fileInfo['name'], 0, 123); + $nameEnd = substr($fileInfo['name'], strlen($fileInfo['name']) - 5, strlen($fileInfo['name'])); + $fileInfo['name'] = $name . $nameEnd; + } + + // 4、写入数据库中 + $file = File::create([ + 'cid' => $cid, + 'type' => FileEnum::FILE_TYPE, + 'name' => $fileInfo['name'], + 'uri' => $saveDir . '/' . str_replace("\\", "/", $fileName), + 'source' => $source, + 'source_id' => $sourceId, + 'create_time' => time(), + ]); + + // 5、返回结果 + return [ + 'id' => $file['id'], + 'cid' => $file['cid'], + 'type' => $file['type'], + 'name' => $file['name'], + 'uri' => FileService::getFileUrl($file['uri']), + 'url' => $file['uri'] + ]; + + } catch (Exception $e) { + throw new Exception($e->getMessage()); + } + } + + /** + * @notes 上传地址 + * @param $saveDir + * @return string + * @author dw + * @date 2023/06/26 + */ + private static function getUploadUrl($saveDir):string + { + return $saveDir . '/' . date('Ymd'); + } +} diff --git a/server/app/common/service/VipService.php b/server/app/common/service/VipService.php new file mode 100644 index 0000000..798e0a4 --- /dev/null +++ b/server/app/common/service/VipService.php @@ -0,0 +1,55 @@ +isEmpty()) { + return false; + } + return $user->is_vip == 1 && $user->vip_expire_time > time(); + } + + /** + * @notes 获取用户当前VIP等级配置(未过期才返回) + */ + public static function getUserVipLevel(int $userId): ?array + { + $user = User::findOrEmpty($userId); + if ($user->isEmpty() || $user->is_vip != 1 || $user->vip_expire_time <= time()) { + return null; + } + $level = VipLevel::where('level', $user->vip_level)->where('status', 1)->findOrEmpty(); + if ($level->isEmpty()) { + return null; + } + return $level->toArray(); + } + + /** + * @notes 检查VIP用户是否享有翻译免费权益 + */ + public static function hasTranslateFree(int $userId): bool + { + $level = self::getUserVipLevel($userId); + return $level && !empty($level['translate_free']); + } + + /** + * @notes 检查VIP用户是否享有解锁帖子免费权益 + */ + public static function hasUnlockFree(int $userId): bool + { + $level = self::getUserVipLevel($userId); + return $level && !empty($level['unlock_free']); + } +} diff --git a/server/app/common/service/ai/AiAssistantContextService.php b/server/app/common/service/ai/AiAssistantContextService.php new file mode 100644 index 0000000..129938d --- /dev/null +++ b/server/app/common/service/ai/AiAssistantContextService.php @@ -0,0 +1,302 @@ + ['BTC', '比特币', 'Bitcoin'], + 'ETH' => ['ETH', '以太坊', 'Ethereum'], + 'BNB' => ['BNB'], + 'SOL' => ['SOL', 'Solana'], + 'XRP' => ['XRP', 'Ripple'], + 'DOGE' => ['DOGE', '狗狗币', 'Dogecoin'], + 'TON' => ['TON', 'Toncoin'], + 'TRX' => ['TRX', 'TRON', '波场'], + 'ADA' => ['ADA', 'Cardano'], + 'AVAX' => ['AVAX', 'Avalanche'], + ]; + + public static function build(string $message, int $userId, int $sessionId): array + { + $topK = max(1, min(12, (int) AiConfig::getVal('assistant_kb_topk', '6'))); + $hits = self::retrieveKnowledge($message, $userId, $sessionId, $topK); + $matchItems = self::searchMatches($message); + $cryptoItems = self::resolveCryptoContext($message); + + $sources = []; + foreach ($hits as $hit) { + $sources[] = self::sourceFromKbHit($hit); + } + foreach ($matchItems as $item) { + $sources[] = $item['source']; + } + foreach ($cryptoItems as $item) { + $sources[] = $item['source']; + } + + $sources = self::uniqueSources($sources); + $context = [ + 'knowledge_hits' => array_map([self::class, 'compactHit'], $hits), + 'match_context' => array_column($matchItems, 'context'), + 'crypto_realtime' => array_column($cryptoItems, 'context'), + ]; + + return [ + 'context_text' => json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), + 'sources' => array_values($sources), + 'has_context' => !empty($hits) || !empty($matchItems) || !empty($cryptoItems), + ]; + } + + private static function retrieveKnowledge(string $message, int $userId, int $sessionId, int $topK): array + { + $result = KbService::retrieve( + 'assistant_chat', + $message, + [ + 'user_id' => $userId, + 'ref_id' => $sessionId, + 'domains' => ['article', 'post', 'lottery', 'match'], + ], + ['title' => $message], + $topK + ); + + if (empty($result['success']) || empty($result['hits']) || !is_array($result['hits'])) { + return []; + } + + return $result['hits']; + } + + private static function searchMatches(string $message): array + { + $terms = self::queryTerms($message); + if (empty($terms)) { + return []; + } + + try { + $query = MatchEvent::where('is_show', 1); + $query->where(function ($q) use ($terms) { + foreach (array_slice($terms, 0, 5) as $term) { + $like = '%' . $term . '%'; + $q->whereOr('home_team', 'like', $like) + ->whereOr('away_team', 'like', $like) + ->whereOr('league_name', 'like', $like); + } + }); + + $rows = $query->field('id,league_name,home_team,away_team,match_time,status,home_score,away_score,home_odds,draw_odds,away_odds') + ->order('match_time desc') + ->limit(3) + ->select() + ->toArray(); + } catch (\Throwable $e) { + return []; + } + + return array_map(static function (array $row) { + $title = trim(($row['home_team'] ?? '') . ' vs ' . ($row['away_team'] ?? '')); + $matchTime = !empty($row['match_time']) && is_numeric($row['match_time']) + ? date('Y-m-d H:i', (int) $row['match_time']) + : (string) ($row['match_time'] ?? ''); + $summary = sprintf( + '%s,%s,比分 %s-%s,赔率 主胜%s 平%s 客胜%s。', + $row['league_name'] ?? '', + $matchTime, + $row['home_score'] ?? 0, + $row['away_score'] ?? 0, + $row['home_odds'] ?? '-', + $row['draw_odds'] ?? '-', + $row['away_odds'] ?? '-' + ); + + return [ + 'context' => [ + 'domain' => 'match', + 'title' => $title, + 'summary' => $summary, + ], + 'source' => [ + 'type' => 'match', + 'title' => $title, + 'summary' => $summary, + 'path' => '/pages/match_detail/match_detail?id=' . (int) $row['id'], + 'source_id' => (int) $row['id'], + ], + ]; + }, $rows); + } + + private static function resolveCryptoContext(string $message): array + { + if ((int) AiConfig::getVal('assistant_enable_crypto_realtime', '1') !== 1) { + return []; + } + + $symbols = self::detectCryptoSymbols($message); + if (empty($symbols)) { + return []; + } + + $items = []; + foreach (array_slice($symbols, 0, 3) as $symbol) { + $ticker = self::fetchBinanceTicker($symbol); + if (empty($ticker)) { + continue; + } + $price = (float) ($ticker['lastPrice'] ?? 0); + $change = (float) ($ticker['priceChangePercent'] ?? 0); + $high = (float) ($ticker['highPrice'] ?? 0); + $low = (float) ($ticker['lowPrice'] ?? 0); + $summary = sprintf( + '%s/USDT 当前价格 %.8g,24小时涨跌 %.2f%%,24小时高低 %.8g / %.8g。', + $symbol, + $price, + $change, + $high, + $low + ); + $items[] = [ + 'context' => [ + 'domain' => 'crypto', + 'symbol' => $symbol, + 'summary' => $summary, + 'raw' => [ + 'price' => $price, + 'change_percent' => $change, + 'high' => $high, + 'low' => $low, + ], + ], + 'source' => [ + 'type' => 'crypto', + 'title' => $symbol . '/USDT 行情', + 'summary' => $summary, + 'path' => '/pages/crypto/crypto_detail?symbol=' . $symbol, + 'source_id' => 0, + ], + ]; + } + + return $items; + } + + private static function detectCryptoSymbols(string $message): array + { + $haystack = mb_strtolower($message); + $symbols = []; + foreach (self::CRYPTO_SYMBOLS as $symbol => $aliases) { + foreach ($aliases as $alias) { + if (mb_strpos($haystack, mb_strtolower($alias)) !== false) { + $symbols[] = $symbol; + break; + } + } + } + return array_values(array_unique($symbols)); + } + + private static function fetchBinanceTicker(string $symbol): array + { + $cacheKey = 'assistant_crypto_ticker_' . $symbol; + $cached = Cache::get($cacheKey); + if (is_array($cached)) { + return $cached; + } + + $url = 'https://api.binance.com/api/v3/ticker/24hr?symbol=' . rawurlencode($symbol . 'USDT'); + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 8, + CURLOPT_CONNECTTIMEOUT => 4, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_HTTPHEADER => ['Accept: application/json'], + ]); + $body = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = $httpCode === 200 ? json_decode((string) $body, true) : []; + if (is_array($data) && !empty($data['symbol'])) { + Cache::set($cacheKey, $data, 10); + return $data; + } + + return []; + } + + private static function sourceFromKbHit(array $hit): array + { + $domain = (string) ($hit['domain'] ?? ''); + $sourceId = (int) ($hit['source_id'] ?? 0); + return [ + 'type' => $domain, + 'title' => (string) ($hit['title'] ?? ''), + 'summary' => self::clipText((string) ($hit['summary'] ?? $hit['chunk_text'] ?? ''), 120), + 'path' => self::sourcePath($domain, $sourceId), + 'source_id' => $sourceId, + ]; + } + + private static function sourcePath(string $domain, int $sourceId): string + { + return match ($domain) { + 'article' => '/pages/news_detail/news_detail?id=' . $sourceId, + 'post' => '/packages_community/pages/post_detail?id=' . $sourceId, + 'match' => '/pages/match_detail/match_detail?id=' . $sourceId, + 'lottery' => '/pages/lottery_analysis/lottery_analysis', + default => '', + }; + } + + private static function compactHit(array $hit): array + { + return [ + 'domain' => $hit['domain'] ?? '', + 'subtype' => $hit['subtype'] ?? '', + 'source_id' => (int) ($hit['source_id'] ?? 0), + 'title' => $hit['title'] ?? '', + 'summary' => $hit['summary'] ?? '', + 'content' => self::clipText((string) ($hit['chunk_text'] ?? ''), 360), + 'score' => $hit['score'] ?? 0, + ]; + } + + private static function uniqueSources(array $sources): array + { + $map = []; + foreach ($sources as $source) { + $key = ($source['type'] ?? '') . ':' . ($source['source_id'] ?? 0) . ':' . ($source['title'] ?? ''); + if (!isset($map[$key]) && trim((string) ($source['title'] ?? '')) !== '') { + $map[$key] = $source; + } + } + return array_slice(array_values($map), 0, 8); + } + + private static function queryTerms(string $message): array + { + $terms = preg_split('/[\s,,。!?;、::\/\\\\]+/u', trim($message)) ?: []; + $terms = array_values(array_filter(array_map('trim', $terms), static function (string $term) { + return mb_strlen($term) >= 2 && !in_array($term, ['今天', '最近', '分析', '什么', '一下'], true); + })); + return array_unique($terms); + } + + private static function clipText(string $text, int $length): string + { + $text = trim(preg_replace('/\s+/u', ' ', $text) ?: ''); + if (mb_strlen($text) <= $length) { + return $text; + } + return mb_substr($text, 0, $length) . '...'; + } +} diff --git a/server/app/common/service/ai/AiAssistantService.php b/server/app/common/service/ai/AiAssistantService.php new file mode 100644 index 0000000..d89a940 --- /dev/null +++ b/server/app/common/service/ai/AiAssistantService.php @@ -0,0 +1,383 @@ + false, 'error' => '请输入要咨询的问题']; + } + if ($userId <= 0 && $clientId === '') { + return ['success' => false, 'error' => '缺少会话标识']; + } + if (!self::checkRateLimit($userId, $clientId, $ip)) { + return ['success' => false, 'error' => '提问太频繁,请稍后再试']; + } + + $session = self::resolveSession($sessionId, $userId, $clientId, $message); + if (!$session) { + return ['success' => false, 'error' => '会话不存在或无权访问']; + } + + $history = self::historyMessages((int) $session->id); + $context = AiAssistantContextService::build($message, $userId, (int) $session->id); + + AiChatMessage::create([ + 'session_id' => (int) $session->id, + 'user_id' => $userId, + 'client_id' => $clientId, + 'role' => AiChatMessage::ROLE_USER, + 'content' => $message, + 'sources_json' => '[]', + 'status' => AiChatMessage::STATUS_SUCCESS, + 'create_time' => time(), + 'update_time' => time(), + ]); + + $messages = self::buildModelMessages($history, $message, $context); + $result = self::callAssistantModel($messages); + + $status = !empty($result['success']) ? AiChatMessage::STATUS_SUCCESS : AiChatMessage::STATUS_FAILED; + $answer = !empty($result['success']) + ? trim((string) ($result['content'] ?? '')) + : 'AI助手暂时没有生成成功,请稍后再试。'; + if ($answer === '') { + $answer = '我暂时没有找到足够的站内依据,请换个关键词再问一次。'; + } + + $assistantMessage = AiChatMessage::create([ + 'session_id' => (int) $session->id, + 'user_id' => $userId, + 'client_id' => $clientId, + 'role' => AiChatMessage::ROLE_ASSISTANT, + 'content' => $answer, + 'sources_json' => json_encode($context['sources'] ?? [], JSON_UNESCAPED_UNICODE), + 'model' => (string) ($result['model'] ?? 'gpt-5.5'), + 'tokens_used' => (int) ($result['tokens'] ?? 0), + 'cost_ms' => (int) ($result['cost_ms'] ?? 0), + 'status' => $status, + 'error_msg' => mb_substr((string) ($result['error'] ?? ''), 0, 500), + 'create_time' => time(), + 'update_time' => time(), + ]); + + self::touchSession((int) $session->id, $message, $answer); + self::recordAiLog($userId, (int) $session->id, $result, $status); + + if (empty($result['success'])) { + return ['success' => false, 'error' => 'AI助手暂时不可用,请稍后再试']; + } + + $session = AiChatSession::findOrEmpty((int) $session->id); + return [ + 'success' => true, + 'data' => [ + 'session' => self::formatSession($session->toArray()), + 'message' => self::formatMessage($assistantMessage->toArray()), + 'sources' => $context['sources'] ?? [], + ], + ]; + } + + public static function sessions(int $userId, string $clientId): array + { + if ($userId <= 0 && $clientId === '') { + return []; + } + + $query = AiChatSession::order('last_active_time desc')->order('id desc')->limit(30); + self::applyOwnerWhere($query, $userId, $clientId); + return array_map([self::class, 'formatSession'], $query->select()->toArray()); + } + + public static function messages(int $sessionId, int $userId, string $clientId): array + { + $session = self::getOwnedSession($sessionId, $userId, $clientId); + if (!$session) { + return ['success' => false, 'error' => '会话不存在或无权访问']; + } + + $messages = AiChatMessage::where('session_id', $sessionId) + ->order('id', 'asc') + ->limit(200) + ->select() + ->toArray(); + + return [ + 'success' => true, + 'data' => [ + 'session' => self::formatSession($session->toArray()), + 'messages' => array_map([self::class, 'formatMessage'], $messages), + ], + ]; + } + + public static function clear(int $sessionId, int $userId, string $clientId): array + { + if ($sessionId > 0) { + $session = self::getOwnedSession($sessionId, $userId, $clientId); + if (!$session) { + return ['success' => false, 'error' => '会话不存在或无权访问']; + } + AiChatMessage::where('session_id', $sessionId)->delete(); + $session->delete(); + return ['success' => true]; + } + + if ($userId <= 0 && $clientId === '') { + return ['success' => false, 'error' => '缺少会话标识']; + } + + $query = AiChatSession::field('id'); + self::applyOwnerWhere($query, $userId, $clientId); + $ids = $query->column('id'); + if (!empty($ids)) { + AiChatMessage::whereIn('session_id', $ids)->delete(); + AiChatSession::whereIn('id', $ids)->delete(); + } + + return ['success' => true]; + } + + private static function resolveSession(int $sessionId, int $userId, string $clientId, string $message): ?AiChatSession + { + if ($sessionId > 0) { + return self::getOwnedSession($sessionId, $userId, $clientId); + } + + $now = time(); + return AiChatSession::create([ + 'user_id' => $userId, + 'client_id' => $clientId, + 'title' => self::makeTitle($message), + 'last_message' => $message, + 'last_active_time' => $now, + 'message_count' => 0, + 'create_time' => $now, + 'update_time' => $now, + ]); + } + + private static function getOwnedSession(int $sessionId, int $userId, string $clientId): ?AiChatSession + { + $query = AiChatSession::where('id', $sessionId); + self::applyOwnerWhere($query, $userId, $clientId); + $session = $query->findOrEmpty(); + return $session->isEmpty() ? null : $session; + } + + private static function applyOwnerWhere($query, int $userId, string $clientId): void + { + if ($userId > 0) { + $query->where('user_id', $userId); + return; + } + $query->where('user_id', 0)->where('client_id', $clientId); + } + + private static function historyMessages(int $sessionId): array + { + $limit = max(2, min(20, (int) AiConfig::getVal('assistant_history_limit', '8'))); + $rows = AiChatMessage::where('session_id', $sessionId) + ->where('status', AiChatMessage::STATUS_SUCCESS) + ->order('id', 'desc') + ->limit($limit) + ->select() + ->toArray(); + return array_reverse($rows); + } + + private static function buildModelMessages(array $history, string $message, array $context): array + { + $messages = [ + [ + 'role' => 'system', + 'content' => AiConfig::getVal('assistant_system_prompt', self::defaultSystemPrompt()), + ], + ]; + + foreach ($history as $row) { + $role = $row['role'] === AiChatMessage::ROLE_ASSISTANT ? 'assistant' : 'user'; + $content = trim((string) ($row['content'] ?? '')); + if ($content !== '') { + $messages[] = ['role' => $role, 'content' => mb_substr($content, 0, 1000)]; + } + } + + $messages[] = [ + 'role' => 'user', + 'content' => "用户问题:\n{$message}\n\n站内资料与实时上下文:\n" + . ($context['context_text'] ?? '{}') + . "\n\n请按照“直接结论、依据摘要、相关内容、风险提示”的结构回答。" + . "如果站内资料不足,请明确说明,不要编造。彩票、赛事预测、加密行情必须提示不构成投资或购彩建议。", + ]; + + return $messages; + } + + private static function callAssistantModel(array $messages): array + { + $client = new DeepSeekClient(); + $optionsList = []; + + $qwenOptions = self::qwenOptions(); + if (!empty($qwenOptions['api_key'])) { + $optionsList[] = $qwenOptions; + } + $optionsList[] = self::defaultModelOptions(); + + $failedErrors = []; + $lastResult = ['success' => false, 'error' => 'AI助手模型服务暂时不可用', 'tokens' => 0]; + foreach ($optionsList as $options) { + $result = $client->chatMessages($messages, $options); + if (!empty($result['success'])) { + return $result; + } + + $lastResult = $result; + $provider = (string) ($result['provider_label'] ?? ($options['provider_label'] ?? 'AI模型')); + $failedErrors[] = $provider . ': ' . (string) ($result['error'] ?? '调用失败'); + } + + if (!empty($failedErrors)) { + $lastResult['error'] = implode(' | ', $failedErrors); + } + return $lastResult; + } + + private static function qwenOptions(): array + { + $apiKey = trim((string) AiConfig::getVal('qwen_api_key', '')); + if ($apiKey === '') { + $apiKey = trim((string) env('DASHSCOPE_API_KEY', '')); + } + if ($apiKey === '') { + return []; + } + + $baseUrl = trim((string) AiConfig::getVal('qwen_base_url', '')); + if ($baseUrl === '') { + $baseUrl = self::QWEN_BASE_URL; + } + $model = trim((string) AiConfig::getVal('qwen_model', '')); + if ($model === '') { + $model = self::QWEN_MODEL; + } + + return self::defaultModelOptions() + [ + 'api_key' => $apiKey, + 'base_url' => $baseUrl, + 'model' => $model, + 'wire_api' => 'chat', + 'provider_label' => 'DashScope(Qwen)', + ]; + } + + private static function defaultModelOptions(): array + { + return [ + 'max_tokens' => 1600, + 'temperature' => 0.45, + ]; + } + + private static function checkRateLimit(int $userId, string $clientId, string $ip): bool + { + $limit = max(1, (int) AiConfig::getVal('assistant_rate_limit_per_minute', '10')); + $identity = $userId > 0 ? 'u' . $userId : 'c' . md5($clientId . '|' . $ip); + $key = 'assistant_rate_' . $identity . '_' . date('YmdHi'); + $count = (int) Cache::get($key); + if ($count >= $limit) { + return false; + } + Cache::set($key, $count + 1, 70); + return true; + } + + private static function touchSession(int $sessionId, string $question, string $answer): void + { + AiChatSession::where('id', $sessionId)->update([ + 'last_message' => mb_substr($question, 0, 500), + 'last_answer' => mb_substr($answer, 0, 500), + 'last_active_time' => time(), + 'message_count' => AiChatMessage::where('session_id', $sessionId)->count(), + 'update_time' => time(), + ]); + } + + private static function recordAiLog(int $userId, int $sessionId, array $result, int $status): void + { + AiLog::create([ + 'user_id' => $userId, + 'type' => self::AI_LOG_TYPE_ASSISTANT, + 'ref_id' => $sessionId, + 'tokens_used' => (int) ($result['tokens'] ?? 0), + 'cost_ms' => (int) ($result['cost_ms'] ?? 0), + 'status' => $status === AiChatMessage::STATUS_SUCCESS ? 1 : 2, + 'create_time' => time(), + ]); + } + + private static function formatSession(array $row): array + { + return [ + 'id' => (int) ($row['id'] ?? 0), + 'title' => (string) ($row['title'] ?? ''), + 'last_message' => (string) ($row['last_message'] ?? ''), + 'last_answer' => (string) ($row['last_answer'] ?? ''), + 'message_count' => (int) ($row['message_count'] ?? 0), + 'last_active_time' => self::formatTime($row['last_active_time'] ?? 0), + 'create_time' => self::formatTime($row['create_time'] ?? 0), + ]; + } + + private static function formatMessage(array $row): array + { + $sources = json_decode((string) ($row['sources_json'] ?? '[]'), true); + return [ + 'id' => (int) ($row['id'] ?? 0), + 'session_id' => (int) ($row['session_id'] ?? 0), + 'role' => (string) ($row['role'] ?? ''), + 'content' => (string) ($row['content'] ?? ''), + 'sources' => is_array($sources) ? $sources : [], + 'status' => (int) ($row['status'] ?? 1), + 'error_msg' => (string) ($row['error_msg'] ?? ''), + 'create_time' => self::formatTime($row['create_time'] ?? 0), + ]; + } + + private static function formatTime($value): string + { + if (is_numeric($value) && (int) $value > 0) { + return date('Y-m-d H:i:s', (int) $value); + } + return (string) ($value ?: ''); + } + + private static function makeTitle(string $message): string + { + $title = trim(preg_replace('/\s+/u', ' ', $message) ?: ''); + return mb_substr($title ?: '新的对话', 0, 24); + } + + private static function defaultSystemPrompt(): string + { + return '你是世博头条 AI 助手,面向普通用户回答站内体育资讯、赛事、社区、彩票和加密行情相关问题。' + . '必须优先依据提供的站内资料和实时上下文回答;资料不足时要直接说明不足并建议用户换关键词。' + . '不要声称可以访问后台、源码、服务器或未提供的内部数据。' + . '回答使用中文,结构清晰,语气专业克制。涉及赛事预测、彩票或加密行情时,必须说明仅供参考,不构成投资或购彩建议。'; + } +} diff --git a/server/app/common/service/ai/AiService.php b/server/app/common/service/ai/AiService.php new file mode 100644 index 0000000..52d323a --- /dev/null +++ b/server/app/common/service/ai/AiService.php @@ -0,0 +1,615 @@ + true, 'data' => $cacheData, 'from_cache' => true]; + } + } + + $result = KbRagService::analyzeMatch($matchId, $matchData, $userId); + + self::saveResult(AiAnalysis::TYPE_MATCH_PREDICT, $matchId, '', $result, $userId); + + if (!$result['success']) { + return ['success' => false, 'error' => $result['error']]; + } + + $data = $result['parsed'] ?? self::buildFallbackMatch($matchData); + return ['success' => true, 'data' => $data, 'from_cache' => false]; + } + + // ── 分段赛事预测 ── + + protected static array $sectionPrompts = [ + 'prediction' => '你是专业体育赛事分析师。请基于数据预测胜负和比分。 +要求输出严格JSON: +{"prediction":{"result":"主胜/平局/客胜","confidence":75,"score_predict":"2-1","reasoning":"简述理由"},"analysis":{"home_strength":78,"away_strength":72}}', + 'factors' => '你是专业体育赛事分析师。请分析影响比赛的关键因素和风险。 +要求输出严格JSON: +{"key_factors":["利好因素1","利好因素2","利好因素3"],"risk_factors":["风险1","风险2"]}', + 'odds' => '你是专业体育赛事分析师。请分析赔率并给出投注建议。 +要求输出严格JSON: +{"odds_opinion":{"value_bet":"主胜/平局/客胜/无","explanation":"赔率分析说明"}}', + 'summary' => '你是专业体育赛事分析师。请给出简洁的分析总结。 +要求输出严格JSON: +{"summary":"完整的分析总结,200字左右"}', + ]; + + public static function predictMatchSection(int $matchId, string $section, array $matchData, int $userId = 0): array + { + $validSections = ['prediction', 'factors', 'odds', 'summary']; + if (!in_array($section, $validSections)) { + return ['success' => false, 'error' => '无效的分析模块']; + } + + $cacheKey = AiAnalysis::TYPE_MATCH_PREDICT * 100 + array_search($section, $validSections); + $cache = AiAnalysis::getCache($cacheKey, $matchId); + if ($cache) { + $cacheData = json_decode($cache['result'], true); + if (is_array($cacheData) && self::isKbEnhancedMatchPayload($cacheData)) { + return ['success' => true, 'data' => $cacheData, 'from_cache' => true, 'section' => $section]; + } + } + + $result = KbRagService::analyzeMatchSection($matchId, $section, $matchData, $userId); + + self::saveResult($cacheKey, $matchId, '', $result, $userId, 6); + + if (!$result['success']) { + return ['success' => false, 'error' => $result['error'], 'section' => $section]; + } + + $data = $result['parsed'] ?? []; + return ['success' => true, 'data' => $data, 'from_cache' => false, 'section' => $section]; + } + + /** + * 资讯AI分析 + */ + public static function analyzeArticle(int $articleId, array $articleData, int $userId = 0): array + { + $cache = AiAnalysis::getCache(AiAnalysis::TYPE_ARTICLE_ANALYSIS, $articleId); + if ($cache) { + return ['success' => true, 'data' => json_decode($cache['result'], true), 'from_cache' => true]; + } + + $result = KbRagService::analyzeArticle($articleId, $articleData, $userId); + + $cacheHours = (int) AiConfig::getVal('article_analysis_cache_hours', '24'); + self::saveResult(AiAnalysis::TYPE_ARTICLE_ANALYSIS, $articleId, '', $result, $userId, $cacheHours); + + if (!$result['success']) { + return ['success' => false, 'error' => $result['error']]; + } + + $data = $result['parsed'] ?? self::buildFallbackArticle($articleData); + return ['success' => true, 'data' => $data, 'from_cache' => false]; + } + + /** + * 帖子AI分析 + */ + public static function analyzePost(int $postId, array $postData, int $userId = 0, bool $isLotteryPost = false, bool $force = false): array + { + $cache = $force ? null : AiAnalysis::getCache(AiAnalysis::TYPE_POST_ANALYSIS, $postId); + if ($cache) { + $cacheData = json_decode($cache['result'], true); + if (is_array($cacheData) && self::isValidPostAnalysisCache($cacheData, $isLotteryPost)) { + return ['success' => true, 'data' => $cacheData, 'from_cache' => true]; + } + } + + $result = KbRagService::analyzePost($postId, $postData, $userId, $isLotteryPost); + self::saveResult(AiAnalysis::TYPE_POST_ANALYSIS, $postId, '', $result, $userId, 12); + + if (!$result['success']) { + return ['success' => false, 'error' => $result['error'] ?? 'AI分析失败']; + } + + return [ + 'success' => true, + 'data' => $result['parsed'] ?? self::buildFallbackPost($postData), + 'from_cache' => false, + ]; + } + + /** + * 用户可信度分析 + */ + public static function analyzeCredibility(int $targetUserId, array $userData, int $requestUserId = 0): array + { + $cache = AiAnalysis::getCache(AiAnalysis::TYPE_USER_CREDIBILITY, $targetUserId); + if ($cache) { + return ['success' => true, 'data' => json_decode($cache['result'], true), 'from_cache' => true]; + } + + $defaultPrompt = '你是体育社区内容审核AI。请根据用户的历史数据评估其可信度。 + +要求输出严格JSON格式: +{"credibility_score":75,"risk_level":"low/medium/high","is_suspicious":false,"analysis":"分析说明,100字左右","dimension_scores":{"hit_rate":80,"stability":85,"rationality":65,"activity":70},"suggestions":["建议1","建议2"]}'; + $systemPrompt = AiConfig::getVal('credibility_analysis_prompt', $defaultPrompt); + + $userMsg = "请分析以下用户数据并评估可信度:\n" . json_encode($userData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + + $client = new DeepSeekClient(); + $result = $client->chatJson($systemPrompt, $userMsg); + + self::saveResult(AiAnalysis::TYPE_USER_CREDIBILITY, $targetUserId, '', $result, $requestUserId, 24); + + if (!$result['success']) { + return ['success' => false, 'error' => $result['error']]; + } + + $data = $result['parsed'] ?? self::buildFallbackCredibility(); + return ['success' => true, 'data' => $data, 'from_cache' => false]; + } + + /** + * 彩票概率分析 + */ + public static function analyzeLottery(array $lotteryData, int $userId = 0): array + { + $refId = crc32(json_encode($lotteryData)); + + $cache = AiAnalysis::getCache(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId); + if ($cache) { + return ['success' => true, 'data' => json_decode($cache['result'], true), 'from_cache' => true]; + } + + $result = KbRagService::analyzeLotteryOverview($lotteryData, $userId); + + self::saveResult(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId, '', $result, $userId, 12); + + if (!$result['success']) { + return ['success' => false, 'error' => $result['error']]; + } + + $data = $result['parsed'] ?? ['disclaimer' => '分析暂时不可用,请稍后重试']; + return ['success' => true, 'data' => $data, 'from_cache' => false]; + } + + public static function translateCommunityPost(string $content): array + { + if ($content === '') { + return ['success' => false, 'error' => '帖子内容为空']; + } + + return MTranServerService::translate($content, 'zh-Hans', 'auto'); + } + + /** + * 资讯翻译 + */ + public static function translateArticle(string $title, string $abstract, string $content): array + { + $title = trim($title); + $abstract = trim($abstract); + $content = trim($content); + + if ($title === '' && $abstract === '' && $content === '') { + return ['success' => false, 'error' => '文章内容为空']; + } + + if (mb_strlen($content) > 5000) { + $content = mb_substr($content, 0, 5000) . '...'; + } + + $parts = []; + if ($title !== '') { + $translatedTitle = MTranServerService::translate($title, 'zh-Hans', 'auto'); + if (!$translatedTitle['success']) { + return $translatedTitle; + } + $parts[] = '标题:' . trim((string) $translatedTitle['content']); + } + + if ($abstract !== '') { + $translatedAbstract = MTranServerService::translate($abstract, 'zh-Hans', 'auto'); + if (!$translatedAbstract['success']) { + return $translatedAbstract; + } + $parts[] = '摘要:' . trim((string) $translatedAbstract['content']); + } + + if ($content !== '') { + $translatedContentLines = []; + foreach (self::splitTextForTranslation($content) as $contentLine) { + $translatedContent = MTranServerService::translate($contentLine, 'zh-Hans', 'auto'); + if (!$translatedContent['success']) { + return $translatedContent; + } + $translatedContentLines[] = trim((string) $translatedContent['content']); + } + $translatedContentLines = array_values(array_filter($translatedContentLines, static fn($line) => $line !== '')); + if (!empty($translatedContentLines)) { + $firstLine = array_shift($translatedContentLines); + $parts[] = '正文:' . $firstLine; + foreach ($translatedContentLines as $line) { + $parts[] = $line; + } + } + } + + $finalText = trim(implode("\n", $parts)); + if ($finalText === '') { + return ['success' => false, 'error' => '翻译结果为空']; + } + + return [ + 'success' => true, + 'content' => $finalText, + 'tokens' => 0, + 'cost_ms' => 0, + ]; + } + + protected static function splitTextForTranslation(string $text): array + { + $normalized = trim(preg_replace('/\r\n|\r/u', "\n", $text)); + if ($normalized === '') { + return []; + } + + $manualLines = array_values(array_filter(array_map(static function ($line) { + $line = preg_replace('/\s+/u', ' ', trim($line)); + return $line === '' ? '' : $line; + }, explode("\n", $normalized)))); + + if (count($manualLines) > 1) { + $segments = []; + foreach ($manualLines as $line) { + $segments = array_merge($segments, self::splitTextForTranslation($line)); + } + return $segments; + } + + $flatText = preg_replace('/\s+/u', ' ', $normalized); + preg_match_all('/[^。!?.!?]+[。!?.!?]?/u', $flatText, $matches); + $segments = array_values(array_filter(array_map('trim', $matches[0] ?? []))); + + return !empty($segments) ? $segments : [$flatText]; + } + + /** + * 资讯评论生成 + */ + public static function generateArticleComment(string $title, string $abstract, string $content, string $persona = ''): array + { + $title = trim($title); + $abstract = trim($abstract); + $content = trim($content); + + if ($title === '' && $abstract === '' && $content === '') { + return ['success' => false, 'error' => '文章内容为空']; + } + + if (mb_strlen($content) > 3500) { + $content = mb_substr($content, 0, 3500) . '...'; + } + + $systemPrompt = AiConfig::getVal( + 'article_comment_prompt', + '你是一名专业、克制、真实的体育资讯评论员。请根据给定资讯内容生成1条中文评论,要求自然、具体、像真实用户发言,评论长度控制在20到60字之间。不要输出标题、前缀、解释、表情、标签、项目符号,不要提及自己是AI,不要复述全文。' + ); + + $personaText = $persona !== '' ? "评论视角:{$persona}\n" : ''; + $userMsg = $personaText . "标题:{$title}\n摘要:{$abstract}\n正文:{$content}\n\n请输出1条适合发布在评论区的中文评论。"; + + $client = new DeepSeekClient(); + return $client->chat($systemPrompt, $userMsg, ['max_tokens' => 200, 'temperature' => 0.75]); + } + + public static function analyzeLotteryPostImages(string $content, array $images): array + { + if (empty($images)) { + return ['success' => false, 'error' => '帖子图片为空,无法分析']; + } + + $imageUrls = array_values(array_filter(array_map(function ($image) { + if (!$image) { + return ''; + } + return self::prepareGptImageUrl(FileService::getFileUrl($image)); + }, $images))); + + if (empty($imageUrls)) { + return ['success' => false, 'error' => '帖子图片为空,无法分析']; + } + + $systemPrompt = AiConfig::getVal( + 'community_lottery_image_analysis_prompt', + '你是彩票图片内容分析助手。只允许依据当前帖子图片和帖子文案中直接给出的当期信息进行分析。' + . '不要引用往期内容、历史期数、知识库信息或你自己的记忆。' + . '如果图片里没有明确展示某项信息,就直接说明无法判断。' + . '请先对图片内容做去噪过滤,只保留与当期分析直接相关的信息。' + . '有效信息包括:期号、号码、波色、单双、大小、生肖、五行、推荐组合、胆码拖码、走势箭头、统计表、图表结论、明确的标题结论。' + . '无关噪音包括:装饰背景、边框花纹、吉祥话、宣传口号、重复标题、水印、无关广告、联系方式、模板化祝福语、与分析无关的排版符号。' + . '对于重复出现的同一信息,只保留一次并合并描述;对于模糊、遮挡、低置信度识别内容,不要强行分析,可直接标记为“无法可靠识别”。' + . '请先完整识别图片中的号码、期号、版面文字、波色/单双/大小/生肖/五行、图表结论、胆码拖码、推荐组合或其他可见资料。' + . '识别出来的每一项内容,都要单独给出对应分析描述,不能只做整体概括。' + . '如果有多张图片,按图片1、图片2逐张拆开写;每张图片内再按“识别内容1/分析1、识别内容2/分析2”的方式一一对应展开。' + . '输出格式尽量清晰,推荐使用:图片X、过滤后识别内容、逐项分析、综合结论、风险提示。' + . '不要承诺中奖,不要编造图片中不存在的数据,最后给出理性购彩风险提示。' + ); + $options = [ + 'max_tokens' => 2000, + 'temperature' => 0.3, + 'model' => self::GPT_PROXY_MODEL, + 'base_url' => self::GPT_PROXY_BASE_URL, + 'api_key' => self::GPT_PROXY_API_KEY, + 'wire_api' => 'responses', + 'provider_label' => 'OpenAI(GPT中转)', + ]; + + $client = new DeepSeekClient(); + $userMessage = "帖子内容:\n" . ($content ?: '无文字内容') . "\n\n请只依据当前帖子图片和上面的帖子文案输出分析。先过滤掉杂乱和无关内容,再提取图片里的有效信息,最后对提取出的每一项内容一一分析描述,不要补充任何往期推断。"; + $result = $client->chatWithImages( + $systemPrompt, + $userMessage, + $imageUrls, + $options + ); + + return $result; + } + + protected static function prepareGptImageUrl(string $imageUrl): string + { + if (!preg_match('/^https?:\/\//i', $imageUrl)) { + return $imageUrl; + } + + $dataUrl = self::downloadImageAsDataUrl($imageUrl); + return $dataUrl !== '' ? $dataUrl : $imageUrl; + } + + protected static function downloadImageAsDataUrl(string $imageUrl): string + { + $ch = curl_init($imageUrl); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 25, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_USERAGENT => 'Mozilla/5.0 SportEraBot/1.0', + ]); + + $body = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + curl_close($ch); + + if ($httpCode !== 200 || !is_string($body) || $body === '' || strlen($body) > 8 * 1024 * 1024) { + return ''; + } + + $mime = strtolower(trim(explode(';', $contentType)[0] ?? '')); + if ($mime === '' || !str_starts_with($mime, 'image/')) { + $path = parse_url($imageUrl, PHP_URL_PATH) ?: ''; + $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + $mime = match ($ext) { + 'png' => 'image/png', + 'webp' => 'image/webp', + 'gif' => 'image/gif', + default => 'image/jpeg', + }; + } + + return 'data:' . $mime . ';base64,' . base64_encode($body); + } + + /** + * 保存分析结果 + */ + protected static function saveResult(int $type, int $refId, string $prompt, array $result, int $userId, int $cacheHours = 6): void + { + $status = $result['success'] ? AiAnalysis::STATUS_SUCCESS : AiAnalysis::STATUS_FAILED; + $resultJson = $result['success'] ? json_encode($result['parsed'] ?? $result['content'], JSON_UNESCAPED_UNICODE) : ''; + + AiAnalysis::create([ + 'type' => $type, + 'ref_id' => $refId, + 'prompt' => mb_substr($prompt, 0, 5000), + 'result' => $resultJson, + 'model' => (string) ($result['model'] ?? self::GPT_PROXY_MODEL), + 'tokens_used' => $result['tokens'] ?? 0, + 'status' => $status, + 'error_msg' => $result['error'] ?? '', + 'expire_time' => time() + $cacheHours * 3600, + ]); + + AiLog::create([ + 'user_id' => $userId, + 'type' => $type, + 'ref_id' => $refId, + 'tokens_used' => $result['tokens'] ?? 0, + 'cost_ms' => $result['cost_ms'] ?? 0, + 'status' => $status, + ]); + } + + /** + * 彩票分模块分析(热冷号/趋势/统计) + */ + public static function analyzeLotteryModule(array $lotteryData, string $module, int $userId = 0): array + { + $latestIssue = $lotteryData['recent_draws'][0]['issue'] ?? ''; + $cacheKey = $lotteryData['lottery_type'] . ':' . $module . ':' . $latestIssue; + $refId = crc32($cacheKey); + + $cache = AiAnalysis::getCache(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId); + if ($cache) { + $cacheData = json_decode($cache['result'], true); + if (is_array($cacheData) && !empty($cacheData)) { + return ['success' => true, 'data' => $cacheData, 'from_cache' => true]; + } + } + + $result = KbRagService::analyzeLotteryModule($lotteryData, $module, $userId); + + if (!$result['success']) { + self::saveResult(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId, '', $result, $userId, 12); + return ['success' => false, 'error' => $result['error']]; + } + + $data = $result['parsed'] ?? null; + if (!is_array($data) || empty($data)) { + $result['success'] = false; + $result['error'] = 'AI返回格式异常'; + self::saveResult(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId, '', $result, $userId, 12); + return ['success' => false, 'error' => $result['error']]; + } + + self::saveResult(AiAnalysis::TYPE_LOTTERY_ANALYSIS, $refId, '', $result, $userId, 12); + + // 持久化到彩票AI分析历史表 + if ($latestIssue) { + $history = LotteryAiAnalysis::create([ + 'code' => $lotteryData['lottery_type'], + 'issue' => $latestIssue, + 'module' => $module, + 'result' => json_encode($data, JSON_UNESCAPED_UNICODE), + ]); + KbSyncService::enqueue('lottery', 'ai_history', (int) $history->id, 'upsert', 90); + } + + return ['success' => true, 'data' => $data, 'from_cache' => false]; + } + + /** + * 赛事预测降级数据 + */ + protected static function buildFallbackMatch(array $matchData): array + { + return [ + 'prediction' => [ + 'result' => '待分析', + 'confidence' => 50, + 'score_predict' => '-', + 'reasoning' => 'AI分析暂时不可用,请稍后重试', + ], + 'analysis' => [ + 'home_strength' => 50, + 'away_strength' => 50, + 'key_factors' => ['数据加载中'], + 'risk_factors' => ['AI服务响应异常'], + ], + 'odds_opinion' => ['value_bet' => '无', 'explanation' => '暂无数据'], + 'summary' => '当前AI分析服务暂时不可用,请稍后重试。', + ]; + } + + /** + * 资讯分析降级数据 + */ + protected static function buildFallbackArticle(array $articleData): array + { + return [ + 'overall_score' => 60, + 'summary' => $articleData['desc'] ?? '暂无摘要', + 'leagues_and_teams' => ['leagues' => [], 'teams' => [], 'match_context' => ''], + 'upcoming_matches' => [], + 'head_to_head' => ['available' => false, 'records' => [], 'summary' => ''], + 'betting_analysis' => [ + 'prediction' => '谨慎观望', + 'confidence' => 50, + 'reasoning' => 'AI分析暂时不可用,请稍后重试。', + 'risk_level' => 'medium', + 'key_factors' => [], + 'value_bet' => '无明显价值', + ], + 'key_points' => ['详细分析正在生成中'], + 'risk_warnings' => [], + 'opposite_views' => [], + 'sentiment' => ['label' => 'neutral', 'confidence' => 50], + 'tags' => [], + 'evidence_list' => [], + 'similar_history' => [], + 'from_kb' => false, + 'retrieval_meta' => ['hit_count' => 0, 'domains' => [], 'top_scores' => []], + ]; + } + + protected static function buildFallbackPost(array $postData): array + { + return [ + 'overall_score' => 60, + 'summary' => self::truncatePlainText((string) ($postData['content'] ?? '当前帖子信息不足,暂时无法给出稳定结论。'), 120), + 'credibility_signals' => [], + 'risk_warning' => ['AI分析暂时不可用,请稍后重试。'], + 'evidence_list' => [], + 'similar_history' => [], + 'from_kb' => false, + 'retrieval_meta' => ['hit_count' => 0, 'domains' => [], 'top_scores' => []], + ]; + } + + /** + * 可信度分析降级数据 + */ + protected static function buildFallbackCredibility(): array + { + return [ + 'credibility_score' => 50, + 'risk_level' => 'medium', + 'is_suspicious' => false, + 'analysis' => '可信度分析服务暂时不可用', + 'dimension_scores' => ['hit_rate' => 50, 'stability' => 50, 'rationality' => 50, 'activity' => 50], + 'suggestions' => [], + ]; + } + + protected static function truncatePlainText(string $text, int $length): string + { + $text = trim(preg_replace('/\s+/u', ' ', $text) ?: ''); + return mb_strlen($text) > $length ? mb_substr($text, 0, $length) . '...' : $text; + } + + protected static function isKbEnhancedMatchPayload(array $payload): bool + { + return isset($payload['retrieval_meta']) || isset($payload['evidence_list']) || isset($payload['from_kb']); + } + + protected static function isValidPostAnalysisCache(array $payload, bool $isLotteryPost): bool + { + if (empty($payload)) { + return false; + } + + if (!$isLotteryPost) { + return true; + } + + return self::isKbEnhancedMatchPayload($payload) + && isset($payload['next_prediction']) + && is_array($payload['next_prediction']) + && ($payload['analysis_source'] ?? '') === 'image_free_v1'; + } +} diff --git a/server/app/common/service/ai/DeepSeekClient.php b/server/app/common/service/ai/DeepSeekClient.php new file mode 100644 index 0000000..c07c015 --- /dev/null +++ b/server/app/common/service/ai/DeepSeekClient.php @@ -0,0 +1,299 @@ +apiKey = self::GPT_PROXY_API_KEY; + $this->baseUrl = self::GPT_PROXY_BASE_URL; + $this->model = self::GPT_PROXY_MODEL; + $this->wireApi = self::GPT_PROXY_WIRE_API; + $this->providerLabel = 'OpenAI(GPT中转)'; + $this->maxTokens = (int) AiConfig::getVal('max_tokens', '2000'); + $this->temperature = (float) AiConfig::getVal('temperature', '0.7'); + } + + /** + * 发送聊天请求 + */ + public function chat(string $systemPrompt, string $userMessage, array $options = []): array + { + $messages = [ + ['role' => 'system', 'content' => $systemPrompt], + ['role' => 'user', 'content' => $userMessage], + ]; + + return $this->request($messages, $options); + } + + public function chatMessages(array $messages, array $options = []): array + { + if (empty($messages)) { + return ['success' => false, 'error' => 'messages不能为空', 'tokens' => 0]; + } + + return $this->request($messages, $options); + } + + public function chatWithImages(string $systemPrompt, string $userMessage, array $imageUrls, array $options = []): array + { + $content = [ + ['type' => 'text', 'text' => $userMessage], + ]; + + foreach ($imageUrls as $imageUrl) { + if ($imageUrl === '') { + continue; + } + $content[] = [ + 'type' => 'image_url', + 'image_url' => ['url' => $imageUrl], + ]; + } + + $messages = [ + ['role' => 'system', 'content' => $systemPrompt], + ['role' => 'user', 'content' => $content], + ]; + + return $this->request($messages, $options); + } + + protected function request(array $messages, array $options = []): array + { + $apiKey = $options['api_key'] ?? $this->apiKey; + $providerLabel = $options['provider_label'] ?? $this->providerLabel; + if (empty($apiKey)) { + return ['success' => false, 'error' => $providerLabel . ' API Key未配置', 'tokens' => 0, 'model' => $options['model'] ?? $this->model, 'provider_label' => $providerLabel]; + } + + $startTime = microtime(true); + $baseUrl = rtrim($options['base_url'] ?? $this->baseUrl, '/'); + $wireApi = strtolower((string) ($options['wire_api'] ?? $this->wireApi)); + $payload = $wireApi === 'responses' + ? $this->buildResponsesPayload($messages, $options) + : $this->buildChatPayload($messages, $options); + $endpoint = $wireApi === 'responses' + ? $this->buildResponsesEndpoint($baseUrl) + : $this->buildChatEndpoint($baseUrl); + $timeout = $this->resolveTimeout($options['timeout'] ?? null); + + $ch = curl_init($endpoint); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $apiKey, + ], + CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => min(15, $timeout), + CURLOPT_TIMEOUT => $timeout, + CURLOPT_SSL_VERIFYPEER => false, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + $costMs = (int) ((microtime(true) - $startTime) * 1000); + + if ($error) { + return ['success' => false, 'error' => 'cURL错误: ' . $error, 'tokens' => 0, 'cost_ms' => $costMs, 'model' => $payload['model'], 'provider_label' => $providerLabel]; + } + + $data = json_decode($response, true); + + $content = $wireApi === 'responses' ? $this->extractResponsesContent($data) : ($data['choices'][0]['message']['content'] ?? ''); + if ($httpCode !== 200 || $content === '') { + $errMsg = $data['error']['message'] ?? ('HTTP ' . $httpCode); + return ['success' => false, 'error' => $providerLabel . ' 返回异常: ' . $errMsg, 'tokens' => 0, 'cost_ms' => $costMs, 'model' => $payload['model'], 'provider_label' => $providerLabel]; + } + + $tokens = $data['usage']['total_tokens'] ?? (($data['usage']['input_tokens'] ?? 0) + ($data['usage']['output_tokens'] ?? 0)); + + return [ + 'success' => true, + 'content' => $content, + 'tokens' => $tokens, + 'cost_ms' => $costMs, + 'model' => $payload['model'], + 'provider_label' => $providerLabel, + ]; + } + + protected function buildChatPayload(array $messages, array $options): array + { + return [ + 'model' => $options['model'] ?? $this->model, + 'messages' => $messages, + 'max_tokens' => $options['max_tokens'] ?? $this->maxTokens, + 'temperature' => $options['temperature'] ?? $this->temperature, + ]; + } + + protected function buildResponsesPayload(array $messages, array $options): array + { + $instructions = []; + $input = []; + foreach ($messages as $message) { + $role = (string) ($message['role'] ?? 'user'); + $content = $message['content'] ?? ''; + if ($role === 'system') { + $text = $this->contentToText($content); + if ($text !== '') { + $instructions[] = $text; + } + continue; + } + + $input[] = [ + 'role' => $role === 'assistant' ? 'assistant' : 'user', + 'content' => $this->convertResponsesContent($content), + ]; + } + + $payload = [ + 'model' => $options['model'] ?? $this->model, + 'input' => $input, + 'max_output_tokens' => $options['max_tokens'] ?? $this->maxTokens, + 'temperature' => $options['temperature'] ?? $this->temperature, + ]; + if (!empty($instructions)) { + $payload['instructions'] = implode("\n\n", $instructions); + } + return $payload; + } + + protected function convertResponsesContent($content): array + { + if (is_string($content)) { + return [['type' => 'input_text', 'text' => $content]]; + } + + $rows = []; + foreach ((array) $content as $item) { + $type = (string) ($item['type'] ?? ''); + if ($type === 'text') { + $rows[] = ['type' => 'input_text', 'text' => (string) ($item['text'] ?? '')]; + } elseif ($type === 'image_url') { + $imageUrl = $item['image_url']['url'] ?? ''; + if ($imageUrl !== '') { + $rows[] = ['type' => 'input_image', 'image_url' => (string) $imageUrl]; + } + } + } + return $rows ?: [['type' => 'input_text', 'text' => '']]; + } + + protected function contentToText($content): string + { + if (is_string($content)) { + return trim($content); + } + + $texts = []; + foreach ((array) $content as $item) { + if (($item['type'] ?? '') === 'text') { + $texts[] = (string) ($item['text'] ?? ''); + } + } + return trim(implode("\n", $texts)); + } + + protected function extractResponsesContent(array $data): string + { + if (!empty($data['output_text'])) { + return (string) $data['output_text']; + } + + $texts = []; + foreach (($data['output'] ?? []) as $output) { + foreach (($output['content'] ?? []) as $content) { + if (isset($content['text'])) { + $texts[] = (string) $content['text']; + } + } + } + return trim(implode("\n", $texts)); + } + + protected function buildChatEndpoint(string $baseUrl): string + { + if (str_ends_with($baseUrl, '/chat/completions')) { + return $baseUrl; + } + + if (str_ends_with($baseUrl, '/v1')) { + return $baseUrl . '/chat/completions'; + } + + return $baseUrl . '/v1/chat/completions'; + } + + protected function buildResponsesEndpoint(string $baseUrl): string + { + if (str_ends_with($baseUrl, '/responses')) { + return $baseUrl; + } + + if (str_ends_with($baseUrl, '/v1')) { + return $baseUrl . '/responses'; + } + + return $baseUrl . '/v1/responses'; + } + + protected function resolveTimeout($timeout): int + { + $value = (int) ($timeout ?: AiConfig::getVal('ai_request_timeout', '180')); + return max(30, min($value, 300)); + } + + /** + * 发送请求并解析JSON结果 + */ + public function chatJson(string $systemPrompt, string $userMessage, array $options = []): array + { + $result = $this->chat($systemPrompt, $userMessage, $options); + if (!$result['success']) { + return $result; + } + + $content = $result['content']; + // 尝试提取JSON块 + if (preg_match('/```json\s*(.*?)\s*```/s', $content, $m)) { + $content = $m[1]; + } elseif (preg_match('/\{.*\}/s', $content, $m)) { + $content = $m[0]; + } + + $parsed = json_decode($content, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $result['parsed'] = null; + $result['raw_content'] = $result['content']; + } else { + $result['parsed'] = $parsed; + } + + return $result; + } +} diff --git a/server/app/common/service/ai/EmbeddingClient.php b/server/app/common/service/ai/EmbeddingClient.php new file mode 100644 index 0000000..976e5af --- /dev/null +++ b/server/app/common/service/ai/EmbeddingClient.php @@ -0,0 +1,111 @@ +apiKey = (string) (AiConfig::getVal('embedding_api_key', '') ?: env('DASHSCOPE_API_KEY', '')); + $this->baseUrl = (string) (AiConfig::getVal('embedding_base_url', '') ?: self::DASHSCOPE_BASE_URL); + $this->model = (string) (AiConfig::getVal('embedding_model', '') ?: self::DASHSCOPE_EMBEDDING_MODEL); + $this->defaultDim = (int) (AiConfig::getVal('embedding_dim', '') ?: '1024'); + } + + /** + * @param string|array $input + */ + public function embed($input, array $options = []): array + { + $apiKey = $options['api_key'] ?? $this->apiKey; + $baseUrl = rtrim($options['base_url'] ?? $this->baseUrl, '/'); + $model = $options['model'] ?? $this->model; + + if ($apiKey === '' || $baseUrl === '' || $model === '') { + return [ + 'success' => false, + 'error' => 'Embedding API配置未完成', + 'embeddings' => [], + 'cost_ms' => 0, + ]; + } + + $payload = [ + 'model' => $model, + 'input' => $input, + ]; + + if (!empty($options['dimensions'])) { + $payload['dimensions'] = (int) $options['dimensions']; + } elseif ($this->defaultDim > 0) { + $payload['dimensions'] = $this->defaultDim; + } + + $startTime = microtime(true); + $ch = curl_init($baseUrl . '/v1/embeddings'); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $apiKey, + ], + CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 60, + CURLOPT_SSL_VERIFYPEER => false, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + $costMs = (int) ((microtime(true) - $startTime) * 1000); + if ($error) { + return [ + 'success' => false, + 'error' => 'Embedding cURL错误: ' . $error, + 'embeddings' => [], + 'cost_ms' => $costMs, + ]; + } + + $data = json_decode((string) $response, true); + if ($httpCode !== 200 || empty($data['data']) || !is_array($data['data'])) { + return [ + 'success' => false, + 'error' => $data['error']['message'] ?? ('HTTP ' . $httpCode), + 'embeddings' => [], + 'cost_ms' => $costMs, + ]; + } + + $embeddings = []; + foreach ($data['data'] as $row) { + $embedding = $row['embedding'] ?? []; + if (is_array($embedding) && !empty($embedding)) { + $embeddings[] = array_map('floatval', $embedding); + } + } + + return [ + 'success' => !empty($embeddings), + 'error' => !empty($embeddings) ? '' : 'Embedding结果为空', + 'embeddings' => $embeddings, + 'model' => $model, + 'dimension' => count($embeddings[0] ?? []), + 'tokens' => (int) ($data['usage']['total_tokens'] ?? 0), + 'cost_ms' => $costMs, + ]; + } +} diff --git a/server/app/common/service/ai/KbRagService.php b/server/app/common/service/ai/KbRagService.php new file mode 100644 index 0000000..cb67be5 --- /dev/null +++ b/server/app/common/service/ai/KbRagService.php @@ -0,0 +1,739 @@ + 1400]); + } + + public static function analyzeMatchSection(int $matchId, string $section, array $matchData, int $userId = 0): array + { + $validSections = ['prediction', 'factors', 'odds', 'summary']; + if (!in_array($section, $validSections, true)) { + return ['success' => false, 'error' => '无效的分析模块']; + } + + $retrieval = self::retrieveMatchEvidence($matchId, $matchData, $userId, 'match_' . $section); + $systemPrompt = AiConfig::getVal('match_predict_' . $section . '_prompt', self::matchSectionPrompt($section)); + $userMessage = self::buildMatchUserMessage($matchData, $retrieval, false, $section); + + return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($section, $retrieval) { + return self::normalizeMatchSectionResult($section, $parsed, $retrieval); + }, ['max_tokens' => 1000]); + } + + public static function analyzeArticle(int $articleId, array $articleData, int $userId = 0): array + { + $retrieval = KbService::retrieve( + 'article_analysis', + self::buildArticleQuery($articleData), + [ + 'user_id' => $userId, + 'ref_id' => $articleId, + 'domains' => ['article', 'post'], + 'preferred_domain' => 'article', + 'cid' => (int) ($articleData['cid'] ?? 0), + 'exclude_source' => ['domain' => 'article', 'source_id' => $articleId], + ], + [ + 'title' => (string) ($articleData['title'] ?? ''), + 'summary' => (string) ($articleData['abstract'] ?? $articleData['desc'] ?? ''), + ], + self::intConfig('kb_final_topk', 8) + ); + + $systemPrompt = AiConfig::getVal('article_analysis_prompt', self::defaultArticlePrompt()); + $userMessage = "当前文章:\n" . json_encode([ + 'id' => $articleId, + 'title' => $articleData['title'] ?? '', + 'abstract' => $articleData['abstract'] ?? '', + 'author' => $articleData['author'] ?? '', + 'category' => $articleData['category'] ?? '', + 'content' => self::clipText(strip_tags((string) ($articleData['content'] ?? '')), 2500), + ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n\n知识库证据:\n" + . json_encode(self::compactHits($retrieval['hits'] ?? []), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n\n请严格依据当前文章和知识库证据输出JSON。"; + + return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($retrieval) { + return self::normalizeArticleResult($parsed, $retrieval); + }); + } + + public static function analyzePost(int $postId, array $postData, int $userId = 0, bool $isLotteryPost = false): array + { + $tags = $postData['tags'] ?? []; + if ($isLotteryPost) { + $retrieval = self::emptyRetrieval(); + $systemPrompt = self::lotteryPostImagePrompt(); + $userMessage = "当前六合彩帖子与图片识别内容:\n" . json_encode([ + 'id' => $postId, + 'content' => self::clipText((string) ($postData['content'] ?? ''), 1800), + 'tags' => $tags, + 'lottery_key' => (string) (($postData['ext']['lottery_key'] ?? '') ?: ''), + 'lottery_analysis_content' => self::clipText((string) ($postData['lottery_analysis_content'] ?? ''), 2200), + ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n\n请只依据当前帖子图片识别内容和帖子文案自由解读,不需要知识库证据,也不要输出“知识库证据不足”。"; + + return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($retrieval, $postData) { + return self::normalizePostResult($parsed, $retrieval, $postData); + }, self::gptPostOptions()); + } + + $domains = ['post', 'article']; + $retrieval = KbService::retrieve( + 'post_analysis', + self::buildPostQuery($postData), + [ + 'user_id' => $userId, + 'ref_id' => $postId, + 'domains' => $domains, + 'preferred_domain' => 'post', + 'tags' => is_array($tags) ? $tags : [], + 'exclude_source' => ['domain' => 'post', 'source_id' => $postId], + ], + [ + 'title' => (string) ($postData['content'] ?? ''), + ], + self::intConfig('kb_final_topk', 8) + ); + + $systemPrompt = AiConfig::getVal('post_analysis_prompt', self::defaultPostPrompt()); + $userMessage = "当前帖子:\n" . json_encode([ + 'id' => $postId, + 'content' => self::clipText((string) ($postData['content'] ?? ''), 1800), + 'tags' => $tags, + 'post_type' => $postData['post_type'] ?? 0, + 'is_paid' => $postData['is_paid'] ?? 0, + 'lottery_key' => (string) (($postData['ext']['lottery_key'] ?? '') ?: ''), + 'lottery_analysis_content' => self::clipText((string) ($postData['lottery_analysis_content'] ?? ''), 1000), + ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n\n知识库证据:\n" + . json_encode(self::compactHits($retrieval['hits'] ?? []), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n\n请输出JSON,不允许编造知识库里不存在的历史事实。"; + + return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($retrieval, $postData) { + return self::normalizePostResult($parsed, $retrieval, $postData); + }, self::gptPostOptions()); + } + + public static function analyzeLotteryOverview(array $lotteryData, int $userId = 0): array + { + $code = (string) ($lotteryData['lottery_type'] ?? ''); + $latestIssue = (string) ($lotteryData['recent_draws'][0]['issue'] ?? ''); + $retrieval = KbService::retrieve( + 'lottery_analysis', + self::buildLotteryQuery($lotteryData), + [ + 'user_id' => $userId, + 'ref_id' => 0, + 'domains' => ['lottery', 'post'], + 'preferred_domain' => 'lottery', + 'code' => $code, + 'issue' => $latestIssue, + ], + [ + 'title' => (string) ($lotteryData['lottery_name'] ?? $code), + ], + self::intConfig('kb_final_topk', 8) + ); + + $systemPrompt = AiConfig::getVal('lottery_analysis_prompt', self::defaultLotteryOverviewPrompt()); + $userMessage = "当前彩种结构化数据:\n" . json_encode($lotteryData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n\n知识库证据:\n" + . json_encode(self::compactHits($retrieval['hits'] ?? []), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n\n请结合历史知识库证据和当前结构化数据输出JSON。"; + + return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($retrieval) { + return self::normalizeLotteryOverview($parsed, $retrieval); + }, ['max_tokens' => 1800]); + } + + public static function analyzeLotteryModule(array $lotteryData, string $module, int $userId = 0): array + { + $code = (string) ($lotteryData['lottery_type'] ?? ''); + $latestIssue = (string) ($lotteryData['recent_draws'][0]['issue'] ?? ''); + $retrieval = KbService::retrieve( + 'lottery_' . $module, + self::buildLotteryQuery($lotteryData), + [ + 'user_id' => $userId, + 'ref_id' => 0, + 'domains' => ['lottery', 'post'], + 'preferred_domain' => 'lottery', + 'code' => $code, + 'issue' => $latestIssue, + ], + [ + 'title' => (string) ($lotteryData['lottery_name'] ?? $code), + ], + self::intConfig('kb_final_topk', 8) + ); + + $systemPrompt = self::modulePrompt($module); + $userMessage = "当前彩种结构化数据:\n" . json_encode($lotteryData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n\n知识库证据:\n" + . json_encode(self::compactHits($retrieval['hits'] ?? []), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n\n请结合结构化数据和知识库证据输出JSON。"; + + return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($module, $retrieval) { + return self::normalizeLotteryModule($module, $parsed, $retrieval); + }, ['max_tokens' => 1200]); + } + + private static function runJsonPrompt(string $systemPrompt, string $userMessage, callable $normalizer, array $options = []): array + { + $client = new DeepSeekClient(); + $result = $client->chatJson($systemPrompt, $userMessage, $options); + if (!$result['success']) { + return $result; + } + + $parsed = is_array($result['parsed'] ?? null) ? $result['parsed'] : []; + $normalized = $normalizer($parsed); + $result['parsed'] = $normalized; + $result['content'] = json_encode($normalized, JSON_UNESCAPED_UNICODE); + return $result; + } + + private static function gptPostOptions(): array + { + return [ + 'model' => self::GPT_PROXY_MODEL, + 'base_url' => self::GPT_PROXY_BASE_URL, + 'api_key' => self::GPT_PROXY_API_KEY, + 'wire_api' => 'responses', + 'provider_label' => 'OpenAI(GPT中转)', + 'max_tokens' => 1200, + 'temperature' => 0.35, + ]; + } + + private static function compactHits(array $hits): array + { + return array_map(static function (array $hit) { + return [ + 'domain' => $hit['domain'] ?? '', + 'subtype' => $hit['subtype'] ?? '', + 'source_id' => $hit['source_id'] ?? 0, + 'title' => $hit['title'] ?? '', + 'summary' => $hit['summary'] ?? '', + 'content' => self::clipText((string) ($hit['chunk_text'] ?? ''), 260), + 'score' => $hit['score'] ?? 0, + 'metadata' => $hit['metadata'] ?? [], + ]; + }, $hits); + } + + private static function normalizeArticleResult(array $parsed, array $retrieval): array + { + $bettingAnalysis = $parsed['betting_analysis'] ?? [ + 'prediction' => '谨慎观望', + 'confidence' => 55, + 'reasoning' => '当前证据不足,建议谨慎观望。', + 'risk_level' => 'medium', + 'key_factors' => [], + 'value_bet' => '无明显价值', + ]; + $bettingAnalysis['risk_level'] = self::normalizeRiskLevel((string) ($bettingAnalysis['risk_level'] ?? 'medium')); + + return [ + 'overall_score' => (int) ($parsed['overall_score'] ?? 68), + 'summary' => (string) ($parsed['summary'] ?? '暂无明确结论,请结合原文谨慎判断。'), + 'leagues_and_teams' => $parsed['leagues_and_teams'] ?? ['leagues' => [], 'teams' => [], 'match_context' => ''], + 'upcoming_matches' => $parsed['upcoming_matches'] ?? [], + 'head_to_head' => $parsed['head_to_head'] ?? ['available' => false, 'records' => [], 'summary' => ''], + 'betting_analysis' => $bettingAnalysis, + 'risk_warnings' => $parsed['risk_warnings'] ?? ['以上内容仅供参考,请理性判断。'], + 'key_points' => $parsed['key_points'] ?? [], + 'opposite_views' => $parsed['opposite_views'] ?? [], + 'sentiment' => $parsed['sentiment'] ?? ['label' => 'neutral', 'confidence' => 55], + 'tags' => $parsed['tags'] ?? [], + 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), + 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), + 'from_kb' => !empty($retrieval['hits']), + 'retrieval_meta' => self::retrievalMeta($retrieval), + ]; + } + + private static function normalizePostResult(array $parsed, array $retrieval, array $postData): array + { + $result = [ + 'overall_score' => (int) ($parsed['overall_score'] ?? 66), + 'summary' => (string) ($parsed['summary'] ?? '当前帖子可参考信息有限,请谨慎判断。'), + 'credibility_signals' => $parsed['credibility_signals'] ?? [], + 'risk_warning' => $parsed['risk_warning'] ?? ['历史样本有限,不宜过度放大单条帖子观点。'], + 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), + 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), + 'from_kb' => !empty($retrieval['hits']), + 'retrieval_meta' => self::retrievalMeta($retrieval), + ]; + + if (self::isLiuhePost($postData, $retrieval['hits'] ?? [])) { + $result['summary'] = self::normalizeLiuheImageText( + $result['summary'], + '本次分析仅依据当前图片和帖子文案进行自由解读,可重点关注图片中出现的诗句、生肖、波色、尾数、胆码和旺弱号码等线索;结果仅供内容解读。' + ); + $result['credibility_signals'] = self::normalizeLiuheImageList( + $result['credibility_signals'], + ['当前分析基于图片识别出的可见文字、号码和图表线索。'] + ); + $result['risk_warning'] = self::normalizeLiuheImageList( + $result['risk_warning'], + ['彩票结果具有随机性,图片解读仅供参考,不构成投注建议。'] + ); + $result['next_prediction'] = self::normalizeLiuhePrediction($parsed['next_prediction'] ?? []); + $result['next_prediction']['reasons'] = self::normalizeLiuheImageList( + $result['next_prediction']['reasons'], + ['推荐仅来自当前图片中的诗句、胆码、波色、尾数或生肖等线索。'] + ); + $result['image_interpretation'] = self::normalizeStringList($parsed['image_interpretation'] ?? []); + $result['poem_analysis'] = (string) ($parsed['poem_analysis'] ?? ''); + $result['analysis_source'] = (string) ($parsed['analysis_source'] ?? 'image_free_v1'); + } + + return $result; + } + + private static function emptyRetrieval(): array + { + return [ + 'hits' => [], + 'hit_count' => 0, + 'domains' => [], + 'top_scores' => [], + ]; + } + + private static function normalizeLotteryOverview(array $parsed, array $retrieval): array + { + return [ + 'overall_score' => (int) ($parsed['overall_score'] ?? 62), + 'summary' => (string) ($parsed['summary'] ?? '当前仅能给出基于历史样本的统计参考。'), + 'hot_numbers' => $parsed['hot_numbers'] ?? [], + 'cold_numbers' => $parsed['cold_numbers'] ?? [], + 'trend_analysis' => $parsed['trend_analysis'] ?? '', + 'recommended_combinations' => $parsed['recommended_combinations'] ?? [], + 'statistics' => $parsed['statistics'] ?? [], + 'trend_direction' => $parsed['trend_direction'] ?? '震荡', + 'risk_level' => $parsed['risk_level'] ?? '中', + 'key_evidence' => $parsed['key_evidence'] ?? [], + 'similar_issues' => $parsed['similar_issues'] ?? self::similarHistory($retrieval['hits'] ?? []), + 'disclaimer' => $parsed['disclaimer'] ?? '本分析仅基于历史数据与知识库证据,不保证准确性,请理性参考。', + 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), + 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), + 'from_kb' => !empty($retrieval['hits']), + 'retrieval_meta' => self::retrievalMeta($retrieval), + ]; + } + + private static function normalizeLotteryModule(string $module, array $parsed, array $retrieval): array + { + $base = [ + 'overall_score' => (int) ($parsed['overall_score'] ?? 60), + 'risk_warning' => $parsed['risk_warning'] ?? ['本结果只反映历史统计规律,不保证未来表现。'], + 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), + 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), + 'from_kb' => !empty($retrieval['hits']), + 'retrieval_meta' => self::retrievalMeta($retrieval), + ]; + + return match ($module) { + 'hot_cold' => array_merge($base, [ + 'hot_numbers' => $parsed['hot_numbers'] ?? [], + 'cold_numbers' => $parsed['cold_numbers'] ?? [], + 'hot_analysis' => $parsed['hot_analysis'] ?? '', + 'cold_analysis' => $parsed['cold_analysis'] ?? '', + ]), + 'trend' => array_merge($base, [ + 'trend_analysis' => $parsed['trend_analysis'] ?? '', + 'recommended_combinations' => $parsed['recommended_combinations'] ?? [], + 'disclaimer' => $parsed['disclaimer'] ?? '趋势分析仅供参考。', + 'trend_direction' => $parsed['trend_direction'] ?? '震荡', + ]), + 'stats' => array_merge($base, [ + 'odd_even_ratio' => $parsed['odd_even_ratio'] ?? '', + 'big_small_ratio' => $parsed['big_small_ratio'] ?? '', + 'sum_range' => $parsed['sum_range'] ?? '', + 'consecutive_analysis' => $parsed['consecutive_analysis'] ?? '', + 'span_analysis' => $parsed['span_analysis'] ?? '', + 'frequency_table' => $parsed['frequency_table'] ?? [], + 'summary' => $parsed['summary'] ?? '', + ]), + 'color_zodiac' => array_merge($base, [ + 'color_analysis' => $parsed['color_analysis'] ?? [], + 'zodiac_analysis' => $parsed['zodiac_analysis'] ?? [], + 'recommended' => $parsed['recommended'] ?? [], + ]), + default => array_merge($base, $parsed), + }; + } + + private static function retrievalMeta(array $retrieval): array + { + return [ + 'hit_count' => (int) ($retrieval['hit_count'] ?? 0), + 'domains' => $retrieval['domains'] ?? [], + 'top_scores' => $retrieval['top_scores'] ?? [], + ]; + } + + private static function similarHistory(array $hits): array + { + $rows = []; + foreach ($hits as $hit) { + $metadata = $hit['metadata'] ?? []; + $rows[] = [ + 'domain' => $hit['domain'] ?? '', + 'source_id' => $hit['source_id'] ?? 0, + 'title' => $hit['title'] ?? '', + 'issue' => $metadata['issue'] ?? '', + 'code' => $metadata['code'] ?? '', + 'score' => $hit['score'] ?? 0, + ]; + } + return array_slice($rows, 0, 5); + } + + private static function buildArticleQuery(array $articleData): string + { + return trim(implode(' ', array_filter([ + $articleData['title'] ?? '', + $articleData['abstract'] ?? '', + $articleData['category'] ?? '', + $articleData['desc'] ?? '', + ]))); + } + + private static function buildPostQuery(array $postData): string + { + $tags = is_array($postData['tags'] ?? null) ? implode(' ', $postData['tags']) : ''; + $lotteryKey = (string) (($postData['ext']['lottery_key'] ?? '') ?: ''); + return trim(implode(' ', array_filter([ + self::clipText((string) ($postData['content'] ?? ''), 300), + $tags, + $lotteryKey, + ]))); + } + + private static function buildLotteryQuery(array $lotteryData): string + { + $recentIssues = array_column($lotteryData['recent_draws'] ?? [], 'issue'); + return trim(implode(' ', array_filter([ + $lotteryData['lottery_type'] ?? '', + $lotteryData['lottery_name'] ?? '', + implode(' ', array_slice($recentIssues, 0, 5)), + ]))); + } + + private static function buildMatchQuery(array $matchData): string + { + $matchInfo = $matchData['match_info'] ?? []; + $headToHead = $matchData['head_to_head'] ?? []; + $keywords = [ + $matchInfo['home_team'] ?? '', + $matchInfo['away_team'] ?? '', + $matchInfo['league'] ?? '', + ]; + + foreach (array_slice($headToHead, 0, 3) as $row) { + $keywords[] = $row['home_team'] ?? ''; + $keywords[] = $row['away_team'] ?? ''; + $keywords[] = $row['league_name'] ?? ($row['league'] ?? ''); + } + + return trim(implode(' ', array_filter($keywords))); + } + + private static function retrieveMatchEvidence(int $matchId, array $matchData, int $userId, string $scene): array + { + $matchInfo = $matchData['match_info'] ?? []; + return KbService::retrieve( + $scene, + self::buildMatchQuery($matchData), + [ + 'user_id' => $userId, + 'ref_id' => $matchId, + 'domains' => ['article', 'post'], + 'preferred_domain' => 'article', + ], + [ + 'title' => trim(($matchInfo['home_team'] ?? '') . ' vs ' . ($matchInfo['away_team'] ?? '')), + 'league' => (string) ($matchInfo['league'] ?? ''), + 'teams' => array_values(array_filter([ + $matchInfo['home_team'] ?? '', + $matchInfo['away_team'] ?? '', + ])), + ], + self::intConfig('kb_final_topk', 8) + ); + } + + private static function buildMatchUserMessage( + array $matchData, + array $retrieval, + bool $full = false, + string $section = '' + ): string { + $label = $full ? '完整赛事分析' : ('赛事分段分析-' . $section); + return $label . ":\n当前赛事结构化数据:\n" + . json_encode([ + 'match_info' => $matchData['match_info'] ?? [], + 'head_to_head' => self::compactMatchRows($matchData['head_to_head'] ?? [], 5), + 'home_recent' => self::compactMatchRows($matchData['home_recent'] ?? [], 5), + 'away_recent' => self::compactMatchRows($matchData['away_recent'] ?? [], 5), + ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n\n知识库证据(资讯/帖子):\n" + . json_encode(self::compactHits($retrieval['hits'] ?? []), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + . "\n\n请优先依据当前赛事结构化数据与知识库证据输出,不允许编造未命中的历史事实。"; + } + + private static function compactMatchRows(array $rows, int $limit = 5): array + { + $compact = []; + foreach (array_slice($rows, 0, $limit) as $row) { + $compact[] = [ + 'match_time' => $row['match_time'] ?? '', + 'league' => $row['league_name'] ?? ($row['league'] ?? ''), + 'home_team' => $row['home_team'] ?? ($row['team_name'] ?? ''), + 'away_team' => $row['away_team'] ?? ($row['opponent'] ?? ''), + 'score' => [ + 'home' => $row['home_score'] ?? ($row['team_score'] ?? null), + 'away' => $row['away_score'] ?? ($row['opponent_score'] ?? null), + ], + 'result' => $row['result'] ?? '', + ]; + } + return $compact; + } + + private static function clipText(string $text, int $length): string + { + $text = trim(preg_replace('/\s+/u', ' ', $text) ?: ''); + return mb_strlen($text) > $length ? mb_substr($text, 0, $length) . '...' : $text; + } + + private static function intConfig(string $name, int $default): int + { + return (int) AiConfig::getVal($name, (string) $default); + } + + private static function defaultArticlePrompt(): string + { + return '你是一位体育资讯AI研报助手。请严格根据当前文章和知识库证据生成分析,不允许虚构未命中的历史事实。' + . '必须输出JSON,字段包含:' + . '{"overall_score":72,"summary":"...","leagues_and_teams":{"leagues":[],"teams":[],"match_context":""},"upcoming_matches":[],"head_to_head":{"available":false,"records":[],"summary":""},"betting_analysis":{"prediction":"谨慎观望","confidence":55,"reasoning":"...","risk_level":"medium","key_factors":[],"value_bet":"无明显价值"},"risk_warnings":["..."],"key_points":["..."],"opposite_views":["..."],"sentiment":{"label":"neutral","confidence":60},"tags":["..."],"key_evidence":["..."]}'; + } + + private static function defaultMatchPrompt(): string + { + return '你是一位体育赛事AI分析师。请结合当前赛事结构化数据(交手记录、双方近期战绩、赔率)与知识库中召回的相关资讯/帖子,生成完整比赛分析。' + . '若知识库证据不足,要明确说明证据不足。' + . '必须输出JSON:{"prediction":{"result":"主胜/平局/客胜","confidence":75,"score_predict":"2-1","reasoning":"..."},"analysis":{"home_strength":78,"away_strength":72,"key_factors":["..."],"risk_factors":["..."]},"odds_opinion":{"value_bet":"主胜/平局/客胜/无","explanation":"..."},"summary":"...","evidence_list":[],"risk_warning":["..."]}'; + } + + private static function defaultPostPrompt(): string + { + return '你是一位社区内容分析助手。请根据当前帖子和知识库证据,判断帖子的核心观点、可信线索与风险。' + . '如果知识库证据不足,必须明确说明证据不足。' + . '如果帖子属于六合彩(例如旧澳六合/新澳六合)资料或开奖类帖子,请额外给出基于当前帖子内容与知识库历史样本的下一期预测,字段包含推荐号码、信心和依据。' + . '输出JSON:{"overall_score":66,"summary":"...","credibility_signals":["..."],"risk_warning":["..."],"next_prediction":{"recommended_numbers":["..."],"confidence":68,"reasons":["..."]}}'; + } + + private static function isLiuhePost(array $postData, array $hits): bool + { + $tags = is_array($postData['tags'] ?? null) ? $postData['tags'] : []; + if (in_array('旧澳六合', $tags, true) || in_array('新澳六合', $tags, true)) { + return true; + } + + $ext = is_array($postData['ext'] ?? null) ? $postData['ext'] : []; + $lotteryKey = (string) ($ext['lottery_key'] ?? ''); + if (in_array($lotteryKey, ['a6', 'xa6'], true)) { + return true; + } + + foreach ($hits as $hit) { + $title = (string) ($hit['title'] ?? ''); + $metadata = $hit['metadata'] ?? []; + $hitLotteryKey = (string) ($metadata['lottery_key'] ?? ''); + if (str_contains($title, '旧澳六合') || str_contains($title, '新澳六合') || in_array($hitLotteryKey, ['a6', 'xa6'], true)) { + return true; + } + } + return false; + } + + private static function normalizeLiuhePrediction(array $prediction): array + { + $numbers = self::normalizeStringList($prediction['recommended_numbers'] ?? []); + $reasons = self::normalizeStringList($prediction['reasons'] ?? []); + + return [ + 'recommended_numbers' => $numbers, + 'confidence' => max(0, min(100, (int) ($prediction['confidence'] ?? 60))), + 'reasons' => $reasons, + ]; + } + + private static function normalizeStringList($items): array + { + if (is_string($items)) { + $items = preg_split('/[\r\n;;]+/u', $items) ?: []; + } + if (!is_array($items)) { + return []; + } + + return array_values(array_filter(array_map(static function ($item) { + $text = trim((string) $item); + return $text === '' ? null : $text; + }, $items))); + } + + private static function normalizeLiuheImageText(string $text, string $fallback): string + { + $text = trim($text); + if ($text === '' || str_contains($text, '知识库')) { + return $fallback; + } + return $text; + } + + private static function normalizeLiuheImageList($items, array $fallback): array + { + $rows = array_values(array_filter(self::normalizeStringList($items), static function (string $item) { + return !str_contains($item, '知识库'); + })); + return $rows ?: $fallback; + } + + private static function lotteryPostImagePrompt(): string + { + return '你是一位六合彩图片资料解读助手。请只依据当前帖子文案与图片识别内容进行分析,不需要知识库证据。' + . '如果识别内容里包含诗句、字谜、藏宝图、玄机图、生肖、波色、尾数、胆码、旺弱号码、推荐组合,请逐项自由解读它们可能指向的生肖或号码。' + . '不要因为没有历史知识库证据而给出“证据不足”的主结论;可以说明彩票随机性和不保证命中,但主体要围绕图片内容展开。' + . '不要承诺必中,不要编造图片中不存在的文字或号码。' + . '输出JSON:{"overall_score":66,"summary":"基于图片内容的综合解读","image_interpretation":["逐项解读"],"poem_analysis":"诗句/字谜解读,没有则为空","credibility_signals":["来自图片的可信线索"],"risk_warning":["理性提示"],"next_prediction":{"recommended_numbers":["..."],"confidence":60,"reasons":["..."]},"analysis_source":"image_free_v1"}'; + } + + private static function defaultLotteryOverviewPrompt(): string + { + return '你是一位彩票数据分析助手。请结合当前结构化开奖数据与知识库中的历史开奖、历史AI分析、相关帖子证据,输出兼容旧接口的JSON。' + . '必须包含字段:{"overall_score":62,"summary":"...","hot_numbers":[],"cold_numbers":[],"trend_analysis":"...","recommended_combinations":[],"statistics":{},"trend_direction":"震荡","risk_level":"中","key_evidence":["..."],"similar_issues":[],"disclaimer":"..."}'; + } + + private static function matchSectionPrompt(string $module): string + { + return match ($module) { + 'prediction' => '你是一位体育赛事AI分析师。请结合当前赛事结构化数据和知识库资讯/帖子证据,预测比赛结果。输出JSON:{"prediction":{"result":"主胜/平局/客胜","confidence":75,"score_predict":"2-1","reasoning":"..."},"analysis":{"home_strength":78,"away_strength":72},"evidence_list":[],"risk_warning":["..."]}', + 'factors' => '你是一位体育赛事AI分析师。请结合当前赛事结构化数据和知识库资讯/帖子证据,分析关键因素与风险。输出JSON:{"key_factors":["..."],"risk_factors":["..."],"evidence_list":[],"risk_warning":["..."]}', + 'odds' => '你是一位体育赛事AI分析师。请结合当前赛事结构化数据和知识库资讯/帖子证据,分析赔率与投注方向。输出JSON:{"odds_opinion":{"value_bet":"主胜/平局/客胜/无","explanation":"..."},"evidence_list":[],"risk_warning":["..."]}', + 'summary' => '你是一位体育赛事AI分析师。请结合当前赛事结构化数据和知识库资讯/帖子证据,给出简洁结论。输出JSON:{"summary":"...","evidence_list":[],"risk_warning":["..."]}', + default => '请输出JSON。', + }; + } + + private static function modulePrompt(string $module): string + { + return match ($module) { + 'hot_cold' => '你是一位彩票热冷号分析助手。请结合当前结构化开奖数据和知识库证据输出JSON:{"overall_score":60,"hot_numbers":[],"cold_numbers":[],"hot_analysis":"...","cold_analysis":"...","risk_warning":["..."]}', + 'trend' => '你是一位彩票趋势分析助手。请结合当前结构化开奖数据和知识库证据输出JSON:{"overall_score":60,"trend_analysis":"...","recommended_combinations":[],"trend_direction":"震荡","disclaimer":"...","risk_warning":["..."]}', + 'stats' => '你是一位彩票统计分析助手。请结合当前结构化开奖数据和知识库证据输出JSON:{"overall_score":60,"odd_even_ratio":"","big_small_ratio":"","sum_range":"","consecutive_analysis":"...","span_analysis":"...","frequency_table":[],"summary":"...","risk_warning":["..."]}', + 'color_zodiac' => '你是一位六合彩颜色生肖分析助手。请结合当前结构化开奖数据和知识库证据输出JSON:{"overall_score":60,"color_analysis":{},"zodiac_analysis":{},"recommended":{},"risk_warning":["..."]}', + default => '请输出JSON。', + }; + } + + private static function normalizeRiskLevel(string $value): string + { + $value = strtolower(trim($value)); + return match ($value) { + '低', 'low' => 'low', + '高', 'high' => 'high', + default => 'medium', + }; + } + + private static function normalizeMatchResult(array $parsed, array $retrieval): array + { + return [ + 'prediction' => $parsed['prediction'] ?? [ + 'result' => '谨慎观望', + 'confidence' => 55, + 'score_predict' => '-', + 'reasoning' => '当前证据不足,建议谨慎观望。', + ], + 'analysis' => array_merge([ + 'home_strength' => 50, + 'away_strength' => 50, + 'key_factors' => [], + 'risk_factors' => [], + ], is_array($parsed['analysis'] ?? null) ? $parsed['analysis'] : []), + 'odds_opinion' => $parsed['odds_opinion'] ?? [ + 'value_bet' => '无', + 'explanation' => '赔率证据不足,建议谨慎观望。', + ], + 'summary' => (string) ($parsed['summary'] ?? '当前仅能给出基于结构化数据和有限知识库证据的保守结论。'), + 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), + 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), + 'from_kb' => !empty($retrieval['hits']), + 'retrieval_meta' => self::retrievalMeta($retrieval), + ]; + } + + private static function normalizeMatchSectionResult(string $section, array $parsed, array $retrieval): array + { + $base = [ + 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), + 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), + 'from_kb' => !empty($retrieval['hits']), + 'retrieval_meta' => self::retrievalMeta($retrieval), + 'risk_warning' => $parsed['risk_warning'] ?? ['以上分析仅供参考,请结合比赛临场信息谨慎判断。'], + ]; + + return match ($section) { + 'prediction' => array_merge($base, [ + 'prediction' => $parsed['prediction'] ?? [ + 'result' => '谨慎观望', + 'confidence' => 55, + 'score_predict' => '-', + 'reasoning' => '当前证据不足,建议谨慎观望。', + ], + 'analysis' => array_merge([ + 'home_strength' => 50, + 'away_strength' => 50, + ], is_array($parsed['analysis'] ?? null) ? $parsed['analysis'] : []), + ]), + 'factors' => array_merge($base, [ + 'key_factors' => $parsed['key_factors'] ?? [], + 'risk_factors' => $parsed['risk_factors'] ?? [], + ]), + 'odds' => array_merge($base, [ + 'odds_opinion' => $parsed['odds_opinion'] ?? [ + 'value_bet' => '无', + 'explanation' => '赔率证据不足,建议谨慎观望。', + ], + ]), + 'summary' => array_merge($base, [ + 'summary' => (string) ($parsed['summary'] ?? '当前仅能给出基于结构化数据和有限知识库证据的保守结论。'), + ]), + default => array_merge($base, $parsed), + }; + } +} diff --git a/server/app/common/service/ai/KbService.php b/server/app/common/service/ai/KbService.php new file mode 100644 index 0000000..e44583f --- /dev/null +++ b/server/app/common/service/ai/KbService.php @@ -0,0 +1,813 @@ + false, 'error' => '源内容不存在或不可用']; + } + + $contentHash = md5(json_encode([ + $payload['title'], + $payload['summary'], + $payload['canonical_text'], + $payload['keywords'], + $payload['metadata_json'], + ], JSON_UNESCAPED_UNICODE)); + + $document = AiKbDocument::where([ + 'domain' => $domain, + 'subtype' => $subtype, + 'source_id' => $sourceId, + ])->findOrEmpty(); + + if ( + !$document->isEmpty() + && (string) $document->content_hash === $contentHash + && (int) $document->is_active === 1 + ) { + $document->save([ + 'title' => $payload['title'], + 'summary' => $payload['summary'], + 'keywords' => $payload['keywords'], + 'metadata_json' => $payload['metadata_json'], + 'source_created_at' => $payload['source_created_at'], + 'source_updated_at' => $payload['source_updated_at'], + 'update_time' => time(), + ]); + + return [ + 'success' => true, + 'document_id' => (int) $document->id, + 'chunk_count' => (int) AiKbChunk::where('document_id', $document->id)->count(), + 'skipped' => true, + ]; + } + + $chunks = self::splitText( + $payload['canonical_text'], + self::intConfig('kb_chunk_size', 600), + self::intConfig('kb_chunk_overlap', 80) + ); + if (empty($chunks)) { + $chunks = [$payload['summary'] ?: $payload['title'] ?: 'empty']; + } + + $embeddingResult = (new EmbeddingClient())->embed($chunks); + if (!$embeddingResult['success']) { + return [ + 'success' => false, + 'error' => $embeddingResult['error'] ?? 'embedding失败', + ]; + } + + Db::startTrans(); + $transactionStarted = true; + if ($document->isEmpty()) { + $document = AiKbDocument::create([ + 'domain' => $domain, + 'subtype' => $subtype, + 'source_id' => $sourceId, + 'title' => $payload['title'], + 'summary' => $payload['summary'], + 'canonical_text' => $payload['canonical_text'], + 'keywords' => $payload['keywords'], + 'metadata_json' => $payload['metadata_json'], + 'source_created_at' => $payload['source_created_at'], + 'source_updated_at' => $payload['source_updated_at'], + 'content_hash' => $contentHash, + 'is_active' => 1, + 'create_time' => time(), + 'update_time' => time(), + ]); + } else { + $document->save([ + 'title' => $payload['title'], + 'summary' => $payload['summary'], + 'canonical_text' => $payload['canonical_text'], + 'keywords' => $payload['keywords'], + 'metadata_json' => $payload['metadata_json'], + 'source_created_at' => $payload['source_created_at'], + 'source_updated_at' => $payload['source_updated_at'], + 'content_hash' => $contentHash, + 'is_active' => 1, + 'update_time' => time(), + ]); + } + + AiKbChunk::where('document_id', $document->id)->delete(); + $dimension = (int) ($embeddingResult['dimension'] ?? 0); + foreach ($chunks as $index => $chunkText) { + $embedding = $embeddingResult['embeddings'][$index] ?? []; + AiKbChunk::create([ + 'document_id' => $document->id, + 'collection_name' => self::COLLECTION_GLOBAL, + 'chunk_index' => $index, + 'chunk_text' => $chunkText, + 'embedding_json' => $embedding, + 'embedding_model' => $embeddingResult['model'] ?? '', + 'embedding_dim' => $dimension, + 'vector_norm' => self::vectorNorm($embedding), + 'keyword_text' => self::buildKeywordText($payload), + 'create_time' => time(), + 'update_time' => time(), + ]); + } + Db::commit(); + + return [ + 'success' => true, + 'document_id' => (int) $document->id, + 'chunk_count' => count($chunks), + ]; + } catch (\Throwable $e) { + if ($transactionStarted) { + Db::rollback(); + } + return ['success' => false, 'error' => $e->getMessage()]; + } + } + + public static function deleteDocument(string $domain, string $subtype, int $sourceId): void + { + AiKbDocument::where([ + 'domain' => $domain, + 'subtype' => $subtype, + 'source_id' => $sourceId, + ])->update([ + 'is_active' => 0, + 'update_time' => time(), + ]); + } + + public static function retrieve( + string $scene, + string $queryText, + array $filters = [], + array $structuredContext = [], + int $topK = 8 + ): array { + try { + $topK = $topK > 0 ? $topK : self::intConfig('kb_final_topk', 8); + $fulltextTopN = self::intConfig('kb_fulltext_topn', 200); + $vectorTopN = self::intConfig('kb_vector_topn', 30); + + $candidates = self::loadCandidates($queryText, $filters, $fulltextTopN); + if (empty($candidates)) { + self::recordQueryLog($filters, $scene, $queryText, [], 0, 0, AiKbQueryLog::STATUS_SUCCESS); + return [ + 'success' => true, + 'hits' => [], + 'hit_count' => 0, + 'domains' => [], + 'top_scores' => [], + ]; + } + + $queryEmbedding = []; + $embeddingResult = (new EmbeddingClient())->embed($queryText); + if (!empty($embeddingResult['success'])) { + $queryEmbedding = $embeddingResult['embeddings'][0] ?? []; + } + + foreach ($candidates as &$candidate) { + $candidate['metadata_json'] = is_array($candidate['metadata_json']) + ? $candidate['metadata_json'] + : (json_decode((string) $candidate['metadata_json'], true) ?: []); + $candidate['embedding_json'] = is_array($candidate['embedding_json']) + ? $candidate['embedding_json'] + : (json_decode((string) $candidate['embedding_json'], true) ?: []); + + $candidate['vector_score'] = !empty($queryEmbedding) + ? self::cosineSimilarity($queryEmbedding, $candidate['embedding_json']) + : max(0.0, min(1.0, (float) ($candidate['text_score'] ?? 0))); + $candidate['freshness_score'] = self::freshnessScore((int) ($candidate['source_updated_at'] ?? 0)); + $candidate['domain_score'] = self::domainScore($candidate, $filters); + $candidate['match_score'] = self::matchScore($candidate, $filters, $structuredContext); + $candidate['score'] = round( + $candidate['vector_score'] * 0.65 + + $candidate['freshness_score'] * 0.15 + + $candidate['domain_score'] * 0.10 + + $candidate['match_score'] * 0.10, + 6 + ); + } + unset($candidate); + + usort($candidates, static fn(array $a, array $b) => $b['score'] <=> $a['score']); + $candidates = array_slice($candidates, 0, $vectorTopN); + + $hits = array_map(static function (array $candidate) { + return [ + 'document_id' => (int) $candidate['document_id'], + 'domain' => $candidate['domain'], + 'subtype' => $candidate['subtype'], + 'source_id' => (int) $candidate['source_id'], + 'title' => $candidate['title'], + 'summary' => $candidate['summary'], + 'chunk_text' => $candidate['chunk_text'], + 'score' => (float) $candidate['score'], + 'vector_score' => (float) $candidate['vector_score'], + 'freshness_score' => (float) $candidate['freshness_score'], + 'match_score' => (float) $candidate['match_score'], + 'source_updated_at' => (int) $candidate['source_updated_at'], + 'metadata' => $candidate['metadata_json'], + ]; + }, array_slice($candidates, 0, $topK)); + + $recordStatus = AiKbQueryLog::STATUS_SUCCESS; + self::recordQueryLog( + $filters, + $scene, + $queryText, + $hits, + (int) ($embeddingResult['tokens'] ?? 0), + (int) ($embeddingResult['cost_ms'] ?? 0), + $recordStatus + ); + + return [ + 'success' => true, + 'hits' => $hits, + 'hit_count' => count($hits), + 'domains' => array_values(array_unique(array_column($hits, 'domain'))), + 'top_scores' => array_values(array_map(static fn(array $hit) => $hit['score'], $hits)), + ]; + } catch (\Throwable $e) { + self::recordQueryLog($filters, $scene, $queryText, [], 0, 0, AiKbQueryLog::STATUS_FAILED); + return [ + 'success' => false, + 'error' => $e->getMessage(), + 'hits' => [], + 'hit_count' => 0, + 'domains' => [], + 'top_scores' => [], + ]; + } + } + + private static function loadCandidates(string $queryText, array $filters, int $limit): array + { + $queryText = trim($queryText); + $query = Db::name('ai_kb_chunk')->alias('c') + ->join('ai_kb_document d', 'd.id = c.document_id') + ->where('d.is_active', 1) + ->field([ + 'c.document_id', + 'c.chunk_text', + 'c.embedding_json', + 'c.embedding_dim', + 'c.vector_norm', + 'd.domain', + 'd.subtype', + 'd.source_id', + 'd.title', + 'd.summary', + 'd.metadata_json', + 'd.source_created_at', + 'd.source_updated_at', + ]); + + if (!empty($filters['domains']) && is_array($filters['domains'])) { + $query->whereIn('d.domain', $filters['domains']); + } + if (!empty($filters['subtypes']) && is_array($filters['subtypes'])) { + $query->whereIn('d.subtype', $filters['subtypes']); + } + + $candidates = []; + if ($queryText !== '') { + try { + $ftQuery = clone $query; + $ftQuery->fieldRaw( + "MATCH(c.chunk_text, c.keyword_text) AGAINST ('" . addslashes($queryText) . "' IN NATURAL LANGUAGE MODE) AS text_score" + ); + $ftQuery->whereRaw( + "MATCH(c.chunk_text, c.keyword_text) AGAINST ('" . addslashes($queryText) . "' IN NATURAL LANGUAGE MODE)" + ); + $candidates = $ftQuery->order('text_score', 'desc')->limit($limit)->select()->toArray(); + } catch (\Throwable $e) { + $candidates = []; + } + } + + if (empty($candidates)) { + $keywordParts = self::splitQueryTerms($queryText); + $fallback = clone $query; + foreach (array_slice($keywordParts, 0, 3) as $term) { + $fallback->where(function ($subQuery) use ($term) { + $subQuery->whereLike('c.chunk_text', '%' . $term . '%') + ->whereOrLike('d.title', '%' . $term . '%') + ->whereOrLike('d.keywords', '%' . $term . '%'); + }); + } + $candidates = $fallback->order('d.source_updated_at', 'desc')->limit($limit)->select()->toArray(); + foreach ($candidates as &$candidate) { + $candidate['text_score'] = 0.2; + } + unset($candidate); + } + + if (!empty($filters['exclude_source']['domain']) && !empty($filters['exclude_source']['source_id'])) { + $excludeDomain = $filters['exclude_source']['domain']; + $excludeSourceId = (int) $filters['exclude_source']['source_id']; + $candidates = array_values(array_filter($candidates, static function (array $candidate) use ($excludeDomain, $excludeSourceId) { + return !($candidate['domain'] === $excludeDomain && (int) $candidate['source_id'] === $excludeSourceId); + })); + } + + return $candidates; + } + + private static function buildDocumentPayload(string $domain, string $subtype, int $sourceId): array + { + return match ($domain . ':' . $subtype) { + 'article:article' => self::buildArticlePayload($sourceId), + 'post:post' => self::buildPostPayload($sourceId), + 'lottery:draw_result' => self::buildLotteryDrawResultPayload($sourceId), + 'lottery:ai_history' => self::buildLotteryAiHistoryPayload($sourceId), + 'match:event' => self::buildMatchEventPayload($sourceId), + default => [], + }; + } + + private static function buildArticlePayload(int $sourceId): array + { + $article = Article::where('id', $sourceId)->findOrEmpty(); + if ($article->isEmpty() || (int) $article->is_show !== 1 || !empty($article->delete_time)) { + return []; + } + + $data = $article->toArray(); + $ext = is_array($data['ext'] ?? null) ? $data['ext'] : (json_decode((string) ($data['ext'] ?? ''), true) ?: []); + $translated = trim((string) ($ext['translated_content'] ?? '')); + $content = self::normalizeHtmlText($translated !== '' ? $translated : (string) ($data['content'] ?? '')); + $summary = trim((string) ($data['abstract'] ?: $data['desc'] ?: '')); + $cateName = ArticleCate::where('id', $data['cid'] ?? 0)->value('name') ?: ''; + $keywords = array_filter([ + $data['title'] ?? '', + $cateName, + $data['author'] ?? '', + $data['category'] ?? '', + ]); + + return [ + 'title' => (string) ($data['title'] ?? ''), + 'summary' => self::normalizePlainText($summary), + 'canonical_text' => trim(implode("\n\n", array_filter([ + $data['title'] ?? '', + $summary, + $content, + ]))), + 'keywords' => implode(',', array_unique($keywords)), + 'metadata_json' => [ + 'cid' => (int) ($data['cid'] ?? 0), + 'cate_name' => $cateName, + 'author' => (string) ($data['author'] ?? ''), + 'category' => (string) ($data['category'] ?? ''), + 'article_id' => (int) ($data['article_id'] ?? 0), + 'published_at' => (string) ($data['published_at'] ?? ''), + 'is_video' => (int) ($data['is_video'] ?? 0), + ], + 'source_created_at' => (int) ($data['create_time'] ?? 0), + 'source_updated_at' => (int) ($data['update_time'] ?? $data['create_time'] ?? 0), + ]; + } + + private static function buildPostPayload(int $sourceId): array + { + $post = CommunityPost::where('id', $sourceId)->findOrEmpty(); + if ($post->isEmpty() || (int) $post->status !== 1 || !empty($post->delete_time)) { + return []; + } + + $data = $post->toArray(); + $ext = is_array($data['ext'] ?? null) ? $data['ext'] : (json_decode((string) ($data['ext'] ?? ''), true) ?: []); + $tagIds = Db::name('community_post_tag')->where('post_id', $sourceId)->column('tag_id'); + $tags = !empty($tagIds) + ? CommunityTag::whereIn('id', $tagIds)->column('name') + : []; + + $segments = array_filter([ + (string) ($data['content'] ?? ''), + (string) ($ext['translated_content'] ?? ''), + (string) ($ext['lottery_analysis_content'] ?? ''), + implode(' ', $tags), + ]); + $canonicalText = self::normalizePlainText(implode("\n\n", $segments)); + $summary = self::truncateText($canonicalText, 200); + + return [ + 'title' => self::truncateText($canonicalText, 40) ?: '社区帖子#' . $sourceId, + 'summary' => $summary, + 'canonical_text' => $canonicalText, + 'keywords' => implode(',', array_unique($tags)), + 'metadata_json' => [ + 'post_type' => (int) ($data['post_type'] ?? 0), + 'match_id' => (int) ($data['match_id'] ?? 0), + 'is_paid' => (int) ($data['is_paid'] ?? 0), + 'user_id' => (int) ($data['user_id'] ?? 0), + 'tags' => array_values($tags), + ], + 'source_created_at' => (int) ($data['create_time'] ?? 0), + 'source_updated_at' => (int) ($data['update_time'] ?? $data['create_time'] ?? 0), + ]; + } + + private static function buildLotteryDrawResultPayload(int $sourceId): array + { + $row = LotteryDrawResult::where('id', $sourceId)->findOrEmpty(); + if ($row->isEmpty()) { + return []; + } + + $data = $row->toArray(); + $game = LotteryGame::where('code', $data['code'])->field('name,template')->findOrEmpty()->toArray(); + $numbers = array_filter(explode(',', (string) ($data['draw_code'] ?? ''))); + $trend = is_array($data['trend'] ?? null) ? $data['trend'] : (json_decode((string) ($data['trend'] ?? ''), true) ?: []); + $trendText = self::normalizePlainText(json_encode($trend, JSON_UNESCAPED_UNICODE)); + $title = ($game['name'] ?? $data['code']) . ' 第' . $data['issue'] . '期开奖'; + $summary = sprintf( + '%s 于 %s 开奖,开奖号码为 %s。', + $title, + (string) ($data['draw_time'] ?? ''), + implode('、', $numbers) + ); + + return [ + 'title' => $title, + 'summary' => $summary, + 'canonical_text' => trim(implode("\n", array_filter([ + $summary, + !empty($data['next_issue']) ? '下一期期号:' . $data['next_issue'] : '', + !empty($data['next_time']) ? '下期开奖时间:' . $data['next_time'] : '', + $trendText !== 'null' ? '走势数据:' . $trendText : '', + ]))), + 'keywords' => implode(',', array_filter([ + $data['code'], + $game['name'] ?? '', + $game['template'] ?? '', + $data['issue'] ?? '', + ])), + 'metadata_json' => [ + 'code' => (string) ($data['code'] ?? ''), + 'issue' => (string) ($data['issue'] ?? ''), + 'template' => (string) ($game['template'] ?? ''), + 'game_name' => (string) ($game['name'] ?? ''), + 'draw_time' => (string) ($data['draw_time'] ?? ''), + ], + 'source_created_at' => (int) ($data['create_time'] ?? 0), + 'source_updated_at' => (int) ($data['update_time'] ?? $data['create_time'] ?? 0), + ]; + } + + private static function buildMatchEventPayload(int $sourceId): array + { + $match = MatchEvent::where('id', $sourceId)->findOrEmpty(); + if ($match->isEmpty() || (int) ($match->is_show ?? 1) !== 1) { + return []; + } + + $data = $match->toArray(); + $homeTeam = (string) ($data['home_team'] ?? ''); + $awayTeam = (string) ($data['away_team'] ?? ''); + $league = (string) ($data['league_name'] ?? ''); + $matchTime = !empty($data['match_time']) && is_numeric($data['match_time']) + ? date('Y-m-d H:i', (int) $data['match_time']) + : (string) ($data['match_time'] ?? ''); + + $history = MatchHistory::where(function ($query) use ($homeTeam, $awayTeam) { + $query->where([['home_team', '=', $homeTeam], ['away_team', '=', $awayTeam]]) + ->whereOr([['home_team', '=', $awayTeam], ['away_team', '=', $homeTeam]]); + })->order('match_time desc')->limit(5)->select()->toArray(); + + $homeRecent = MatchRecent::where('team_name', $homeTeam) + ->order('match_time desc')->limit(5)->select()->toArray(); + $awayRecent = MatchRecent::where('team_name', $awayTeam) + ->order('match_time desc')->limit(5)->select()->toArray(); + + $title = trim($homeTeam . ' vs ' . $awayTeam); + $summary = sprintf( + '%s %s,%s 对阵 %s,比赛时间 %s,当前比分 %s-%s。', + $league, + $title, + $homeTeam, + $awayTeam, + $matchTime, + $data['home_score'] ?? 0, + $data['away_score'] ?? 0 + ); + + $canonical = [ + $summary, + '赔率:主胜' . ($data['home_odds'] ?? '-') . ',平局' . ($data['draw_odds'] ?? '-') . ',客胜' . ($data['away_odds'] ?? '-') . '。', + '历史交锋:' . self::normalizePlainText(json_encode($history, JSON_UNESCAPED_UNICODE)), + $homeTeam . '近期战绩:' . self::normalizePlainText(json_encode($homeRecent, JSON_UNESCAPED_UNICODE)), + $awayTeam . '近期战绩:' . self::normalizePlainText(json_encode($awayRecent, JSON_UNESCAPED_UNICODE)), + ]; + + return [ + 'title' => $title ?: '赛事#' . $sourceId, + 'summary' => self::normalizePlainText($summary), + 'canonical_text' => trim(implode("\n", array_filter($canonical))), + 'keywords' => implode(',', array_filter([$league, $homeTeam, $awayTeam])), + 'metadata_json' => [ + 'league' => $league, + 'teams' => array_values(array_filter([$homeTeam, $awayTeam])), + 'match_time' => $matchTime, + 'status' => (int) ($data['status'] ?? 0), + ], + 'source_created_at' => (int) ($data['create_time'] ?? $data['match_time'] ?? 0), + 'source_updated_at' => (int) ($data['update_time'] ?? $data['match_time'] ?? $data['create_time'] ?? 0), + ]; + } + + private static function buildLotteryAiHistoryPayload(int $sourceId): array + { + $row = LotteryAiAnalysis::where('id', $sourceId)->findOrEmpty(); + if ($row->isEmpty()) { + return []; + } + + $data = $row->toArray(); + $resultText = is_string($data['result'] ?? '') + ? $data['result'] + : json_encode($data['result'] ?? [], JSON_UNESCAPED_UNICODE); + $canonicalText = self::normalizePlainText($resultText); + $title = sprintf('%s 第%s期 %s 历史AI分析', $data['code'] ?? '彩票', $data['issue'] ?? '-', $data['module'] ?? 'module'); + + return [ + 'title' => $title, + 'summary' => self::truncateText($canonicalText, 220), + 'canonical_text' => $canonicalText, + 'keywords' => implode(',', array_filter([ + $data['code'] ?? '', + $data['issue'] ?? '', + $data['module'] ?? '', + ])), + 'metadata_json' => [ + 'code' => (string) ($data['code'] ?? ''), + 'issue' => (string) ($data['issue'] ?? ''), + 'module' => (string) ($data['module'] ?? ''), + ], + 'source_created_at' => (int) ($data['create_time'] ?? 0), + 'source_updated_at' => (int) ($data['update_time'] ?? $data['create_time'] ?? 0), + ]; + } + + private static function buildKeywordText(array $payload): string + { + return implode(' ', array_filter([ + $payload['title'] ?? '', + $payload['summary'] ?? '', + $payload['keywords'] ?? '', + json_encode($payload['metadata_json'] ?? [], JSON_UNESCAPED_UNICODE), + ])); + } + + private static function splitText(string $text, int $chunkSize, int $overlap): array + { + $text = trim($text); + if ($text === '') { + return []; + } + + $length = mb_strlen($text); + if ($length <= $chunkSize) { + return [$text]; + } + + $chunks = []; + $start = 0; + while ($start < $length) { + $chunks[] = mb_substr($text, $start, $chunkSize); + if ($start + $chunkSize >= $length) { + break; + } + $start += max(1, $chunkSize - $overlap); + } + + return array_values(array_filter(array_map('trim', $chunks))); + } + + private static function cosineSimilarity(array $a, array $b): float + { + if (empty($a) || empty($b) || count($a) !== count($b)) { + return 0.0; + } + + $dot = 0.0; + $normA = 0.0; + $normB = 0.0; + foreach ($a as $i => $value) { + $value = (float) $value; + $other = (float) ($b[$i] ?? 0.0); + $dot += $value * $other; + $normA += $value * $value; + $normB += $other * $other; + } + + if ($normA <= 0.0 || $normB <= 0.0) { + return 0.0; + } + + return max(0.0, min(1.0, $dot / (sqrt($normA) * sqrt($normB)))); + } + + private static function vectorNorm(array $vector): float + { + $sum = 0.0; + foreach ($vector as $value) { + $value = (float) $value; + $sum += $value * $value; + } + return $sum > 0 ? sqrt($sum) : 0.0; + } + + private static function freshnessScore(int $timestamp): float + { + if ($timestamp <= 0) { + return 0.1; + } + + $days = max(0, (time() - $timestamp) / 86400); + if ($days <= 1) { + return 1.0; + } + if ($days <= 7) { + return 0.85; + } + if ($days <= 30) { + return 0.65; + } + if ($days <= 180) { + return 0.4; + } + return 0.2; + } + + private static function domainScore(array $candidate, array $filters): float + { + $preferred = $filters['preferred_domain'] ?? ''; + if ($preferred !== '' && ($candidate['domain'] ?? '') === $preferred) { + return 1.0; + } + return 0.5; + } + + private static function matchScore(array $candidate, array $filters, array $structuredContext): float + { + $score = 0.2; + $metadata = $candidate['metadata_json'] ?? []; + $title = self::normalizePlainText((string) ($candidate['title'] ?? '')); + $summary = self::normalizePlainText((string) ($candidate['summary'] ?? '')); + $chunkText = self::normalizePlainText((string) ($candidate['chunk_text'] ?? '')); + $haystack = $title . ' ' . $summary . ' ' . $chunkText; + + if (!empty($filters['cid']) && !empty($metadata['cid']) && (int) $filters['cid'] === (int) $metadata['cid']) { + $score += 0.4; + } + if (!empty($filters['code']) && !empty($metadata['code']) && $filters['code'] === $metadata['code']) { + $score += 0.4; + } + if (!empty($filters['issue']) && !empty($metadata['issue']) && $filters['issue'] === $metadata['issue']) { + $score += 0.2; + } + if (!empty($filters['tags']) && !empty($metadata['tags']) && is_array($metadata['tags'])) { + $common = array_intersect($filters['tags'], $metadata['tags']); + if (!empty($common)) { + $score += 0.3; + } + } + if (!empty($structuredContext['title'])) { + $sourceTitle = self::normalizePlainText((string) $structuredContext['title']); + if ($title !== '' && $sourceTitle !== '' && mb_strpos($title, mb_substr($sourceTitle, 0, 8)) !== false) { + $score += 0.2; + } + } + if (!empty($structuredContext['league'])) { + $league = self::normalizePlainText((string) $structuredContext['league']); + if ($league !== '' && mb_strpos($haystack, $league) !== false) { + $score += 0.15; + } + } + if (!empty($structuredContext['teams']) && is_array($structuredContext['teams'])) { + $teamHits = 0; + foreach ($structuredContext['teams'] as $team) { + $team = self::normalizePlainText((string) $team); + if ($team !== '' && mb_strpos($haystack, $team) !== false) { + $teamHits++; + } + } + if ($teamHits > 0) { + $score += min(0.3, 0.12 * $teamHits); + } + } + + return max(0.0, min(1.0, $score)); + } + + private static function recordQueryLog( + array $filters, + string $scene, + string $queryText, + array $hits, + int $tokens, + int $costMs, + int $status + ): void { + try { + $userId = (int) ($filters['user_id'] ?? 0); + $refId = (int) ($filters['ref_id'] ?? 0); + AiKbQueryLog::create([ + 'user_id' => $userId, + 'scene' => $scene, + 'ref_id' => $refId, + 'query_text' => $queryText, + 'filters_json' => $filters, + 'hit_docs_json' => $hits, + 'tokens_used' => $tokens, + 'cost_ms' => $costMs, + 'status' => $status, + 'create_time' => time(), + 'update_time' => time(), + ]); + + AiLog::create([ + 'user_id' => $userId, + 'type' => 6, + 'ref_id' => $refId, + 'tokens_used' => $tokens, + 'cost_ms' => $costMs, + 'status' => $status === AiKbQueryLog::STATUS_SUCCESS ? 1 : 2, + ]); + } catch (\Throwable $e) { + // Ignore logging failures to avoid affecting main flow. + } + } + + private static function splitQueryTerms(string $queryText): array + { + $queryText = trim(self::normalizePlainText($queryText)); + if ($queryText === '') { + return []; + } + + $terms = preg_split('/[\s,,。!?;、::\/\\\\]+/u', $queryText) ?: []; + $terms = array_values(array_filter(array_map('trim', $terms), static fn(string $term) => mb_strlen($term) >= 2)); + return array_unique($terms); + } + + private static function normalizeHtmlText(string $html): string + { + $html = str_replace(['
', '
', '
', '

', '', ''], "\n", $html); + return self::normalizePlainText(strip_tags($html)); + } + + private static function normalizePlainText(string $text): string + { + $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $text = preg_replace('/\s+/u', ' ', trim($text)) ?: ''; + return trim($text); + } + + private static function truncateText(string $text, int $length): string + { + $text = trim($text); + if ($text === '') { + return ''; + } + return mb_strlen($text) > $length ? mb_substr($text, 0, $length) . '...' : $text; + } + + private static function intConfig(string $name, int $default): int + { + return (int) AiConfig::getVal($name, (string) $default); + } +} diff --git a/server/app/common/service/ai/KbSyncService.php b/server/app/common/service/ai/KbSyncService.php new file mode 100644 index 0000000..2094fcc --- /dev/null +++ b/server/app/common/service/ai/KbSyncService.php @@ -0,0 +1,104 @@ + $domain, + 'subtype' => $subtype, + 'source_id' => $sourceId, + ])->whereIn('status', [AiKbSyncJob::STATUS_PENDING, AiKbSyncJob::STATUS_PROCESSING]) + ->order('id', 'desc') + ->findOrEmpty(); + + if (!$pending->isEmpty() && (int) $pending->status === AiKbSyncJob::STATUS_PENDING) { + $pending->action = $action; + $pending->priority = $priority; + $pending->scheduled_at = $scheduledAt; + $pending->error_msg = ''; + $pending->update_time = time(); + $pending->save(); + return; + } + + AiKbSyncJob::create([ + 'domain' => $domain, + 'subtype' => $subtype, + 'source_id' => $sourceId, + 'action' => $action, + 'priority' => $priority, + 'status' => AiKbSyncJob::STATUS_PENDING, + 'retry_count' => 0, + 'error_msg' => '', + 'scheduled_at' => $scheduledAt, + 'processed_at' => 0, + 'create_time' => time(), + 'update_time' => time(), + ]); + } + + public static function markLotteryBackfillJobs(int $limit = 200): void + { + self::enqueueMissingLotteryRows('draw_result', 'lottery_draw_result', $limit); + self::enqueueMissingLotteryRows('ai_history', 'lottery_ai_analysis', $limit); + } + + public static function markMatchBackfillJobs(int $limit = 200): void + { + self::enqueueMissingRows('match', 'event', 'match', $limit); + } + + private static function enqueueMissingLotteryRows(string $subtype, string $table, int $limit): void + { + self::enqueueMissingRows('lottery', $subtype, $table, $limit); + } + + private static function enqueueMissingRows(string $domain, string $subtype, string $table, int $limit): void + { + try { + $rows = Db::name($table)->alias('s') + ->leftJoin('ai_kb_document d', "d.domain = '{$domain}' AND d.subtype = '{$subtype}' AND d.source_id = s.id") + ->whereNull('d.id') + ->where('s.id', '>', 0) + ->order('s.id', 'desc') + ->limit($limit) + ->column('s.id'); + + foreach ($rows as $sourceId) { + self::enqueue($domain, $subtype, (int) $sourceId, AiKbSyncJob::ACTION_UPSERT, 80); + } + } catch (\Throwable $e) { + // Ignore in environments where KB tables are not ready yet. + } + } + + public static function markDocumentInactive(string $domain, string $subtype, int $sourceId): void + { + AiKbDocument::where([ + 'domain' => $domain, + 'subtype' => $subtype, + 'source_id' => $sourceId, + ])->update([ + 'is_active' => 0, + 'update_time' => time(), + ]); + } +} diff --git a/server/app/common/service/article/ArticleAiCommentService.php b/server/app/common/service/article/ArticleAiCommentService.php new file mode 100644 index 0000000..316f04c --- /dev/null +++ b/server/app/common/service/article/ArticleAiCommentService.php @@ -0,0 +1,285 @@ + count($virtualUsers), + 'seeded' => $seeded, + 'processed' => $processed, + ]; + } + + protected static function seedTasks(array $virtualUsers, int $limit = 10): array + { + if (empty($virtualUsers)) { + return ['created' => 0, 'skipped' => 0]; + } + + $articles = Article::field('id,title,desc,content,author,ext,click_virtual,click_actual,comment_count,create_time,published_at') + ->where('is_show', 1) + ->whereNull('delete_time') + ->where('create_time', '>=', strtotime('-14 days')) + ->orderRaw('comment_count asc, (click_actual + click_virtual) desc, id desc') + ->limit($limit) + ->select() + ->toArray(); + + $created = 0; + $skipped = 0; + foreach ($articles as $index => $article) { + $articleId = (int) ($article['id'] ?? 0); + if ($articleId <= 0) { + $skipped++; + continue; + } + + $existingPending = ArticleAiCommentTask::where('article_id', $articleId) + ->whereIn('status', [0, 1]) + ->count(); + if ($existingPending > 0) { + $skipped++; + continue; + } + + $desiredCount = self::resolveTaskCount($article); + if ($desiredCount <= 0) { + $skipped++; + continue; + } + + for ($i = 0; $i < $desiredCount; $i++) { + $virtualUserId = $virtualUsers[($articleId + $index + $i) % count($virtualUsers)]; + $comment = self::generateCommentContent($article, $virtualUserId); + if ($comment === '') { + continue; + } + + $runAt = time() + random_int(600, 43200); + ArticleAiCommentTask::create([ + 'article_id' => $articleId, + 'virtual_user_id' => $virtualUserId, + 'comment_content' => $comment, + 'run_at' => $runAt, + 'status' => 0, + 'create_time' => time(), + 'update_time' => time(), + ]); + $created++; + } + } + + return compact('created', 'skipped'); + } + + protected static function processDueTasks(int $limit = 20): array + { + $tasks = ArticleAiCommentTask::where('status', 0) + ->where('run_at', '<=', time()) + ->order('run_at', 'asc') + ->limit($limit) + ->select() + ->toArray(); + + $done = 0; + $failed = 0; + + foreach ($tasks as $task) { + $articleId = (int) ($task['article_id'] ?? 0); + $content = trim((string) ($task['comment_content'] ?? '')); + $virtualUserId = (int) ($task['virtual_user_id'] ?? 0); + + if ($articleId <= 0 || $content === '' || $virtualUserId <= 0) { + self::markTaskFailed((int) $task['id'], '任务数据不完整'); + $failed++; + continue; + } + + $article = Article::where('id', $articleId) + ->where('is_show', 1) + ->whereNull('delete_time') + ->findOrEmpty(); + + if ($article->isEmpty()) { + self::markTaskFailed((int) $task['id'], '文章不存在或已下架'); + $failed++; + continue; + } + + Db::startTrans(); + try { + ArticleComment::create([ + 'article_id' => $articleId, + 'user_id' => $virtualUserId, + 'parent_id' => 0, + 'reply_user_id' => 0, + 'content' => $content, + 'is_show' => 1, + 'create_time' => time(), + 'update_time' => time(), + ]); + + Article::where('id', $articleId)->inc('comment_count')->update(); + ArticleAiCommentTask::where('id', $task['id'])->update([ + 'status' => 1, + 'update_time' => time(), + ]); + + Db::commit(); + $done++; + } catch (\Throwable $e) { + Db::rollback(); + self::markTaskFailed((int) $task['id'], $e->getMessage()); + $failed++; + } + } + + return compact('done', 'failed'); + } + + protected static function ensureVirtualUsers(int $targetCount = self::VIRTUAL_POOL_SIZE): array + { + $existing = User::where('account', 'like', self::VIRTUAL_ACCOUNT_PREFIX . '%') + ->order('id', 'asc') + ->column('id'); + + $existing = array_values(array_map('intval', $existing)); + $missing = max(0, $targetCount - count($existing)); + if ($missing > 0) { + $avatar = (string) Config::get('project.default_image.user_avatar', ''); + $passwordSalt = (string) Config::get('project.unique_identification', 'sport-era'); + + for ($i = 1; $i <= $missing; $i++) { + $accountIndex = count($existing) + $i; + $sn = User::createUserSn('9'); + $password = create_password(bin2hex(random_bytes(6)), $passwordSalt); + + $user = User::create([ + 'sn' => $sn, + 'avatar' => $avatar, + 'real_name' => '', + 'nickname' => self::VIRTUAL_NICKNAME_PREFIX . str_pad((string) $accountIndex, 2, '0', STR_PAD_LEFT), + 'account' => self::VIRTUAL_ACCOUNT_PREFIX . str_pad((string) $accountIndex, 4, '0', STR_PAD_LEFT), + 'password' => $password, + 'mobile' => '', + 'sex' => 0, + 'channel' => 0, + 'is_disable' => 0, + 'is_new_user' => 0, + 'user_money' => 0, + 'total_recharge_amount' => 0, + ]); + + $existing[] = (int) $user->id; + } + } + + return $existing; + } + + protected static function resolveTaskCount(array $article): int + { + $commentCount = (int) ($article['comment_count'] ?? 0); + $clickCount = (int) ($article['click_actual'] ?? 0) + (int) ($article['click_virtual'] ?? 0); + + if ($commentCount >= 20) { + return 0; + } + + if ($clickCount >= 500) { + return 2; + } + + return 1; + } + + protected static function generateCommentContent(array $article, int $virtualUserId): string + { + $title = trim((string) ($article['title'] ?? '')); + $abstract = trim((string) ($article['desc'] ?? '')); + $content = self::normalizeContent((string) ($article['content'] ?? '')); + if ($title === '' && $abstract === '' && $content === '') { + return ''; + } + + $personaPool = [ + '资深球迷', + '战术观察员', + '数据控', + '赛事评论员', + '球迷视角', + ]; + $persona = $personaPool[$virtualUserId % count($personaPool)]; + + $result = AiService::generateArticleComment($title, $abstract, $content, $persona); + if (empty($result['success'])) { + return self::fallbackComment($title, $abstract, $content); + } + + $comment = trim((string) ($result['content'] ?? '')); + $comment = preg_replace('/^评论[::]\s*/u', '', $comment); + $comment = preg_replace('/^[“"‘’\'\']|[”"‘’\'\']$/u', '', $comment); + $comment = preg_replace('/\s+/u', ' ', $comment); + $comment = trim($comment, " \t\n\r\0\x0B,。,.!!??;;::"); + + if ($comment === '' || mb_strlen($comment) < 8) { + return self::fallbackComment($title, $abstract, $content); + } + + return mb_substr($comment, 0, 120); + } + + protected static function fallbackComment(string $title, string $abstract, string $content): string + { + $focus = $title ?: ($abstract ?: mb_substr($content, 0, 18)); + if ($focus === '') { + return ''; + } + + return mb_substr($focus, 0, 18) . '这条信息量挺足,后续关键数据变化值得继续关注。'; + } + + protected static function markTaskFailed(int $taskId, string $message): void + { + if ($taskId <= 0) { + return; + } + + ArticleAiCommentTask::where('id', $taskId)->update([ + 'status' => 2, + 'error_msg' => mb_substr($message, 0, 255), + 'update_time' => time(), + ]); + } + + protected static function normalizeContent(string $content): string + { + if ($content === '') { + return ''; + } + + $content = strip_tags($content); + $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $content = preg_replace('/\s+/u', ' ', $content); + return trim($content); + } +} diff --git a/server/app/common/service/article/ArticleImageProxyService.php b/server/app/common/service/article/ArticleImageProxyService.php new file mode 100644 index 0000000..1719fe0 --- /dev/null +++ b/server/app/common/service/article/ArticleImageProxyService.php @@ -0,0 +1,241 @@ + [ + 'res.hkjc.com', + 'racingnews.hkjc.com', + ], + 'taiwan_lottery' => [ + 'cdn.taiwanlottery.com.tw', + 'www.taiwanlottery.com', + 'taiwanlottery.com', + ], + ]; + + private const SOURCE_REFERERS = [ + 'hkjc_racingnews' => 'https://racingnews.hkjc.com/', + 'taiwan_lottery' => 'https://www.taiwanlottery.com/', + ]; + + public static function rewriteArticlePayload(array $article): array + { + $ext = self::decodeExt($article['ext'] ?? []); + if (!self::shouldRewrite($ext)) { + return $article; + } + + if (!empty($article['image'])) { + $article['image'] = self::buildProxyUrl((string) $article['image'], $ext); + } + + if (!empty($article['content'])) { + $article['content'] = preg_replace_callback( + '/(]*\bsrc=["\'])([^"\']+)(["\'][^>]*>)/i', + static function (array $matches) use ($ext) { + $proxyUrl = self::buildProxyUrl($matches[2] ?? '', $ext); + if ($proxyUrl === '' || $proxyUrl === ($matches[2] ?? '')) { + return $matches[0]; + } + return $matches[1] + . htmlspecialchars($proxyUrl, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') + . $matches[3]; + }, + (string) $article['content'] + ); + } + + return $article; + } + + public static function proxyRemoteImage(string $url): Response + { + $normalizedUrl = self::normalizeUrl($url); + if ($normalizedUrl === '' || !self::isAllowedRemoteUrl($normalizedUrl)) { + return response('image not found', 404)->header([ + 'Content-Type' => 'text/plain; charset=utf-8', + 'Cache-Control' => 'public, max-age=300', + ]); + } + + $source = self::detectSourceByUrl($normalizedUrl); + $headers = [ + 'Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', + 'User-Agent: Mozilla/5.0 (compatible; SportEra/1.0)', + ]; + $referer = self::SOURCE_REFERERS[$source] ?? ''; + if ($referer !== '') { + $headers[] = 'Referer: ' . $referer; + } + + $ch = curl_init($normalizedUrl); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 20, + CURLOPT_ENCODING => '', + CURLOPT_HTTPHEADER => $headers, + ]); + + $body = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + $contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + $effectiveUrl = (string) curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); + $curlError = curl_error($ch); + curl_close($ch); + + if ($body === false || $curlError !== '' || $httpCode < 200 || $httpCode >= 300) { + return response('image not found', 404)->header([ + 'Content-Type' => 'text/plain; charset=utf-8', + 'Cache-Control' => 'public, max-age=300', + ]); + } + + $effectiveUrl = $effectiveUrl !== '' ? $effectiveUrl : $normalizedUrl; + if (!self::isAllowedRemoteUrl($effectiveUrl)) { + return response('image not found', 404)->header([ + 'Content-Type' => 'text/plain; charset=utf-8', + 'Cache-Control' => 'public, max-age=300', + ]); + } + + $contentType = self::normalizeContentType($contentType, $effectiveUrl); + if (!str_starts_with($contentType, 'image/')) { + return response('image not found', 404)->header([ + 'Content-Type' => 'text/plain; charset=utf-8', + 'Cache-Control' => 'public, max-age=300', + ]); + } + + return response($body, 200)->header([ + 'Content-Type' => $contentType, + 'Cache-Control' => 'public, max-age=86400', + 'Access-Control-Allow-Origin' => '*', + ]); + } + + private static function shouldRewrite(array $ext): bool + { + $source = (string) ($ext['source'] ?? ''); + return isset(self::SOURCE_HOSTS[$source]); + } + + private static function buildProxyUrl(string $url, array $ext = []): string + { + $normalizedUrl = self::normalizeUrl($url); + if ($normalizedUrl === '' || self::isLocalProxyUrl($normalizedUrl)) { + return $url; + } + + if (!self::isAllowedRemoteUrl($normalizedUrl, $ext)) { + return $url; + } + + $path = '/api/article/imageProxy?url=' . rawurlencode($normalizedUrl); + $domain = rtrim((string) request()->domain(), '/'); + return $domain !== '' ? $domain . $path : $path; + } + + private static function normalizeUrl(string $url): string + { + $url = trim(html_entity_decode($url, ENT_QUOTES | ENT_HTML5, 'UTF-8')); + if ($url === '') { + return ''; + } + if (str_starts_with($url, '//')) { + return 'https:' . $url; + } + return $url; + } + + private static function isAllowedRemoteUrl(string $url, array $ext = []): bool + { + $parts = parse_url($url); + $scheme = strtolower((string) ($parts['scheme'] ?? '')); + $host = strtolower((string) ($parts['host'] ?? '')); + if ($host === '' || !in_array($scheme, ['http', 'https'], true)) { + return false; + } + + $allowedHosts = self::getAllowedHosts($ext); + foreach ($allowedHosts as $allowedHost) { + $allowedHost = strtolower($allowedHost); + if ($host === $allowedHost || str_ends_with($host, '.' . $allowedHost)) { + return true; + } + } + + return false; + } + + private static function getAllowedHosts(array $ext = []): array + { + $source = (string) ($ext['source'] ?? ''); + if ($source !== '' && isset(self::SOURCE_HOSTS[$source])) { + return self::SOURCE_HOSTS[$source]; + } + + $allHosts = []; + foreach (self::SOURCE_HOSTS as $hosts) { + $allHosts = array_merge($allHosts, $hosts); + } + return array_values(array_unique($allHosts)); + } + + private static function detectSourceByUrl(string $url): string + { + $parts = parse_url($url); + $host = strtolower((string) ($parts['host'] ?? '')); + foreach (self::SOURCE_HOSTS as $source => $hosts) { + foreach ($hosts as $allowedHost) { + $allowedHost = strtolower($allowedHost); + if ($host === $allowedHost || str_ends_with($host, '.' . $allowedHost)) { + return $source; + } + } + } + return ''; + } + + private static function isLocalProxyUrl(string $url): bool + { + return str_contains($url, '/api/article/imageProxy?'); + } + + private static function normalizeContentType(string $contentType, string $url): string + { + $contentType = strtolower(trim(explode(';', $contentType)[0] ?? '')); + if (str_starts_with($contentType, 'image/')) { + return $contentType; + } + + $path = strtolower((string) parse_url($url, PHP_URL_PATH)); + return match (pathinfo($path, PATHINFO_EXTENSION)) { + 'jpg', 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + 'svg' => 'image/svg+xml', + default => 'application/octet-stream', + }; + } + + private static function decodeExt($ext): array + { + if (is_array($ext)) { + return $ext; + } + if (is_string($ext) && $ext !== '') { + $decoded = json_decode($ext, true); + return is_array($decoded) ? $decoded : []; + } + return []; + } +} diff --git a/server/app/common/service/chat/PrivateChatService.php b/server/app/common/service/chat/PrivateChatService.php new file mode 100644 index 0000000..116bee9 --- /dev/null +++ b/server/app/common/service/chat/PrivateChatService.php @@ -0,0 +1,301 @@ +where('id', $targetUserId)->findOrEmpty(); + if ($target->isEmpty()) { + return self::fail('用户不存在'); + } + + $pair = self::pairIds($userId, $targetUserId); + $session = PrivateChatSession::where([ + 'user_a_id' => $pair[0], + 'user_b_id' => $pair[1], + ])->findOrEmpty(); + + if ($session->isEmpty()) { + if ($requireFollow && !self::isFollowing($userId, $targetUserId)) { + return self::fail('请先加好友后再私聊'); + } + + $now = time(); + $session = PrivateChatSession::create([ + 'user_a_id' => $pair[0], + 'user_b_id' => $pair[1], + 'last_active_time' => $now, + 'create_time' => $now, + 'update_time' => $now, + ]); + } + + return self::success(self::formatSession($session->toArray(), $userId)); + } + + public static function sessions(int $userId, int $page, int $size): array + { + if ($userId <= 0) { + return ['lists' => [], 'page_no' => $page, 'page_size' => $size, 'count' => 0]; + } + + $page = max(1, $page); + $size = max(1, min(50, $size)); + $query = PrivateChatSession::where(function ($query) use ($userId) { + $query->where('user_a_id', $userId)->whereOr('user_b_id', $userId); + }); + + $count = (clone $query)->count(); + $rows = $query->order('last_active_time desc') + ->order('id desc') + ->page($page, $size) + ->select() + ->toArray(); + + return [ + 'lists' => array_map(fn($row) => self::formatSession($row, $userId), $rows), + 'page_no' => $page, + 'page_size' => $size, + 'count' => $count, + ]; + } + + public static function messages(int $userId, int $sessionId, int $afterId, int $page, int $size): array + { + $session = self::getOwnedSession($sessionId, $userId); + if (!$session) { + return self::fail('会话不存在或无权访问'); + } + + $page = max(1, $page); + $size = max(1, min(100, $size)); + $query = PrivateChatMessage::where('session_id', $sessionId); + if ($afterId > 0) { + $query->where('id', '>', $afterId)->order('id', 'asc')->limit($size); + } else { + $query->order('id', 'desc')->page($page, $size); + } + $rows = $query->select()->toArray(); + if ($afterId <= 0) { + $rows = array_reverse($rows); + } + + self::markRead($session, $userId); + $session = PrivateChatSession::where('id', $sessionId)->findOrEmpty(); + + return self::success([ + 'session' => self::formatSession($session->toArray(), $userId), + 'lists' => array_map(fn($row) => self::formatMessage($row, $userId), $rows), + 'page_no' => $page, + 'page_size' => $size, + ]); + } + + public static function send(int $userId, int $sessionId, string $messageType, string $content): array + { + $session = self::getOwnedSession($sessionId, $userId); + if (!$session) { + return self::fail('会话不存在或无权访问'); + } + + $messageType = trim($messageType) ?: self::MESSAGE_TYPE_TEXT; + if (!in_array($messageType, [self::MESSAGE_TYPE_TEXT, self::MESSAGE_TYPE_IMAGE], true)) { + return self::fail('消息类型错误'); + } + + $content = trim($content); + if ($content === '') { + return self::fail($messageType === self::MESSAGE_TYPE_IMAGE ? '请选择图片' : '请输入消息内容'); + } + if ($messageType === self::MESSAGE_TYPE_TEXT) { + if (mb_strlen($content) > 1000) { + return self::fail('文字消息最多1000字'); + } + } else { + $content = mb_substr($content, 0, 500); + } + + $row = null; + Db::startTrans(); + try { + $now = time(); + $receiverId = self::targetUserId($session->toArray(), $userId); + $row = PrivateChatMessage::create([ + 'session_id' => $sessionId, + 'sender_id' => $userId, + 'receiver_id' => $receiverId, + 'message_type' => $messageType, + 'content' => $content, + 'read_time' => 0, + 'create_time' => $now, + 'update_time' => $now, + ]); + + $unreadField = ((int) $session->user_a_id === $receiverId) ? 'user_a_unread' : 'user_b_unread'; + PrivateChatSession::where('id', $sessionId) + ->inc($unreadField) + ->update([ + 'last_message_id' => (int) $row->id, + 'last_message_type' => $messageType, + 'last_message_content' => self::lastMessageContent($messageType, $content), + 'last_sender_id' => $userId, + 'last_active_time' => $now, + 'update_time' => $now, + ]); + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + return self::fail('发送失败:' . $e->getMessage()); + } + + return self::success(self::formatMessage($row->toArray(), $userId)); + } + + public static function relationship(int $userId, int $targetUserId): array + { + $isFollowedByMe = $userId > 0 && $targetUserId > 0 && self::isFollowing($userId, $targetUserId); + $isFollowingMe = $userId > 0 && $targetUserId > 0 && self::isFollowing($targetUserId, $userId); + $isMutual = $isFollowedByMe && $isFollowingMe; + + return [ + 'is_followed_by_me' => $isFollowedByMe, + 'is_following_me' => $isFollowingMe, + 'is_mutual' => $isMutual, + 'relationship_text' => $isMutual ? '互相关注' : '不是好友', + ]; + } + + public static function unreadCount(int $userId): int + { + if ($userId <= 0) { + return 0; + } + return (int) PrivateChatSession::where('user_a_id', $userId)->sum('user_a_unread') + + (int) PrivateChatSession::where('user_b_id', $userId)->sum('user_b_unread'); + } + + private static function getOwnedSession(int $sessionId, int $userId): ?PrivateChatSession + { + if ($sessionId <= 0 || $userId <= 0) { + return null; + } + $session = PrivateChatSession::where('id', $sessionId) + ->where(function ($query) use ($userId) { + $query->where('user_a_id', $userId)->whereOr('user_b_id', $userId); + }) + ->findOrEmpty(); + return $session->isEmpty() ? null : $session; + } + + private static function formatSession(array $row, int $viewerId): array + { + $targetUserId = self::targetUserId($row, $viewerId); + $target = User::field('id,sn,nickname,avatar')->where('id', $targetUserId)->findOrEmpty(); + $unread = ((int) $row['user_a_id'] === $viewerId) + ? (int) ($row['user_a_unread'] ?? 0) + : (int) ($row['user_b_unread'] ?? 0); + + return [ + 'id' => (int) $row['id'], + 'target_user_id' => $targetUserId, + 'target_user' => $target->isEmpty() ? null : $target->toArray(), + 'last_message_id' => (int) ($row['last_message_id'] ?? 0), + 'last_message_type' => (string) ($row['last_message_type'] ?? self::MESSAGE_TYPE_TEXT), + 'last_message_content' => (string) ($row['last_message_content'] ?? ''), + 'last_sender_id' => (int) ($row['last_sender_id'] ?? 0), + 'unread_count' => $unread, + 'last_active_time' => $row['last_active_time'] ?? 0, + 'create_time' => $row['create_time'] ?? 0, + 'relationship' => self::relationship($viewerId, $targetUserId), + ]; + } + + private static function formatMessage(array $row, int $viewerId): array + { + return [ + 'id' => (int) $row['id'], + 'session_id' => (int) $row['session_id'], + 'sender_id' => (int) $row['sender_id'], + 'receiver_id' => (int) $row['receiver_id'], + 'message_type' => (string) $row['message_type'], + 'content' => (string) ($row['content'] ?? ''), + 'is_self' => (int) $row['sender_id'] === $viewerId, + 'read_time' => (int) ($row['read_time'] ?? 0), + 'create_time' => $row['create_time'] ?? 0, + ]; + } + + private static function markRead(PrivateChatSession $session, int $viewerId): void + { + $now = time(); + PrivateChatMessage::where('session_id', (int) $session->id) + ->where('receiver_id', $viewerId) + ->where('read_time', 0) + ->update(['read_time' => $now, 'update_time' => $now]); + + $field = ((int) $session->user_a_id === $viewerId) ? 'user_a_unread' : 'user_b_unread'; + PrivateChatSession::where('id', (int) $session->id)->update([ + $field => 0, + 'update_time' => $now, + ]); + } + + private static function targetUserId(array $session, int $viewerId): int + { + return ((int) $session['user_a_id'] === $viewerId) + ? (int) $session['user_b_id'] + : (int) $session['user_a_id']; + } + + private static function pairIds(int $userId, int $targetUserId): array + { + return $userId < $targetUserId ? [$userId, $targetUserId] : [$targetUserId, $userId]; + } + + private static function isFollowing(int $userId, int $targetUserId): bool + { + return CommunityFollow::where([ + 'user_id' => $userId, + 'follow_user_id' => $targetUserId, + ])->count() > 0; + } + + private static function lastMessageContent(string $messageType, string $content): string + { + if ($messageType === self::MESSAGE_TYPE_IMAGE) { + return '[图片]'; + } + return mb_substr($content, 0, 200); + } + + private static function success(array $data): array + { + return ['success' => true, 'data' => $data]; + } + + private static function fail(string $error): array + { + return ['success' => false, 'error' => $error]; + } +} diff --git a/server/app/common/service/crontab/CrontabAlertService.php b/server/app/common/service/crontab/CrontabAlertService.php new file mode 100644 index 0000000..2619e05 --- /dev/null +++ b/server/app/common/service/crontab/CrontabAlertService.php @@ -0,0 +1,118 @@ +where('crontab_id', $crontabId) + ->where('status', 'open') + ->update([ + 'status' => 'resolved', + 'resolved_at' => $now, + 'update_time' => $now, + ]); + } + + public static function upsertCommandException(array $item, string $errorMessage, int $crontabLogId = 0): void + { + self::ensureTable(); + $now = time(); + $dedupeKey = self::buildDedupeKey((int) ($item['id'] ?? 0), 'command_exception'); + $detail = json_encode([ + 'summary_text' => mb_substr($errorMessage, 0, 1000), + 'error_message' => mb_substr($errorMessage, 0, 1000), + 'output_excerpt' => mb_substr($errorMessage, 0, 2000), + ], JSON_UNESCAPED_UNICODE); + + $existing = Db::name('crontab_alert')->where('dedupe_key', $dedupeKey)->find(); + if ($existing) { + $update = [ + 'crontab_log_id' => $crontabLogId, + 'task_name' => mb_substr((string) ($item['name'] ?? ''), 0, 100), + 'command' => mb_substr((string) ($item['command'] ?? ''), 0, 100), + 'params' => mb_substr((string) ($item['params'] ?? ''), 0, 255), + 'alert_type' => 'command_exception', + 'http_status' => 0, + 'detail' => $detail, + 'last_seen_at' => $now, + 'update_time' => $now, + ]; + if (($existing['status'] ?? '') === 'resolved') { + $update['status'] = 'open'; + $update['first_seen_at'] = $now; + $update['last_notified_at'] = 0; + $update['resolved_at'] = 0; + $update['notify_count'] = 0; + } + Db::name('crontab_alert')->where('id', $existing['id'])->update($update); + return; + } + + Db::name('crontab_alert')->insert([ + 'crontab_id' => (int) ($item['id'] ?? 0), + 'crontab_log_id' => $crontabLogId, + 'task_name' => mb_substr((string) ($item['name'] ?? ''), 0, 100), + 'command' => mb_substr((string) ($item['command'] ?? ''), 0, 100), + 'params' => mb_substr((string) ($item['params'] ?? ''), 0, 255), + 'alert_type' => 'command_exception', + 'http_status' => 0, + 'detail' => $detail, + 'dedupe_key' => $dedupeKey, + 'status' => 'open', + 'first_seen_at' => $now, + 'last_seen_at' => $now, + 'create_time' => $now, + 'update_time' => $now, + ]); + } + + private static function buildDedupeKey(int $crontabId, string $alertType): string + { + return 'crontab:' . $crontabId . ':' . $alertType; + } +} diff --git a/server/app/common/service/generator/GenerateService.php b/server/app/common/service/generator/GenerateService.php new file mode 100644 index 0000000..13e6b5b --- /dev/null +++ b/server/app/common/service/generator/GenerateService.php @@ -0,0 +1,230 @@ +generatePath = root_path() . 'runtime/generate/'; + $this->runtimePath = root_path() . 'runtime/'; + } + + + /** + * @notes 删除生成文件夹内容 + * @author 段誉 + * @date 2022/6/23 18:52 + */ + public function delGenerateDirContent() + { + // 删除runtime目录制定文件夹 + !is_dir($this->generatePath) && mkdir($this->generatePath, 0755, true); + del_target_dir($this->generatePath, false); + } + + + /** + * @notes 设置生成状态 + * @param $name + * @param false $status + * @author 段誉 + * @date 2022/6/23 18:53 + */ + public function setGenerateFlag($name, $status = false) + { + $this->flag = $name; + cache($name, (int)$status, 3600); + } + + + /** + * @notes 获取生成状态标记 + * @return mixed|object|\think\App + * @author 段誉 + * @date 2022/6/23 18:53 + */ + public function getGenerateFlag() + { + return cache($this->flag); + } + + + /** + * @notes 删除标记时间 + * @author 段誉 + * @date 2022/6/23 18:53 + */ + public function delGenerateFlag() + { + cache($this->flag, null); + } + + + /** + * @notes 生成器相关类 + * @return string[] + * @author 段誉 + * @date 2022/6/23 17:17 + */ + public function getGeneratorClass() + { + return [ + ControllerGenerator::class, + ListsGenerator::class, + ModelGenerator::class, + ValidateGenerator::class, + LogicGenerator::class, + VueApiGenerator::class, + VueIndexGenerator::class, + VueEditGenerator::class, + SqlGenerator::class, + ]; + } + + + /** + * @notes 生成文件 + * @param array $tableData + * @author 段誉 + * @date 2022/6/23 18:52 + */ + public function generate(array $tableData) + { + foreach ($this->getGeneratorClass() as $item) { + $generator = app()->make($item); + $generator->initGenerateData($tableData); + $generator->generate(); + // 是否为压缩包下载 + if ($generator->isGenerateTypeZip()) { + $this->setGenerateFlag($this->flag, true); + } + // 是否构建菜单 + if ($item == 'app\common\service\generator\core\SqlGenerator') { + $generator->isBuildMenu() && $generator->buildMenuHandle(); + } + } + } + + + /** + * @notes 预览文件 + * @param array $tableData + * @return array + * @author 段誉 + * @date 2022/6/23 18:52 + */ + public function preview(array $tableData) + { + $data = []; + foreach ($this->getGeneratorClass() as $item) { + $generator = app()->make($item); + $generator->initGenerateData($tableData); + $data[] = $generator->fileInfo(); + } + return $data; + } + + + /** + * @notes 压缩文件 + * @author 段誉 + * @date 2022/6/23 19:02 + */ + public function zipFile() + { + $fileName = 'curd-' . date('YmdHis') . '.zip'; + $this->zipTempName = $fileName; + $this->zipTempPath = $this->generatePath . $fileName; + $zip = new \ZipArchive(); + $zip->open($this->zipTempPath, \ZipArchive::CREATE); + $this->addFileZip($this->runtimePath, 'generate', $zip); + $zip->close(); + } + + + /** + * @notes 往压缩包写入文件 + * @param $basePath + * @param $dirName + * @param $zip + * @author 段誉 + * @date 2022/6/23 19:02 + */ + public function addFileZip($basePath, $dirName, $zip) + { + $handler = opendir($basePath . $dirName); + while (($filename = readdir($handler)) !== false) { + if ($filename != '.' && $filename != '..') { + if (is_dir($basePath . $dirName . '/' . $filename)) { + // 当前路径是文件夹 + $this->addFileZip($basePath, $dirName . '/' . $filename, $zip); + } else { + // 写入文件到压缩包 + $zip->addFile($basePath . $dirName . '/' . $filename, $dirName . '/' . $filename); + } + } + } + closedir($handler); + } + + + /** + * @notes 返回压缩包临时路径 + * @return mixed + * @author 段誉 + * @date 2022/6/24 9:41 + */ + public function getDownloadUrl() + { + $vars = ['file' => $this->zipTempName]; + cache('curd_file_name' . $this->zipTempName, $this->zipTempName, 3600); + return (string)url("adminapi/tools.generator/download", $vars, false, true); + } + +} \ No newline at end of file diff --git a/server/app/common/service/generator/core/BaseGenerator.php b/server/app/common/service/generator/core/BaseGenerator.php new file mode 100644 index 0000000..dcf17ce --- /dev/null +++ b/server/app/common/service/generator/core/BaseGenerator.php @@ -0,0 +1,483 @@ +basePath = base_path(); + $this->rootPath = root_path(); + $this->templateDir = $this->basePath . 'common/service/generator/stub/'; + $this->generatorDir = $this->rootPath . 'runtime/generate/'; + $this->checkDir($this->generatorDir); + } + + + /** + * @notes 初始化表表数据 + * @param array $tableData + * @author 段誉 + * @date 2022/6/22 18:03 + */ + public function initGenerateData(array $tableData) + { + // 设置当前表信息 + $this->setTableData($tableData); + // 设置模块名 + $this->setModuleName($tableData['module_name']); + // 设置类目录 + $this->setClassDir($tableData['class_dir'] ?? ''); + // 替换模板变量 + $this->replaceVariables(); + } + + + /** + * @notes 菜单配置 + * @author 段誉 + * @date 2022/12/13 15:14 + */ + public function setMenuConfig() + { + $this->menuConfig = [ + 'pid' => $this->tableData['menu']['pid'] ?? 0, + 'type' => $this->tableData['menu']['type'] ?? GeneratorEnum::DELETE_TRUE, + 'name' => $this->tableData['menu']['name'] ?? $this->tableData['table_comment'] + ]; + } + + + /** + * @notes 删除配置 + * @return array + * @author 段誉 + * @date 2022/12/13 15:09 + */ + public function setDeleteConfig() + { + $this->deleteConfig = [ + 'type' => $this->tableData['delete']['type'] ?? GeneratorEnum::DELETE_TRUE, + 'name' => $this->tableData['delete']['name'] ?? GeneratorEnum::DELETE_NAME, + ]; + } + + + /** + * @notes 关联模型配置 + * @author 段誉 + * @date 2022/12/14 11:28 + */ + public function setRelationConfig() + { + $this->relationConfig = empty($this->tableData['relations']) ? [] : $this->tableData['relations']; + } + + + /** + * @notes 设置树表配置 + * @author 段誉 + * @date 2022/12/20 14:30 + */ + public function setTreeConfig() + { + $this->treeConfig = [ + 'tree_id' => $this->tableData['tree']['tree_id'] ?? '', + 'tree_pid' => $this->tableData['tree']['tree_pid'] ?? '', + 'tree_name' => $this->tableData['tree']['tree_name'] ?? '', + ]; + } + + + /** + * @notes 生成文件到模块或runtime目录 + * @author 段誉 + * @date 2022/6/22 18:03 + */ + public function generate() + { + //生成方式 0-压缩包下载 1-生成到模块 + if ($this->tableData['generate_type']) { + // 生成路径 + $path = $this->getModuleGenerateDir() . $this->getGenerateName(); + } else { + // 生成到runtime目录 + $path = $this->getRuntimeGenerateDir() . $this->getGenerateName(); + } + // 写入内容 + file_put_contents($path, $this->content); + } + + + /** + * @notes 获取文件生成到模块的文件夹路径 + * @return mixed + * @author 段誉 + * @date 2022/6/22 18:05 + */ + abstract public function getModuleGenerateDir(); + + + /** + * @notes 获取文件生成到runtime的文件夹路径 + * @return mixed + * @author 段誉 + * @date 2022/6/22 18:05 + */ + abstract public function getRuntimeGenerateDir(); + + + /** + * @notes 替换模板变量 + * @return mixed + * @author 段誉 + * @date 2022/6/22 18:06 + */ + abstract public function replaceVariables(); + + + /** + * @notes 生成文件名 + * @return mixed + * @author 段誉 + * @date 2022/6/22 18:17 + */ + abstract public function getGenerateName(); + + + /** + * @notes 文件夹不存在则创建 + * @param string $path + * @author 段誉 + * @date 2022/6/22 18:07 + */ + public function checkDir(string $path) + { + !is_dir($path) && mkdir($path, 0755, true); + } + + + /** + * @notes 设置表信息 + * @param $tableData + * @author 段誉 + * @date 2022/6/22 18:07 + */ + public function setTableData($tableData) + { + $this->tableData = !empty($tableData) ? $tableData : []; + $this->tableColumn = $tableData['table_column'] ?? []; + // 菜单配置 + $this->setMenuConfig(); + // 删除配置 + $this->setDeleteConfig(); + // 关联模型配置 + $this->setRelationConfig(); + // 设置树表配置 + $this->setTreeConfig(); + } + + + /** + * @notes 设置模块名 + * @param string $moduleName + * @author 段誉 + * @date 2022/6/22 18:07 + */ + public function setModuleName(string $moduleName): void + { + $this->moduleName = strtolower($moduleName); + } + + + /** + * @notes 设置类目录 + * @param string $classDir + * @author 段誉 + * @date 2022/6/22 18:08 + */ + public function setClassDir(string $classDir): void + { + $this->classDir = $classDir; + } + + + /** + * @notes 设置生成文件内容 + * @param string $content + * @author 段誉 + * @date 2022/6/22 18:08 + */ + public function setContent(string $content): void + { + $this->content = $content; + } + + + /** + * @notes 获取模板路径 + * @param string $templateName + * @return string + * @author 段誉 + * @date 2022/6/22 18:09 + */ + public function getTemplatePath(string $templateName): string + { + return $this->templateDir . $templateName . '.stub'; + } + + + /** + * @notes 小驼峰命名 + * @return string + * @author 段誉 + * @date 2022/6/27 18:44 + */ + public function getLowerCamelName() + { + return Str::camel($this->getTableName()); + } + + + /** + * @notes 大驼峰命名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:09 + */ + public function getUpperCamelName() + { + return Str::studly($this->getTableName()); + } + + + /** + * @notes 表名小写 + * @return string + * @author 段誉 + * @date 2022/7/12 10:41 + */ + public function getLowerTableName() + { + return Str::lower($this->getTableName()); + } + + + /** + * @notes 获取表名 + * @return array|string|string[] + * @author 段誉 + * @date 2022/6/22 18:09 + */ + public function getTableName() + { + return get_no_prefix_table_name($this->tableData['table_name']); + } + + + /** + * @notes 获取表主键 + * @return mixed|string + * @author 段誉 + * @date 2022/6/22 18:09 + */ + public function getPkContent() + { + $pk = 'id'; + if (empty($this->tableColumn)) { + return $pk; + } + + foreach ($this->tableColumn as $item) { + if ($item['is_pk']) { + $pk = $item['column_name']; + } + } + return $pk; + } + + + /** + * @notes 获取作者信息 + * @return mixed|string + * @author 段誉 + * @date 2022/6/24 10:18 + */ + public function getAuthorContent() + { + return empty($this->tableData['author']) ? 'likeadmin' : $this->tableData['author']; + } + + + /** + * @notes 代码生成备注时间 + * @return false|string + * @author 段誉 + * @date 2022/6/24 10:28 + */ + public function getNoteDateContent() + { + return date('Y/m/d H:i'); + } + + + /** + * @notes 设置空额占位符 + * @param $content + * @param $blankpace + * @return string + * @author 段誉 + * @date 2022/6/22 18:09 + */ + public function setBlankSpace($content, $blankpace) + { + $content = explode(PHP_EOL, $content); + foreach ($content as $line => $text) { + $content[$line] = $blankpace . $text; + } + return (implode(PHP_EOL, $content)); + } + + + /** + * @notes 替换内容 + * @param $needReplace + * @param $waitReplace + * @param $template + * @return array|false|string|string[] + * @author 段誉 + * @date 2022/6/23 9:52 + */ + public function replaceFileData($needReplace, $waitReplace, $template) + { + return str_replace($needReplace, $waitReplace, file_get_contents($template)); + } + + + /** + * @notes 生成方式是否为压缩包 + * @return bool + * @author 段誉 + * @date 2022/6/23 17:02 + */ + public function isGenerateTypeZip() + { + return $this->tableData['generate_type'] == GeneratorEnum::GENERATE_TYPE_ZIP; + } + + + /** + * @notes 是否为树表crud + * @return bool + * @author 段誉 + * @date 2022/12/23 11:25 + */ + public function isTreeCrud() + { + return $this->tableData['template_type'] == GeneratorEnum::TEMPLATE_TYPE_TREE; + } + +} \ No newline at end of file diff --git a/server/app/common/service/generator/core/ControllerGenerator.php b/server/app/common/service/generator/core/ControllerGenerator.php new file mode 100644 index 0000000..f21e909 --- /dev/null +++ b/server/app/common/service/generator/core/ControllerGenerator.php @@ -0,0 +1,223 @@ +getNameSpaceContent(), + $this->getUseContent(), + $this->getClassCommentContent(), + $this->getUpperCamelName(), + $this->moduleName, + $this->getPackageNameContent(), + $this->getExtendsControllerContent(), + $this->tableData['class_comment'], + $this->getAuthorContent(), + $this->getNoteDateContent(), + ]; + + $templatePath = $this->getTemplatePath('php/controller'); + + // 替换内容 + $content = $this->replaceFileData($needReplace, $waitReplace, $templatePath); + + $this->setContent($content); + } + + + /** + * @notes 获取命名空间内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:10 + */ + public function getNameSpaceContent() + { + if (!empty($this->classDir)) { + return "namespace app\\" . $this->moduleName . "\\controller\\" . $this->classDir . ';'; + } + return "namespace app\\" . $this->moduleName . "\\controller;"; + } + + + /** + * @notes 获取use模板内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:10 + */ + public function getUseContent() + { + if ($this->moduleName == 'adminapi') { + $tpl = "use app\\" . $this->moduleName . "\\controller\\BaseAdminController;" . PHP_EOL; + } else { + $tpl = "use app\\common\\controller\\BaseLikeAdminController;" . PHP_EOL; + } + + if (!empty($this->classDir)) { + $tpl .= "use app\\" . $this->moduleName . "\\lists\\" . $this->classDir . "\\" . $this->getUpperCamelName() . "Lists;" . PHP_EOL . + "use app\\" . $this->moduleName . "\\logic\\" . $this->classDir . "\\" . $this->getUpperCamelName() . "Logic;" . PHP_EOL . + "use app\\" . $this->moduleName . "\\validate\\" . $this->classDir . "\\" . $this->getUpperCamelName() . "Validate;"; + } else { + $tpl .= "use app\\" . $this->moduleName . "\\lists\\" . $this->getUpperCamelName() . "Lists;" . PHP_EOL . + "use app\\" . $this->moduleName . "\\logic\\" . $this->getUpperCamelName() . "Logic;" . PHP_EOL . + "use app\\" . $this->moduleName . "\\validate\\" . $this->getUpperCamelName() . "Validate;"; + } + + return $tpl; + } + + + /** + * @notes 获取类描述内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:10 + */ + public function getClassCommentContent() + { + if (!empty($this->tableData['class_comment'])) { + $tpl = $this->tableData['class_comment'] . '控制器'; + } else { + $tpl = $this->getUpperCamelName() . '控制器'; + } + return $tpl; + } + + + /** + * @notes 获取包名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:10 + */ + public function getPackageNameContent() + { + return !empty($this->classDir) ? '\\' . $this->classDir : ''; + } + + + /** + * @notes 获取继承控制器 + * @return string + * @author 段誉 + * @date 2022/6/22 18:10 + */ + public function getExtendsControllerContent() + { + $tpl = 'BaseAdminController'; + if ($this->moduleName != 'adminapi') { + $tpl = 'BaseLikeAdminController'; + } + return $tpl; + } + + + /** + * @notes 获取文件生成到模块的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:10 + */ + public function getModuleGenerateDir() + { + $dir = $this->basePath . $this->moduleName . '/controller/'; + if (!empty($this->classDir)) { + $dir .= $this->classDir . '/'; + $this->checkDir($dir); + } + return $dir; + } + + + /** + * @notes 获取文件生成到runtime的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:11 + */ + public function getRuntimeGenerateDir() + { + $dir = $this->generatorDir . 'php/app/' . $this->moduleName . '/controller/'; + $this->checkDir($dir); + if (!empty($this->classDir)) { + $dir .= $this->classDir . '/'; + $this->checkDir($dir); + } + return $dir; + } + + + /** + * @notes 生成文件名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:11 + */ + public function getGenerateName() + { + return $this->getUpperCamelName() . 'Controller.php'; + } + + + /** + * @notes 文件信息 + * @return array + * @author 段誉 + * @date 2022/6/23 15:57 + */ + public function fileInfo(): array + { + return [ + 'name' => $this->getGenerateName(), + 'type' => 'php', + 'content' => $this->content + ]; + } + +} \ No newline at end of file diff --git a/server/app/common/service/generator/core/GenerateInterface.php b/server/app/common/service/generator/core/GenerateInterface.php new file mode 100644 index 0000000..74a7ceb --- /dev/null +++ b/server/app/common/service/generator/core/GenerateInterface.php @@ -0,0 +1,23 @@ +getNameSpaceContent(), + $this->getUseContent(), + $this->getClassCommentContent(), + $this->getUpperCamelName(), + $this->moduleName, + $this->getPackageNameContent(), + $this->getExtendsListsContent(), + $this->getPkContent(), + $this->getQueryConditionContent(), + $this->getFieldDataContent(), + $this->tableData['class_comment'], + $this->getAuthorContent(), + $this->getNoteDateContent(), + ]; + + $templatePath = $this->getTemplatePath('php/lists'); + if ($this->isTreeCrud()) { + // 插入树表相关 + array_push($needReplace, '{TREE_ID}', '{TREE_PID}'); + array_push($waitReplace, $this->treeConfig['tree_id'], $this->treeConfig['tree_pid']); + + $templatePath = $this->getTemplatePath('php/tree_lists'); + } + + // 替换内容 + $content = $this->replaceFileData($needReplace, $waitReplace, $templatePath); + + $this->setContent($content); + } + + + /** + * @notes 获取命名空间内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:12 + */ + public function getNameSpaceContent() + { + if (!empty($this->classDir)) { + return "namespace app\\" . $this->moduleName . "\\lists\\" . $this->classDir . ';'; + } + return "namespace app\\" . $this->moduleName . "\\lists;"; + } + + + /** + * @notes 获取use内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:12 + */ + public function getUseContent() + { + if ($this->moduleName == 'adminapi') { + $tpl = "use app\\" . $this->moduleName . "\\lists\\BaseAdminDataLists;" . PHP_EOL; + } else { + $tpl = "use app\\common\\lists\\BaseDataLists;" . PHP_EOL; + } + + if (!empty($this->classDir)) { + $tpl .= "use app\\common\\model\\" . $this->classDir . "\\" . $this->getUpperCamelName() . ';'; + } else { + $tpl .= "use app\\common\\model\\" . $this->getUpperCamelName() . ';'; + } + + return $tpl; + } + + + /** + * @notes 获取类描述 + * @return string + * @author 段誉 + * @date 2022/6/22 18:12 + */ + public function getClassCommentContent() + { + if (!empty($this->tableData['class_comment'])) { + $tpl = $this->tableData['class_comment'] . '列表'; + } else { + $tpl = $this->getUpperCamelName() . '列表'; + } + return $tpl; + } + + + /** + * @notes 获取包名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:12 + */ + public function getPackageNameContent() + { + return !empty($this->classDir) ? $this->classDir : ''; + } + + + /** + * @notes 获取继承控制器 + * @return string + * @author 段誉 + * @date 2022/6/22 18:12 + */ + public function getExtendsListsContent() + { + $tpl = 'BaseAdminDataLists'; + if ($this->moduleName != 'adminapi') { + $tpl = 'BaseDataLists'; + } + return $tpl; + } + + + /** + * @notes 获取查询条件内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:12 + */ + public function getQueryConditionContent() + { + $columnQuery = array_column($this->tableColumn, 'query_type'); + $query = array_unique($columnQuery); + + $conditon = ''; + + $specQueryHandle = ['between', 'like']; + + foreach ($query as $queryName) { + $columnValue = ''; + foreach ($this->tableColumn as $column) { + if (empty($column['query_type']) || $column['is_pk']) { + continue; + } + if ($queryName == $column['query_type'] && $column['is_query'] && !in_array($queryName, $specQueryHandle)) { + $columnValue .= "'" . $column['column_name'] . "', "; + } + } + if (!empty($columnValue)) { + $columnValue = substr($columnValue, 0, -2); + $conditon .= "'$queryName' => [" . trim($columnValue) . "]," . PHP_EOL; + } + } + + $likeColumn = ''; + $betweenColumn = ''; + $betweenTimeColumn = ''; + + // 另外处理between,like 等查询条件 + foreach ($this->tableColumn as $item) { + if (!$item['is_query']) { + continue; + } + // like + if ($item['query_type'] == 'like') { + $likeColumn .= "'" . $item['column_name'] . "', "; + continue; + } + // between + if ($item['query_type'] == 'between') { + if ($item['view_type'] == 'datetime') { + $betweenTimeColumn .= "'" . $item['column_name'] . "', "; + } else { + $betweenColumn .= "'" . $item['column_name'] . "', "; + } + } + } + + if (!empty($likeColumn)) { + $likeColumn = substr($likeColumn, 0, -2); + $conditon .= "'%like%' => " . "[" . trim($likeColumn) . "]," . PHP_EOL; + } + + if (!empty($betweenColumn)) { + $betweenColumn = substr($betweenColumn, 0, -2); + $conditon .= "'between' => " . "[" . trim($betweenColumn) . "]," . PHP_EOL; + } + + if (!empty($betweenTimeColumn)) { + $betweenTimeColumn = substr($betweenTimeColumn, 0, -2); + $conditon .= "'between_time' => " . "[" . trim($betweenTimeColumn) . "]," . PHP_EOL; + } + + $content = substr($conditon, 0, -1); + return $this->setBlankSpace($content, " "); + } + + + /** + * @notes 获取查询字段 + * @return false|string + * @author 段誉 + * @date 2022/6/22 18:13 + */ + public function getFieldDataContent() + { + $content = "'" . $this->getPkContent() . "', "; + $isExist = [$this->getPkContent()]; + foreach ($this->tableColumn as $column) { + if ($column['is_lists'] && !in_array($column['column_name'], $isExist)) { + $content .= "'" . $column['column_name'] . "', "; + $isExist[] = $column['column_name']; + } + + if ($this->isTreeCrud() && !in_array($column['column_name'], $isExist) + && in_array($column['column_name'], [$this->treeConfig['tree_id'], $this->treeConfig['tree_pid']]) + ) { + $content .= "'" . $column['column_name'] . "', "; + } + } + return substr($content, 0, -2); + } + + + /** + * @notes 获取文件生成到模块的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:13 + */ + public function getModuleGenerateDir() + { + $dir = $this->basePath . $this->moduleName . '/lists/'; + $this->checkDir($dir); + if (!empty($this->classDir)) { + $dir .= $this->classDir . '/'; + $this->checkDir($dir); + } + return $dir; + } + + + /** + * @notes 获取文件生成到runtime的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:13 + */ + public function getRuntimeGenerateDir() + { + $dir = $this->generatorDir . 'php/app/' . $this->moduleName . '/lists/'; + $this->checkDir($dir); + if (!empty($this->classDir)) { + $dir .= $this->classDir . '/'; + $this->checkDir($dir); + } + return $dir; + } + + + /** + * @notes 生成的文件名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:13 + */ + public function getGenerateName() + { + return $this->getUpperCamelName() . 'Lists.php'; + } + + + /** + * @notes 文件信息 + * @return array + * @author 段誉 + * @date 2022/6/23 15:57 + */ + public function fileInfo(): array + { + return [ + 'name' => $this->getGenerateName(), + 'type' => 'php', + 'content' => $this->content + ]; + } + + +} \ No newline at end of file diff --git a/server/app/common/service/generator/core/LogicGenerator.php b/server/app/common/service/generator/core/LogicGenerator.php new file mode 100644 index 0000000..c97ec1c --- /dev/null +++ b/server/app/common/service/generator/core/LogicGenerator.php @@ -0,0 +1,268 @@ +getNameSpaceContent(), + $this->getUseContent(), + $this->getClassCommentContent(), + $this->getUpperCamelName(), + $this->moduleName, + $this->getPackageNameContent(), + $this->getPkContent(), + $this->getCreateDataContent(), + $this->getUpdateDataContent(), + $this->tableData['class_comment'], + $this->getAuthorContent(), + $this->getNoteDateContent(), + ]; + + $templatePath = $this->getTemplatePath('php/logic'); + + // 替换内容 + $content = $this->replaceFileData($needReplace, $waitReplace, $templatePath); + + $this->setContent($content); + } + + + /** + * @notes 添加内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:14 + */ + public function getCreateDataContent() + { + $content = ''; + foreach ($this->tableColumn as $column) { + if (!$column['is_insert']) { + continue; + } + $content .= $this->addEditColumn($column); + } + if (empty($content)) { + return $content; + } + $content = substr($content, 0, -2); + return $this->setBlankSpace($content, " "); + } + + + /** + * @notes 编辑内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:14 + */ + public function getUpdateDataContent() + { + $columnContent = ''; + foreach ($this->tableColumn as $column) { + if (!$column['is_update']) { + continue; + } + $columnContent .= $this->addEditColumn($column); + } + + if (empty($columnContent)) { + return $columnContent; + } + + $columnContent = substr($columnContent, 0, -2); + $content = $columnContent; + return $this->setBlankSpace($content, " "); + } + + + /** + * @notes 添加编辑字段内容 + * @param $column + * @return mixed + * @author 段誉 + * @date 2022/6/27 15:37 + */ + public function addEditColumn($column) + { + if ($column['column_type'] == 'int' && $column['view_type'] == 'datetime') { + // 物理类型为int,显示类型选择日期的情况 + $content = "'" . $column['column_name'] . "' => " . 'strtotime($params[' . "'" . $column['column_name'] . "'" . ']),' . PHP_EOL; + } else { + $content = "'" . $column['column_name'] . "' => " . '$params[' . "'" . $column['column_name'] . "'" . '],' . PHP_EOL; + } + return $content; + } + + + /** + * @notes 获取命名空间内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:14 + */ + public function getNameSpaceContent() + { + if (!empty($this->classDir)) { + return "namespace app\\" . $this->moduleName . "\\logic\\" . $this->classDir . ';'; + } + return "namespace app\\" . $this->moduleName . "\\logic;"; + } + + + /** + * @notes 获取use内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:14 + */ + public function getUseContent() + { + $tpl = "use app\\common\\model\\" . $this->getUpperCamelName() . ';'; + if (!empty($this->classDir)) { + $tpl = "use app\\common\\model\\" . $this->classDir . "\\" . $this->getUpperCamelName() . ';'; + } + return $tpl; + } + + + /** + * @notes 获取类描述 + * @return string + * @author 段誉 + * @date 2022/6/22 18:14 + */ + public function getClassCommentContent() + { + if (!empty($this->tableData['class_comment'])) { + $tpl = $this->tableData['class_comment'] . '逻辑'; + } else { + $tpl = $this->getUpperCamelName() . '逻辑'; + } + return $tpl; + } + + + /** + * @notes 获取包名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:14 + */ + public function getPackageNameContent() + { + return !empty($this->classDir) ? '\\' . $this->classDir : ''; + } + + + /** + * @notes 获取文件生成到模块的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:15 + */ + public function getModuleGenerateDir() + { + $dir = $this->basePath . $this->moduleName . '/logic/'; + if (!empty($this->classDir)) { + $dir .= $this->classDir . '/'; + $this->checkDir($dir); + } + return $dir; + } + + + /** + * @notes 获取文件生成到runtime的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:15 + */ + public function getRuntimeGenerateDir() + { + $dir = $this->generatorDir . 'php/app/' . $this->moduleName . '/logic/'; + $this->checkDir($dir); + if (!empty($this->classDir)) { + $dir .= $this->classDir . '/'; + $this->checkDir($dir); + } + return $dir; + } + + + /** + * @notes 生成的文件名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:15 + */ + public function getGenerateName() + { + return $this->getUpperCamelName() . 'Logic.php'; + } + + + /** + * @notes 文件信息 + * @return array + * @author 段誉 + * @date 2022/6/23 15:57 + */ + public function fileInfo(): array + { + return [ + 'name' => $this->getGenerateName(), + 'type' => 'php', + 'content' => $this->content + ]; + } + + +} \ No newline at end of file diff --git a/server/app/common/service/generator/core/ModelGenerator.php b/server/app/common/service/generator/core/ModelGenerator.php new file mode 100644 index 0000000..3d32ff4 --- /dev/null +++ b/server/app/common/service/generator/core/ModelGenerator.php @@ -0,0 +1,275 @@ +getNameSpaceContent(), + $this->getClassCommentContent(), + $this->getUpperCamelName(), + $this->getPackageNameContent(), + $this->getTableName(), + $this->getUseContent(), + $this->getDeleteUseContent(), + $this->getDeleteTimeContent(), + $this->getRelationModel(), + ]; + + $templatePath = $this->getTemplatePath('php/model'); + + // 替换内容 + $content = $this->replaceFileData($needReplace, $waitReplace, $templatePath); + + $this->setContent($content); + } + + + /** + * @notes 获取命名空间模板内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:16 + */ + public function getNameSpaceContent() + { + if (!empty($this->classDir)) { + return "namespace app\\common\\model\\" . $this->classDir . ';'; + } + return "namespace app\\common\\model;"; + } + + + /** + * @notes 获取类描述 + * @return string + * @author 段誉 + * @date 2022/6/22 18:16 + */ + public function getClassCommentContent() + { + if (!empty($this->tableData['class_comment'])) { + $tpl = $this->tableData['class_comment'] . '模型'; + } else { + $tpl = $this->getUpperCamelName() . '模型'; + } + return $tpl; + } + + + /** + * @notes 获取包名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:16 + */ + public function getPackageNameContent() + { + return !empty($this->classDir) ? '\\' . $this->classDir : ''; + } + + + /** + * @notes 引用内容 + * @return string + * @author 段誉 + * @date 2022/12/12 17:32 + */ + public function getUseContent() + { + $tpl = ""; + if ($this->deleteConfig['type']) { + $tpl = "use think\\model\\concern\\SoftDelete;"; + } + return $tpl; + } + + + /** + * @notes 软删除引用 + * @return string + * @author 段誉 + * @date 2022/12/12 17:34 + */ + public function getDeleteUseContent() + { + $tpl = ""; + if ($this->deleteConfig['type']) { + $tpl = "use SoftDelete;"; + } + return $tpl; + } + + + /** + * @notes 软删除时间字段定义 + * @return string + * @author 段誉 + * @date 2022/12/12 17:38 + */ + public function getDeleteTimeContent() + { + $tpl = ""; + if ($this->deleteConfig['type']) { + $deleteTime = $this->deleteConfig['name']; + $tpl = 'protected $deleteTime = ' . "'". $deleteTime ."';"; + } + return $tpl; + } + + + /** + * @notes 关联模型 + * @return string + * @author 段誉 + * @date 2022/12/14 14:46 + */ + public function getRelationModel() + { + $tpl = ''; + if (empty($this->relationConfig)) { + return $tpl; + } + + // 遍历关联配置 + foreach ($this->relationConfig as $config) { + if (empty($config) || empty($config['name']) || empty($config['model'])) { + continue; + } + + $needReplace = [ + '{RELATION_NAME}', + '{AUTHOR}', + '{DATE}', + '{RELATION_MODEL}', + '{FOREIGN_KEY}', + '{LOCAL_KEY}', + ]; + + $waitReplace = [ + $config['name'], + $this->getAuthorContent(), + $this->getNoteDateContent(), + $config['model'], + $config['foreign_key'], + $config['local_key'], + ]; + + $templatePath = $this->getTemplatePath('php/model/' . $config['type']); + if (!file_exists($templatePath)) { + continue; + } + $tpl .= $this->replaceFileData($needReplace, $waitReplace, $templatePath) . PHP_EOL; + } + + return $tpl; + } + + + /** + * @notes 获取文件生成到模块的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:16 + */ + public function getModuleGenerateDir() + { + $dir = $this->basePath . 'common/model/'; + if (!empty($this->classDir)) { + $dir .= $this->classDir . '/'; + $this->checkDir($dir); + } + return $dir; + } + + + /** + * @notes 获取文件生成到runtime的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:17 + */ + public function getRuntimeGenerateDir() + { + $dir = $this->generatorDir . 'php/app/common/model/'; + $this->checkDir($dir); + if (!empty($this->classDir)) { + $dir .= $this->classDir . '/'; + $this->checkDir($dir); + } + return $dir; + } + + + /** + * @notes 生成的文件名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:17 + */ + public function getGenerateName() + { + return $this->getUpperCamelName() . '.php'; + } + + + /** + * @notes 文件信息 + * @return array + * @author 段誉 + * @date 2022/6/23 15:57 + */ + public function fileInfo(): array + { + return [ + 'name' => $this->getGenerateName(), + 'type' => 'php', + 'content' => $this->content + ]; + } + + +} \ No newline at end of file diff --git a/server/app/common/service/generator/core/SqlGenerator.php b/server/app/common/service/generator/core/SqlGenerator.php new file mode 100644 index 0000000..442ed3b --- /dev/null +++ b/server/app/common/service/generator/core/SqlGenerator.php @@ -0,0 +1,191 @@ +getMenuTableNameContent(), + $this->menuConfig['pid'], + $this->menuConfig['name'], + $this->getPermsNameContent(), + $this->getLowerTableName(), + $this->getLowerTableName(), + time(), + time() + ]; + + $templatePath = $this->getTemplatePath('sql/sql'); + + // 替换内容 + $content = $this->replaceFileData($needReplace, $waitReplace, $templatePath); + + $this->setContent($content); + } + + + /** + * @notes 路由权限内容 + * @return string + * @author 段誉 + * @date 2022/8/11 17:18 + */ + public function getPermsNameContent() + { + if (!empty($this->classDir)) { + return $this->classDir . '.' . Str::lower($this->getTableName()); + } + return Str::lower($this->getTableName()); + } + + + /** + * @notes 获取菜单表内容 + * @return string + * @author 段誉 + * @date 2022/7/7 15:57 + */ + public function getMenuTableNameContent() + { + $tablePrefix = config('database.connections.mysql.prefix'); + return $tablePrefix . 'system_menu'; + } + + + /** + * @notes 是否构建菜单 + * @return bool + * @author 段誉 + * @date 2022/7/8 14:24 + */ + public function isBuildMenu() + { + return $this->menuConfig['type'] == GeneratorEnum::GEN_AUTO; + } + + + /** + * @notes 构建菜单 + * @return bool + * @author 段誉 + * @date 2022/7/8 15:27 + */ + public function buildMenuHandle() + { + if (empty($this->content)) { + return false; + } + $sqls = explode(';', trim($this->content)); + //执行sql + foreach ($sqls as $sql) { + if (!empty(trim($sql))) { + Db::execute($sql . ';'); + } + } + return true; + } + + + /** + * @notes 获取文件生成到模块的文件夹路径 + * @return mixed|void + * @author 段誉 + * @date 2022/6/22 18:19 + */ + public function getModuleGenerateDir() + { + $dir = $this->generatorDir . 'sql/'; + $this->checkDir($dir); + return $dir; + } + + + /** + * @notes 获取文件生成到runtime的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:20 + */ + public function getRuntimeGenerateDir() + { + $dir = $this->generatorDir . 'sql/'; + $this->checkDir($dir); + return $dir; + } + + + /** + * @notes 生成的文件名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:20 + */ + public function getGenerateName() + { + return 'menu.sql'; + } + + + /** + * @notes 文件信息 + * @return array + * @author 段誉 + * @date 2022/6/23 15:57 + */ + public function fileInfo(): array + { + return [ + 'name' => $this->getGenerateName(), + 'type' => 'sql', + 'content' => $this->content + ]; + } + + +} \ No newline at end of file diff --git a/server/app/common/service/generator/core/ValidateGenerator.php b/server/app/common/service/generator/core/ValidateGenerator.php new file mode 100644 index 0000000..5366868 --- /dev/null +++ b/server/app/common/service/generator/core/ValidateGenerator.php @@ -0,0 +1,278 @@ +getNameSpaceContent(), + $this->getClassCommentContent(), + $this->getUpperCamelName(), + $this->moduleName, + $this->getPackageNameContent(), + $this->getPkContent(), + $this->getRuleContent(), + $this->tableData['class_comment'], + $this->getAuthorContent(), + $this->getNoteDateContent(), + $this->getAddParamsContent(), + $this->getEditParamsContent(), + $this->getFiledContent(), + ]; + + $templatePath = $this->getTemplatePath('php/validate'); + + // 替换内容 + $content = $this->replaceFileData($needReplace, $waitReplace, $templatePath); + + $this->setContent($content); + } + + + /** + * @notes 验证规则 + * @return mixed|string + * @author 段誉 + * @date 2022/6/22 18:18 + */ + public function getRuleContent() + { + $content = "'" . $this->getPkContent() . "' => 'require'," . PHP_EOL; + foreach ($this->tableColumn as $column) { + if ($column['is_required'] == 1) { + $content .= "'" . $column['column_name'] . "' => 'require'," . PHP_EOL; + } + } + $content = substr($content, 0, -1); + return $this->setBlankSpace($content, " "); + } + + + /** + * @notes 添加场景验证参数 + * @return string + * @author 段誉 + * @date 2022/12/7 15:26 + */ + public function getAddParamsContent() + { + $content = ""; + foreach ($this->tableColumn as $column) { + if ($column['is_required'] == 1 && $column['column_name'] != $this->getPkContent()) { + $content .= "'" . $column['column_name'] . "',"; + } + } + $content = substr($content, 0, -1); + + // 若无设置添加场景校验字段时, 排除主键 + if (!empty($content)) { + $content = 'return $this->only([' . $content . ']);'; + } else { + $content = 'return $this->remove(' . "'". $this->getPkContent() . "'" . ', true);'; + } + + return $this->setBlankSpace($content, ""); + } + + + /** + * @notes 编辑场景验证参数 + * @return string + * @author 段誉 + * @date 2022/12/7 15:20 + */ + public function getEditParamsContent() + { + $content = "'" . $this->getPkContent() . "'," ; + foreach ($this->tableColumn as $column) { + if ($column['is_required'] == 1) { + $content .= "'" . $column['column_name'] . "',"; + } + } + $content = substr($content, 0, -1); + if (!empty($content)) { + $content = 'return $this->only([' . $content . ']);'; + } + return $this->setBlankSpace($content, ""); + } + + + /** + * @notes 验证字段描述 + * @return string + * @author 段誉 + * @date 2022/12/9 15:09 + */ + public function getFiledContent() + { + $content = "'" . $this->getPkContent() . "' => '" . $this->getPkContent() . "'," . PHP_EOL; + foreach ($this->tableColumn as $column) { + if ($column['is_required'] == 1) { + $columnComment = $column['column_comment']; + if (empty($column['column_comment'])) { + $columnComment = $column['column_name']; + } + $content .= "'" . $column['column_name'] . "' => '" . $columnComment . "'," . PHP_EOL; + } + } + $content = substr($content, 0, -1); + return $this->setBlankSpace($content, " "); + } + + + /** + * @notes 获取命名空间模板内容 + * @return string + * @author 段誉 + * @date 2022/6/22 18:18 + */ + public function getNameSpaceContent() + { + if (!empty($this->classDir)) { + return "namespace app\\" . $this->moduleName . "\\validate\\" . $this->classDir . ';'; + } + return "namespace app\\" . $this->moduleName . "\\validate;"; + } + + + /** + * @notes 获取类描述 + * @return string + * @author 段誉 + * @date 2022/6/22 18:18 + */ + public function getClassCommentContent() + { + if (!empty($this->tableData['class_comment'])) { + $tpl = $this->tableData['class_comment'] . '验证器'; + } else { + $tpl = $this->getUpperCamelName() . '验证器'; + } + return $tpl; + } + + + /** + * @notes 获取包名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:18 + */ + public function getPackageNameContent() + { + return !empty($this->classDir) ? '\\' . $this->classDir : ''; + } + + + /** + * @notes 获取文件生成到模块的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:18 + */ + public function getModuleGenerateDir() + { + $dir = $this->basePath . $this->moduleName . '/validate/'; + if (!empty($this->classDir)) { + $dir .= $this->classDir . '/'; + $this->checkDir($dir); + } + return $dir; + } + + + /** + * @notes 获取文件生成到runtime的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:18 + */ + public function getRuntimeGenerateDir() + { + $dir = $this->generatorDir . 'php/app/' . $this->moduleName . '/validate/'; + $this->checkDir($dir); + if (!empty($this->classDir)) { + $dir .= $this->classDir . '/'; + $this->checkDir($dir); + } + return $dir; + } + + + /** + * @notes 生成的文件名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:19 + */ + public function getGenerateName() + { + return $this->getUpperCamelName() . 'Validate.php'; + } + + + /** + * @notes 文件信息 + * @return array + * @author 段誉 + * @date 2022/6/23 15:57 + */ + public function fileInfo(): array + { + return [ + 'name' => $this->getGenerateName(), + 'type' => 'php', + 'content' => $this->content + ]; + } + + +} \ No newline at end of file diff --git a/server/app/common/service/generator/core/VueApiGenerator.php b/server/app/common/service/generator/core/VueApiGenerator.php new file mode 100644 index 0000000..37101e0 --- /dev/null +++ b/server/app/common/service/generator/core/VueApiGenerator.php @@ -0,0 +1,144 @@ +getCommentContent(), + $this->getUpperCamelName(), + $this->getRouteContent(), + ]; + + $templatePath = $this->getTemplatePath('vue/api'); + + // 替换内容 + $content = $this->replaceFileData($needReplace, $waitReplace, $templatePath); + + $this->setContent($content); + } + + + /** + * @notes 描述 + * @return mixed + * @author 段誉 + * @date 2022/6/22 18:19 + */ + public function getCommentContent() + { + return $this->tableData['table_comment']; + } + + + /** + * @notes 路由名称 + * @return array|string|string[] + * @author 段誉 + * @date 2022/6/22 18:19 + */ + public function getRouteContent() + { + $content = $this->getTableName(); + if (!empty($this->classDir)) { + $content = $this->classDir . '.' . $this->getTableName(); + } + return Str::lower($content); + } + + + /** + * @notes 获取文件生成到模块的文件夹路径 + * @return mixed|void + * @author 段誉 + * @date 2022/6/22 18:19 + */ + public function getModuleGenerateDir() + { + $dir = dirname(app()->getRootPath()) . '/admin/src/api/'; + $this->checkDir($dir); + return $dir; + } + + + /** + * @notes 获取文件生成到runtime的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:20 + */ + public function getRuntimeGenerateDir() + { + $dir = $this->generatorDir . 'vue/src/api/'; + $this->checkDir($dir); + return $dir; + } + + + /** + * @notes 生成的文件名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:20 + */ + public function getGenerateName() + { + return $this->getLowerTableName() . '.ts'; + } + + + /** + * @notes 文件信息 + * @return array + * @author 段誉 + * @date 2022/6/23 15:57 + */ + public function fileInfo(): array + { + return [ + 'name' => $this->getGenerateName(), + 'type' => 'ts', + 'content' => $this->content + ]; + } + + +} \ No newline at end of file diff --git a/server/app/common/service/generator/core/VueEditGenerator.php b/server/app/common/service/generator/core/VueEditGenerator.php new file mode 100644 index 0000000..9a1fcea --- /dev/null +++ b/server/app/common/service/generator/core/VueEditGenerator.php @@ -0,0 +1,493 @@ +getFormViewContent(), + $this->getUpperCamelName(), + $this->getDictDataContent(), + $this->getDictDataApiContent(), + $this->getFormDataContent(), + $this->getFormValidateContent(), + $this->tableData['table_comment'], + $this->getPkContent(), + $this->getTableName(), + $this->getCheckBoxJoinContent(), + $this->getCheckBoxSplitContent(), + $this->getFormDateContent(), + $this->getLowerCamelName(), + $this->getImportListsContent(), + $this->getTreeConstContent(), + $this->getTreeListsContent(), + ]; + + $templatePath = $this->getTemplatePath('vue/edit'); + + // 替换内容 + $content = $this->replaceFileData($needReplace, $waitReplace, $templatePath); + + $this->setContent($content); + } + + + /** + * @notes 复选框处理 + * @return string + * @author 段誉 + * @date 2022/6/24 19:30 + */ + public function getCheckBoxJoinContent() + { + $content = ''; + foreach ($this->tableColumn as $column) { + if (empty($column['view_type']) || $column['is_pk']) { + continue; + } + if ($column['view_type'] != 'checkbox') { + continue; + } + $content .= $column['column_name'] . ': formData.' . $column['column_name'] . '.join(",")' . PHP_EOL; + } + if (!empty($content)) { + $content = substr($content, 0, -1); + } + return $content; + } + + + /** + * @notes 复选框处理 + * @return string + * @author 段誉 + * @date 2022/6/24 19:30 + */ + public function getCheckBoxSplitContent() + { + $content = ''; + foreach ($this->tableColumn as $column) { + if (empty($column['view_type']) || $column['is_pk']) { + continue; + } + if ($column['view_type'] != 'checkbox') { + continue; + } + $content .= '//@ts-ignore' . PHP_EOL; + $content .= 'data.' . $column['column_name'] . ' && ' .'(formData.' . $column['column_name'] . ' = String(data.' . $column['column_name'] . ').split(","))' . PHP_EOL; + } + if (!empty($content)) { + $content = substr($content, 0, -1); + } + return $this->setBlankSpace($content, ' '); + } + + + /** + * @notes 树表contst + * @return string + * @author 段誉 + * @date 2022/12/22 18:19 + */ + public function getTreeConstContent() + { + $content = ""; + if ($this->isTreeCrud()) { + $content = file_get_contents($this->getTemplatePath('vue/other_item/editTreeConst')); + } + return $content; + } + + + /** + * @notes 获取树表列表 + * @return string + * @author 段誉 + * @date 2022/12/22 18:26 + */ + public function getTreeListsContent() + { + $content = ''; + if (!$this->isTreeCrud()) { + return $content; + } + + $needReplace = [ + '{TREE_ID}', + '{TREE_NAME}', + '{UPPER_CAMEL_NAME}', + ]; + $waitReplace = [ + $this->treeConfig['tree_id'], + $this->treeConfig['tree_name'], + $this->getUpperCamelName(), + ]; + + $templatePath = $this->getTemplatePath('vue/other_item/editTreeLists'); + if (file_exists($templatePath)) { + $content = $this->replaceFileData($needReplace, $waitReplace, $templatePath); + } + + return $content; + } + + + /** + * @notes 表单日期处理 + * @return string + * @author 段誉 + * @date 2022/6/27 16:45 + */ + public function getFormDateContent() + { + $content = ''; + foreach ($this->tableColumn as $column) { + if (empty($column['view_type']) || $column['is_pk']) { + continue; + } + if ($column['view_type'] != 'datetime' || $column['column_type'] != 'int') { + continue; + } + $content .= '//@ts-ignore' . PHP_EOL; + $content .= 'formData.' . $column['column_name'] . ' = timeFormat(formData.' . $column['column_name'] . ','."'yyyy-mm-dd hh:MM:ss'".') ' . PHP_EOL; + } + if (!empty($content)) { + $content = substr($content, 0, -1); + } + return $this->setBlankSpace($content, ' '); + } + + + /** + * @notes 获取表单内容 + * @return string + * @author 段誉 + * @date 2022/6/23 11:57 + */ + public function getFormViewContent() + { + $content = ''; + foreach ($this->tableColumn as $column) { + if (!$column['is_insert'] || !$column['is_update'] || $column['is_pk']) { + continue; + } + $needReplace = [ + '{COLUMN_COMMENT}', + '{COLUMN_NAME}', + '{DICT_TYPE}', + ]; + $waitReplace = [ + $column['column_comment'], + $column['column_name'], + $column['dict_type'], + ]; + + $viewType = $column['view_type']; + // 树表,树状结构下拉框 + if ($this->isTreeCrud() && $column['column_name'] == $this->treeConfig['tree_pid']) { + $viewType = 'treeSelect'; + array_push($needReplace, '{TREE_ID}', '{TREE_NAME}'); + array_push($waitReplace, $this->treeConfig['tree_id'], $this->treeConfig['tree_name']); + } + + $templatePath = $this->getTemplatePath('vue/form_item/' . $viewType); + if (!file_exists($templatePath)) { + continue; + } + + // 单选框值处理 + if ($column['view_type'] == 'radio' || $column['view_type'] == 'select') { + $stubItemValue = 'item.value'; + $intFieldValue = ['tinyint', 'smallint', 'mediumint', 'int', 'integer', 'bigint']; + if (in_array($column['column_type'], $intFieldValue)) { + $stubItemValue = 'parseInt(item.value)'; + } + array_push($needReplace, '{ITEM_VALUE}'); + array_push($waitReplace, $stubItemValue); + } + + $content .= $this->replaceFileData($needReplace, $waitReplace, $templatePath) . PHP_EOL; + } + + if (!empty($content)) { + $content = substr($content, 0, -1); + } + + $content = $this->setBlankSpace($content, ' '); + return $content; + } + + + /** + * @notes 获取字典数据内容 + * @return string + * @author 段誉 + * @date 2022/6/23 11:58 + */ + public function getDictDataContent() + { + $content = ''; + $isExist = []; + foreach ($this->tableColumn as $column) { + if (empty($column['dict_type']) || $column['is_pk']) { + continue; + } + if (in_array($column['dict_type'], $isExist)) { + continue; + } + $content .= $column['dict_type'] . ': ' . "[]," . PHP_EOL; + $isExist[] = $column['dict_type']; + } + if (!empty($content)) { + $content = substr($content, 0, -1); + } + return $this->setBlankSpace($content, ' '); + } + + + /** + * @notes 获取字典数据api内容 + * @return false|string + * @author 段誉 + * @date 2022/6/23 11:58 + */ + public function getDictDataApiContent() + { + $content = ''; + $isExist = []; + foreach ($this->tableColumn as $column) { + if (empty($column['dict_type']) || $column['is_pk']) { + continue; + } + if (in_array($column['dict_type'], $isExist)) { + continue; + } + $needReplace = [ + '{UPPER_CAMEL_NAME}', + '{DICT_TYPE}', + ]; + $waitReplace = [ + $this->getUpperCamelName(), + $column['dict_type'], + ]; + $templatePath = $this->getTemplatePath('vue/other_item/dictDataApi'); + if (!file_exists($templatePath)) { + continue; + } + $content .= $this->replaceFileData($needReplace, $waitReplace, $templatePath) . '' . PHP_EOL; + + $isExist[] = $column['dict_type']; + } + $content = substr($content, 0, -1); + return $content; + } + + + /** + * @notes 获取表单默认字段内容 + * @return string + * @author 段誉 + * @date 2022/6/23 15:15 + */ + public function getFormDataContent() + { + $content = ''; + $isExist = []; + foreach ($this->tableColumn as $column) { + if (!$column['is_insert'] || !$column['is_update'] || $column['is_pk']) { + continue; + } + if (in_array($column['column_name'], $isExist)) { + continue; + } + + // 复选框类型返回数组 + if ($column['view_type'] == 'checkbox') { + $content .= $column['column_name'] . ': ' . "[]," . PHP_EOL; + } else { + $content .= $column['column_name'] . ': ' . "''," . PHP_EOL; + } + + $isExist[] = $column['column_name']; + } + if (!empty($content)) { + $content = substr($content, 0, -1); + } + return $this->setBlankSpace($content, ' '); + } + + + /** + * @notes 表单验证内容 + * @return false|string + * @author 段誉 + * @date 2022/6/23 15:16 + */ + public function getFormValidateContent() + { + $content = ''; + $isExist = []; + $specDictType = ['input', 'textarea', 'editor']; + + foreach ($this->tableColumn as $column) { + if (!$column['is_required'] || $column['is_pk']) { + continue; + } + if (in_array($column['column_name'], $isExist)) { + continue; + } + + $validateMsg = in_array($column['view_type'], $specDictType) ? '请输入' : '请选择'; + $validateMsg .= $column['column_comment']; + + $needReplace = [ + '{COLUMN_NAME}', + '{VALIDATE_MSG}', + ]; + $waitReplace = [ + $column['column_name'], + $validateMsg, + ]; + $templatePath = $this->getTemplatePath('vue/other_item/formValidate'); + if (!file_exists($templatePath)) { + continue; + } + + $content .= $this->replaceFileData($needReplace, $waitReplace, $templatePath) . ',' . PHP_EOL; + + $isExist[] = $column['column_name']; + } + $content = substr($content, 0, -2); + return $content; + } + + + /** + * @notes 树表时导入列表 + * @author 段誉 + * @date 2022/12/23 9:56 + */ + public function getImportListsContent() + { + $content = ""; + if ($this->isTreeCrud()) { + $content = "api". $this->getUpperCamelName(). 'Lists,'; + } + + if (empty($content)) { + return $content; + } + + return $this->setBlankSpace($content, ' '); + } + + + /** + * @notes 获取文件生成到模块的文件夹路径 + * @return mixed|void + * @author 段誉 + * @date 2022/6/22 18:19 + */ + public function getModuleGenerateDir() + { + $dir = dirname(app()->getRootPath()) . '/admin/src/views/' . $this->getTableName() . '/'; + $this->checkDir($dir); + return $dir; + } + + + /** + * @notes 获取文件生成到runtime的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:20 + */ + public function getRuntimeGenerateDir() + { + $dir = $this->generatorDir . 'vue/src/views/' . $this->getTableName() . '/'; + $this->checkDir($dir); + return $dir; + } + + + /** + * @notes 生成的文件名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:20 + */ + public function getGenerateName() + { + return 'edit.vue'; + } + + + /** + * @notes 文件信息 + * @return array + * @author 段誉 + * @date 2022/6/23 15:57 + */ + public function fileInfo(): array + { + return [ + 'name' => $this->getGenerateName(), + 'type' => 'vue', + 'content' => $this->content + ]; + } + + +} \ No newline at end of file diff --git a/server/app/common/service/generator/core/VueIndexGenerator.php b/server/app/common/service/generator/core/VueIndexGenerator.php new file mode 100644 index 0000000..a3c5b4f --- /dev/null +++ b/server/app/common/service/generator/core/VueIndexGenerator.php @@ -0,0 +1,308 @@ +getSearchViewContent(), + $this->getListsViewContent(), + $this->getUpperCamelName(), + $this->getQueryParamsContent(), + $this->getDictDataContent(), + $this->getPkContent(), + $this->getTableName(), + $this->getPermsContent(), + $this->getPermsContent('edit'), + $this->getPermsContent('delete'), + $this->getLowerCamelName() + ]; + + $templatePath = $this->getTemplatePath('vue/index'); + + if ($this->isTreeCrud()) { + // 插入树表相关 + array_push($needReplace, '{TREE_ID}', '{TREE_PID}'); + array_push($waitReplace, $this->treeConfig['tree_id'], $this->treeConfig['tree_pid']); + $templatePath = $this->getTemplatePath('vue/index-tree'); + } + + // 替换内容 + $content = $this->replaceFileData($needReplace, $waitReplace, $templatePath); + + $this->setContent($content); + } + + + /** + * @notes 获取搜索内容 + * @return string + * @author 段誉 + * @date 2022/6/23 11:57 + */ + public function getSearchViewContent() + { + $content = ''; + foreach ($this->tableColumn as $column) { + if (!$column['is_query'] || $column['is_pk']) { + continue; + } + + $needReplace = [ + '{COLUMN_COMMENT}', + '{COLUMN_NAME}', + '{DICT_TYPE}', + ]; + $waitReplace = [ + $column['column_comment'], + $column['column_name'], + $column['dict_type'], + ]; + + $searchStubType = $column['view_type']; + if ($column['view_type'] == 'radio') { + $searchStubType = 'select'; + } + + $templatePath = $this->getTemplatePath('vue/search_item/' . $searchStubType); + if (!file_exists($templatePath)) { + continue; + } + $content .= $this->replaceFileData($needReplace, $waitReplace, $templatePath) . PHP_EOL; + } + + if (!empty($content)) { + $content = substr($content, 0, -1); + } + + $content = $this->setBlankSpace($content, ' '); + return $content; + } + + + /** + * @notes 获取列表内容 + * @return string + * @author 段誉 + * @date 2022/6/23 11:57 + */ + public function getListsViewContent() + { + $content = ''; + foreach ($this->tableColumn as $column) { + if (!$column['is_lists']) { + continue; + } + + $needReplace = [ + '{COLUMN_COMMENT}', + '{COLUMN_NAME}', + '{DICT_TYPE}', + ]; + $waitReplace = [ + $column['column_comment'], + $column['column_name'], + $column['dict_type'], + ]; + + $templatePath = $this->getTemplatePath('vue/table_item/default'); + if ($column['view_type'] == 'imageSelect') { + $templatePath = $this->getTemplatePath('vue/table_item/image'); + } + if (in_array($column['view_type'], ['select', 'radio', 'checkbox'])) { + $templatePath = $this->getTemplatePath('vue/table_item/options'); + } + if ($column['column_type'] == 'int' && $column['view_type'] == 'datetime') { + $templatePath = $this->getTemplatePath('vue/table_item/datetime'); + } + if (!file_exists($templatePath)) { + continue; + } + + $content .= $this->replaceFileData($needReplace, $waitReplace, $templatePath) . PHP_EOL; + } + if (!empty($content)) { + $content = substr($content, 0, -1); + } + return $this->setBlankSpace($content, ' '); + } + + + /** + * @notes 获取查询条件内容 + * @return string + * @author 段誉 + * @date 2022/6/23 11:57 + */ + public function getQueryParamsContent() + { + $content = ''; + $queryDate = false; + foreach ($this->tableColumn as $column) { + if (!$column['is_query'] || $column['is_pk']) { + continue; + } + $content .= $column['column_name'] . ": ''," . PHP_EOL; + if ($column['query_type'] == 'between' && $column['view_type'] == 'datetime') { + $queryDate = true; + } + } + if ($queryDate) { + $content .= "start_time: ''," . PHP_EOL; + $content .= "end_time: ''," . PHP_EOL; + } + $content = substr($content, 0, -2); + return $this->setBlankSpace($content, ' '); + } + + + /** + * @notes 获取字典数据内容 + * @return string + * @author 段誉 + * @date 2022/6/23 11:58 + */ + public function getDictDataContent() + { + $content = ''; + $isExist = []; + foreach ($this->tableColumn as $column) { + if (empty($column['dict_type']) || $column['is_pk']) { + continue; + } + if (in_array($column['dict_type'], $isExist)) { + continue; + } + $content .= $column['dict_type'] .","; + $isExist[] = $column['dict_type']; + } + if (!empty($content)) { + $content = substr($content, 0, -1); + } + return $this->setBlankSpace($content, ''); + } + + + /** + * @notes 权限规则 + * @param string $type + * @return string + * @author 段誉 + * @date 2022/7/7 9:47 + */ + public function getPermsContent($type = 'add') + { + if (!empty($this->classDir)) { + $classDir = $this->classDir . '.'; + } else { + $classDir = ''; + } + return trim($classDir . $this->getLowerTableName() . '/' . $type); + } + + + /** + * @notes 获取文件生成到模块的文件夹路径 + * @return mixed|void + * @author 段誉 + * @date 2022/6/22 18:19 + */ + public function getModuleGenerateDir() + { + $dir = dirname(app()->getRootPath()) . '/admin/src/views/' . $this->getLowerTableName() . '/'; + $this->checkDir($dir); + return $dir; + } + + + /** + * @notes 获取文件生成到runtime的文件夹路径 + * @return string + * @author 段誉 + * @date 2022/6/22 18:20 + */ + public function getRuntimeGenerateDir() + { + $dir = $this->generatorDir . 'vue/src/views/' . $this->getLowerTableName() . '/'; + $this->checkDir($dir); + return $dir; + } + + + /** + * @notes 生成的文件名 + * @return string + * @author 段誉 + * @date 2022/6/22 18:20 + */ + public function getGenerateName() + { + return 'index.vue'; + } + + + /** + * @notes 文件信息 + * @return array + * @author 段誉 + * @date 2022/6/23 15:57 + */ + public function fileInfo(): array + { + return [ + 'name' => $this->getGenerateName(), + 'type' => 'vue', + 'content' => $this->content + ]; + } + + +} \ No newline at end of file diff --git a/server/app/common/service/generator/stub/php/controller.stub b/server/app/common/service/generator/stub/php/controller.stub new file mode 100644 index 0000000..44622d2 --- /dev/null +++ b/server/app/common/service/generator/stub/php/controller.stub @@ -0,0 +1,105 @@ +dataLists(new {UPPER_CAMEL_NAME}Lists()); + } + + + /** + * @notes 添加{NOTES} + * @return \think\response\Json + * @author {AUTHOR} + * @date {DATE} + */ + public function add() + { + $params = (new {UPPER_CAMEL_NAME}Validate())->post()->goCheck('add'); + $result = {UPPER_CAMEL_NAME}Logic::add($params); + if (true === $result) { + return $this->success('添加成功', [], 1, 1); + } + return $this->fail({UPPER_CAMEL_NAME}Logic::getError()); + } + + + /** + * @notes 编辑{NOTES} + * @return \think\response\Json + * @author {AUTHOR} + * @date {DATE} + */ + public function edit() + { + $params = (new {UPPER_CAMEL_NAME}Validate())->post()->goCheck('edit'); + $result = {UPPER_CAMEL_NAME}Logic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail({UPPER_CAMEL_NAME}Logic::getError()); + } + + + /** + * @notes 删除{NOTES} + * @return \think\response\Json + * @author {AUTHOR} + * @date {DATE} + */ + public function delete() + { + $params = (new {UPPER_CAMEL_NAME}Validate())->post()->goCheck('delete'); + {UPPER_CAMEL_NAME}Logic::delete($params); + return $this->success('删除成功', [], 1, 1); + } + + + /** + * @notes 获取{NOTES}详情 + * @return \think\response\Json + * @author {AUTHOR} + * @date {DATE} + */ + public function detail() + { + $params = (new {UPPER_CAMEL_NAME}Validate())->goCheck('detail'); + $result = {UPPER_CAMEL_NAME}Logic::detail($params); + return $this->data($result); + } + + +} \ No newline at end of file diff --git a/server/app/common/service/generator/stub/php/lists.stub b/server/app/common/service/generator/stub/php/lists.stub new file mode 100644 index 0000000..ab69032 --- /dev/null +++ b/server/app/common/service/generator/stub/php/lists.stub @@ -0,0 +1,76 @@ +searchWhere) + ->field([{FIELD_DATA}]) + ->limit($this->limitOffset, $this->limitLength) + ->order(['{PK}' => 'desc']) + ->select() + ->toArray(); + } + + + /** + * @notes 获取{NOTES}数量 + * @return int + * @author {AUTHOR} + * @date {DATE} + */ + public function count(): int + { + return {UPPER_CAMEL_NAME}::where($this->searchWhere)->count(); + } + +} \ No newline at end of file diff --git a/server/app/common/service/generator/stub/php/logic.stub b/server/app/common/service/generator/stub/php/logic.stub new file mode 100644 index 0000000..4d4b7a6 --- /dev/null +++ b/server/app/common/service/generator/stub/php/logic.stub @@ -0,0 +1,106 @@ +getMessage()); + return false; + } + } + + + /** + * @notes 编辑{NOTES} + * @param array $params + * @return bool + * @author {AUTHOR} + * @date {DATE} + */ + public static function edit(array $params): bool + { + Db::startTrans(); + try { + {UPPER_CAMEL_NAME}::where('{PK}', $params['{PK}'])->update([ +{UPDATE_DATA} + ]); + + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + self::setError($e->getMessage()); + return false; + } + } + + + /** + * @notes 删除{NOTES} + * @param array $params + * @return bool + * @author {AUTHOR} + * @date {DATE} + */ + public static function delete(array $params): bool + { + return {UPPER_CAMEL_NAME}::destroy($params['{PK}']); + } + + + /** + * @notes 获取{NOTES}详情 + * @param $params + * @return array + * @author {AUTHOR} + * @date {DATE} + */ + public static function detail($params): array + { + return {UPPER_CAMEL_NAME}::findOrEmpty($params['{PK}'])->toArray(); + } +} \ No newline at end of file diff --git a/server/app/common/service/generator/stub/php/model.stub b/server/app/common/service/generator/stub/php/model.stub new file mode 100644 index 0000000..61acb2f --- /dev/null +++ b/server/app/common/service/generator/stub/php/model.stub @@ -0,0 +1,34 @@ +hasMany({RELATION_MODEL}::class, '{FOREIGN_KEY}', '{LOCAL_KEY}'); + } \ No newline at end of file diff --git a/server/app/common/service/generator/stub/php/model/has_one.stub b/server/app/common/service/generator/stub/php/model/has_one.stub new file mode 100644 index 0000000..21bd83b --- /dev/null +++ b/server/app/common/service/generator/stub/php/model/has_one.stub @@ -0,0 +1,11 @@ + + /** + * @notes 关联{RELATION_NAME} + * @return \think\model\relation\HasOne + * @author {AUTHOR} + * @date {DATE} + */ + public function {RELATION_NAME}() + { + return $this->hasOne({RELATION_MODEL}::class, '{FOREIGN_KEY}', '{LOCAL_KEY}'); + } \ No newline at end of file diff --git a/server/app/common/service/generator/stub/php/tree_lists.stub b/server/app/common/service/generator/stub/php/tree_lists.stub new file mode 100644 index 0000000..cee3870 --- /dev/null +++ b/server/app/common/service/generator/stub/php/tree_lists.stub @@ -0,0 +1,77 @@ +searchWhere) + ->field([{FIELD_DATA}]) + ->order(['{PK}' => 'desc']) + ->select() + ->toArray(); + + return linear_to_tree($lists, 'children', '{TREE_ID}', '{TREE_PID}'); + } + + + /** + * @notes 获取{NOTES}数量 + * @return int + * @author {AUTHOR} + * @date {DATE} + */ + public function count(): int + { + return {UPPER_CAMEL_NAME}::where($this->searchWhere)->count(); + } + +} \ No newline at end of file diff --git a/server/app/common/service/generator/stub/php/validate.stub b/server/app/common/service/generator/stub/php/validate.stub new file mode 100644 index 0000000..a984d47 --- /dev/null +++ b/server/app/common/service/generator/stub/php/validate.stub @@ -0,0 +1,94 @@ +only(['{PK}']); + } + + + /** + * @notes 详情场景 + * @return {UPPER_CAMEL_NAME}Validate + * @author {AUTHOR} + * @date {DATE} + */ + public function sceneDetail() + { + return $this->only(['{PK}']); + } + +} \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/api.stub b/server/app/common/service/generator/stub/vue/api.stub new file mode 100644 index 0000000..1858e9a --- /dev/null +++ b/server/app/common/service/generator/stub/vue/api.stub @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// {COMMENT}列表 +export function api{UPPER_CAMEL_NAME}Lists(params: any) { + return request.get({ url: '/{ROUTE}/lists', params }) +} + +// 添加{COMMENT} +export function api{UPPER_CAMEL_NAME}Add(params: any) { + return request.post({ url: '/{ROUTE}/add', params }) +} + +// 编辑{COMMENT} +export function api{UPPER_CAMEL_NAME}Edit(params: any) { + return request.post({ url: '/{ROUTE}/edit', params }) +} + +// 删除{COMMENT} +export function api{UPPER_CAMEL_NAME}Delete(params: any) { + return request.post({ url: '/{ROUTE}/delete', params }) +} + +// {COMMENT}详情 +export function api{UPPER_CAMEL_NAME}Detail(params: any) { + return request.get({ url: '/{ROUTE}/detail', params }) +} \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/edit.stub b/server/app/common/service/generator/stub/vue/edit.stub new file mode 100644 index 0000000..c0a69d2 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/edit.stub @@ -0,0 +1,103 @@ + + + diff --git a/server/app/common/service/generator/stub/vue/form_item/checkbox.stub b/server/app/common/service/generator/stub/vue/form_item/checkbox.stub new file mode 100644 index 0000000..98e6329 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/form_item/checkbox.stub @@ -0,0 +1,11 @@ + + + + {{ item.name }} + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/form_item/datetime.stub b/server/app/common/service/generator/stub/vue/form_item/datetime.stub new file mode 100644 index 0000000..48d0dfa --- /dev/null +++ b/server/app/common/service/generator/stub/vue/form_item/datetime.stub @@ -0,0 +1,10 @@ + + + + diff --git a/server/app/common/service/generator/stub/vue/form_item/datetime2.stub b/server/app/common/service/generator/stub/vue/form_item/datetime2.stub new file mode 100644 index 0000000..b182bd9 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/form_item/datetime2.stub @@ -0,0 +1,6 @@ + + + diff --git a/server/app/common/service/generator/stub/vue/form_item/editor.stub b/server/app/common/service/generator/stub/vue/form_item/editor.stub new file mode 100644 index 0000000..f150681 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/form_item/editor.stub @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/form_item/imageSelect.stub b/server/app/common/service/generator/stub/vue/form_item/imageSelect.stub new file mode 100644 index 0000000..22703fc --- /dev/null +++ b/server/app/common/service/generator/stub/vue/form_item/imageSelect.stub @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/form_item/input.stub b/server/app/common/service/generator/stub/vue/form_item/input.stub new file mode 100644 index 0000000..ffe3884 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/form_item/input.stub @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/form_item/radio.stub b/server/app/common/service/generator/stub/vue/form_item/radio.stub new file mode 100644 index 0000000..2768885 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/form_item/radio.stub @@ -0,0 +1,11 @@ + + + + {{ item.name }} + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/form_item/select.stub b/server/app/common/service/generator/stub/vue/form_item/select.stub new file mode 100644 index 0000000..5c2fa50 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/form_item/select.stub @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/form_item/textarea.stub b/server/app/common/service/generator/stub/vue/form_item/textarea.stub new file mode 100644 index 0000000..6f80318 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/form_item/textarea.stub @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/form_item/treeSelect.stub b/server/app/common/service/generator/stub/vue/form_item/treeSelect.stub new file mode 100644 index 0000000..54c5dd9 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/form_item/treeSelect.stub @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/index-tree.stub b/server/app/common/service/generator/stub/vue/index-tree.stub new file mode 100644 index 0000000..1058e7b --- /dev/null +++ b/server/app/common/service/generator/stub/vue/index-tree.stub @@ -0,0 +1,167 @@ + + + + diff --git a/server/app/common/service/generator/stub/vue/index.stub b/server/app/common/service/generator/stub/vue/index.stub new file mode 100644 index 0000000..3c9a71b --- /dev/null +++ b/server/app/common/service/generator/stub/vue/index.stub @@ -0,0 +1,123 @@ + + + + diff --git a/server/app/common/service/generator/stub/vue/other_item/dictDataApi.stub b/server/app/common/service/generator/stub/vue/other_item/dictDataApi.stub new file mode 100644 index 0000000..fb9176a --- /dev/null +++ b/server/app/common/service/generator/stub/vue/other_item/dictDataApi.stub @@ -0,0 +1,6 @@ + dictDataLists({ + type_value: '{DICT_TYPE}', + page_type: 0 + }).then((res: any) => { + dictData.{DICT_TYPE} = res.lists + }) \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/other_item/editTreeConst.stub b/server/app/common/service/generator/stub/vue/other_item/editTreeConst.stub new file mode 100644 index 0000000..e98eb7d --- /dev/null +++ b/server/app/common/service/generator/stub/vue/other_item/editTreeConst.stub @@ -0,0 +1 @@ +const treeList = ref([]) \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/other_item/editTreeLists.stub b/server/app/common/service/generator/stub/vue/other_item/editTreeLists.stub new file mode 100644 index 0000000..5de63f3 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/other_item/editTreeLists.stub @@ -0,0 +1,8 @@ +const getLists = async () => { + const data: any = await api{UPPER_CAMEL_NAME}Lists() + const item = { {TREE_ID}: 0, {TREE_NAME}: '顶级', children: [] } + item.children = data.lists + treeList.value.push(item) +} + +getLists() \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/other_item/formValidate.stub b/server/app/common/service/generator/stub/vue/other_item/formValidate.stub new file mode 100644 index 0000000..336052c --- /dev/null +++ b/server/app/common/service/generator/stub/vue/other_item/formValidate.stub @@ -0,0 +1,5 @@ + {COLUMN_NAME}: [{ + required: true, + message: '{VALIDATE_MSG}', + trigger: ['blur'] + }] \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/search_item/datetime.stub b/server/app/common/service/generator/stub/vue/search_item/datetime.stub new file mode 100644 index 0000000..e0e6882 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/search_item/datetime.stub @@ -0,0 +1,6 @@ + + + diff --git a/server/app/common/service/generator/stub/vue/search_item/input.stub b/server/app/common/service/generator/stub/vue/search_item/input.stub new file mode 100644 index 0000000..9c09034 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/search_item/input.stub @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/search_item/select.stub b/server/app/common/service/generator/stub/vue/search_item/select.stub new file mode 100644 index 0000000..854152f --- /dev/null +++ b/server/app/common/service/generator/stub/vue/search_item/select.stub @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/table_item/datetime.stub b/server/app/common/service/generator/stub/vue/table_item/datetime.stub new file mode 100644 index 0000000..2cbbc6d --- /dev/null +++ b/server/app/common/service/generator/stub/vue/table_item/datetime.stub @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/table_item/default.stub b/server/app/common/service/generator/stub/vue/table_item/default.stub new file mode 100644 index 0000000..54ae4c7 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/table_item/default.stub @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/table_item/image.stub b/server/app/common/service/generator/stub/vue/table_item/image.stub new file mode 100644 index 0000000..8ce9342 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/table_item/image.stub @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/server/app/common/service/generator/stub/vue/table_item/options.stub b/server/app/common/service/generator/stub/vue/table_item/options.stub new file mode 100644 index 0000000..8820cd8 --- /dev/null +++ b/server/app/common/service/generator/stub/vue/table_item/options.stub @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/server/app/common/service/pay/AliPayService.php b/server/app/common/service/pay/AliPayService.php new file mode 100644 index 0000000..ccf0cf9 --- /dev/null +++ b/server/app/common/service/pay/AliPayService.php @@ -0,0 +1,372 @@ +terminal = $terminal; + //初始化支付配置 + Factory::setOptions($this->getOptions()); + $this->pay = Factory::payment(); + } + + + /** + * @notes 支付设置 + * @return Config + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 段誉 + * @date 2021/7/28 17:43 + */ + public function getOptions() + { + $config = (new PayConfig())->where(['pay_way' => PayEnum::ALI_PAY])->find(); + if (empty($config)) { + throw new \Exception('请配置好支付设置'); + } + $options = new Config(); + $options->protocol = 'https'; + $options->gatewayHost = 'openapi.alipay.com'; +// $options->gatewayHost = 'openapi.alipaydev.com'; //测试沙箱地址 + $options->signType = 'RSA2'; + $options->appId = $config['config']['app_id'] ?? ''; + // 应用私钥 + $options->merchantPrivateKey = $config['config']['private_key'] ?? ''; + //接口加签方式 + // 秘钥模式 + if ($config['config']['mode'] == 'normal_mode') { + //支付宝公钥 + $options->alipayPublicKey = $config['config']['ali_public_key'] ?? ''; + } + + //证书模式 + if ($config['config']['mode'] == 'certificate') { + //判断是否已经存在证书文件夹,不存在则新建 + if (!file_exists(app()->getRootPath() . 'runtime/certificate')) { + mkdir(app()->getRootPath() . 'runtime/certificate', 0775, true); + } + //写入文件 + $publicCert = $config['config']['public_cert'] ?? ''; + $aliPublicCert = $config['config']['ali_public_cert'] ?? ''; + $aliRootCert = $config['config']['ali_root_cert'] ?? ''; + $publicCertPath = app()->getRootPath() . 'runtime/certificate/' . md5($publicCert) . '.crt'; + $aliPublicCertPath = app()->getRootPath() . 'runtime/certificate/' . md5($aliPublicCert) . '.crt'; + $aliRootCertPath = app()->getRootPath() . 'runtime/certificate/' . md5($aliRootCert) . '.crt'; + if (!file_exists($publicCertPath)) { + $fopenPublicCertPath = fopen($publicCertPath, 'w'); + fwrite($fopenPublicCertPath, $publicCert); + fclose($fopenPublicCertPath); + } + if (!file_exists($aliPublicCertPath)) { + $fopenAliPublicCertPath = fopen($aliPublicCertPath, 'w'); + fwrite($fopenAliPublicCertPath, $aliPublicCert); + fclose($fopenAliPublicCertPath); + } + if (!file_exists($aliRootCertPath)) { + $fopenAliRootCertPath = fopen($aliRootCertPath, 'w'); + fwrite($fopenAliRootCertPath, $aliRootCert); + fclose($fopenAliRootCertPath); + } + //应用公钥证书路径 + $options->merchantCertPath = $publicCertPath; + //支付宝公钥证书路径 + $options->alipayCertPath = $aliPublicCertPath; + //支付宝根证书路径 + $options->alipayRootCertPath = $aliRootCertPath; + } + //回调地址 + $options->notifyUrl = (string)url('pay/aliNotify', [], false, true); + return $options; + } + + + /** + * @notes 支付 + * @param $from //订单来源;order-商品订单;recharge-充值订单 + * @param $order //订单信息 + * @return false|string[] + * @author 段誉 + * @date 2021/8/13 17:08 + */ + public function pay($from, $order): array|bool + { + try { + $result = match ($this->terminal) { + UserTerminalEnum::PC => $this->pagePay($from, $order), + UserTerminalEnum::IOS, UserTerminalEnum::ANDROID => $this->appPay($from, $order), + UserTerminalEnum::WECHAT_OA, UserTerminalEnum::H5 => $this->wapPay($from, $order), + default => throw new \Exception('支付方式错误'), + }; + return [ + 'config' => $result, + 'pay_way' => PayEnum::ALI_PAY + ]; + } catch (\Exception $e) { + $this->error = $e->getMessage(); + return false; + } + } + + /** + * Notes: 支付回调 + * @param $data + * @return bool + * @author 段誉(2021/3/22 17:22) + */ + public function notify($data) + { + try { + $verify = $this->pay->common()->verifyNotify($data); + if (false === $verify) { + throw new \Exception('异步通知验签失败'); + } + if (!in_array($data['trade_status'], ['TRADE_SUCCESS', 'TRADE_FINISHED'])) { + return true; + } + $extra['transaction_id'] = $data['trade_no']; + //验证订单是否已支付 + switch ($data['passback_params']) { + case 'recharge': + $order = RechargeOrder::where(['sn' => $data['out_trade_no']])->findOrEmpty(); + if ($order->isEmpty() || $order->pay_status == PayEnum::ISPAID) { + return true; + } + PayNotifyLogic::handle('recharge', $data['out_trade_no'], $extra); + break; + } + + return true; + } catch (\Exception $e) { + $record = [ + __CLASS__, + __FUNCTION__, + $e->getFile(), + $e->getLine(), + $e->getMessage() + ]; + Log::write(implode('-', $record)); + $this->setError($e->getMessage()); + return false; + } + } + + /** + * @notes PC支付 + * @param $attach //附加参数(在回调时会返回) + * @param $order //订单信息 + * @return string + * @author 段誉 + * @date 2021/7/28 17:34 + */ + public function pagePay($attach, $order) + { + $domain = request()->domain(); + $result = $this->pay->page()->optional('passback_params', $attach)->pay( + '订单:' . $order['sn'], + $order['sn'], + $order['order_amount'], + $domain . $order['redirect_url'] + ); + return $result->body; + } + + /** + * @notes APP支付 + * @param $attach //附加参数(在回调时会返回) + * @param $order //订单信息 + * @return string + * @author 段誉 + * @date 2021/7/28 17:34 + */ + public function appPay($attach, $order) + { + $result = $this->pay->app()->optional('passback_params', $attach)->pay( + $order['sn'], + $order['sn'], + $order['order_amount'] + ); + return $result->body; + } + + /** + * @notes 手机网页支付 + * @param $attach //附加参数(在回调时会返回) + * @param $order //订单信息 + * @return string + * @author 段誉 + * @date 2021/7/28 17:34 + */ + public function wapPay($attach, $order) + { + $domain = request()->domain(); + $url = $domain . '/mobile' . $order['redirect_url'] .'?id=' . $order['id'] . '&from='. $attach . '&checkPay=true';; + $result = $this->pay->wap()->optional('passback_params', $attach)->pay( + '订单:' . $order['sn'], + $order['sn'], + $order['order_amount'], + $url, + $url + ); + return $result->body; + } + + /** + * @notes 查询订单 + * @param $orderSn + * @return \Alipay\EasySDK\Payment\Common\Models\AlipayTradeQueryResponse + * @throws \Exception + * @author 段誉 + * @date 2021/7/28 17:36 + */ + public function checkPay($orderSn) + { + return $this->pay->common()->query($orderSn); + } + + /** + * @notes 退款 + * @param $orderSn + * @param $orderAmount + * @param $outRequestNo + * @return \Alipay\EasySDK\Payment\Common\Models\AlipayTradeRefundResponse + * @throws \Exception + * @author 段誉 + * @date 2021/7/28 17:37 + */ + public function refund($orderSn, $orderAmount, $outRequestNo) + { + return $this->pay->common()->optional('out_request_no', $outRequestNo)->refund($orderSn, $orderAmount); + } + + /** + * @notes 查询退款 + * @author Tab + * @date 2021/9/13 11:38 + */ + public function queryRefund($orderSn, $refundSn) + { + return $this->pay->common()->queryRefund($orderSn, $refundSn); + } + + /** + * @notes 捕获错误 + * @param $result + * @throws \Exception + * @author 段誉 + * @date 2023/2/28 12:09 + */ + public function checkResultFail($result) + { + if (isset($result['alipay_trade_precreate_response']['code']) && 10000 != $result['alipay_trade_precreate_response']['code']) { + throw new \Exception('支付宝:' . $result['alipay_trade_precreate_response']['msg']); + } + } + + /** + * @notes 转账到支付宝账号 + * @param $withdraw + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author ljj + * @date 2023/10/9 10:58 上午 + */ + public function transfer($withdraw) + { + //请求参数 + $data = [ + 'out_biz_no' => $withdraw['sn'],//商家侧唯一订单号,由商家自定义。对于不同转账请求,商家需保证该订单号在自身系统唯一。 + 'trans_amount' => $withdraw['left_money'],//订单总金额,单位为元,不支持千位分隔符,精确到小数点后两位 + 'product_code' => 'TRANS_ACCOUNT_NO_PWD',//销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD。 + 'biz_scene' => 'DIRECT_TRANSFER',//业务场景。单笔无密转账固定为 DIRECT_TRANSFER。 + 'order_title' => '佣金提现',//转账业务的标题 + 'payee_info' => [//收款方信息 + 'identity' => $withdraw['account'],//参与方的标识 ID。当 identity_type=ALIPAY_USER_ID 时,填写支付宝用户 UID;当 identity_type=ALIPAY_LOGON_ID 时,填写支付宝登录号。 + 'identity_type' => 'ALIPAY_LOGON_ID',//参与方的标识类型。ALIPAY_USER_ID:支付宝会员的用户 ID;ALIPAY_LOGON_ID:支付宝登录号; + 'name' => $withdraw['real_name'],//参与方真实姓名。如果非空,将校验收款支付宝账号姓名一致性。当 identity_type=ALIPAY_LOGON_ID 时,本字段必填。 + ], + 'remark' => '',//业务备注 + ]; + + $result = Factory::util()->generic()->execute("alipay.fund.trans.uni.transfer", [], $data); + $result = json_decode($result->httpBody, true); + $result = $result['alipay_fund_trans_uni_transfer_response'] ?? []; + if ($result['code'] != 10000) {//接口调用失败 + throw new \Exception($result['sub_msg'] ?? $result['msg']); + } + return $result; + } + + /** + * @notes 转账查询 + * @param $withdraw + * @return mixed + * @throws \Exception + * @author ljj + * @date 2023/10/9 11:47 上午 + */ + public function transferQuery($withdraw) + { + //请求参数 + $data = [ + 'out_biz_no' => $withdraw['sn'],//商户转账唯一订单号:发起转账来源方定义的转账单据 ID。 + 'product_code' => 'TRANS_ACCOUNT_NO_PWD',//销售产品码,如果传了 out_biz_no,则该字段必传。单笔无密转账固定为TRANS_ACCOUNT_NO_PWD。 + 'biz_scene' => 'DIRECT_TRANSFER',//描述特定的业务场景,如果传递了out_biz_no 则该字段为必传。单笔无密转账固定为DIRECT_TRANSFER。 + ]; + + $result = Factory::util()->generic()->execute("alipay.fund.trans.common.query", [], $data); + $result = json_decode($result->httpBody, true); + return $result['alipay_fund_trans_common_query_response'] ?? []; + } +} + diff --git a/server/app/common/service/pay/BasePayService.php b/server/app/common/service/pay/BasePayService.php new file mode 100644 index 0000000..81c158f --- /dev/null +++ b/server/app/common/service/pay/BasePayService.php @@ -0,0 +1,98 @@ +error; + } + + + /** + * @notes 设置错误信息 + * @param $error + * @author 段誉 + * @date 2021/7/21 18:20 + */ + public function setError($error) + { + $this->error = $error; + } + + + /** + * @notes 是否存在错误 + * @return bool + * @author 段誉 + * @date 2021/7/21 18:32 + */ + public function hasError() + { + return !empty($this->error); + } + + + /** + * @notes 设置状态码 + * @param $code + * @author 段誉 + * @date 2021/7/28 17:05 + */ + public function setReturnCode($code) + { + $this->returnCode = $code; + } + + + /** + * @notes 特殊场景返回指定状态码,默认为0 + * @return int + * @author 段誉 + * @date 2021/7/28 15:14 + */ + public function getReturnCode() + { + return $this->returnCode; + } + +} \ No newline at end of file diff --git a/server/app/common/service/pay/WeChatPayService.php b/server/app/common/service/pay/WeChatPayService.php new file mode 100644 index 0000000..a1d1dcd --- /dev/null +++ b/server/app/common/service/pay/WeChatPayService.php @@ -0,0 +1,402 @@ +terminal = $terminal; + $this->config = WeChatConfigService::getPayConfigByTerminal($terminal); + $this->app = new Application($this->config); + if ($userId !== null) { + $this->auth = UserAuth::where(['user_id' => $userId, 'terminal' => $terminal])->findOrEmpty(); + } + } + + + /** + * @notes 发起微信支付统一下单 + * @param $from + * @param $order + * @return array|false|string + * @author 段誉 + * @date 2021/8/4 15:05 + */ + public function pay($from, $order) + { + try { + switch ($this->terminal) { + case UserTerminalEnum::WECHAT_MMP: + $config = WeChatConfigService::getMnpConfig(); + $result = $this->jsapiPay($from, $order, $config['app_id']); + break; + case UserTerminalEnum::WECHAT_OA: + $config = WeChatConfigService::getOaConfig(); + $result = $this->jsapiPay($from, $order, $config['app_id']); + break; + case UserTerminalEnum::IOS: + case UserTerminalEnum::ANDROID: + $config = WeChatConfigService::getOpConfig(); + $result = $this->appPay($from, $order, $config['app_id']); + break; + case UserTerminalEnum::H5: + $config = WeChatConfigService::getOaConfig(); + $result = $this->mwebPay($from, $order, $config['app_id']); + break; + case UserTerminalEnum::PC: + $config = WeChatConfigService::getOaConfig(); + $result = $this->nativePay($from, $order, $config['app_id']); + break; + default: + throw new \Exception('支付方式错误'); + } + + return [ + 'config' => $result, + 'pay_way' => PayEnum::WECHAT_PAY + ]; + } catch (\Exception $e) { + $this->setError($e->getMessage()); + return false; + } + } + + + /** + * @notes jsapiPay + * @param $from + * @param $order + * @param $appId + * @return mixed + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @author 段誉 + * @date 2023/2/28 12:12 + */ + public function jsapiPay($from, $order, $appId) + { + $response = $this->app->getClient()->postJson("v3/pay/transactions/jsapi", [ + "appid" => $appId, + "mchid" => $this->config['mch_id'], + "description" => $this->payDesc($from), + "out_trade_no" => $order['pay_sn'], + "notify_url" => $this->config['notify_url'], + "amount" => [ + "total" => intval($order['order_amount'] * 100), + ], + "payer" => [ + "openid" => $this->auth['openid'] + ], + 'attach' => $from + ]); + + $result = $response->toArray(false); + $this->checkResultFail($result); + return $this->getPrepayConfig($result['prepay_id'], $appId); + } + + + /** + * @notes 网站native + * @param $from + * @param $order + * @param $appId + * @return mixed + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @author 段誉 + * @date 2023/2/28 12:12 + */ + public function nativePay($from, $order, $appId) + { + $response = $this->app->getClient()->postJson('v3/pay/transactions/native', [ + 'appid' => $appId, + 'mchid' => $this->config['mch_id'], + 'description' => $this->payDesc($from), + 'out_trade_no' => $order['pay_sn'], + 'notify_url' => $this->config['notify_url'], + 'amount' => [ + 'total' => intval($order['order_amount'] * 100), + ], + 'attach' => $from + ]); + $result = $response->toArray(false); + $this->checkResultFail($result); + return $result['code_url']; + } + + + /** + * @notes appPay + * @param $from + * @param $order + * @param $appId + * @return mixed + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @author 段誉 + * @date 2023/2/28 12:12 + */ + public function appPay($from, $order, $appId) + { + $response = $this->app->getClient()->postJson('v3/pay/transactions/app', [ + 'appid' => $appId, + 'mchid' => $this->config['mch_id'], + 'description' => $this->payDesc($from), + 'out_trade_no' => $order['pay_sn'], + 'notify_url' => $this->config['notify_url'], + 'amount' => [ + 'total' => intval($order['order_amount'] * 100), + ], + 'attach' => $from + ]); + $result = $response->toArray(false); + $this->checkResultFail($result); + return $result['prepay_id']; + } + + + /** + * @notes h5 + * @param $from + * @param $order + * @param $appId + * @param $redirectUrl + * @return mixed + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @author 段誉 + * @date 2023/2/28 12:13 + */ + public function mwebPay($from, $order, $appId) + { + $ip = request()->ip(); + if (!empty(env('project.test_web_ip')) && env('APP_DEBUG')) { + $ip = env('project.test_web_ip'); + } + + $response = $this->app->getClient()->postJson('v3/pay/transactions/h5', [ + 'appid' => $appId, + 'mchid' => $this->config['mch_id'], + 'description' => $this->payDesc($from), + 'out_trade_no' => $order['pay_sn'], + 'notify_url' => $this->config['notify_url'], + 'amount' => [ + 'total' => intval(strval($order['order_amount'] * 100)), + ], + 'attach' => $from, + 'scene_info' => [ + 'payer_client_ip' => $ip, + 'h5_info' => [ + 'type' => 'Wap', + ] + ] + ]); + $result = $response->toArray(false); + $this->checkResultFail($result); + + $domain = request()->domain(); + if (!empty(env('project.test_web_domain')) && env('APP_DEBUG')) { + $domain = env('project.test_web_domain'); + } + $redirectUrl = $domain . '/mobile'. $order['redirect_url'] .'?id=' . $order['id'] . '&from='. $from . '&checkPay=true'; + return $result['h5_url'] . '&redirect_url=' . urlencode($redirectUrl); + } + + + /** + * @notes 退款 + * @param array $refundData + * @return mixed + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @author 段誉 + * @date 2023/2/28 16:53 + */ + public function refund(array $refundData) + { + $response = $this->app->getClient()->postJson('v3/refund/domestic/refunds', [ + 'transaction_id' => $refundData['transaction_id'], + 'out_refund_no' => $refundData['refund_sn'], + 'amount' => [ + 'refund' => intval($refundData['refund_amount'] * 100), + 'total' => intval($refundData['total_amount'] * 100), + 'currency' => 'CNY', + ] + ]); + $result = $response->toArray(false); + $this->checkResultFail($result); + return $result; + } + + + /** + * @notes 查询退款 + * @param $refundSn + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @author 段誉 + * @date 2023/3/1 11:16 + */ + public function queryRefund($refundSn) + { + $response = $this->app->getClient()->get("v3/refund/domestic/refunds/{$refundSn}"); + return $response->toArray(false); + } + + + /** + * @notes 支付描述 + * @param $from + * @return string + * @author 段誉 + * @date 2023/2/27 17:54 + */ + public function payDesc($from) + { + $desc = [ + 'order' => '商品', + 'recharge' => '充值', + ]; + return $desc[$from] ?? '商品'; + } + + + /** + * @notes 捕获错误 + * @param $result + * @throws \Exception + * @author 段誉 + * @date 2023/2/28 12:09 + */ + public function checkResultFail($result) + { + if (!empty($result['code']) || !empty($result['message'])) { + throw new \Exception('微信:'. $result['code'] . '-' . $result['message']); + } + } + + + /** + * @notes 预支付配置 + * @param $prepayId + * @param $appId + * @return mixed[] + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @author 段誉 + * @date 2023/2/28 17:38 + */ + public function getPrepayConfig($prepayId, $appId) + { + return $this->app->getUtils()->buildBridgeConfig($prepayId, $appId); + } + + + /** + * @notes 支付回调 + * @return \Psr\Http\Message\ResponseInterface + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException + * @throws \ReflectionException + * @throws \Throwable + * @author 段誉 + * @date 2023/2/28 14:20 + */ + public function notify() + { + $server = $this->app->getServer(); + // 支付通知 + $server->handlePaid(function (Message $message) { + if ($message['trade_state'] === 'SUCCESS') { + $extra['transaction_id'] = $message['transaction_id']; + $attach = $message['attach']; + $message['out_trade_no'] = mb_substr($message['out_trade_no'], 0, 18); + switch ($attach) { + case 'recharge': + $order = RechargeOrder::where(['sn' => $message['out_trade_no']])->findOrEmpty(); + if($order->isEmpty() || $order->pay_status == PayEnum::ISPAID) { + return true; + } + PayNotifyLogic::handle('recharge', $message['out_trade_no'], $extra); + break; + } + } + return true; + }); + + // 退款通知 + $server->handleRefunded(function (Message $message) { + return true; + }); + return $server->serve(); + } + + + + + +} \ No newline at end of file diff --git a/server/app/common/service/sms/SmsDriver.php b/server/app/common/service/sms/SmsDriver.php new file mode 100644 index 0000000..3cb459c --- /dev/null +++ b/server/app/common/service/sms/SmsDriver.php @@ -0,0 +1,208 @@ +initialize(); + } + + /** + * @notes 初始化 + * @return bool + * @author 段誉 + * @date 2022/9/15 16:29 + */ + public function initialize() + { + try { + $defaultEngine = ConfigService::get('sms', 'engine', false); + if ($defaultEngine === false) { + throw new \Exception('请开启短信配置'); + } + $this->defaultEngine = $defaultEngine; + $classSpace = __NAMESPACE__ . '\\engine\\' . ucfirst(strtolower($defaultEngine)) . 'Sms'; + if (!class_exists($classSpace)) { + throw new \Exception('没有相应的短信驱动类'); + } + $engineConfig = ConfigService::get('sms', strtolower($defaultEngine), false); + if ($engineConfig === false) { + throw new \Exception($defaultEngine . '未配置'); + } + if ($engineConfig['status'] != 1) { + throw new \Exception('短信服务未开启'); + } + $this->engine = new $classSpace($engineConfig); + if (!is_null($this->engine->getError())) { + throw new \Exception($this->engine->getError()); + } + return true; + } catch (\Exception $e) { + $this->error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 获取错误信息 + * @return null + * @author 段誉 + * @date 2022/9/15 16:29 + */ + public function getError() + { + return $this->error; + } + + + /** + * @notes 发送短信 + * @param $mobile + * @param $data + * @return false + * @author 段誉 + * @date 2022/9/15 16:29 + */ + public function send($mobile, $data) + { + try { + // 发送频率限制 + $this->sendLimit($mobile); + // 开始发送 + $result = $this->engine + ->setMobile($mobile) + ->setTemplateId($data['template_id']) + ->setTemplateParams($data['params']) + ->send(); + if (false === $result) { + throw new \Exception($this->engine->getError()); + } + return $result; + } catch (\Exception $e) { + $this->error = $e->getMessage(); + return false; + } + } + + + /** + * @notes 发送频率限制 + * @param $mobile + * @throws \Exception + * @author 段誉 + * @date 2022/9/15 16:29 + */ + public function sendLimit($mobile) + { + $smsLog = SmsLog::where([ + ['mobile', '=', $mobile], + ['send_status', '=', SmsEnum::SEND_SUCCESS], + ['scene_id', 'in', NoticeEnum::SMS_SCENE], + ]) + ->order('send_time', 'desc') + ->findOrEmpty() + ->toArray(); + if (!empty($smsLog) && ($smsLog['send_time'] > time() - 60)) { + throw new \Exception('同一手机号1分钟只能发送1条短信'); + } + } + + + /** + * @notes 校验手机验证码 + * @param $mobile + * @param $code + * @return bool + * @author 段誉 + * @date 2022/9/15 16:29 + */ + public function verify($mobile, $code, $sceneId = 0) + { + // 测试通用验证码 + if ($code === '0000') { + return true; + } + + $where = [ + ['mobile', '=', $mobile], + ['send_status', '=', SmsEnum::SEND_SUCCESS], + ['scene_id', 'in', NoticeEnum::SMS_SCENE], + ['is_verify', '=', YesNoEnum::NO], + ]; + + if (!empty($sceneId)) { + $where[] = ['scene_id', '=', $sceneId]; + } + + $smsLog = SmsLog::where($where) + ->order('send_time', 'desc') + ->findOrEmpty(); + + // 没有验证码 或 最新验证码已校验 或 已过期(有效期:5分钟) + if ($smsLog->isEmpty() || $smsLog->is_verify || ($smsLog->send_time < time() - 5 * 60)) { + return false; + } + + // 更新校验状态 + if ($smsLog->code == $code) { + $smsLog->check_num = $smsLog->check_num + 1; + $smsLog->is_verify = YesNoEnum::YES; + $smsLog->save(); + return true; + } + + // 更新验证次数 + $smsLog->check_num = $smsLog->check_num + 1; + $smsLog->save(); + return false; + } +} diff --git a/server/app/common/service/sms/SmsMessageService.php b/server/app/common/service/sms/SmsMessageService.php new file mode 100644 index 0000000..410f8c9 --- /dev/null +++ b/server/app/common/service/sms/SmsMessageService.php @@ -0,0 +1,180 @@ +findOrEmpty()->toArray(); + // 替换通知模板参数 + $content = $this->contentFormat($noticeSetting, $params); + // 添加短信记录 + $this->smsLog = $this->addSmsLog($params, $content); + // 添加通知记录 + $this->notice = NoticeLogic::addNotice($params, $noticeSetting, NoticeEnum::SMS, $content); + // 发送短信 + $smsDriver = new SmsDriver(); + if(!is_null($smsDriver->getError())) { + throw new \Exception($smsDriver->getError()); + } + + $result = $smsDriver->send($params['params']['mobile'], [ + 'template_id' => $noticeSetting['sms_notice']['template_id'], + 'params' => $this->setSmsParams($noticeSetting, $params) + ]); + if ($result === false) { + // 发送失败更新短信记录 + $this->updateSmsLog($this->smsLog['id'], SmsEnum::SEND_FAIL, $smsDriver->getError()); + throw new \Exception($smsDriver->getError()); + } + // 发送成功更新短信记录 + $this->updateSmsLog($this->smsLog['id'], SmsEnum::SEND_SUCCESS, $result); + return true; + } catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + } + + + /** + * @notes 格式化消息内容 + * @param $noticeSetting + * @param $params + * @return array|mixed|string|string[] + * @author 段誉 + * @date 2022/9/15 16:24 + */ + public function contentFormat($noticeSetting, $params) + { + $content = $noticeSetting['sms_notice']['content']; + foreach($params['params'] as $k => $v) { + $search = '${' . $k . '}'; + $content = str_replace($search, $v, $content); + } + return $content; + } + + + /** + * @notes 添加短信记录 + * @param $params + * @param $content + * @return SmsLog|\think\Model + * @author 段誉 + * @date 2022/9/15 16:24 + */ + public function addSmsLog($params, $content) + { + $data = [ + 'scene_id' => $params['scene_id'], + 'mobile' => $params['params']['mobile'], + 'content' => $content, + 'code' => $params['params']['code'] ?? '', + 'send_status' => SmsEnum::SEND_ING, + 'send_time' => time(), + ]; + return SmsLog::create($data); + } + + + /** + * @notes 处理腾讯云短信参数 + * @param $noticeSetting + * @param $params + * @return array|mixed + * @author 段誉 + * @date 2022/9/15 16:25 + */ + public function setSmsParams($noticeSetting, $params) + { + $defaultEngine = ConfigService::get('sms', 'engine', false); + // 阿里云 且是 验证码类型 + if($defaultEngine != 'TENCENT' && in_array($params['scene_id'], NoticeEnum::SMS_SCENE)) { + return ['code' => $params['params']['code']]; + } + + if($defaultEngine != 'TENCENT') { + return $params['params']; + } + + //腾讯云特殊处理 + $arr = []; + $content = $noticeSetting['sms_notice']['content']; + foreach ($params['params'] as $item => $val) { + $search = '${' . $item . '}'; + if(strpos($content, $search) !== false && !in_array($item, $arr)) { + //arr => 获的数组[nickname, order_sn] //顺序可能是乱的 + $arr[] = $item; + } + } + + //arr2 => 获得数组[nickname, order_sn] //调整好顺序的变量名数组 + $arr2 = []; + if (!empty($arr)) { + foreach ($arr as $v) { + $key = strpos($content, $v); + $arr2[$key] = $v; + } + } + + //格式化 arr2 => 以小到大的排序的数组 + ksort($arr2); + $arr3 = array_values($arr2); + + //arr4 => 获取到变量数组的对应的值 [mofung, 123456789] + $arr4 = []; + foreach ($arr3 as $v2) { + if(isset($params['params'][$v2])) { + $arr4[] = $params['params'][$v2] . ""; + } + } + return $arr4; + } + + + /** + * @notes 更新短信记录 + * @param $id + * @param $status + * @param $result + * @author 段誉 + * @date 2022/9/15 16:25 + */ + public function updateSmsLog($id, $status, $result) + { + SmsLog::update([ + 'id' => $id, + 'send_status' => $status, + 'results' => json_encode($result, JSON_UNESCAPED_UNICODE) + ]); + } +} \ No newline at end of file diff --git a/server/app/common/service/sms/engine/AliSms.php b/server/app/common/service/sms/engine/AliSms.php new file mode 100644 index 0000000..159dbcf --- /dev/null +++ b/server/app/common/service/sms/engine/AliSms.php @@ -0,0 +1,135 @@ +error = '请联系管理员配置参数'; + return false; + } + $this->config = $config; + } + + + /** + * @notes 设置手机号 + * @param $mobile + * @return $this + * @author 段誉 + * @date 2022/9/15 16:28 + */ + public function setMobile($mobile) + { + $this->mobile = $mobile; + return $this; + } + + + /** + * @notes 设置模板id + * @param $templateId + * @return $this + * @author 段誉 + * @date 2022/9/15 16:28 + */ + public function setTemplateId($templateId) + { + $this->templateId = $templateId; + return $this; + } + + + /** + * @notes 设置模板参数 + * @param $templateParams + * @return $this + * @author 段誉 + * @date 2022/9/15 16:28 + */ + public function setTemplateParams($templateParams) + { + $this->templateParams = json_encode($templateParams, JSON_UNESCAPED_UNICODE); + return $this; + } + + + /** + * @notes 错误信息 + * @return string|null + * @author 段誉 + * @date 2022/9/15 16:27 + */ + public function getError() + { + return $this->error; + } + + + /** + * @notes 发送短信 + * @return array|false + * @author 段誉 + * @date 2022/9/15 16:27 + */ + public function send() + { + try { + AlibabaCloud::accessKeyClient($this->config['app_key'], $this->config['secret_key']) + ->regionId('cn-hangzhou') + ->asDefaultClient(); + + $result = AlibabaCloud::rpcRequest() + ->product('Dysmsapi') + ->host('dysmsapi.aliyuncs.com') + ->version('2017-05-25') + ->action('SendSms') + ->method('POST') + ->options([ + 'query' => [ + 'PhoneNumbers' => $this->mobile, //发送手机号 + 'SignName' => $this->config['sign'], //短信签名 + 'TemplateCode' => $this->templateId, //短信模板CODE + 'TemplateParam' => $this->templateParams, //自定义随机数 + ], + ]) + ->request(); + + $res = $result->toArray(); + if (isset($res['Code']) && $res['Code'] == 'OK') { + return $res; + } + $message = $res['Message'] ?? $res; + throw new \Exception('阿里云短信错误:' . $message); + } catch(\Exception $e) { + $this->error = $e->getMessage(); + return false; + } + } +} \ No newline at end of file diff --git a/server/app/common/service/sms/engine/TencentSms.php b/server/app/common/service/sms/engine/TencentSms.php new file mode 100644 index 0000000..a3820ae --- /dev/null +++ b/server/app/common/service/sms/engine/TencentSms.php @@ -0,0 +1,138 @@ +error = '请联系管理员配置参数'; + return false; + } + $this->config = $config; + } + + + /** + * @notes 设置手机号 + * @param $mobile + * @return $this + * @author 段誉 + * @date 2022/9/15 16:26 + */ + public function setMobile($mobile) + { + $this->mobile = $mobile; + return $this; + } + + + /** + * @notes 设置模板id + * @param $templateId + * @return $this + * @author 段誉 + * @date 2022/9/15 16:26 + */ + public function setTemplateId($templateId) + { + $this->templateId = $templateId; + return $this; + } + + + /** + * @notes 设置模板参数 + * @param $templateParams + * @return $this + * @author 段誉 + * @date 2022/9/15 16:27 + */ + public function setTemplateParams($templateParams) + { + $this->templateParams = $templateParams; + return $this; + } + + + /** + * @notes 获取错误信息 + * @return string|null + * @author 段誉 + * @date 2022/9/15 16:27 + */ + public function getError() + { + return $this->error; + } + + + /** + * @notes 发送短信 + * @return false|mixed + * @author 段誉 + * @date 2022/9/15 16:27 + */ + public function send() + { + try { + $cred = new Credential($this->config['secret_id'], $this->config['secret_key']); + $httpProfile = new HttpProfile(); + $httpProfile->setEndpoint("sms.tencentcloudapi.com"); + + $clientProfile = new ClientProfile(); + $clientProfile->setHttpProfile($httpProfile); + + $client = new SmsClient($cred, 'ap-guangzhou', $clientProfile); + $params = [ + 'PhoneNumberSet' => ['+86' . $this->mobile], + 'TemplateID' => $this->templateId, + 'Sign' => $this->config['sign'], + 'TemplateParamSet' => $this->templateParams, + 'SmsSdkAppid' => $this->config['app_id'], + ]; + $req = new SendSmsRequest(); + $req->fromJsonString(json_encode($params)); + $resp = json_decode($client->SendSms($req)->toJsonString(), true); + if (isset($resp['SendStatusSet']) && $resp['SendStatusSet'][0]['Code'] == 'Ok') { + return $resp; + } else { + $message = $res['SendStatusSet'][0]['Message'] ?? json_encode($resp); + throw new \Exception('腾讯云短信错误:' . $message); + } + } catch(\Exception $e) { + $this->error = $e->getMessage(); + return false; + } + } +} \ No newline at end of file diff --git a/server/app/common/service/storage/Driver.php b/server/app/common/service/storage/Driver.php new file mode 100644 index 0000000..cdd64c1 --- /dev/null +++ b/server/app/common/service/storage/Driver.php @@ -0,0 +1,128 @@ +config = $config; + $this->engine = $this->getEngineClass($storage); + } + + /** + * 设置上传的文件信息 + * @param string $name + * @return mixed + */ + public function setUploadFile($name = 'iFile') + { + return $this->engine->setUploadFile($name); + } + + /** + * 设置上传的文件信息 + * @param string $filePath + * @return mixed + */ + public function setUploadFileByReal($filePath) + { + return $this->engine->setUploadFileByReal($filePath); + } + + /** + * 执行文件上传 + * @param $save_dir (保存路径) + * @return mixed + */ + public function upload($save_dir) + { + return $this->engine->upload($save_dir); + } + + /** + * Notes: 抓取网络资源 + * @param $url + * @param $key + * @author 张无忌(2021/3/2 14:16) + * @return mixed + */ + public function fetch($url, $key) { + return $this->engine->fetch($url, $key); + } + + /** + * 执行文件删除 + * @param $fileName + * @return mixed + */ + public function delete($fileName) + { + return $this->engine->delete($fileName); + } + + /** + * 获取错误信息 + * @return mixed + */ + public function getError() + { + return $this->engine->getError(); + } + + /** + * 获取文件路径 + * @return mixed + */ + public function getFileName() + { + return $this->engine->getFileName(); + } + + /** + * 返回文件信息 + * @return mixed + */ + public function getFileInfo() + { + return $this->engine->getFileInfo(); + } + + /** + * 获取当前的存储引擎 + * @param null|string $storage 指定存储方式,如不指定则为系统默认 + * @return mixed + * @throws Exception + */ + private function getEngineClass($storage = null) + { + $engineName = is_null($storage) ? $this->config['default'] : $storage; + $classSpace = __NAMESPACE__ . '\\engine\\' . ucfirst($engineName); + + if (!class_exists($classSpace)) { + throw new Exception('未找到存储引擎类: ' . $engineName); + } + if($engineName == 'local') { + return new $classSpace(); + } + return new $classSpace($this->config['engine'][$engineName]); + } + +} diff --git a/server/app/common/service/storage/engine/Aliyun.php b/server/app/common/service/storage/engine/Aliyun.php new file mode 100644 index 0000000..365b477 --- /dev/null +++ b/server/app/common/service/storage/engine/Aliyun.php @@ -0,0 +1,115 @@ +config = $config; + } + + /** + * 执行上传 + * @param $save_dir (保存路径) + * @return bool|mixed + */ + public function upload($save_dir) + { + try { + $ossClient = new OssClient( + $this->config['access_key'], + $this->config['secret_key'], + $this->config['domain'], + true + ); + $ossClient->uploadFile( + $this->config['bucket'], + $save_dir . '/' . $this->fileName, + $this->getRealPath() + ); + } catch (OssException $e) { + $this->error = $e->getMessage(); + return false; + } + return true; + } + + /** + * Notes: 抓取远程资源 + * @param $url + * @param null $key + * @return mixed|void + * @author 张无忌(2021/3/2 14:36) + */ + public function fetch($url, $key = null) + { + try { + $ossClient = new OssClient( + $this->config['access_key'], + $this->config['secret_key'], + $this->config['domain'], + true + ); + + $content = file_get_contents($url); + $ossClient->putObject( + $this->config['bucket'], + $key, + $content + ); + } catch (OssException $e) { + $this->error = $e->getMessage(); + return false; + } + return true; + } + + /** + * 删除文件 + * @param $fileName + * @return bool|mixed + */ + public function delete($fileName) + { + try { + $ossClient = new OssClient( + $this->config['access_key'], + $this->config['secret_key'], + $this->config['domain'], + true + ); + $ossClient->deleteObject($this->config['bucket'], $fileName); + } catch (OssException $e) { + $this->error = $e->getMessage(); + return false; + } + return true; + } + + /** + * 返回文件路径 + * @return mixed + */ + public function getFileName() + { + return $this->fileName; + } + +} diff --git a/server/app/common/service/storage/engine/Local.php b/server/app/common/service/storage/engine/Local.php new file mode 100644 index 0000000..a193820 --- /dev/null +++ b/server/app/common/service/storage/engine/Local.php @@ -0,0 +1,60 @@ +file->move($save_dir, $this->fileName); + if (empty($info)) { + $this->error = $this->file->getError(); + return false; + } + return true; + } + + public function fetch($url, $key=null) {} + + /** + * 删除文件 + * @param $fileName + * @return bool|mixed + */ + public function delete($fileName) + { + $check = strpos($fileName, '/'); + if ($check !== false && $check == 0) { + // 文件所在目录 + $fileName = substr_replace($fileName,"",0,1); + } + $filePath = public_path() . "{$fileName}"; + return !file_exists($filePath) ?: unlink($filePath); + } + + /** + * 返回文件路径 + * @return mixed + */ + public function getFileName() + { + return $this->fileName; + } +} diff --git a/server/app/common/service/storage/engine/Qcloud.php b/server/app/common/service/storage/engine/Qcloud.php new file mode 100644 index 0000000..5328013 --- /dev/null +++ b/server/app/common/service/storage/engine/Qcloud.php @@ -0,0 +1,116 @@ +config = $config; + // 创建COS控制类 + $this->createCosClient(); + } + + /** + * 创建COS控制类 + */ + private function createCosClient() + { + $this->cosClient = new Client([ + 'region' => $this->config['region'], + 'credentials' => [ + 'secretId' => $this->config['access_key'], + 'secretKey' => $this->config['secret_key'], + ], + ]); + } + + /** + * 执行上传 + * @param $save_dir (保存路径) + * @return bool|mixed + */ + public function upload($save_dir) + { + // 上传文件 + // putObject(上传接口,最大支持上传5G文件) + try { + $result = $this->cosClient->putObject([ + 'Bucket' => $this->config['bucket'], + 'Key' => $save_dir . '/' . $this->fileName, + 'Body' => fopen($this->getRealPath(), 'rb') + ]); + return true; + } catch (Exception $e) { + $this->error = $e->getMessage(); + return false; + } + } + + /** + * notes: 抓取远程资源(最大支持上传5G文件) + * @param $url + * @param null $key + * @author 张无忌(2021/3/2 14:36) + * @return mixed|void + */ + public function fetch($url, $key=null) { + try { + $this->cosClient->putObject([ + 'Bucket' => $this->config['bucket'], + 'Key' => $key, + 'Body' => fopen($url, 'rb') + ]); + return true; + } catch (Exception $e) { + $this->error = $e->getMessage(); + return false; + } + } + + /** + * 删除文件 + * @param $fileName + * @return bool|mixed + */ + public function delete($fileName) + { + try { + $this->cosClient->deleteObject(array( + 'Bucket' => $this->config['bucket'], + 'Key' => $fileName + )); + return true; + } catch (Exception $e) { + $this->error = $e->getMessage(); + return false; + } + } + + /** + * 返回文件路径 + * @return mixed + */ + public function getFileName() + { + return $this->fileName; + } + +} diff --git a/server/app/common/service/storage/engine/Qiniu.php b/server/app/common/service/storage/engine/Qiniu.php new file mode 100644 index 0000000..c04a03a --- /dev/null +++ b/server/app/common/service/storage/engine/Qiniu.php @@ -0,0 +1,136 @@ +config = $config; + } + + /** + * @notes 执行上传 + * @param $save_dir + * @return bool|mixed + * @author 张无忌 + * @date 2021/7/27 16:02 + */ + public function upload($save_dir) + { + // 要上传图片的本地路径 + $realPath = $this->getRealPath(); + + // 构建鉴权对象 + $auth = new Auth($this->config['access_key'], $this->config['secret_key']); + + // 要上传的空间 + $token = $auth->uploadToken($this->config['bucket']); + + // 初始化 UploadManager 对象并进行文件的上传 + $uploadMgr = new UploadManager(); + + try { + // 调用 UploadManager 的 putFile 方法进行文件的上传 + $key = $save_dir . '/' . $this->fileName; + list(, $error) = $uploadMgr->putFile($token, $key, $realPath); + + if ($error !== null) { + $this->error = $error->message(); + return false; + } + return true; + } catch (Exception $e) { + $this->error = $e->getMessage(); + return false; + } + } + + /** + * @notes 抓取远程资源 + * @param $url + * @param null $key + * @return bool|mixed + * @author 张无忌 + * @date 2021/7/27 16:02 + */ + public function fetch($url, $key=null) + { + try { + if (substr($url, 0, 1) !== '/' || strstr($url, 'http://') || strstr($url, 'https://')) { + $auth = new Auth($this->config['access_key'], $this->config['secret_key']); + $bucketManager = new BucketManager($auth); + list(, $err) = $bucketManager->fetch($url, $this->config['bucket'], $key); + } else { + $auth = new Auth($this->config['access_key'], $this->config['secret_key']); + $token = $auth->uploadToken($this->config['bucket']); + $uploadMgr = new UploadManager(); + list(, $err) = $uploadMgr->putFile($token, $key, $url); + } + + if ($err !== null) { + $this->error = $err->message(); + return false; + } + + return true; + } catch (Exception $e) { + $this->error = $e->getMessage(); + return false; + } + } + + /** + * @notes 删除文件 + * @param $fileName + * @return bool|mixed + * @author 张无忌 + * @date 2021/7/27 16:02 + */ + public function delete($fileName) + { + // 构建鉴权对象 + $auth = new Auth($this->config['access_key'], $this->config['secret_key']); + // 初始化 UploadManager 对象并进行文件的上传 + $bucketMgr = new BucketManager($auth); + + try { + list($res, $error) = $bucketMgr->delete($this->config['bucket'], $fileName); + if ($error !== null) { + $this->error = $error->message(); + return false; + } + return true; + } catch (Exception $e) { + $this->error = $e->getMessage(); + return false; + } + } + + /** + * 返回文件路径 + * @return mixed + */ + public function getFileName() + { + return $this->fileName; + } +} diff --git a/server/app/common/service/storage/engine/Server.php b/server/app/common/service/storage/engine/Server.php new file mode 100644 index 0000000..27276e2 --- /dev/null +++ b/server/app/common/service/storage/engine/Server.php @@ -0,0 +1,147 @@ +file = request()->file($name); + if (empty($this->file)) { + throw new Exception('未找到上传文件的信息'); + } + + // 校验上传文件后缀 + $limit = array_merge(config('project.file_image'), config('project.file_video'), config('project.file_file')); + if (!in_array(strtolower($this->file->extension()), $limit)) { + throw new Exception('不允许上传' . $this->file->extension() . '后缀文件'); + } + + // 文件信息 + $this->fileInfo = [ + 'ext' => $this->file->extension(), + 'size' => $this->file->getSize(), + 'mime' => $this->file->getMime(), + 'name' => $this->file->getOriginalName(), + 'realPath' => $this->file->getRealPath(), + ]; + // 生成保存文件名 + $this->fileName = $this->buildSaveName(); + } + + /** + * 设置上传的文件信息 + * @param string $filePath + */ + public function setUploadFileByReal($filePath) + { + // 设置为系统内部上传 + $this->isInternal = true; + // 文件信息 + $this->fileInfo = [ + 'name' => basename($filePath), + 'size' => filesize($filePath), + 'tmp_name' => $filePath, + 'error' => 0, + ]; + // 生成保存文件名 + $this->fileName = $this->buildSaveName(); + } + + /** + * Notes: 抓取网络资源 + * @param $url + * @param $key + * @author 张无忌(2021/3/2 14:15) + * @return mixed + */ + abstract protected function fetch($url, $key); + + /** + * 文件上传 + * @param $save_dir (保存路径) + * @return mixed + */ + abstract protected function upload($save_dir); + + /** + * 文件删除 + * @param $fileName + * @return mixed + */ + abstract protected function delete($fileName); + + /** + * 返回上传后文件路径 + * @return mixed + */ + abstract public function getFileName(); + + /** + * 返回文件信息 + * @return mixed + */ + public function getFileInfo() + { + return $this->fileInfo; + } + + protected function getRealPath() + { + return $this->fileInfo['realPath']; + } + + /** + * 返回错误信息 + * @return mixed + */ + public function getError() + { + return $this->error; + } + + /** + * 生成保存文件名 + */ + private function buildSaveName() + { + // 要上传图片的本地路径 + $realPath = $this->getRealPath(); + // 扩展名 + $ext = pathinfo($this->getFileInfo()['name'], PATHINFO_EXTENSION); + // 自动生成文件名 + return date('YmdHis') . substr(md5($realPath), 0, 5) + . str_pad(rand(0, 9999), 4, '0', STR_PAD_LEFT) . ".{$ext}"; + } + +} diff --git a/server/app/common/service/wechat/WeChatConfigService.php b/server/app/common/service/wechat/WeChatConfigService.php new file mode 100644 index 0000000..271b8c3 --- /dev/null +++ b/server/app/common/service/wechat/WeChatConfigService.php @@ -0,0 +1,165 @@ + ConfigService::get('mnp_setting', 'app_id'), + 'secret' => ConfigService::get('mnp_setting', 'app_secret'), + 'response_type' => 'array', + 'log' => [ + 'level' => 'debug', + 'file' => app()->getRootPath() . 'runtime/wechat/' . date('Ym') . '/' . date('d') . '.log' + ], + ]; + } + + + /** + * @notes 获取微信公众号配置 + * @return array + * @author 段誉 + * @date 2022/9/6 19:49 + */ + public static function getOaConfig() + { + return [ + 'app_id' => ConfigService::get('oa_setting', 'app_id'), + 'secret' => ConfigService::get('oa_setting', 'app_secret'), + 'token' => ConfigService::get('oa_setting', 'token'), + 'response_type' => 'array', + 'log' => [ + 'level' => 'debug', + 'file' => app()->getRootPath() . 'runtime/wechat/' . date('Ym') . '/' . date('d') . '.log' + ], + ]; + } + + + /** + * @notes 获取微信开放平台配置 + * @return array + * @author 段誉 + * @date 2022/10/20 15:51 + */ + public static function getOpConfig() + { + return [ + 'app_id' => ConfigService::get('open_platform', 'app_id'), + 'secret' => ConfigService::get('open_platform', 'app_secret'), + 'response_type' => 'array', + 'log' => [ + 'level' => 'debug', + 'file' => app()->getRootPath() . 'runtime/wechat/' . date('Ym') . '/' . date('d') . '.log' + ], + ]; + } + + + /** + * @notes 根据终端获取支付配置 + * @param $terminal + * @return array + * @author 段誉 + * @date 2023/2/27 15:45 + */ + public static function getPayConfigByTerminal($terminal) + { + switch ($terminal) { + case UserTerminalEnum::WECHAT_MMP: + $notifyUrl = (string)url('pay/notifyMnp', [], false, true); + break; + case UserTerminalEnum::WECHAT_OA: + case UserTerminalEnum::PC: + case UserTerminalEnum::H5: + $notifyUrl = (string)url('pay/notifyOa', [], false, true); + break; + case UserTerminalEnum::ANDROID: + case UserTerminalEnum::IOS: + $notifyUrl = (string)url('pay/notifyApp', [], false, true); + break; + } + + $pay = PayConfig::where(['pay_way' => PayEnum::WECHAT_PAY])->findOrEmpty()->toArray(); + //判断是否已经存在证书文件夹,不存在则新建 + if (!file_exists(app()->getRootPath() . 'runtime/cert')) { + mkdir(app()->getRootPath() . 'runtime/cert', 0775, true); + } + //写入文件 + $apiclientCert = $pay['config']['apiclient_cert'] ?? ''; + $apiclientKey = $pay['config']['apiclient_key'] ?? ''; + + $certPath = app()->getRootPath() . 'runtime/cert/' . md5($apiclientCert) . '.pem'; + $keyPath = app()->getRootPath() . 'runtime/cert/' . md5($apiclientKey) . '.pem'; + + if (!empty($apiclientCert) && !file_exists($certPath)) { + static::setCert($certPath, trim($apiclientCert)); + } + if (!empty($apiclientKey) && !file_exists($keyPath)) { + static::setCert($keyPath, trim($apiclientKey)); + } + + return [ + // 商户号 + 'mch_id' => $pay['config']['mch_id'] ?? '', + // 商户证书 + 'private_key' => $keyPath, + 'certificate' => $certPath, + // v3 API 秘钥 + 'secret_key' => $pay['config']['pay_sign_key'] ?? '', + 'notify_url' => $notifyUrl, + 'http' => [ + 'throw' => true, // 状态码非 200、300 时是否抛出异常,默认为开启 + 'timeout' => 5.0, + ] + ]; + } + + + /** + * @notes 临时写入证书 + * @param $path + * @param $cert + * @author 段誉 + * @date 2023/2/27 15:48 + */ + public static function setCert($path, $cert) + { + $fopenPath = fopen($path, 'w'); + fwrite($fopenPath, $cert); + fclose($fopenPath); + } + + +} \ No newline at end of file diff --git a/server/app/common/service/wechat/WeChatMnpService.php b/server/app/common/service/wechat/WeChatMnpService.php new file mode 100644 index 0000000..ee1c230 --- /dev/null +++ b/server/app/common/service/wechat/WeChatMnpService.php @@ -0,0 +1,101 @@ +config = $this->getConfig(); + $this->app = new Application($this->config); + } + + + /** + * @notes 配置 + * @return array + * @throws \Exception + * @author 段誉 + * @date 2023/2/27 12:03 + */ + protected function getConfig() + { + $config = WeChatConfigService::getMnpConfig(); + if (empty($config['app_id']) || empty($config['secret'])) { + throw new \Exception('请先设置小程序配置'); + } + return $config; + } + + + /** + * @notes 小程序-根据code获取微信信息 + * @param string $code + * @return array + * @throws Exception + * @throws \EasyWeChat\Kernel\Exceptions\HttpException + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface + * @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface + * @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface + * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface + * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface + * @author 段誉 + * @date 2023/2/27 11:03 + */ + public function getMnpResByCode(string $code) + { + $utils = $this->app->getUtils(); + $response = $utils->codeToSession($code); + + if (!isset($response['openid']) || empty($response['openid'])) { + throw new Exception('获取openID失败'); + } + + return $response; + } + + + /** + * @notes 获取手机号 + * @param string $code + * @return \EasyWeChat\Kernel\HttpClient\Response|\Symfony\Contracts\HttpClient\ResponseInterface + * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface + * @author 段誉 + * @date 2023/2/27 11:46 + */ + public function getUserPhoneNumber(string $code) + { + return $this->app->getClient()->postJson('wxa/business/getuserphonenumber', [ + 'code' => $code, + ]); + } + + +} \ No newline at end of file diff --git a/server/app/common/service/wechat/WeChatOaService.php b/server/app/common/service/wechat/WeChatOaService.php new file mode 100644 index 0000000..36493c4 --- /dev/null +++ b/server/app/common/service/wechat/WeChatOaService.php @@ -0,0 +1,157 @@ +config = $this->getConfig(); + $this->app = new Application($this->config); + } + + + /** + * @notes easywechat服务端 + * @return \EasyWeChat\Kernel\Contracts\Server|\EasyWeChat\OfficialAccount\Server + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @throws \ReflectionException + * @throws \Throwable + * @author 段誉 + * @date 2023/2/27 14:22 + */ + public function getServer() + { + return $this->app->getServer(); + } + + + /** + * @notes 配置 + * @return array + * @throws Exception + * @author 段誉 + * @date 2023/2/27 12:03 + */ + protected function getConfig() + { + $config = WeChatConfigService::getOaConfig(); + if (empty($config['app_id']) || empty($config['secret'])) { + throw new Exception('请先设置公众号配置'); + } + return $config; + } + + + /** + * @notes 公众号-根据code获取微信信息 + * @param string $code + * @return mixed + * @throws Exception + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @author 段誉 + * @date 2023/2/27 11:04 + */ + public function getOaResByCode(string $code) + { + $response = $this->app->getOAuth() + ->scopes(['snsapi_userinfo']) + ->userFromCode($code) + ->getRaw(); + + if (!isset($response['openid']) || empty($response['openid'])) { + throw new Exception('获取openID失败'); + } + + return $response; + } + + + /** + * @notes 公众号跳转url + * @param string $url + * @return mixed + * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException + * @author 段誉 + * @date 2023/2/27 10:35 + */ + public function getCodeUrl(string $url) + { + return $this->app->getOAuth() + ->scopes(['snsapi_userinfo']) + ->redirect($url); + } + + + /** + * @notes 创建公众号菜单 + * @param array $buttons + * @param array $matchRule + * @return \EasyWeChat\Kernel\HttpClient\Response|\Symfony\Contracts\HttpClient\ResponseInterface + * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface + * @author 段誉 + * @date 2023/2/27 12:07 + */ + public function createMenu(array $buttons, array $matchRule = []) + { + if (!empty($matchRule)) { + return $this->app->getClient()->postJson('cgi-bin/menu/addconditional', [ + 'button' => $buttons, + 'matchrule' => $matchRule, + ]); + } + + return $this->app->getClient()->postJson('cgi-bin/menu/create', ['button' => $buttons]); + } + + + /** + * @notes 获取jssdkConfig + * @param $url + * @param $jsApiList + * @param array $openTagList + * @param false $debug + * @return mixed[] + * @throws \EasyWeChat\Kernel\Exceptions\HttpException + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface + * @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface + * @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface + * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface + * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface + * @author 段誉 + * @date 2023/3/1 11:46 + */ + public function getJsConfig($url, $jsApiList, $openTagList = [], $debug = false) + { + return $this->app->getUtils()->buildJsSdkConfig($url, $jsApiList, $openTagList, $debug); + } + +} \ No newline at end of file diff --git a/server/app/common/service/wechat/WeChatRequestService.php b/server/app/common/service/wechat/WeChatRequestService.php new file mode 100644 index 0000000..2b79147 --- /dev/null +++ b/server/app/common/service/wechat/WeChatRequestService.php @@ -0,0 +1,82 @@ +body, true); + } + + + /** + * @notes 通过授权信息获取用户信息 + * @param $accessToken + * @param $openId + * @return mixed + * @author 段誉 + * @date 2022/10/21 10:21 + */ + public static function getUserInfoByAuth($accessToken, $openId) + { + $url = 'https://api.weixin.qq.com/sns/userinfo'; + $url .= '?access_token=' . $accessToken . '&openid=' . $openId; + $response = Requests::get($url); + return json_decode($response->body, true); + } + + +} \ No newline at end of file diff --git a/server/app/common/validate/BaseValidate.php b/server/app/common/validate/BaseValidate.php new file mode 100644 index 0000000..93912c5 --- /dev/null +++ b/server/app/common/validate/BaseValidate.php @@ -0,0 +1,87 @@ +request->isPost()) { + JsonService::throw('请求方式错误,请使用post请求方式'); + } + $this->method = 'POST'; + return $this; + } + + /** + * @notes 设置请求方式 + * @author 令狐冲 + * @date 2021/12/27 14:13 + */ + public function get() + { + if (!$this->request->isGet()) { + JsonService::throw('请求方式错误,请使用get请求方式'); + } + return $this; + } + + + /** + * @notes 切面验证接收到的参数 + * @param null $scene 场景验证 + * @param array $validateData 验证参数,可追加和覆盖掉接收的参数 + * @return array + * @author 令狐冲 + * @date 2021/12/27 14:13 + */ + public function goCheck($scene = null, array $validateData = []): array + { + //接收参数 + if ($this->method == 'GET') { + $params = request()->get(); + } else { + $params = request()->post(); + } + //合并验证参数 + $params = array_merge($params, $validateData); + + //场景 + if ($scene) { + $result = $this->scene($scene)->check($params); + } else { + $result = $this->check($params); + } + + if (!$result) { + $exception = is_array($this->error) ? implode(';', $this->error) : $this->error; + JsonService::throw($exception); + } + // 3.成功返回数据 + return $params; + } +} \ No newline at end of file diff --git a/server/app/common/validate/ListsValidate.php b/server/app/common/validate/ListsValidate.php new file mode 100644 index 0000000..261129d --- /dev/null +++ b/server/app/common/validate/ListsValidate.php @@ -0,0 +1,65 @@ + 'integer|gt:0', + 'page_size' => 'integer|gt:0|pageSizeMax', + 'page_start' => 'integer|gt:0', + 'page_end' => 'integer|gt:0|egt:page_start', + 'page_type' => 'in:0,1', + 'order_by' => 'in:desc,asc', + 'start_time' => 'date', + 'end_time' => 'date|gt:start_time', + 'start' => 'number', + 'end' => 'number', + 'export' => 'in:1,2', + ]; + + protected $message = [ + 'page_end.egt' => '导出范围设置不正确,请重新选择', + 'end_time.gt' => '搜索的时间范围不正确', + ]; + + /** + * @notes 查询数据量判断 + * @param $value + * @param $rule + * @param $data + * @return bool + * @author 令狐冲 + * @date 2021/7/30 15:13 + */ + public function pageSizeMax($value, $rule, $data) + { + $pageSizeMax = Config::get('project.lists.page_size_max'); + if ($pageSizeMax < $value) { + return '已超出系统限制数量,请分页查询或导出,' . '当前最多记录数为:' . $pageSizeMax; + } + return true; + } + + +} \ No newline at end of file diff --git a/server/app/event.php b/server/app/event.php new file mode 100644 index 0000000..95570d2 --- /dev/null +++ b/server/app/event.php @@ -0,0 +1,22 @@ + [ + ], + + 'listen' => [ + 'AppInit' => [], + 'HttpRun' => [], + 'HttpEnd' => [], + 'LogLevel' => [], + 'LogWrite' => [], + + // 通知 + 'Notice' => [NoticeListener::class], + ], + + 'subscribe' => [ + ], +]; diff --git a/server/app/index/controller/IndexController.php b/server/app/index/controller/IndexController.php new file mode 100644 index 0000000..7aa76b6 --- /dev/null +++ b/server/app/index/controller/IndexController.php @@ -0,0 +1,32 @@ +getRootPath() . 'public/pc/index.html'; + if (Request::isMobile()) { + $template = app()->getRootPath() . 'public/mobile/index.html'; + } + if (file_exists($template)) { + return view($template); + } + return JsonService::success($name); + } + + +} diff --git a/server/app/middleware.php b/server/app/middleware.php new file mode 100644 index 0000000..9f8e3c5 --- /dev/null +++ b/server/app/middleware.php @@ -0,0 +1,8 @@ + Request::class, + 'think\exception\Handle' => ExceptionHandle::class, +]; diff --git a/server/app/service.php b/server/app/service.php new file mode 100644 index 0000000..db1ee6a --- /dev/null +++ b/server/app/service.php @@ -0,0 +1,9 @@ +=8.0", + "topthink/framework": "^8.0.2", + "topthink/think-orm": "^3.0", + "topthink/think-multi-app": "^1.0", + "topthink/think-view": "^2.0", + "dragonmantank/cron-expression": "^3.3", + "phpoffice/phpspreadsheet": "^1.22", + "qiniu/php-sdk": "7.4", + "qcloud/cos-sdk-v5": "^2.5", + "aliyuncs/oss-sdk-php": "^2.4", + "alibabacloud/client": "^1.5", + "rmccue/requests": "^2.0", + "w7corp/easywechat": "^6.8", + "tencentcloud/sms": "^3.0", + "alipaysdk/easysdk": "^2.2" + }, + "require-dev": { + "symfony/var-dumper": ">=4.2", + "topthink/think-trace": "^1.0" + }, + "autoload": { + "psr-4": { + "app\\": "app" + }, + "psr-0": { + "": "extend/" + } + }, + "config": { + "preferred-install": "dist", + "allow-plugins": { + "easywechat-composer/easywechat-composer": false + } + }, + "scripts": { + "post-autoload-dump": [ + "@php think service:discover", + "@php think vendor:publish" + ] + } +} diff --git a/server/composer.lock b/server/composer.lock new file mode 100644 index 0000000..4578c2f --- /dev/null +++ b/server/composer.lock @@ -0,0 +1,5026 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "9ad490f5c91c889266a4b29eb605ac59", + "packages": [ + { + "name": "adbario/php-dot-notation", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/adbario/php-dot-notation.git", + "reference": "081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/adbario/php-dot-notation/zipball/081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae", + "reference": "081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "php": "^5.5 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8|^5.7|^6.6|^7.5|^8.5|^9.5", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Adbar\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Riku Särkinen", + "email": "riku@adbar.io" + } + ], + "description": "PHP dot notation access to arrays", + "homepage": "https://github.com/adbario/php-dot-notation", + "keywords": [ + "ArrayAccess", + "dotnotation" + ], + "support": { + "issues": "https://github.com/adbario/php-dot-notation/issues", + "source": "https://github.com/adbario/php-dot-notation/tree/2.5.0" + }, + "time": "2022-10-14T20:31:46+00:00" + }, + { + "name": "alibabacloud/client", + "version": "1.5.32", + "source": { + "type": "git", + "url": "https://github.com/aliyun/openapi-sdk-php-client.git", + "reference": "5bc6f6d660797dcee2c3aef29700ab41ee764f4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aliyun/openapi-sdk-php-client/zipball/5bc6f6d660797dcee2c3aef29700ab41ee764f4d", + "reference": "5bc6f6d660797dcee2c3aef29700ab41ee764f4d", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "adbario/php-dot-notation": "^2.4.1", + "clagiordano/weblibs-configmanager": "^1.0", + "ext-curl": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "ext-xmlwriter": "*", + "guzzlehttp/guzzle": "^6.3|^7.0", + "mtdowling/jmespath.php": "^2.5", + "php": ">=5.5" + }, + "require-dev": { + "composer/composer": "^1.8", + "drupal/coder": "^8.3", + "ext-dom": "*", + "ext-pcre": "*", + "ext-sockets": "*", + "ext-spl": "*", + "league/climate": "^3.2.4", + "mikey179/vfsstream": "^1.6", + "monolog/monolog": "^1.24", + "phpunit/phpunit": "^5.7|^6.6|^7.5|^8.5|^9.5", + "psr/cache": "^1.0", + "symfony/dotenv": "^3.4", + "symfony/var-dumper": "^3.4" + }, + "suggest": { + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "AlibabaCloud\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Alibaba Cloud SDK", + "email": "sdk-team@alibabacloud.com", + "homepage": "http://www.alibabacloud.com" + } + ], + "description": "Alibaba Cloud Client for PHP - Use Alibaba Cloud in your PHP project", + "homepage": "https://www.alibabacloud.com/", + "keywords": [ + "alibaba", + "alibabacloud", + "aliyun", + "client", + "cloud", + "library", + "sdk", + "tool" + ], + "support": { + "issues": "https://github.com/aliyun/openapi-sdk-php-client/issues", + "source": "https://github.com/aliyun/openapi-sdk-php-client" + }, + "time": "2022-12-09T04:05:55+00:00" + }, + { + "name": "alibabacloud/tea", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/aliyun/tea-php.git", + "reference": "1619cb96c158384f72b873e1f85de8b299c9c367" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aliyun/tea-php/zipball/1619cb96c158384f72b873e1f85de8b299c9c367", + "reference": "1619cb96c158384f72b873e1f85de8b299c9c367", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "adbario/php-dot-notation": "^2.4", + "ext-curl": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "ext-xmlwriter": "*", + "guzzlehttp/guzzle": "^6.3|^7.0", + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "*", + "symfony/dotenv": "^3.4", + "symfony/var-dumper": "^3.4" + }, + "suggest": { + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "autoload": { + "psr-4": { + "AlibabaCloud\\Tea\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Alibaba Cloud SDK", + "email": "sdk-team@alibabacloud.com", + "homepage": "http://www.alibabacloud.com" + } + ], + "description": "Client of Tea for PHP", + "homepage": "https://www.alibabacloud.com/", + "keywords": [ + "alibabacloud", + "client", + "cloud", + "tea" + ], + "support": { + "issues": "https://github.com/aliyun/tea-php/issues", + "source": "https://github.com/aliyun/tea-php" + }, + "time": "2023-05-16T06:43:41+00:00" + }, + { + "name": "alibabacloud/tea-fileform", + "version": "0.3.4", + "source": { + "type": "git", + "url": "https://github.com/alibabacloud-sdk-php/tea-fileform.git", + "reference": "4bf0c75a045c8115aa8cb1a394bd08d8bb833181" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/alibabacloud-sdk-php/tea-fileform/zipball/4bf0c75a045c8115aa8cb1a394bd08d8bb833181", + "reference": "4bf0c75a045c8115aa8cb1a394bd08d8bb833181", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "alibabacloud/tea": "^3.0", + "php": ">5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|^5.4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "AlibabaCloud\\Tea\\FileForm\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Alibaba Cloud SDK", + "email": "sdk-team@alibabacloud.com" + } + ], + "description": "Alibaba Cloud Tea File Library for PHP", + "support": { + "issues": "https://github.com/alibabacloud-sdk-php/tea-fileform/issues", + "source": "https://github.com/alibabacloud-sdk-php/tea-fileform/tree/0.3.4" + }, + "time": "2020-12-01T07:24:35+00:00" + }, + { + "name": "alipaysdk/easysdk", + "version": "2.2.3", + "source": { + "type": "git", + "url": "https://github.com/alipay/alipay-easysdk.git", + "reference": "c6008839a22a5fca08e9f8536730f7abfed522d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/alipay/alipay-easysdk/zipball/c6008839a22a5fca08e9f8536730f7abfed522d5", + "reference": "c6008839a22a5fca08e9f8536730f7abfed522d5", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "alibabacloud/tea": "^3.1", + "alibabacloud/tea-fileform": "^0.3.2", + "ext-ctype": "*", + "ext-curl": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "ext-xmlwriter": "*", + "guzzlehttp/guzzle": ">=6.3", + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Alipay\\EasySDK\\": "php/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "junying.wjy", + "email": "junying.wjy@antfin.com" + } + ], + "description": "支付宝官方 Alipay Easy SDK", + "support": { + "source": "https://github.com/alipay/alipay-easysdk/tree/v2.2.3" + }, + "time": "2022-11-28T14:04:57+00:00" + }, + { + "name": "aliyuncs/oss-sdk-php", + "version": "v2.6.0", + "source": { + "type": "git", + "url": "https://github.com/aliyun/aliyun-oss-php-sdk.git", + "reference": "572d0f8e099e8630ae7139ed3fdedb926c7a760f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/572d0f8e099e8630ae7139ed3fdedb926c7a760f", + "reference": "572d0f8e099e8630ae7139ed3fdedb926c7a760f", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "*", + "satooshi/php-coveralls": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "OSS\\": "src/OSS" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aliyuncs", + "homepage": "http://www.aliyun.com" + } + ], + "description": "Aliyun OSS SDK for PHP", + "homepage": "http://www.aliyun.com/product/oss/", + "support": { + "issues": "https://github.com/aliyun/aliyun-oss-php-sdk/issues", + "source": "https://github.com/aliyun/aliyun-oss-php-sdk/tree/v2.6.0" + }, + "time": "2022-08-03T08:06:01+00:00" + }, + { + "name": "clagiordano/weblibs-configmanager", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/clagiordano/weblibs-configmanager.git", + "reference": "8802c7396d61a923c9a73e37ead062b24bb1b273" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clagiordano/weblibs-configmanager/zipball/8802c7396d61a923c9a73e37ead062b24bb1b273", + "reference": "8802c7396d61a923c9a73e37ead062b24bb1b273", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "php": ">=5.4", + "symfony/yaml": "^2.8" + }, + "require-dev": { + "clagiordano/phpunit-result-printer": "^1", + "php-coveralls/php-coveralls": "^1.1", + "phpunit/phpunit": "^4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "clagiordano\\weblibs\\configmanager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Claudio Giordano", + "email": "claudio.giordano@autistici.org", + "role": "Developer" + } + ], + "description": "weblibs-configmanager is a tool library for easily read and access to php config array file and direct read/write configuration file / object", + "keywords": [ + "clagiordano", + "configuration", + "manager", + "tool", + "weblibs" + ], + "support": { + "issues": "https://github.com/clagiordano/weblibs-configmanager/issues", + "source": "https://github.com/clagiordano/weblibs-configmanager/tree/v1.5.0" + }, + "time": "2021-07-12T15:27:21+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", + "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2|^8.0", + "webmozart/assert": "^1.0" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-webmozart-assert": "^1.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.3" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2023-08-10T19:36:49+00:00" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.16.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8", + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0" + }, + "time": "2022-09-18T07:06:19+00:00" + }, + { + "name": "guzzlehttp/command", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/command.git", + "reference": "3372bcfd79d4b357b6871665bf06155515e8d844" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/command/zipball/3372bcfd79d4b357b6871665bf06155515e8d844", + "reference": "3372bcfd79d4b357b6871665bf06155515e8d844", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "guzzlehttp/guzzle": "^7.5.1", + "guzzlehttp/promises": "^1.5.3 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "phpunit/phpunit": "^8.5.19 || ^9.5.8" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Command\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "Provides the foundation for building command-based web service clients", + "support": { + "issues": "https://github.com/guzzle/command/issues", + "source": "https://github.com/guzzle/command/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/command", + "type": "tidelift" + } + ], + "time": "2023-05-21T14:15:09+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fb7566caccf22d74d1ab270de3551f72a58399f5", + "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-05-21T14:04:53+00:00" + }, + { + "name": "guzzlehttp/guzzle-services", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle-services.git", + "reference": "f4bb1c205152a56741624b88753732e01a60565c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle-services/zipball/f4bb1c205152a56741624b88753732e01a60565c", + "reference": "f4bb1c205152a56741624b88753732e01a60565c", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "guzzlehttp/command": "^1.3", + "guzzlehttp/guzzle": "^7.7", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "guzzlehttp/uri-template": "^1.0.1", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "phpunit/phpunit": "^8.5.19 || ^9.5.8" + }, + "suggest": { + "gimler/guzzle-description-loader": "^0.0.4" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Command\\Guzzle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Stefano Kowalke", + "email": "blueduck@mail.org", + "homepage": "https://github.com/Konafets" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "Provides an implementation of the Guzzle Command library that uses Guzzle service descriptions to describe web services, serialize requests, and parse responses into easy to use model structures.", + "support": { + "issues": "https://github.com/guzzle/guzzle-services/issues", + "source": "https://github.com/guzzle/guzzle-services/tree/1.4.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle-services", + "type": "tidelift" + } + ], + "time": "2023-05-21T14:21:30+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-08-03T15:11:55+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/8bd7c33a0734ae1c5d074360512beb716bef3f77", + "reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-08-03T15:06:02+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "b945d74a55a25a949158444f09ec0d3c120d69e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/b945d74a55a25a949158444f09ec0d3c120d69e2", + "reference": "b945d74a55a25a949158444f09ec0d3c120d69e2", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.19 || ^9.5.8", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2021-10-07T12:57:01+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "3fa72e4c71a43f9e9118752a5c90e476a8dc9eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/3fa72e4c71a43f9e9118752a5c90e476a8dc9eb3", + "reference": "3fa72e4c71a43f9e9118752a5c90e476a8dc9eb3", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-mbstring": "*", + "myclabs/php-enum": "^1.5", + "php": "^8.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.9", + "guzzlehttp/guzzle": "^6.5.3 || ^7.2.0", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.4", + "phpunit/phpunit": "^8.5.8 || ^9.4.2", + "vimeo/psalm": "^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/2.4.0" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + }, + { + "url": "https://opencollective.com/zipstream", + "type": "open_collective" + } + ], + "time": "2022-12-08T12:29:14+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "f259e2b15fb95494c83f52d3caad003bbf5ffaa1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f259e2b15fb95494c83f52d3caad003bbf5ffaa1", + "reference": "f259e2b15fb95494c83f52d3caad003bbf5ffaa1", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^0.12.91", + "phpunit/phpunit": "^8.5.14", + "predis/predis": "^1.1 || ^2.0", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.9.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2023-02-06T13:44:46+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9b87907a81b87bc76d19a7fb2d61e61486ee9edb", + "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^1.4 || ^2.0", + "phpunit/phpunit": "^4.8.36 || ^7.5.15" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.6.1" + }, + "time": "2021-06-14T00:11:39+00:00" + }, + { + "name": "myclabs/php-enum", + "version": "1.8.3", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "b942d263c641ddb5190929ff840c68f78713e937" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/b942d263c641ddb5190929ff840c68f78713e937", + "reference": "b942d263c641ddb5190929ff840c68f78713e937", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.6.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.3" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "time": "2021-07-05T08:18:36+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "3cb4d163b58589e47b35103e8e5e6a6a475b47be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/3cb4d163b58589e47b35103e8e5e6a6a475b47be", + "reference": "3cb4d163b58589e47b35103e8e5e6a6a475b47be", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.0" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-05-02T11:26:24+00:00" + }, + { + "name": "nyholm/psr7-server", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7-server.git", + "reference": "b846a689844cef114e8079d8c80f0afd96745ae3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/b846a689844cef114e8079d8c80f0afd96745ae3", + "reference": "b846a689844cef114e8079d8c80f0afd96745ae3", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "nyholm/psr7": "^1.3", + "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nyholm\\Psr7Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "Helper classes to handle PSR-7 server requests", + "homepage": "http://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7-server/issues", + "source": "https://github.com/Nyholm/psr7-server/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2021-05-12T11:11:27+00:00" + }, + { + "name": "overtrue/socialite", + "version": "4.8.1", + "source": { + "type": "git", + "url": "https://github.com/overtrue/socialite.git", + "reference": "470b781f288fbb24c8b105cfdada215d83d84d4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/overtrue/socialite/zipball/470b781f288fbb24c8b105cfdada215d83d84d4b", + "reference": "470b781f288fbb24c8b105cfdada215d83d84d4b", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^7.0", + "php": ">=8.0.2", + "symfony/http-foundation": "^6.0", + "symfony/psr-http-message-bridge": "^2.1" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.0", + "laravel/pint": "^1.2", + "mockery/mockery": "^1.3", + "phpstan/phpstan": "^1.7", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Contracts/FactoryInterface.php", + "src/Contracts/UserInterface.php", + "src/Contracts/ProviderInterface.php" + ], + "psr-4": { + "Overtrue\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ], + "description": "A collection of OAuth 2 packages.", + "keywords": [ + "Feishu", + "login", + "oauth", + "qcloud", + "qq", + "social", + "wechat", + "weibo" + ], + "support": { + "issues": "https://github.com/overtrue/socialite/issues", + "source": "https://github.com/overtrue/socialite/tree/4.8.1" + }, + "funding": [ + { + "url": "https://github.com/overtrue", + "type": "github" + } + ], + "time": "2023-07-17T08:56:49+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.28.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a", + "reference": "6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.15", + "maennchen/zipstream-php": "^2.1", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^7.4 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^1.0 || ^2.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.2.4", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.28.0" + }, + "time": "2023-02-25T12:24:49+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "time": "2023-04-10T20:10:41+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "qcloud/cos-sdk-v5", + "version": "v2.6.5", + "source": { + "type": "git", + "url": "https://github.com/tencentyun/cos-php-sdk-v5.git", + "reference": "0eb08d6920df91b2c8fef3a8bfe409649dc8f877" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tencentyun/cos-php-sdk-v5/zipball/0eb08d6920df91b2c8fef3a8bfe409649dc8f877", + "reference": "0eb08d6920df91b2c8fef3a8bfe409649dc8f877", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.2.1 || ^7.0", + "guzzlehttp/guzzle-services": "^1.1", + "guzzlehttp/psr7": "^1.3.1 || ^2.0", + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "files": [ + "src/Common.php" + ], + "psr-4": { + "Qcloud\\Cos\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "yaozongyou", + "email": "yaozongyou@vip.qq.com" + }, + { + "name": "lewzylu", + "email": "327874225@qq.com" + }, + { + "name": "tuunalai", + "email": "550566181@qq.com" + } + ], + "description": "PHP SDK for QCloud COS", + "keywords": [ + "cos", + "php", + "qcloud" + ], + "support": { + "issues": "https://github.com/tencentyun/cos-php-sdk-v5/issues", + "source": "https://github.com/tencentyun/cos-php-sdk-v5/tree/v2.6.5" + }, + "time": "2023-08-16T02:26:48+00:00" + }, + { + "name": "qiniu/php-sdk", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/qiniu/php-sdk.git", + "reference": "1c6bc89166e524a40ee42bf516fb99ffc6401c82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/qiniu/php-sdk/zipball/1c6bc89166e524a40ee42bf516fb99ffc6401c82", + "reference": "1c6bc89166e524a40ee42bf516fb99ffc6401c82", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~3.6" + }, + "type": "library", + "autoload": { + "files": [ + "src/Qiniu/functions.php" + ], + "psr-4": { + "Qiniu\\": "src/Qiniu" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Qiniu", + "email": "sdk@qiniu.com", + "homepage": "http://www.qiniu.com" + } + ], + "description": "Qiniu Resource (Cloud) Storage SDK for PHP", + "homepage": "http://developer.qiniu.com/", + "keywords": [ + "cloud", + "qiniu", + "sdk", + "storage" + ], + "support": { + "issues": "https://github.com/qiniu/php-sdk/issues", + "source": "https://github.com/qiniu/php-sdk/tree/v7.4.0" + }, + "time": "2021-07-19T07:41:36+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "rmccue/requests", + "version": "v2.0.5", + "source": { + "type": "git", + "url": "https://github.com/WordPress/Requests.git", + "reference": "b717f1d2f4ef7992ec0c127747ed8b7e170c2f49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/Requests/zipball/b717f1d2f4ef7992ec0c127747ed8b7e170c2f49", + "reference": "b717f1d2f4ef7992ec0c127747ed8b7e170c2f49", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "php": ">=5.6" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "php-parallel-lint/php-console-highlighter": "^0.5.0", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0", + "requests/test-server": "dev-main", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.6", + "wp-coding-standards/wpcs": "^2.0", + "yoast/phpunit-polyfills": "^1.0.0" + }, + "type": "library", + "autoload": { + "files": [ + "library/Deprecated.php" + ], + "psr-4": { + "WpOrg\\Requests\\": "src/" + }, + "classmap": [ + "library/Requests.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Ryan McCue", + "homepage": "https://rmccue.io/" + }, + { + "name": "Alain Schlesser", + "homepage": "https://github.com/schlessera" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl" + }, + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/Requests/graphs/contributors" + } + ], + "description": "A HTTP library written in PHP, for human beings.", + "homepage": "https://requests.ryanmccue.info/", + "keywords": [ + "curl", + "fsockopen", + "http", + "idna", + "ipv6", + "iri", + "sockets" + ], + "support": { + "docs": "https://requests.ryanmccue.info/", + "issues": "https://github.com/WordPress/Requests/issues", + "source": "https://github.com/WordPress/Requests" + }, + "time": "2022-10-11T08:15:28+00:00" + }, + { + "name": "symfony/cache", + "version": "v6.0.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "81ca309f056e836480928b20280ec52ce8369bb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/81ca309f056e836480928b20280ec52ce8369bb3", + "reference": "81ca309f056e836480928b20280ec52ce8369bb3", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^1.1.7|^2|^3", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/var-exporter": "^5.4|^6.0" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "symfony/dependency-injection": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/var-dumper": "<5.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^2.13.1|^3.0", + "predis/predis": "^1.1", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v6.0.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-20T17:44:14+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "1c0a181c9ee221afe4fa55b2d13fc63c5ae14348" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/1c0a181c9ee221afe4fa55b2d13fc63c5ae14348", + "reference": "1c0a181c9ee221afe4fa55b2d13fc63c5ae14348", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.2", + "psr/cache": "^3.0" + }, + "suggest": { + "symfony/cache-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.0.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:55:41+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", + "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:55:41+00:00" + }, + { + "name": "symfony/http-client", + "version": "v6.0.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "541c04560da1875f62c963c3aab6ea12a7314e11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/541c04560da1875f62c963c3aab6ea12a7314e11", + "reference": "541c04560da1875f62c963c3aab6ea12a7314e11", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.2", + "psr/log": "^1|^2|^3", + "symfony/http-client-contracts": "^3", + "symfony/service-contracts": "^1.0|^2|^3" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-client/tree/v6.0.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-30T15:41:07+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "4184b9b63af1edaf35b6a7974c6f1f9f33294129" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4184b9b63af1edaf35b6a7974c6f1f9f33294129", + "reference": "4184b9b63af1edaf35b6a7974c6f1f9f33294129", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.2" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.0.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-12T16:11:42+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.0.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "e16b2676a4b3b1fa12378a20b29c364feda2a8d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e16b2676a4b3b1fa12378a20b29c364feda2a8d6", + "reference": "e16b2676a4b3b1fa12378a20b29c364feda2a8d6", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.2", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.1" + }, + "require-dev": { + "predis/predis": "~1.0", + "symfony/cache": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^5.4|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" + }, + "suggest": { + "symfony/mime": "To use the file extension guesser" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.0.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-30T15:41:07+00:00" + }, + { + "name": "symfony/mime", + "version": "v6.0.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "d7052547a0070cbeadd474e172b527a00d657301" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/d7052547a0070cbeadd474e172b527a00d657301", + "reference": "d7052547a0070cbeadd474e172b527a00d657301", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.2", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4", + "symfony/serializer": "<5.4.14|>=6.0,<6.0.14|>=6.1,<6.1.6" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/serializer": "^5.4.14|~6.0.14|^6.1.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.0.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-11T11:50:03+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "639084e360537a19f9ee352433b84ce831f3d2da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da", + "reference": "639084e360537a19f9ee352433b84ce831f3d2da", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "869329b1e9894268a8a61dabb69153029b7a8c97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97", + "reference": "869329b1e9894268a8a61dabb69153029b7a8c97", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a", + "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "3c62b814477165dbf07c9c18ddb413ccdadaa93c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/3c62b814477165dbf07c9c18ddb413ccdadaa93c", + "reference": "3c62b814477165dbf07c9c18ddb413ccdadaa93c", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/http-foundation": "^5.4 || ^6.0" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "psr/log": "^1.1 || ^2 || ^3", + "symfony/browser-kit": "^5.4 || ^6.0", + "symfony/config": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/framework-bundle": "^5.4 || ^6.0", + "symfony/http-kernel": "^5.4 || ^6.0", + "symfony/phpunit-bridge": "^6.2" + }, + "suggest": { + "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + }, + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-main": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "http://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/symfony/psr-http-message-bridge/issues", + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-25T14:44:36+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d78d39c1599bd1188b8e26bb341da52c3c6d8a66", + "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.2", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.0.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-30T19:17:58+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v6.0.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "df56f53818c2d5d9f683f4ad2e365ba73a3b69d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/df56f53818c2d5d9f683f4ad2e365ba73a3b69d2", + "reference": "df56f53818c2d5d9f683f4ad2e365ba73a3b69d2", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.2" + }, + "require-dev": { + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v6.0.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-13T08:34:10+00:00" + }, + { + "name": "symfony/yaml", + "version": "v2.8.52", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "02c1859112aa779d9ab394ae4f3381911d84052b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/02c1859112aa779d9ab394ae4f3381911d84052b", + "reference": "02c1859112aa779d9ab394ae4f3381911d84052b", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v2.8.52" + }, + "time": "2018-11-11T11:18:13+00:00" + }, + { + "name": "tencentcloud/common", + "version": "3.0.990", + "source": { + "type": "git", + "url": "https://github.com/tencentcloud-sdk-php/common.git", + "reference": "0c2705d31c42443ab54422aec7965561141e99ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tencentcloud-sdk-php/common/zipball/0c2705d31c42443ab54422aec7965561141e99ca", + "reference": "0c2705d31c42443ab54422aec7965561141e99ca", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "guzzlehttp/guzzle": "^6.3||^7.0", + "php": ">=5.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "TencentCloud\\": "./src/TencentCloud" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "tencentcloudapi", + "email": "tencentcloudapi@tencent.com", + "homepage": "https://cloud.tencent.com/document/sdk/PHP", + "role": "Developer" + } + ], + "description": "TencentCloudApi php sdk", + "homepage": "https://github.com/tencentcloud-sdk-php/common", + "support": { + "issues": "https://github.com/tencentcloud-sdk-php/common/issues", + "source": "https://github.com/tencentcloud-sdk-php/common/tree/3.0.990" + }, + "time": "2023-10-09T00:19:14+00:00" + }, + { + "name": "tencentcloud/sms", + "version": "3.0.990", + "source": { + "type": "git", + "url": "https://github.com/tencentcloud-sdk-php/sms.git", + "reference": "2dc4e09ae59950778370f6f438643046deee9882" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tencentcloud-sdk-php/sms/zipball/2dc4e09ae59950778370f6f438643046deee9882", + "reference": "2dc4e09ae59950778370f6f438643046deee9882", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "tencentcloud/common": "3.0.990" + }, + "type": "library", + "autoload": { + "psr-4": { + "TencentCloud\\": "./src/TencentCloud" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "tencentcloudapi", + "email": "tencentcloudapi@tencent.com", + "homepage": "https://github.com/tencentcloud-sdk-php/sms", + "role": "Developer" + } + ], + "description": "TencentCloudApi php sdk sms", + "homepage": "https://github.com/tencentcloud-sdk-php/sms", + "support": { + "issues": "https://github.com/tencentcloud-sdk-php/sms/issues", + "source": "https://github.com/tencentcloud-sdk-php/sms/tree/3.0.990" + }, + "time": "2023-10-09T00:41:03+00:00" + }, + { + "name": "thenorthmemory/xml", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/TheNorthMemory/xml.git", + "reference": "6f50c63450a0b098772423f8bdc3c4ad2c4c30bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TheNorthMemory/xml/zipball/6f50c63450a0b098772423f8bdc3c4ad2c4c30bb", + "reference": "6f50c63450a0b098772423f8bdc3c4ad2c4c30bb", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-libxml": "*", + "ext-simplexml": "*", + "php": ">=7.1.2" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.89 || ^1.0", + "phpunit/phpunit": "^7.5 || ^8.5.16 || ^9.3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "TheNorthMemory\\Xml\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "James ZHANG", + "homepage": "https://github.com/TheNorthMemory" + } + ], + "description": "A wrapper of the XML parser and builder", + "homepage": "https://github.com/TheNorthMemory/xml", + "keywords": [ + "xml-builder", + "xml-parser" + ], + "support": { + "issues": "https://github.com/TheNorthMemory/xml/issues", + "source": "https://github.com/TheNorthMemory/xml/tree/1.1.1" + }, + "time": "2023-01-15T06:01:13+00:00" + }, + { + "name": "topthink/framework", + "version": "v8.0.3", + "source": { + "type": "git", + "url": "https://github.com/top-think/framework.git", + "reference": "5e59fb1e2bcb400c6f82e99d1a40dd058afc8563" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/framework/zipball/5e59fb1e2bcb400c6f82e99d1a40dd058afc8563", + "reference": "5e59fb1e2bcb400c6f82e99d1a40dd058afc8563", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=8.0.0", + "psr/container": "^2.0", + "psr/http-message": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "topthink/think-helper": "^3.1", + "topthink/think-orm": "^3.0" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.1.0", + "mikey179/vfsstream": "^1.6", + "mockery/mockery": "^1.2", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "files": [], + "psr-4": { + "think\\": "src/think/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP Framework.", + "homepage": "http://thinkphp.cn/", + "keywords": [ + "framework", + "orm", + "thinkphp" + ], + "support": { + "issues": "https://github.com/top-think/framework/issues", + "source": "https://github.com/top-think/framework/tree/v8.0.3" + }, + "time": "2023-10-17T08:31:52+00:00" + }, + { + "name": "topthink/think-helper", + "version": "v3.1.6", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-helper.git", + "reference": "769acbe50a4274327162f9c68ec2e89a38eb2aff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-helper/zipball/769acbe50a4274327162f9c68ec2e89a38eb2aff", + "reference": "769acbe50a4274327162f9c68ec2e89a38eb2aff", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP6 Helper Package", + "support": { + "issues": "https://github.com/top-think/think-helper/issues", + "source": "https://github.com/top-think/think-helper/tree/v3.1.6" + }, + "time": "2021-12-15T04:27:55+00:00" + }, + { + "name": "topthink/think-multi-app", + "version": "v1.0.17", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-multi-app.git", + "reference": "4055a6187296ac16c0bc7bbab4ed5d92f82f791c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-multi-app/zipball/4055a6187296ac16c0bc7bbab4ed5d92f82f791c", + "reference": "4055a6187296ac16c0bc7bbab4ed5d92f82f791c", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1.0", + "topthink/framework": "^6.0|^8.0" + }, + "type": "library", + "extra": { + "think": { + "services": [ + "think\\app\\Service" + ] + } + }, + "autoload": { + "psr-4": { + "think\\app\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "thinkphp multi app support", + "support": { + "issues": "https://github.com/top-think/think-multi-app/issues", + "source": "https://github.com/top-think/think-multi-app/tree/v1.0.17" + }, + "time": "2023-03-29T02:04:29+00:00" + }, + { + "name": "topthink/think-orm", + "version": "v3.0.11", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-orm.git", + "reference": "38a6da3ae03bcae4ea2f484a4cf05687a88a5488" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-orm/zipball/38a6da3ae03bcae4ea2f484a4cf05687a88a5488", + "reference": "38a6da3ae03bcae4ea2f484a4cf05687a88a5488", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "ext-pdo": "*", + "php": ">=8.0.0", + "psr/log": ">=1.0", + "psr/simple-cache": ">=1.0", + "topthink/think-helper": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^8|^9.5|^10" + }, + "type": "library", + "autoload": { + "files": [ + "stubs/load_stubs.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "the PHP Database&ORM Framework", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/top-think/think-orm/issues", + "source": "https://github.com/top-think/think-orm/tree/v3.0.11" + }, + "time": "2023-07-18T11:04:31+00:00" + }, + { + "name": "topthink/think-template", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-template.git", + "reference": "4352d2cf627abfb8b49f830686c25c02f59c23f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-template/zipball/4352d2cf627abfb8b49f830686c25c02f59c23f2", + "reference": "4352d2cf627abfb8b49f830686c25c02f59c23f2", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.0", + "psr/simple-cache": ">=1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "the php template engine", + "support": { + "issues": "https://github.com/top-think/think-template/issues", + "source": "https://github.com/top-think/think-template/tree/v3.0.0" + }, + "time": "2023-02-25T12:11:14+00:00" + }, + { + "name": "topthink/think-view", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-view.git", + "reference": "d2a076011c96d2edd8016703a827fb54b2683c62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-view/zipball/d2a076011c96d2edd8016703a827fb54b2683c62", + "reference": "d2a076011c96d2edd8016703a827fb54b2683c62", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.0", + "topthink/think-template": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "think\\view\\driver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "thinkphp template driver", + "support": { + "issues": "https://github.com/top-think/think-view/issues", + "source": "https://github.com/top-think/think-view/tree/v2.0.0" + }, + "time": "2023-02-25T12:18:09+00:00" + }, + { + "name": "w7corp/easywechat", + "version": "6.8.0", + "source": { + "type": "git", + "url": "https://github.com/w7corp/easywechat.git", + "reference": "60f0b4ba2ac3144df1a2291193daa34beb949d26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/w7corp/easywechat/zipball/60f0b4ba2ac3144df1a2291193daa34beb949d26", + "reference": "60f0b4ba2ac3144df1a2291193daa34beb949d26", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-curl": "*", + "ext-fileinfo": "*", + "ext-libxml": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "ext-sodium": "*", + "monolog/monolog": "^2.2", + "nyholm/psr7": "^1.5", + "nyholm/psr7-server": "^1.0", + "overtrue/socialite": "^3.5|^4.0.1", + "php": ">=8.0.2", + "psr/http-client": "^1.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/cache": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/polyfill-php81": "^1.25", + "symfony/psr-http-message-bridge": "^2.1.2", + "thenorthmemory/xml": "^1.0" + }, + "require-dev": { + "brainmaestro/composer-git-hooks": "^2.8", + "jetbrains/phpstorm-attributes": "^1.0", + "laravel/pint": "^1.2", + "mikey179/vfsstream": "^1.6", + "mockery/mockery": "^1.4.4", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/var-dumper": "^5.2" + }, + "type": "library", + "extra": { + "hooks": { + "pre-commit": [ + "composer check-style", + "composer phpstan", + "composer test" + ], + "pre-push": [ + "composer check-style" + ], + "config": { + "stop-on-failure": [ + "pre-commit", + "pre-push" + ] + } + } + }, + "autoload": { + "psr-4": { + "EasyWeChat\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ], + "description": "微信SDK", + "keywords": [ + "easywechat", + "sdk", + "wechat", + "weixin", + "weixin-sdk" + ], + "support": { + "issues": "https://github.com/w7corp/easywechat/issues", + "source": "https://github.com/w7corp/easywechat/tree/6.8.0" + }, + "funding": [ + { + "url": "https://github.com/overtrue", + "type": "github" + } + ], + "time": "2022-09-25T13:05:18+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "packages-dev": [ + { + "name": "symfony/var-dumper", + "version": "v6.0.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "eb980457fa6899840fe1687e8627a03a7d8a3d52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/eb980457fa6899840fe1687e8627a03a7d8a3d52", + "reference": "eb980457fa6899840fe1687e8627a03a7d8a3d52", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.2", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "phpunit/phpunit": "<5.4.3", + "symfony/console": "<5.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v6.0.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-20T17:44:14+00:00" + }, + { + "name": "topthink/think-trace", + "version": "v1.6", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-trace.git", + "reference": "136cd5d97e8bdb780e4b5c1637c588ed7ca3e142" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-trace/zipball/136cd5d97e8bdb780e4b5c1637c588ed7ca3e142", + "reference": "136cd5d97e8bdb780e4b5c1637c588ed7ca3e142", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1.0", + "topthink/framework": "^6.0|^8.0" + }, + "type": "library", + "extra": { + "think": { + "services": [ + "think\\trace\\Service" + ], + "config": { + "trace": "src/config.php" + } + } + }, + "autoload": { + "psr-4": { + "think\\trace\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "thinkphp debug trace", + "support": { + "issues": "https://github.com/top-think/think-trace/issues", + "source": "https://github.com/top-think/think-trace/tree/v1.6" + }, + "time": "2023-02-07T08:36:32+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.0" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/server/config/app.php b/server/config/app.php new file mode 100644 index 0000000..3dada4b --- /dev/null +++ b/server/config/app.php @@ -0,0 +1,32 @@ + env('app.host', ''), + // 应用的命名空间 + 'app_namespace' => '', + // 是否启用路由 + 'with_route' => true, + // 默认应用 + 'default_app' => 'index', + // 默认时区 + 'default_timezone' => 'Asia/Shanghai', + + // 应用映射(自动多应用模式有效) + 'app_map' => [], + // 域名绑定(自动多应用模式有效) + 'domain_bind' => [], + // 禁止URL访问的应用列表(自动多应用模式有效) + 'deny_app_list' => [], + + // 异常页面的模板文件 + 'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl', + + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => false, +]; diff --git a/server/config/cache.php b/server/config/cache.php new file mode 100644 index 0000000..d21153d --- /dev/null +++ b/server/config/cache.php @@ -0,0 +1,43 @@ + env('cache.driver', 'redis'), + + // 缓存连接方式配置 + 'stores' => [ + 'file' => [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + 'path' => '', + // 缓存前缀 + 'prefix' => 'la', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, + // 缓存标签前缀 + 'tag_prefix' => 'tag:', + // 序列化机制 例如 ['serialize', 'unserialize'] + 'serialize' => [], + ], + // redis缓存 + 'redis' => [ + // 驱动方式 + 'type' => 'redis', + // 服务器地址 + 'host' => env('cache.host', '127.0.0.1'), + // 端口 + 'port' => env('cache.port', '6377'), + // 密码 + 'password' => env('cache.password', 'hajiminanbeilvdou'), + // 缓存前缀 + 'prefix' => 'la:', + // 数据库编号 + 'select' => env('cache.select', 0), + ], + ], +]; diff --git a/server/config/console.php b/server/config/console.php new file mode 100644 index 0000000..8750899 --- /dev/null +++ b/server/config/console.php @@ -0,0 +1,20 @@ + [ + // 定时任务 + 'crontab' => 'app\common\command\Crontab', + // 退款查询 + 'query_refund' => 'app\common\command\QueryRefund', + // 懂球帝爬虫 + 'crawler' => 'app\common\command\CrawlerCommand', + 'worldcup_article_translate' => 'app\common\command\WorldcupArticleTranslate', + 'article_ai_comment' => 'app\common\command\ArticleAiComment', + 'sms_verify' => 'app\common\command\SmsVerifyCommand', + 'kb:rebuild' => 'app\common\command\KbRebuild', + 'kb:consume' => 'app\common\command\KbConsume', + ], +]; diff --git a/server/config/cookie.php b/server/config/cookie.php new file mode 100644 index 0000000..d3b3aab --- /dev/null +++ b/server/config/cookie.php @@ -0,0 +1,20 @@ + 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => false, + // 是否使用 setcookie + 'setcookie' => true, + // samesite 设置,支持 'strict' 'lax' + 'samesite' => '', +]; diff --git a/server/config/database.php b/server/config/database.php new file mode 100644 index 0000000..b09a9bd --- /dev/null +++ b/server/config/database.php @@ -0,0 +1,62 @@ + env('database.driver', 'mysql'), + + // 自定义时间查询规则 + 'time_query_rule' => [], + + // 自动写入时间戳字段 + // true为自动识别类型 false关闭 + // 字符串则明确指定时间字段类型 支持 int timestamp datetime date + 'auto_timestamp' => true, + + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + + // 时间字段配置 配置格式:create_time,update_time + 'datetime_field' => '', + + // 数据库连接配置信息 + 'connections' => [ + 'mysql' => [ + // 数据库类型 + 'type' => env('database.type', 'mysql'), + // 服务器地址 + 'hostname' => env('database.hostname', 'likeshop-mysql'), + // 数据库名 + 'database' => env('database.database', 'localhost_likeadmin'), + // 用户名 + 'username' => env('database.username', 'root'), + // 密码 + 'password' => env('database.password', 'root'), + // 端口 + 'hostport' => env('database.hostport', '3306'), + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => env('database.charset', 'utf8mb4'), + // 数据库表前缀 + 'prefix' => env('database.prefix', 'la_'), + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 是否需要断线重连 + 'break_reconnect' => false, + // 监听SQL + 'trigger_sql' => env('app_debug', true), + // 开启字段缓存 + 'fields_cache' => false, + ], + + // 更多的数据库配置信息 + ], +]; diff --git a/server/config/filesystem.php b/server/config/filesystem.php new file mode 100644 index 0000000..965297e --- /dev/null +++ b/server/config/filesystem.php @@ -0,0 +1,24 @@ + env('filesystem.driver', 'local'), + // 磁盘列表 + 'disks' => [ + 'local' => [ + 'type' => 'local', + 'root' => app()->getRuntimePath() . 'storage', + ], + 'public' => [ + // 磁盘类型 + 'type' => 'local', + // 磁盘路径 + 'root' => app()->getRootPath() . 'public/storage', + // 磁盘路径对应的外部URL路径 + 'url' => '/storage', + // 可见性 + 'visibility' => 'public', + ], + // 更多的磁盘配置信息 + ], +]; diff --git a/server/config/lang.php b/server/config/lang.php new file mode 100644 index 0000000..59f320f --- /dev/null +++ b/server/config/lang.php @@ -0,0 +1,27 @@ + env('lang.default_lang', 'zh-cn'), + // 允许的语言列表 + 'allow_lang_list' => [], + // 多语言自动侦测变量名 + 'detect_var' => 'lang', + // 是否使用Cookie记录 + 'use_cookie' => true, + // 多语言cookie变量 + 'cookie_var' => 'think_lang', + // 多语言header变量 + 'header_var' => 'think-lang', + // 扩展语言包 + 'extend_list' => [], + // Accept-Language转义为对应语言包名称 + 'accept_language' => [ + 'zh-hans-cn' => 'zh-cn', + ], + // 是否支持语言分组 + 'allow_group' => false, +]; diff --git a/server/config/log.php b/server/config/log.php new file mode 100644 index 0000000..27b140e --- /dev/null +++ b/server/config/log.php @@ -0,0 +1,45 @@ + env('log.channel', 'file'), + // 日志记录级别 + 'level' => ['error', 'warning', 'info', 'sql'], + // 日志类型记录的通道 ['error'=>'email',...] + 'type_channel' => [], + // 关闭全局日志写入 + 'close' => false, + // 全局日志处理 支持闭包 + 'processor' => null, + + // 日志通道列表 + 'channels' => [ + 'file' => [ + // 日志记录方式 + 'type' => 'File', + // 日志保存目录 + 'path' => '', + // 单文件日志写入 + 'single' => false, + // 独立日志级别 + 'apart_level' => [], + // 最大日志文件数量 + 'max_files' => 0, + // 使用JSON格式记录 + 'json' => false, + // 日志处理 + 'processor' => null, + // 关闭通道日志写入 + 'close' => false, + // 日志输出格式化 + 'format' => '[%s][%s] %s', + // 是否实时写入 + 'realtime_write' => false, + ], + // 其它日志通道配置 + ], + +]; diff --git a/server/config/middleware.php b/server/config/middleware.php new file mode 100644 index 0000000..7e1972f --- /dev/null +++ b/server/config/middleware.php @@ -0,0 +1,8 @@ + [], + // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行 + 'priority' => [], +]; diff --git a/server/config/project.php b/server/config/project.php new file mode 100644 index 0000000..9d00ad3 --- /dev/null +++ b/server/config/project.php @@ -0,0 +1,137 @@ + '1.9.4', + + // 官网 + 'website' => [ + 'name' => env('project.web_name', 'likeadmin'), // 网站名称 + 'url' => env('project.web_url', 'www.likeadmin.cn/'), // 网站地址 + 'login_image' => 'resource/image/adminapi/default/login_image.png', + 'web_logo' => 'resource/image/adminapi/default/web_logo.jpg', // 网站logo + 'web_favicon' => 'resource/image/adminapi/default/web_favicon.ico', // 网站图标 + 'shop_name' => 'likeadmin', // 商城名称 + 'shop_logo' => 'resource/image/adminapi/default/shop_logo.png', // 商城图标 + 'pc_logo' => 'resource/image/adminapi/default/pc_logo.png', // pc_logo + 'pc_ico' => 'resource/image/adminapi/default/web_favicon.ico', // pc_ico + 'pc_title' => 'likeadmin', // PC网站标题 + 'h5_favicon' => 'resource/image/adminapi/default/web_favicon.ico', // 网站图标 + ], + + // 后台登录 + 'admin_login' => [ + // 管理后台登录限制 0-不限制 1-需要限制 + 'login_restrictions' => 1, + // 限制密码错误次数 + 'password_error_times' => 5, + // 限制禁止多少分钟不能登录 + 'limit_login_time' => 30, + ], + + // 唯一标识,密码盐、路径加密等 + 'unique_identification' => env('project.unique_identification', 'likeadmin'), + + // 后台管理员token(登录令牌)配置 + 'admin_token' => [ + 'expire_duration' => 3600 * 8,//管理后台token过期时长(单位秒) + 'be_expire_duration' => 3600,//管理后台token临时过期前时长,自动续期 + ], + + // 商城用户token(登录令牌)配置 + 'user_token' => [ + 'expire_duration' => 3600 * 8,//用户token过期时长(单位秒) + 'be_expire_duration' => 3600,//用户token临时过期前时长,自动续期 + ], + + // 列表页 + 'lists' => [ + 'page_size_max' => 25000,//列表页查询数量限制(列表页每页数量、导出每页数量) + 'page_size' => 25, //默认每页数量 + ], + + // 各种默认图片 + 'default_image' => [ + 'admin_avatar' => 'resource/image/adminapi/default/avatar.png', + 'user_avatar' => 'resource/image/adminapi/default/default_avatar.png', + 'qq_group' => 'resource/image/adminapi/default/qq_group.png', // qq群 + 'customer_service' => 'resource/image/adminapi/default/customer_service.jpg', // 客服 + 'menu_admin' => 'resource/image/adminapi/default/menu_admin.png',// 首页快捷菜单-管理员 + 'menu_role' => 'resource/image/adminapi/default/menu_role.png', // 首页快捷菜单-角色 + 'menu_dept' => 'resource/image/adminapi/default/menu_dept.png',// 首页快捷菜单-部门 + 'menu_dict' => 'resource/image/adminapi/default/menu_dict.png',// 首页快捷菜单-字典 + 'menu_generator' => 'resource/image/adminapi/default/menu_generator.png',// 首页快捷菜单-代码生成器 + 'menu_auth' => 'resource/image/adminapi/default/menu_auth.png',// 首页快捷菜单-菜单权限 + 'menu_web' => 'resource/image/adminapi/default/menu_web.png',// 首页快捷菜单-网站信息 + 'menu_file' => 'resource/image/adminapi/default/menu_file.png',// 首页快捷菜单-素材中心 + ], + + // 文件上传限制 (图片) + 'file_image' => [ + 'jpg', + 'png', + 'gif', + 'jpeg', + 'webp', + 'ico' + ], + + // 文件上传限制 (视频) + 'file_video' => [ + 'wmv', + 'avi', + 'mpg', + 'mpeg', + '3gp', + 'mov', + 'mp4', + 'flv', + 'f4v', + 'rmvb', + 'mkv' + ], + + //上传文件的格式 (文件) + 'file_file' => [ + 'zip', + 'rar', + 'txt', + 'pdf', + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'csv', + 'txt', + 'ftr', + '7z', + 'gz', + 'wgt', + 'apk', + 'ipa' + ], + + // 登录设置 + 'login' => [ + // 登录方式:1-账号密码登录;2-手机短信验证码登录 + 'login_way' => ['1', '2'], + // 注册强制绑定手机 0-关闭 1-开启 + 'coerce_mobile' => 1, + // 第三方授权登录 0-关闭 1-开启 + 'third_auth' => 1, + // 微信授权登录 0-关闭 1-开启 + 'wechat_auth' => 1, + // qq授权登录 0-关闭 1-开启 + 'qq_auth' => 0, + // 登录政策协议 0-关闭 1-开启 + 'login_agreement' => 1, + ], + + // 后台装修 + 'decorate' => [ + // 底部导航栏样式设置 + 'tabbar_style' => ['default_color' => '#999999', 'selected_color' => '#0b55eb'], + ] + +]; diff --git a/server/config/route.php b/server/config/route.php new file mode 100644 index 0000000..b317ed5 --- /dev/null +++ b/server/config/route.php @@ -0,0 +1,45 @@ + '/', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + // URL普通方式参数 用于自动生成 + 'url_common_param' => true, + // 是否开启路由延迟解析 + 'url_lazy_route' => false, + // 是否强制使用路由 + 'url_route_must' => false, + // 合并路由规则 + 'route_rule_merge' => false, + // 路由是否完全匹配 + 'route_complete_match' => false, + // 访问控制器层名称 + 'controller_layer' => 'controller', + // 空控制器名 + 'empty_controller' => 'Error', + // 是否使用控制器后缀 + 'controller_suffix' => true, + // 默认的路由变量规则 + 'default_route_pattern' => '[\w\.]+', + // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则 + 'request_cache_key' => false, + // 请求缓存有效期 + 'request_cache_expire' => null, + // 全局请求缓存排除规则 + 'request_cache_except' => [], + // 默认控制器名 + 'default_controller' => 'Index', + // 默认操作名 + 'default_action' => 'index', + // 操作方法后缀 + 'action_suffix' => '', + // 默认JSONP格式返回的处理方法 + 'default_jsonp_handler' => 'jsonpReturn', + // 默认JSONP处理方法 + 'var_jsonp_handler' => 'callback', +]; diff --git a/server/config/session.php b/server/config/session.php new file mode 100644 index 0000000..c1ef6e1 --- /dev/null +++ b/server/config/session.php @@ -0,0 +1,19 @@ + 'PHPSESSID', + // SESSION_ID的提交变量,解决flash上传跨域 + 'var_session_id' => '', + // 驱动方式 支持file cache + 'type' => 'file', + // 存储连接标识 当type使用cache的时候有效 + 'store' => null, + // 过期时间 + 'expire' => 1440, + // 前缀 + 'prefix' => '', +]; diff --git a/server/config/trace.php b/server/config/trace.php new file mode 100644 index 0000000..fad2392 --- /dev/null +++ b/server/config/trace.php @@ -0,0 +1,10 @@ + 'Html', + // 读取的日志通道名 + 'channel' => '', +]; diff --git a/server/config/view.php b/server/config/view.php new file mode 100644 index 0000000..01259a0 --- /dev/null +++ b/server/config/view.php @@ -0,0 +1,25 @@ + 'Think', + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法 + 'auto_rule' => 1, + // 模板目录名 + 'view_dir_name' => 'view', + // 模板后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + // 模板引擎普通标签开始标记 + 'tpl_begin' => '{', + // 模板引擎普通标签结束标记 + 'tpl_end' => '}', + // 标签库标签开始标记 + 'taglib_begin' => '{', + // 标签库标签结束标记 + 'taglib_end' => '}', +]; diff --git a/server/extend/.gitignore b/server/extend/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/server/extend/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/server/public/admin/assets/403-BKxVR8XN.js b/server/public/admin/assets/403-BKxVR8XN.js new file mode 100644 index 0000000..ef20b2f --- /dev/null +++ b/server/public/admin/assets/403-BKxVR8XN.js @@ -0,0 +1 @@ +import r from"./error-Dx8xMQIp.js";import{d as p,c as i,o as m,U as e,L as s,a as o}from"./@vue-DjVelRet.js";import"./element-plus-9rG8B66a.js";import"./@element-plus-RuzzlbSF.js";import"./lodash-es-VsDguZh5.js";import"./dayjs-Mv9jwnDt.js";import"./@popperjs-D_chPuIy.js";import"./async-validator-9PlIezaS.js";import"./@ctrl-r5W6hzzQ.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-DifUqGFW.js";import"./index-Cjwo1RxW.js";import"./nprogress-DYbUr3Ql.js";import"./pinia-D1_qSsvX.js";import"./axios-DIsA03vz.js";import"./lodash-wrSAtCJj.js";import"./@vueuse-DbKvT5Ch.js";import"./css-color-function-DsMtFL6v.js";import"./balanced-match-BdS7OldZ.js";import"./color-DYO1E7y4.js";import"./clone-Ddk2tjDh.js";import"./color-convert-DsNsD289.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DXsJvHIW.js";import"./clipboard-DPX9bJ1T.js";import"./echarts-CXZcVnUo.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DO4NV0px.js";import"./highlight.js--xG_hASo.js";import"./@highlightjs-DcY7_PRf.js";const a="/assets/no_perms-jDxcYpYC.png",n={class:"error404"},O=p({__name:"403",setup(c){return(_,t)=>(m(),i("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])),_:1})]))}});export{O as default}; diff --git a/server/public/admin/assets/404-CpjwwFw5.js b/server/public/admin/assets/404-CpjwwFw5.js new file mode 100644 index 0000000..903ba10 --- /dev/null +++ b/server/public/admin/assets/404-CpjwwFw5.js @@ -0,0 +1 @@ +import o from"./error-Dx8xMQIp.js";import{d as r,c as t,o as m,U as p}from"./@vue-DjVelRet.js";import"./element-plus-9rG8B66a.js";import"./@element-plus-RuzzlbSF.js";import"./lodash-es-VsDguZh5.js";import"./dayjs-Mv9jwnDt.js";import"./@popperjs-D_chPuIy.js";import"./async-validator-9PlIezaS.js";import"./@ctrl-r5W6hzzQ.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-DifUqGFW.js";import"./index-Cjwo1RxW.js";import"./nprogress-DYbUr3Ql.js";import"./pinia-D1_qSsvX.js";import"./axios-DIsA03vz.js";import"./lodash-wrSAtCJj.js";import"./@vueuse-DbKvT5Ch.js";import"./css-color-function-DsMtFL6v.js";import"./balanced-match-BdS7OldZ.js";import"./color-DYO1E7y4.js";import"./clone-Ddk2tjDh.js";import"./color-convert-DsNsD289.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DXsJvHIW.js";import"./clipboard-DPX9bJ1T.js";import"./echarts-CXZcVnUo.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DO4NV0px.js";import"./highlight.js--xG_hASo.js";import"./@highlightjs-DcY7_PRf.js";const i={class:"error404"},L=r({__name:"404",setup(e){return(c,s)=>(m(),t("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{L as default}; diff --git a/server/public/admin/assets/@ctrl-r5W6hzzQ.js b/server/public/admin/assets/@ctrl-r5W6hzzQ.js new file mode 100644 index 0000000..5386c12 --- /dev/null +++ b/server/public/admin/assets/@ctrl-r5W6hzzQ.js @@ -0,0 +1 @@ +function h(r,t){F(r)&&(r="100%");var e=I(r);return r=t===360?r:Math.min(t,Math.max(0,parseFloat(r))),e&&(r=parseInt(String(r*t),10)/100),Math.abs(r-t)<1e-6?1:(t===360?r=(r<0?r%t+t:r%t)/parseFloat(String(t)):r=r%t/parseFloat(String(t)),r)}function v(r){return Math.min(1,Math.max(0,r))}function F(r){return typeof r=="string"&&r.indexOf(".")!==-1&&parseFloat(r)===1}function I(r){return typeof r=="string"&&r.indexOf("%")!==-1}function A(r){return r=parseFloat(r),(isNaN(r)||r<0||r>1)&&(r=1),r}function p(r){return r<=1?"".concat(Number(r)*100,"%"):r}function b(r){return r.length===1?"0"+r:String(r)}function E(r,t,e){return{r:h(r,255)*255,g:h(t,255)*255,b:h(e,255)*255}}function M(r,t,e){r=h(r,255),t=h(t,255),e=h(e,255);var a=Math.max(r,t,e),n=Math.min(r,t,e),i=0,f=0,s=(a+n)/2;if(a===n)f=0,i=0;else{var u=a-n;switch(f=s>.5?u/(2-a-n):u/(a+n),a){case r:i=(t-e)/u+(t1&&(e-=1),e<1/6?r+(t-r)*(6*e):e<1/2?t:e<2/3?r+(t-r)*(2/3-e)*6:r}function B(r,t,e){var a,n,i;if(r=h(r,360),t=h(t,100),e=h(e,100),t===0)n=e,i=e,a=e;else{var f=e<.5?e*(1+t):e+t-e*t,s=2*e-f;a=l(s,f,r+1/3),n=l(s,f,r),i=l(s,f,r-1/3)}return{r:a*255,g:n*255,b:i*255}}function S(r,t,e){r=h(r,255),t=h(t,255),e=h(e,255);var a=Math.max(r,t,e),n=Math.min(r,t,e),i=0,f=a,s=a-n,u=a===0?0:s/a;if(a===n)i=0;else{switch(a){case r:i=(t-e)/s+(t>16,g:(r&65280)>>8,b:r&255}}var x={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",goldenrod:"#daa520",gold:"#ffd700",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavenderblush:"#fff0f5",lavender:"#e6e6fa",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",rebeccapurple:"#663399",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"};function W(r){var t={r:0,g:0,b:0},e=1,a=null,n=null,i=null,f=!1,s=!1;return typeof r=="string"&&(r=U(r)),typeof r=="object"&&(g(r.r)&&g(r.g)&&g(r.b)?(t=E(r.r,r.g,r.b),f=!0,s=String(r.r).substr(-1)==="%"?"prgb":"rgb"):g(r.h)&&g(r.s)&&g(r.v)?(a=p(r.s),n=p(r.v),t=N(r.h,a,n),f=!0,s="hsv"):g(r.h)&&g(r.s)&&g(r.l)&&(a=p(r.s),i=p(r.l),t=B(r.h,a,i),f=!0,s="hsl"),Object.prototype.hasOwnProperty.call(r,"a")&&(e=r.a)),e=A(e),{ok:f,format:r.format||s,r:Math.min(255,Math.max(t.r,0)),g:Math.min(255,Math.max(t.g,0)),b:Math.min(255,Math.max(t.b,0)),a:e}}var G="[-\\+]?\\d+%?",q="[-\\+]?\\d*\\.\\d+%?",d="(?:".concat(q,")|(?:").concat(G,")"),y="[\\s|\\(]+(".concat(d,")[,|\\s]+(").concat(d,")[,|\\s]+(").concat(d,")\\s*\\)?"),m="[\\s|\\(]+(".concat(d,")[,|\\s]+(").concat(d,")[,|\\s]+(").concat(d,")[,|\\s]+(").concat(d,")\\s*\\)?"),c={CSS_UNIT:new RegExp(d),rgb:new RegExp("rgb"+y),rgba:new RegExp("rgba"+m),hsl:new RegExp("hsl"+y),hsla:new RegExp("hsla"+m),hsv:new RegExp("hsv"+y),hsva:new RegExp("hsva"+m),hex3:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex6:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,hex4:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex8:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/};function U(r){if(r=r.trim().toLowerCase(),r.length===0)return!1;var t=!1;if(x[r])r=x[r],t=!0;else if(r==="transparent")return{r:0,g:0,b:0,a:0,format:"name"};var e=c.rgb.exec(r);return e?{r:e[1],g:e[2],b:e[3]}:(e=c.rgba.exec(r),e?{r:e[1],g:e[2],b:e[3],a:e[4]}:(e=c.hsl.exec(r),e?{h:e[1],s:e[2],l:e[3]}:(e=c.hsla.exec(r),e?{h:e[1],s:e[2],l:e[3],a:e[4]}:(e=c.hsv.exec(r),e?{h:e[1],s:e[2],v:e[3]}:(e=c.hsva.exec(r),e?{h:e[1],s:e[2],v:e[3],a:e[4]}:(e=c.hex8.exec(r),e?{r:o(e[1]),g:o(e[2]),b:o(e[3]),a:w(e[4]),format:t?"name":"hex8"}:(e=c.hex6.exec(r),e?{r:o(e[1]),g:o(e[2]),b:o(e[3]),format:t?"name":"hex"}:(e=c.hex4.exec(r),e?{r:o(e[1]+e[1]),g:o(e[2]+e[2]),b:o(e[3]+e[3]),a:w(e[4]+e[4]),format:t?"name":"hex8"}:(e=c.hex3.exec(r),e?{r:o(e[1]+e[1]),g:o(e[2]+e[2]),b:o(e[3]+e[3]),format:t?"name":"hex"}:!1)))))))))}function g(r){return!!c.CSS_UNIT.exec(String(r))}var D=function(){function r(t,e){t===void 0&&(t=""),e===void 0&&(e={});var a;if(t instanceof r)return t;typeof t=="number"&&(t=O(t)),this.originalInput=t;var n=W(t);this.originalInput=t,this.r=n.r,this.g=n.g,this.b=n.b,this.a=n.a,this.roundA=Math.round(100*this.a)/100,this.format=(a=e.format)!==null&&a!==void 0?a:n.format,this.gradientType=e.gradientType,this.r<1&&(this.r=Math.round(this.r)),this.g<1&&(this.g=Math.round(this.g)),this.b<1&&(this.b=Math.round(this.b)),this.isValid=n.ok}return r.prototype.isDark=function(){return this.getBrightness()<128},r.prototype.isLight=function(){return!this.isDark()},r.prototype.getBrightness=function(){var t=this.toRgb();return(t.r*299+t.g*587+t.b*114)/1e3},r.prototype.getLuminance=function(){var t=this.toRgb(),e,a,n,i=t.r/255,f=t.g/255,s=t.b/255;return i<=.03928?e=i/12.92:e=Math.pow((i+.055)/1.055,2.4),f<=.03928?a=f/12.92:a=Math.pow((f+.055)/1.055,2.4),s<=.03928?n=s/12.92:n=Math.pow((s+.055)/1.055,2.4),.2126*e+.7152*a+.0722*n},r.prototype.getAlpha=function(){return this.a},r.prototype.setAlpha=function(t){return this.a=A(t),this.roundA=Math.round(100*this.a)/100,this},r.prototype.isMonochrome=function(){var t=this.toHsl().s;return t===0},r.prototype.toHsv=function(){var t=S(this.r,this.g,this.b);return{h:t.h*360,s:t.s,v:t.v,a:this.a}},r.prototype.toHsvString=function(){var t=S(this.r,this.g,this.b),e=Math.round(t.h*360),a=Math.round(t.s*100),n=Math.round(t.v*100);return this.a===1?"hsv(".concat(e,", ").concat(a,"%, ").concat(n,"%)"):"hsva(".concat(e,", ").concat(a,"%, ").concat(n,"%, ").concat(this.roundA,")")},r.prototype.toHsl=function(){var t=M(this.r,this.g,this.b);return{h:t.h*360,s:t.s,l:t.l,a:this.a}},r.prototype.toHslString=function(){var t=M(this.r,this.g,this.b),e=Math.round(t.h*360),a=Math.round(t.s*100),n=Math.round(t.l*100);return this.a===1?"hsl(".concat(e,", ").concat(a,"%, ").concat(n,"%)"):"hsla(".concat(e,", ").concat(a,"%, ").concat(n,"%, ").concat(this.roundA,")")},r.prototype.toHex=function(t){return t===void 0&&(t=!1),k(this.r,this.g,this.b,t)},r.prototype.toHexString=function(t){return t===void 0&&(t=!1),"#"+this.toHex(t)},r.prototype.toHex8=function(t){return t===void 0&&(t=!1),P(this.r,this.g,this.b,this.a,t)},r.prototype.toHex8String=function(t){return t===void 0&&(t=!1),"#"+this.toHex8(t)},r.prototype.toHexShortString=function(t){return t===void 0&&(t=!1),this.a===1?this.toHexString(t):this.toHex8String(t)},r.prototype.toRgb=function(){return{r:Math.round(this.r),g:Math.round(this.g),b:Math.round(this.b),a:this.a}},r.prototype.toRgbString=function(){var t=Math.round(this.r),e=Math.round(this.g),a=Math.round(this.b);return this.a===1?"rgb(".concat(t,", ").concat(e,", ").concat(a,")"):"rgba(".concat(t,", ").concat(e,", ").concat(a,", ").concat(this.roundA,")")},r.prototype.toPercentageRgb=function(){var t=function(e){return"".concat(Math.round(h(e,255)*100),"%")};return{r:t(this.r),g:t(this.g),b:t(this.b),a:this.a}},r.prototype.toPercentageRgbString=function(){var t=function(e){return Math.round(h(e,255)*100)};return this.a===1?"rgb(".concat(t(this.r),"%, ").concat(t(this.g),"%, ").concat(t(this.b),"%)"):"rgba(".concat(t(this.r),"%, ").concat(t(this.g),"%, ").concat(t(this.b),"%, ").concat(this.roundA,")")},r.prototype.toName=function(){if(this.a===0)return"transparent";if(this.a<1)return!1;for(var t="#"+k(this.r,this.g,this.b,!1),e=0,a=Object.entries(x);e=0,i=!e&&n&&(t.startsWith("hex")||t==="name");return i?t==="name"&&this.a===0?this.toName():this.toRgbString():(t==="rgb"&&(a=this.toRgbString()),t==="prgb"&&(a=this.toPercentageRgbString()),(t==="hex"||t==="hex6")&&(a=this.toHexString()),t==="hex3"&&(a=this.toHexString(!0)),t==="hex4"&&(a=this.toHex8String(!0)),t==="hex8"&&(a=this.toHex8String()),t==="name"&&(a=this.toName()),t==="hsl"&&(a=this.toHslString()),t==="hsv"&&(a=this.toHsvString()),a||this.toHexString())},r.prototype.toNumber=function(){return(Math.round(this.r)<<16)+(Math.round(this.g)<<8)+Math.round(this.b)},r.prototype.clone=function(){return new r(this.toString())},r.prototype.lighten=function(t){t===void 0&&(t=10);var e=this.toHsl();return e.l+=t/100,e.l=v(e.l),new r(e)},r.prototype.brighten=function(t){t===void 0&&(t=10);var e=this.toRgb();return e.r=Math.max(0,Math.min(255,e.r-Math.round(255*-(t/100)))),e.g=Math.max(0,Math.min(255,e.g-Math.round(255*-(t/100)))),e.b=Math.max(0,Math.min(255,e.b-Math.round(255*-(t/100)))),new r(e)},r.prototype.darken=function(t){t===void 0&&(t=10);var e=this.toHsl();return e.l-=t/100,e.l=v(e.l),new r(e)},r.prototype.tint=function(t){return t===void 0&&(t=10),this.mix("white",t)},r.prototype.shade=function(t){return t===void 0&&(t=10),this.mix("black",t)},r.prototype.desaturate=function(t){t===void 0&&(t=10);var e=this.toHsl();return e.s-=t/100,e.s=v(e.s),new r(e)},r.prototype.saturate=function(t){t===void 0&&(t=10);var e=this.toHsl();return e.s+=t/100,e.s=v(e.s),new r(e)},r.prototype.greyscale=function(){return this.desaturate(100)},r.prototype.spin=function(t){var e=this.toHsl(),a=(e.h+t)%360;return e.h=a<0?360+a:a,new r(e)},r.prototype.mix=function(t,e){e===void 0&&(e=50);var a=this.toRgb(),n=new r(t).toRgb(),i=e/100,f={r:(n.r-a.r)*i+a.r,g:(n.g-a.g)*i+a.g,b:(n.b-a.b)*i+a.b,a:(n.a-a.a)*i+a.a};return new r(f)},r.prototype.analogous=function(t,e){t===void 0&&(t=6),e===void 0&&(e=30);var a=this.toHsl(),n=360/e,i=[this];for(a.h=(a.h-(n*t>>1)+720)%360;--t;)a.h=(a.h+n)%360,i.push(new r(a));return i},r.prototype.complement=function(){var t=this.toHsl();return t.h=(t.h+180)%360,new r(t)},r.prototype.monochromatic=function(t){t===void 0&&(t=6);for(var e=this.toHsv(),a=e.h,n=e.s,i=e.v,f=[],s=1/t;t--;)f.push(new r({h:a,s:n,v:i})),i=(i+s)%1;return f},r.prototype.splitcomplement=function(){var t=this.toHsl(),e=t.h;return[this,new r({h:(e+72)%360,s:t.s,l:t.l}),new r({h:(e+216)%360,s:t.s,l:t.l})]},r.prototype.onBackground=function(t){var e=this.toRgb(),a=new r(t).toRgb(),n=e.a+a.a*(1-e.a);return new r({r:(e.r*e.a+a.r*a.a*(1-e.a))/n,g:(e.g*e.a+a.g*a.a*(1-e.a))/n,b:(e.b*e.a+a.b*a.a*(1-e.a))/n,a:n})},r.prototype.triad=function(){return this.polyad(3)},r.prototype.tetrad=function(){return this.polyad(4)},r.prototype.polyad=function(t){for(var e=this.toHsl(),a=e.h,n=[this],i=360/t,f=1;f(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M288 896h448q32 0 32 32t-32 32H288q-32 0-32-32t32-32"}),e("path",{fill:"currentColor",d:"M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416M512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544"}),e("path",{fill:"currentColor",d:"M544 384h96a32 32 0 1 1 0 64h-96v96a32 32 0 0 1-64 0v-96h-96a32 32 0 0 1 0-64h96v-96a32 32 0 0 1 64 0z"})]))}}),o=p,v=a({name:"Aim",__name:"aim",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),e("path",{fill:"currentColor",d:"M512 96a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V128a32 32 0 0 1 32-32m0 576a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V704a32 32 0 0 1 32-32M96 512a32 32 0 0 1 32-32h192a32 32 0 0 1 0 64H128a32 32 0 0 1-32-32m576 0a32 32 0 0 1 32-32h192a32 32 0 1 1 0 64H704a32 32 0 0 1-32-32"})]))}}),s=v,c=a({name:"AlarmClock",__name:"alarm-clock",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 832a320 320 0 1 0 0-640 320 320 0 0 0 0 640m0 64a384 384 0 1 1 0-768 384 384 0 0 1 0 768"}),e("path",{fill:"currentColor",d:"m292.288 824.576 55.424 32-48 83.136a32 32 0 1 1-55.424-32zm439.424 0-55.424 32 48 83.136a32 32 0 1 0 55.424-32zM512 512h160a32 32 0 1 1 0 64H480a32 32 0 0 1-32-32V320a32 32 0 0 1 64 0zM90.496 312.256A160 160 0 0 1 312.32 90.496l-46.848 46.848a96 96 0 0 0-128 128L90.56 312.256zm835.264 0A160 160 0 0 0 704 90.496l46.848 46.848a96 96 0 0 1 128 128z"})]))}}),n=c,h=a({name:"Apple",__name:"apple",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M599.872 203.776a189.44 189.44 0 0 1 64.384-4.672l2.624.128c31.168 1.024 51.2 4.096 79.488 16.32 37.632 16.128 74.496 45.056 111.488 89.344 96.384 115.264 82.752 372.8-34.752 521.728-7.68 9.728-32 41.6-30.72 39.936a426.624 426.624 0 0 1-30.08 35.776c-31.232 32.576-65.28 49.216-110.08 50.048-31.36.64-53.568-5.312-84.288-18.752l-6.528-2.88c-20.992-9.216-30.592-11.904-47.296-11.904-18.112 0-28.608 2.88-51.136 12.672l-6.464 2.816c-28.416 12.224-48.32 18.048-76.16 19.2-74.112 2.752-116.928-38.08-180.672-132.16-96.64-142.08-132.608-349.312-55.04-486.4 46.272-81.92 129.92-133.632 220.672-135.04 32.832-.576 60.288 6.848 99.648 22.72 27.136 10.88 34.752 13.76 37.376 14.272 16.256-20.16 27.776-36.992 34.56-50.24 13.568-26.304 27.2-59.968 40.704-100.8a32 32 0 1 1 60.8 20.224c-12.608 37.888-25.408 70.4-38.528 97.664zm-51.52 78.08c-14.528 17.792-31.808 37.376-51.904 58.816a32 32 0 1 1-46.72-43.776l12.288-13.248c-28.032-11.2-61.248-26.688-95.68-26.112-70.4 1.088-135.296 41.6-171.648 105.792C121.6 492.608 176 684.16 247.296 788.992c34.816 51.328 76.352 108.992 130.944 106.944 52.48-2.112 72.32-34.688 135.872-34.688 63.552 0 81.28 34.688 136.96 33.536 56.448-1.088 75.776-39.04 126.848-103.872 107.904-136.768 107.904-362.752 35.776-449.088-72.192-86.272-124.672-84.096-151.68-85.12-41.472-4.288-81.6 12.544-113.664 25.152z"})]))}}),i=h,m=a({name:"ArrowDownBold",__name:"arrow-down-bold",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M104.704 338.752a64 64 0 0 1 90.496 0l316.8 316.8 316.8-316.8a64 64 0 0 1 90.496 90.496L557.248 791.296a64 64 0 0 1-90.496 0L104.704 429.248a64 64 0 0 1 0-90.496z"})]))}}),w=m,d=a({name:"ArrowDown",__name:"arrow-down",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"})]))}}),g=d,f=a({name:"ArrowLeftBold",__name:"arrow-left-bold",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M685.248 104.704a64 64 0 0 1 0 90.496L368.448 512l316.8 316.8a64 64 0 0 1-90.496 90.496L232.704 557.248a64 64 0 0 1 0-90.496l362.048-362.048a64 64 0 0 1 90.496 0z"})]))}}),x=f,z=a({name:"ArrowLeft",__name:"arrow-left",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M609.408 149.376 277.76 489.6a32 32 0 0 0 0 44.672l331.648 340.352a29.12 29.12 0 0 0 41.728 0 30.592 30.592 0 0 0 0-42.752L339.264 511.936l311.872-319.872a30.592 30.592 0 0 0 0-42.688 29.12 29.12 0 0 0-41.728 0z"})]))}}),M=z,C=a({name:"ArrowRightBold",__name:"arrow-right-bold",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M338.752 104.704a64 64 0 0 0 0 90.496l316.8 316.8-316.8 316.8a64 64 0 0 0 90.496 90.496l362.048-362.048a64 64 0 0 0 0-90.496L429.248 104.704a64 64 0 0 0-90.496 0z"})]))}}),H=C,V=a({name:"ArrowRight",__name:"arrow-right",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.872a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"})]))}}),y=V,B=a({name:"ArrowUpBold",__name:"arrow-up-bold",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M104.704 685.248a64 64 0 0 0 90.496 0l316.8-316.8 316.8 316.8a64 64 0 0 0 90.496-90.496L557.248 232.704a64 64 0 0 0-90.496 0L104.704 594.752a64 64 0 0 0 0 90.496z"})]))}}),L=B,A=a({name:"ArrowUp",__name:"arrow-up",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m488.832 344.32-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872 319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0"})]))}}),b=A,k=a({name:"Avatar",__name:"avatar",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M628.736 528.896A416 416 0 0 1 928 928H96a415.872 415.872 0 0 1 299.264-399.104L512 704zM720 304a208 208 0 1 1-416 0 208 208 0 0 1 416 0"})]))}}),S=k,q=a({name:"Back",__name:"back",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M224 480h640a32 32 0 1 1 0 64H224a32 32 0 0 1 0-64"}),e("path",{fill:"currentColor",d:"m237.248 512 265.408 265.344a32 32 0 0 1-45.312 45.312l-288-288a32 32 0 0 1 0-45.312l288-288a32 32 0 1 1 45.312 45.312z"})]))}}),F=q,D=a({name:"Baseball",__name:"baseball",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M195.2 828.8a448 448 0 1 1 633.6-633.6 448 448 0 0 1-633.6 633.6zm45.248-45.248a384 384 0 1 0 543.104-543.104 384 384 0 0 0-543.104 543.104"}),e("path",{fill:"currentColor",d:"M497.472 96.896c22.784 4.672 44.416 9.472 64.896 14.528a256.128 256.128 0 0 0 350.208 350.208c5.056 20.48 9.856 42.112 14.528 64.896A320.128 320.128 0 0 1 497.472 96.896zM108.48 491.904a320.128 320.128 0 0 1 423.616 423.68c-23.04-3.648-44.992-7.424-65.728-11.52a256.128 256.128 0 0 0-346.496-346.432 1736.64 1736.64 0 0 1-11.392-65.728z"})]))}}),P=D,R=a({name:"Basketball",__name:"basketball",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M778.752 788.224a382.464 382.464 0 0 0 116.032-245.632 256.512 256.512 0 0 0-241.728-13.952 762.88 762.88 0 0 1 125.696 259.584zm-55.04 44.224a699.648 699.648 0 0 0-125.056-269.632 256.128 256.128 0 0 0-56.064 331.968 382.72 382.72 0 0 0 181.12-62.336m-254.08 61.248A320.128 320.128 0 0 1 557.76 513.6a715.84 715.84 0 0 0-48.192-48.128 320.128 320.128 0 0 1-379.264 88.384 382.4 382.4 0 0 0 110.144 229.696 382.4 382.4 0 0 0 229.184 110.08zM129.28 481.088a256.128 256.128 0 0 0 331.072-56.448 699.648 699.648 0 0 0-268.8-124.352 382.656 382.656 0 0 0-62.272 180.8m106.56-235.84a762.88 762.88 0 0 1 258.688 125.056 256.512 256.512 0 0 0-13.44-241.088A382.464 382.464 0 0 0 235.84 245.248zm318.08-114.944c40.576 89.536 37.76 193.92-8.448 281.344a779.84 779.84 0 0 1 66.176 66.112 320.832 320.832 0 0 1 282.112-8.128 382.4 382.4 0 0 0-110.144-229.12 382.4 382.4 0 0 0-229.632-110.208zM828.8 828.8a448 448 0 1 1-633.6-633.6 448 448 0 0 1 633.6 633.6"})]))}}),T=R,O=a({name:"BellFilled",__name:"bell-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M640 832a128 128 0 0 1-256 0zm192-64H134.4a38.4 38.4 0 0 1 0-76.8H192V448c0-154.88 110.08-284.16 256.32-313.6a64 64 0 1 1 127.36 0A320.128 320.128 0 0 1 832 448v243.2h57.6a38.4 38.4 0 0 1 0 76.8z"})]))}}),G=O,I=a({name:"Bell",__name:"bell",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a64 64 0 0 1 64 64v64H448v-64a64 64 0 0 1 64-64"}),e("path",{fill:"currentColor",d:"M256 768h512V448a256 256 0 1 0-512 0zm256-640a320 320 0 0 1 320 320v384H192V448a320 320 0 0 1 320-320"}),e("path",{fill:"currentColor",d:"M96 768h832q32 0 32 32t-32 32H96q-32 0-32-32t32-32m352 128h128a64 64 0 0 1-128 0"})]))}}),U=I,W=a({name:"Bicycle",__name:"bicycle",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 832a128 128 0 1 0 0-256 128 128 0 0 0 0 256m0 64a192 192 0 1 1 0-384 192 192 0 0 1 0 384"}),e("path",{fill:"currentColor",d:"M288 672h320q32 0 32 32t-32 32H288q-32 0-32-32t32-32"}),e("path",{fill:"currentColor",d:"M768 832a128 128 0 1 0 0-256 128 128 0 0 0 0 256m0 64a192 192 0 1 1 0-384 192 192 0 0 1 0 384"}),e("path",{fill:"currentColor",d:"M480 192a32 32 0 0 1 0-64h160a32 32 0 0 1 31.04 24.256l96 384a32 32 0 0 1-62.08 15.488L615.04 192zM96 384a32 32 0 0 1 0-64h128a32 32 0 0 1 30.336 21.888l64 192a32 32 0 1 1-60.672 20.224L200.96 384z"}),e("path",{fill:"currentColor",d:"m373.376 599.808-42.752-47.616 320-288 42.752 47.616z"})]))}}),E=W,N=a({name:"BottomLeft",__name:"bottom-left",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 768h416a32 32 0 1 1 0 64H224a32 32 0 0 1-32-32V352a32 32 0 0 1 64 0z"}),e("path",{fill:"currentColor",d:"M246.656 822.656a32 32 0 0 1-45.312-45.312l544-544a32 32 0 0 1 45.312 45.312l-544 544z"})]))}}),Z=N,K=a({name:"BottomRight",__name:"bottom-right",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M352 768a32 32 0 1 0 0 64h448a32 32 0 0 0 32-32V352a32 32 0 0 0-64 0v416z"}),e("path",{fill:"currentColor",d:"M777.344 822.656a32 32 0 0 0 45.312-45.312l-544-544a32 32 0 0 0-45.312 45.312z"})]))}}),Q=K,j=a({name:"Bottom",__name:"bottom",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M544 805.888V168a32 32 0 1 0-64 0v637.888L246.656 557.952a30.72 30.72 0 0 0-45.312 0 35.52 35.52 0 0 0 0 48.064l288 306.048a30.72 30.72 0 0 0 45.312 0l288-306.048a35.52 35.52 0 0 0 0-48 30.72 30.72 0 0 0-45.312 0L544 805.824z"})]))}}),J=j,X=a({name:"Bowl",__name:"bowl",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M714.432 704a351.744 351.744 0 0 0 148.16-256H161.408a351.744 351.744 0 0 0 148.16 256zM288 766.592A415.68 415.68 0 0 1 96 416a32 32 0 0 1 32-32h768a32 32 0 0 1 32 32 415.68 415.68 0 0 1-192 350.592V832a64 64 0 0 1-64 64H352a64 64 0 0 1-64-64zM493.248 320h-90.496l254.4-254.4a32 32 0 1 1 45.248 45.248zm187.328 0h-128l269.696-155.712a32 32 0 0 1 32 55.424zM352 768v64h320v-64z"})]))}}),Y=X,$=a({name:"Box",__name:"box",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M317.056 128 128 344.064V896h768V344.064L706.944 128zm-14.528-64h418.944a32 32 0 0 1 24.064 10.88l206.528 236.096A32 32 0 0 1 960 332.032V928a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V332.032a32 32 0 0 1 7.936-21.12L278.4 75.008A32 32 0 0 1 302.528 64z"}),e("path",{fill:"currentColor",d:"M64 320h896v64H64z"}),e("path",{fill:"currentColor",d:"M448 327.872V640h128V327.872L526.08 128h-28.16zM448 64h128l64 256v352a32 32 0 0 1-32 32H416a32 32 0 0 1-32-32V320z"})]))}}),e2=$,a2=a({name:"Briefcase",__name:"briefcase",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M320 320V128h384v192h192v192H128V320zM128 576h768v320H128zm256-256h256.064V192H384z"})]))}}),t2=a2,_2=a({name:"BrushFilled",__name:"brush-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M608 704v160a96 96 0 0 1-192 0V704h-96a128 128 0 0 1-128-128h640a128 128 0 0 1-128 128zM192 512V128.064h640V512z"})]))}}),r2=_2,l2=a({name:"Brush",__name:"brush",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M896 448H128v192a64 64 0 0 0 64 64h192v192h256V704h192a64 64 0 0 0 64-64zm-770.752-64c0-47.552 5.248-90.24 15.552-128 14.72-54.016 42.496-107.392 83.2-160h417.28l-15.36 70.336L736 96h211.2c-24.832 42.88-41.92 96.256-51.2 160a663.872 663.872 0 0 0-6.144 128H960v256a128 128 0 0 1-128 128H704v160a32 32 0 0 1-32 32H352a32 32 0 0 1-32-32V768H192A128 128 0 0 1 64 640V384h61.248zm64 0h636.544c-2.048-45.824.256-91.584 6.848-137.216 4.48-30.848 10.688-59.776 18.688-86.784h-96.64l-221.12 141.248L561.92 160H256.512c-25.856 37.888-43.776 75.456-53.952 112.832-8.768 32.064-13.248 69.12-13.312 111.168z"})]))}}),u2=l2,p2=a({name:"Burger",__name:"burger",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M160 512a32 32 0 0 0-32 32v64a32 32 0 0 0 30.08 32H864a32 32 0 0 0 32-32v-64a32 32 0 0 0-32-32zm736-58.56A96 96 0 0 1 960 544v64a96 96 0 0 1-51.968 85.312L855.36 833.6a96 96 0 0 1-89.856 62.272H258.496A96 96 0 0 1 168.64 833.6l-52.608-140.224A96 96 0 0 1 64 608v-64a96 96 0 0 1 64-90.56V448a384 384 0 1 1 768 5.44M832 448a320 320 0 0 0-640 0zM512 704H188.352l40.192 107.136a32 32 0 0 0 29.952 20.736h507.008a32 32 0 0 0 29.952-20.736L835.648 704z"})]))}}),o2=p2,v2=a({name:"Calendar",__name:"calendar",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 384v512h768V192H768v32a32 32 0 1 1-64 0v-32H320v32a32 32 0 0 1-64 0v-32H128v128h768v64zm192-256h384V96a32 32 0 1 1 64 0v32h160a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h160V96a32 32 0 0 1 64 0zm-32 384h64a32 32 0 0 1 0 64h-64a32 32 0 0 1 0-64m0 192h64a32 32 0 1 1 0 64h-64a32 32 0 1 1 0-64m192-192h64a32 32 0 0 1 0 64h-64a32 32 0 0 1 0-64m0 192h64a32 32 0 1 1 0 64h-64a32 32 0 1 1 0-64m192-192h64a32 32 0 1 1 0 64h-64a32 32 0 1 1 0-64m0 192h64a32 32 0 1 1 0 64h-64a32 32 0 1 1 0-64"})]))}}),s2=v2,c2=a({name:"CameraFilled",__name:"camera-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M160 224a64 64 0 0 0-64 64v512a64 64 0 0 0 64 64h704a64 64 0 0 0 64-64V288a64 64 0 0 0-64-64H748.416l-46.464-92.672A64 64 0 0 0 644.736 96H379.328a64 64 0 0 0-57.216 35.392L275.776 224zm352 435.2a115.2 115.2 0 1 0 0-230.4 115.2 115.2 0 0 0 0 230.4m0 140.8a256 256 0 1 1 0-512 256 256 0 0 1 0 512"})]))}}),n2=c2,h2=a({name:"Camera",__name:"camera",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M896 256H128v576h768zm-199.424-64-32.064-64h-304.96l-32 64zM96 192h160l46.336-92.608A64 64 0 0 1 359.552 64h304.96a64 64 0 0 1 57.216 35.328L768.192 192H928a32 32 0 0 1 32 32v640a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V224a32 32 0 0 1 32-32m416 512a160 160 0 1 0 0-320 160 160 0 0 0 0 320m0 64a224 224 0 1 1 0-448 224 224 0 0 1 0 448"})]))}}),i2=h2,m2=a({name:"CaretBottom",__name:"caret-bottom",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m192 384 320 384 320-384z"})]))}}),w2=m2,d2=a({name:"CaretLeft",__name:"caret-left",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M672 192 288 511.936 672 832z"})]))}}),g2=d2,f2=a({name:"CaretRight",__name:"caret-right",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M384 192v640l384-320.064z"})]))}}),x2=f2,z2=a({name:"CaretTop",__name:"caret-top",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 320 192 704h639.936z"})]))}}),M2=z2,C2=a({name:"Cellphone",__name:"cellphone",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 128a64 64 0 0 0-64 64v640a64 64 0 0 0 64 64h512a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64zm0-64h512a128 128 0 0 1 128 128v640a128 128 0 0 1-128 128H256a128 128 0 0 1-128-128V192A128 128 0 0 1 256 64m128 128h256a32 32 0 1 1 0 64H384a32 32 0 0 1 0-64m128 640a64 64 0 1 1 0-128 64 64 0 0 1 0 128"})]))}}),H2=C2,V2=a({name:"ChatDotRound",__name:"chat-dot-round",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m174.72 855.68 135.296-45.12 23.68 11.84C388.096 849.536 448.576 864 512 864c211.84 0 384-166.784 384-352S723.84 160 512 160 128 326.784 128 512c0 69.12 24.96 139.264 70.848 199.232l22.08 28.8-46.272 115.584zm-45.248 82.56A32 32 0 0 1 89.6 896l58.368-145.92C94.72 680.32 64 596.864 64 512 64 299.904 256 96 512 96s448 203.904 448 416-192 416-448 416a461.056 461.056 0 0 1-206.912-48.384l-175.616 58.56z"}),e("path",{fill:"currentColor",d:"M512 563.2a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4m192 0a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4m-384 0a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4"})]))}}),y2=V2,B2=a({name:"ChatDotSquare",__name:"chat-dot-square",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M273.536 736H800a64 64 0 0 0 64-64V256a64 64 0 0 0-64-64H224a64 64 0 0 0-64 64v570.88zM296 800 147.968 918.4A32 32 0 0 1 96 893.44V256a128 128 0 0 1 128-128h576a128 128 0 0 1 128 128v416a128 128 0 0 1-128 128z"}),e("path",{fill:"currentColor",d:"M512 499.2a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4zm192 0a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4zm-384 0a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4z"})]))}}),L2=B2,A2=a({name:"ChatLineRound",__name:"chat-line-round",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m174.72 855.68 135.296-45.12 23.68 11.84C388.096 849.536 448.576 864 512 864c211.84 0 384-166.784 384-352S723.84 160 512 160 128 326.784 128 512c0 69.12 24.96 139.264 70.848 199.232l22.08 28.8-46.272 115.584zm-45.248 82.56A32 32 0 0 1 89.6 896l58.368-145.92C94.72 680.32 64 596.864 64 512 64 299.904 256 96 512 96s448 203.904 448 416-192 416-448 416a461.056 461.056 0 0 1-206.912-48.384l-175.616 58.56z"}),e("path",{fill:"currentColor",d:"M352 576h320q32 0 32 32t-32 32H352q-32 0-32-32t32-32m32-192h256q32 0 32 32t-32 32H384q-32 0-32-32t32-32"})]))}}),b2=A2,k2=a({name:"ChatLineSquare",__name:"chat-line-square",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M160 826.88 273.536 736H800a64 64 0 0 0 64-64V256a64 64 0 0 0-64-64H224a64 64 0 0 0-64 64zM296 800 147.968 918.4A32 32 0 0 1 96 893.44V256a128 128 0 0 1 128-128h576a128 128 0 0 1 128 128v416a128 128 0 0 1-128 128z"}),e("path",{fill:"currentColor",d:"M352 512h320q32 0 32 32t-32 32H352q-32 0-32-32t32-32m0-192h320q32 0 32 32t-32 32H352q-32 0-32-32t32-32"})]))}}),S2=k2,q2=a({name:"ChatRound",__name:"chat-round",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m174.72 855.68 130.048-43.392 23.424 11.392C382.4 849.984 444.352 864 512 864c223.744 0 384-159.872 384-352 0-192.832-159.104-352-384-352S128 319.168 128 512a341.12 341.12 0 0 0 69.248 204.288l21.632 28.8-44.16 110.528zm-45.248 82.56A32 32 0 0 1 89.6 896l56.512-141.248A405.12 405.12 0 0 1 64 512C64 299.904 235.648 96 512 96s448 203.904 448 416-173.44 416-448 416c-79.68 0-150.848-17.152-211.712-46.72l-170.88 56.96z"})]))}}),F2=q2,D2=a({name:"ChatSquare",__name:"chat-square",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M273.536 736H800a64 64 0 0 0 64-64V256a64 64 0 0 0-64-64H224a64 64 0 0 0-64 64v570.88zM296 800 147.968 918.4A32 32 0 0 1 96 893.44V256a128 128 0 0 1 128-128h576a128 128 0 0 1 128 128v416a128 128 0 0 1-128 128z"})]))}}),P2=D2,R2=a({name:"Check",__name:"check",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M406.656 706.944 195.84 496.256a32 32 0 1 0-45.248 45.248l256 256 512-512a32 32 0 0 0-45.248-45.248L406.592 706.944z"})]))}}),T2=R2,O2=a({name:"Checked",__name:"checked",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M704 192h160v736H160V192h160.064v64H704zM311.616 537.28l-45.312 45.248L447.36 763.52l316.8-316.8-45.312-45.184L447.36 673.024zM384 192V96h256v96z"})]))}}),G2=O2,I2=a({name:"Cherry",__name:"cherry",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M261.056 449.6c13.824-69.696 34.88-128.96 63.36-177.728 23.744-40.832 61.12-88.64 112.256-143.872H320a32 32 0 0 1 0-64h384a32 32 0 1 1 0 64H554.752c14.912 39.168 41.344 86.592 79.552 141.76 47.36 68.48 84.8 106.752 106.304 114.304a224 224 0 1 1-84.992 14.784c-22.656-22.912-47.04-53.76-73.92-92.608-38.848-56.128-67.008-105.792-84.352-149.312-55.296 58.24-94.528 107.52-117.76 147.2-23.168 39.744-41.088 88.768-53.568 147.072a224.064 224.064 0 1 1-64.96-1.6zM288 832a160 160 0 1 0 0-320 160 160 0 0 0 0 320m448-64a160 160 0 1 0 0-320 160 160 0 0 0 0 320"})]))}}),U2=I2,W2=a({name:"Chicken",__name:"chicken",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M349.952 716.992 478.72 588.16a106.688 106.688 0 0 1-26.176-19.072 106.688 106.688 0 0 1-19.072-26.176L304.704 671.744c.768 3.072 1.472 6.144 2.048 9.216l2.048 31.936 31.872 1.984c3.136.64 6.208 1.28 9.28 2.112zm57.344 33.152a128 128 0 1 1-216.32 114.432l-1.92-32-32-1.92a128 128 0 1 1 114.432-216.32L416.64 469.248c-2.432-101.44 58.112-239.104 149.056-330.048 107.328-107.328 231.296-85.504 316.8 0 85.44 85.44 107.328 209.408 0 316.8-91.008 90.88-228.672 151.424-330.112 149.056L407.296 750.08zm90.496-226.304c49.536 49.536 233.344-7.04 339.392-113.088 78.208-78.208 63.232-163.072 0-226.304-63.168-63.232-148.032-78.208-226.24 0C504.896 290.496 448.32 474.368 497.792 523.84M244.864 708.928a64 64 0 1 0-59.84 59.84l56.32-3.52zm8.064 127.68a64 64 0 1 0 59.84-59.84l-56.32 3.52-3.52 56.32z"})]))}}),E2=W2,N2=a({name:"ChromeFilled",__name:"chrome-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",style:{"enable-background":"new 0 0 1024 1024"},viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M938.67 512.01c0-44.59-6.82-87.6-19.54-128H682.67a212.372 212.372 0 0 1 42.67 128c.06 38.71-10.45 76.7-30.42 109.87l-182.91 316.8c235.65-.01 426.66-191.02 426.66-426.67z"}),e("path",{fill:"currentColor",d:"M576.79 401.63a127.92 127.92 0 0 0-63.56-17.6c-22.36-.22-44.39 5.43-63.89 16.38s-35.79 26.82-47.25 46.02a128.005 128.005 0 0 0-2.16 127.44l1.24 2.13a127.906 127.906 0 0 0 46.36 46.61 127.907 127.907 0 0 0 63.38 17.44c22.29.2 44.24-5.43 63.68-16.33a127.94 127.94 0 0 0 47.16-45.79v-.01l1.11-1.92a127.984 127.984 0 0 0 .29-127.46 127.957 127.957 0 0 0-46.36-46.91"}),e("path",{fill:"currentColor",d:"M394.45 333.96A213.336 213.336 0 0 1 512 298.67h369.58A426.503 426.503 0 0 0 512 85.34a425.598 425.598 0 0 0-171.74 35.98 425.644 425.644 0 0 0-142.62 102.22l118.14 204.63a213.397 213.397 0 0 1 78.67-94.21m117.56 604.72H512zm-97.25-236.73a213.284 213.284 0 0 1-89.54-86.81L142.48 298.6c-36.35 62.81-57.13 135.68-57.13 213.42 0 203.81 142.93 374.22 333.95 416.55h.04l118.19-204.71a213.315 213.315 0 0 1-122.77-21.91z"})]))}}),Z2=N2,K2=a({name:"CircleCheckFilled",__name:"circle-check-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m-55.808 536.384-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.272 38.272 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336z"})]))}}),Q2=K2,j2=a({name:"CircleCheck",__name:"circle-check",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),e("path",{fill:"currentColor",d:"M745.344 361.344a32 32 0 0 1 45.312 45.312l-288 288a32 32 0 0 1-45.312 0l-160-160a32 32 0 1 1 45.312-45.312L480 626.752l265.344-265.408z"})]))}}),J2=j2,X2=a({name:"CircleCloseFilled",__name:"circle-close-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 393.664L407.936 353.6a38.4 38.4 0 1 0-54.336 54.336L457.664 512 353.6 616.064a38.4 38.4 0 1 0 54.336 54.336L512 566.336 616.064 670.4a38.4 38.4 0 1 0 54.336-54.336L566.336 512 670.4 407.936a38.4 38.4 0 1 0-54.336-54.336z"})]))}}),Y2=X2,$2=a({name:"CircleClose",__name:"circle-close",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m466.752 512-90.496-90.496a32 32 0 0 1 45.248-45.248L512 466.752l90.496-90.496a32 32 0 1 1 45.248 45.248L557.248 512l90.496 90.496a32 32 0 1 1-45.248 45.248L512 557.248l-90.496 90.496a32 32 0 0 1-45.248-45.248z"}),e("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"})]))}}),e0=$2,a0=a({name:"CirclePlusFilled",__name:"circle-plus-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m-38.4 409.6H326.4a38.4 38.4 0 1 0 0 76.8h147.2v147.2a38.4 38.4 0 0 0 76.8 0V550.4h147.2a38.4 38.4 0 0 0 0-76.8H550.4V326.4a38.4 38.4 0 1 0-76.8 0v147.2z"})]))}}),t0=a0,_0=a({name:"CirclePlus",__name:"circle-plus",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M352 480h320a32 32 0 1 1 0 64H352a32 32 0 0 1 0-64"}),e("path",{fill:"currentColor",d:"M480 672V352a32 32 0 1 1 64 0v320a32 32 0 0 1-64 0"}),e("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"})]))}}),r0=_0,l0=a({name:"Clock",__name:"clock",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),e("path",{fill:"currentColor",d:"M480 256a32 32 0 0 1 32 32v256a32 32 0 0 1-64 0V288a32 32 0 0 1 32-32"}),e("path",{fill:"currentColor",d:"M480 512h256q32 0 32 32t-32 32H480q-32 0-32-32t32-32"})]))}}),u0=l0,p0=a({name:"CloseBold",__name:"close-bold",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M195.2 195.2a64 64 0 0 1 90.496 0L512 421.504 738.304 195.2a64 64 0 0 1 90.496 90.496L602.496 512 828.8 738.304a64 64 0 0 1-90.496 90.496L512 602.496 285.696 828.8a64 64 0 0 1-90.496-90.496L421.504 512 195.2 285.696a64 64 0 0 1 0-90.496z"})]))}}),o0=p0,v0=a({name:"Close",__name:"close",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"})]))}}),s0=v0,c0=a({name:"Cloudy",__name:"cloudy",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M598.4 831.872H328.192a256 256 0 0 1-34.496-510.528A352 352 0 1 1 598.4 831.872m-271.36-64h272.256a288 288 0 1 0-248.512-417.664L335.04 381.44l-34.816 3.584a192 192 0 0 0 26.88 382.848z"})]))}}),n0=c0,h0=a({name:"CoffeeCup",__name:"coffee-cup",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M768 192a192 192 0 1 1-8 383.808A256.128 256.128 0 0 1 512 768H320A256 256 0 0 1 64 512V160a32 32 0 0 1 32-32h640a32 32 0 0 1 32 32zm0 64v256a128 128 0 1 0 0-256M96 832h640a32 32 0 1 1 0 64H96a32 32 0 1 1 0-64m32-640v320a192 192 0 0 0 192 192h192a192 192 0 0 0 192-192V192z"})]))}}),i0=h0,m0=a({name:"Coffee",__name:"coffee",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M822.592 192h14.272a32 32 0 0 1 31.616 26.752l21.312 128A32 32 0 0 1 858.24 384h-49.344l-39.04 546.304A32 32 0 0 1 737.92 960H285.824a32 32 0 0 1-32-29.696L214.912 384H165.76a32 32 0 0 1-31.552-37.248l21.312-128A32 32 0 0 1 187.136 192h14.016l-6.72-93.696A32 32 0 0 1 226.368 64h571.008a32 32 0 0 1 31.936 34.304zm-64.128 0 4.544-64H260.736l4.544 64h493.184m-548.16 128H820.48l-10.688-64H214.208l-10.688 64h6.784m68.736 64 36.544 512H708.16l36.544-512z"})]))}}),w0=m0,d0=a({name:"Coin",__name:"coin",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m161.92 580.736 29.888 58.88C171.328 659.776 160 681.728 160 704c0 82.304 155.328 160 352 160s352-77.696 352-160c0-22.272-11.392-44.16-31.808-64.32l30.464-58.432C903.936 615.808 928 657.664 928 704c0 129.728-188.544 224-416 224S96 833.728 96 704c0-46.592 24.32-88.576 65.92-123.264z"}),e("path",{fill:"currentColor",d:"m161.92 388.736 29.888 58.88C171.328 467.84 160 489.792 160 512c0 82.304 155.328 160 352 160s352-77.696 352-160c0-22.272-11.392-44.16-31.808-64.32l30.464-58.432C903.936 423.808 928 465.664 928 512c0 129.728-188.544 224-416 224S96 641.728 96 512c0-46.592 24.32-88.576 65.92-123.264z"}),e("path",{fill:"currentColor",d:"M512 544c-227.456 0-416-94.272-416-224S284.544 96 512 96s416 94.272 416 224-188.544 224-416 224m0-64c196.672 0 352-77.696 352-160S708.672 160 512 160s-352 77.696-352 160 155.328 160 352 160"})]))}}),g0=d0,f0=a({name:"ColdDrink",__name:"cold-drink",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M768 64a192 192 0 1 1-69.952 370.88L480 725.376V896h96a32 32 0 1 1 0 64H320a32 32 0 1 1 0-64h96V725.376L76.8 273.536a64 64 0 0 1-12.8-38.4v-10.688a32 32 0 0 1 32-32h71.808l-65.536-83.84a32 32 0 0 1 50.432-39.424l96.256 123.264h337.728A192.064 192.064 0 0 1 768 64M656.896 192.448H800a32 32 0 0 1 32 32v10.624a64 64 0 0 1-12.8 38.4l-80.448 107.2a128 128 0 1 0-81.92-188.16v-.064zm-357.888 64 129.472 165.76a32 32 0 0 1-50.432 39.36l-160.256-205.12H144l304 404.928 304-404.928z"})]))}}),x0=f0,z0=a({name:"CollectionTag",__name:"collection-tag",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 128v698.88l196.032-156.864a96 96 0 0 1 119.936 0L768 826.816V128zm-32-64h576a32 32 0 0 1 32 32v797.44a32 32 0 0 1-51.968 24.96L531.968 720a32 32 0 0 0-39.936 0L243.968 918.4A32 32 0 0 1 192 893.44V96a32 32 0 0 1 32-32"})]))}}),M0=z0,C0=a({name:"Collection",__name:"collection",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M192 736h640V128H256a64 64 0 0 0-64 64zm64-672h608a32 32 0 0 1 32 32v672a32 32 0 0 1-32 32H160l-32 57.536V192A128 128 0 0 1 256 64"}),e("path",{fill:"currentColor",d:"M240 800a48 48 0 1 0 0 96h592v-96zm0-64h656v160a64 64 0 0 1-64 64H240a112 112 0 0 1 0-224m144-608v250.88l96-76.8 96 76.8V128zm-64-64h320v381.44a32 32 0 0 1-51.968 24.96L480 384l-108.032 86.4A32 32 0 0 1 320 445.44z"})]))}}),H0=C0,V0=a({name:"Comment",__name:"comment",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M736 504a56 56 0 1 1 0-112 56 56 0 0 1 0 112m-224 0a56 56 0 1 1 0-112 56 56 0 0 1 0 112m-224 0a56 56 0 1 1 0-112 56 56 0 0 1 0 112M128 128v640h192v160l224-160h352V128z"})]))}}),y0=V0,B0=a({name:"Compass",__name:"compass",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),e("path",{fill:"currentColor",d:"M725.888 315.008C676.48 428.672 624 513.28 568.576 568.64c-55.424 55.424-139.968 107.904-253.568 157.312a12.8 12.8 0 0 1-16.896-16.832c49.536-113.728 102.016-198.272 157.312-253.632 55.36-55.296 139.904-107.776 253.632-157.312a12.8 12.8 0 0 1 16.832 16.832"})]))}}),L0=B0,A0=a({name:"Connection",__name:"connection",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M640 384v64H448a128 128 0 0 0-128 128v128a128 128 0 0 0 128 128h320a128 128 0 0 0 128-128V576a128 128 0 0 0-64-110.848V394.88c74.56 26.368 128 97.472 128 181.056v128a192 192 0 0 1-192 192H448a192 192 0 0 1-192-192V576a192 192 0 0 1 192-192z"}),e("path",{fill:"currentColor",d:"M384 640v-64h192a128 128 0 0 0 128-128V320a128 128 0 0 0-128-128H256a128 128 0 0 0-128 128v128a128 128 0 0 0 64 110.848v70.272A192.064 192.064 0 0 1 64 448V320a192 192 0 0 1 192-192h320a192 192 0 0 1 192 192v128a192 192 0 0 1-192 192z"})]))}}),b0=A0,k0=a({name:"Coordinate",__name:"coordinate",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M480 512h64v320h-64z"}),e("path",{fill:"currentColor",d:"M192 896h640a64 64 0 0 0-64-64H256a64 64 0 0 0-64 64m64-128h512a128 128 0 0 1 128 128v64H128v-64a128 128 0 0 1 128-128m256-256a192 192 0 1 0 0-384 192 192 0 0 0 0 384m0 64a256 256 0 1 1 0-512 256 256 0 0 1 0 512"})]))}}),S0=k0,q0=a({name:"CopyDocument",__name:"copy-document",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M768 832a128 128 0 0 1-128 128H192A128 128 0 0 1 64 832V384a128 128 0 0 1 128-128v64a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64z"}),e("path",{fill:"currentColor",d:"M384 128a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64zm0-64h448a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H384a128 128 0 0 1-128-128V192A128 128 0 0 1 384 64"})]))}}),F0=q0,D0=a({name:"Cpu",__name:"cpu",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M320 256a64 64 0 0 0-64 64v384a64 64 0 0 0 64 64h384a64 64 0 0 0 64-64V320a64 64 0 0 0-64-64zm0-64h384a128 128 0 0 1 128 128v384a128 128 0 0 1-128 128H320a128 128 0 0 1-128-128V320a128 128 0 0 1 128-128"}),e("path",{fill:"currentColor",d:"M512 64a32 32 0 0 1 32 32v128h-64V96a32 32 0 0 1 32-32m160 0a32 32 0 0 1 32 32v128h-64V96a32 32 0 0 1 32-32m-320 0a32 32 0 0 1 32 32v128h-64V96a32 32 0 0 1 32-32m160 896a32 32 0 0 1-32-32V800h64v128a32 32 0 0 1-32 32m160 0a32 32 0 0 1-32-32V800h64v128a32 32 0 0 1-32 32m-320 0a32 32 0 0 1-32-32V800h64v128a32 32 0 0 1-32 32M64 512a32 32 0 0 1 32-32h128v64H96a32 32 0 0 1-32-32m0-160a32 32 0 0 1 32-32h128v64H96a32 32 0 0 1-32-32m0 320a32 32 0 0 1 32-32h128v64H96a32 32 0 0 1-32-32m896-160a32 32 0 0 1-32 32H800v-64h128a32 32 0 0 1 32 32m0-160a32 32 0 0 1-32 32H800v-64h128a32 32 0 0 1 32 32m0 320a32 32 0 0 1-32 32H800v-64h128a32 32 0 0 1 32 32"})]))}}),P0=D0,R0=a({name:"CreditCard",__name:"credit-card",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M896 324.096c0-42.368-2.496-55.296-9.536-68.48a52.352 52.352 0 0 0-22.144-22.08c-13.12-7.04-26.048-9.536-68.416-9.536H228.096c-42.368 0-55.296 2.496-68.48 9.536a52.352 52.352 0 0 0-22.08 22.144c-7.04 13.12-9.536 26.048-9.536 68.416v375.808c0 42.368 2.496 55.296 9.536 68.48a52.352 52.352 0 0 0 22.144 22.08c13.12 7.04 26.048 9.536 68.416 9.536h567.808c42.368 0 55.296-2.496 68.48-9.536a52.352 52.352 0 0 0 22.08-22.144c7.04-13.12 9.536-26.048 9.536-68.416zm64 0v375.808c0 57.088-5.952 77.76-17.088 98.56-11.136 20.928-27.52 37.312-48.384 48.448-20.864 11.136-41.6 17.088-98.56 17.088H228.032c-57.088 0-77.76-5.952-98.56-17.088a116.288 116.288 0 0 1-48.448-48.384c-11.136-20.864-17.088-41.6-17.088-98.56V324.032c0-57.088 5.952-77.76 17.088-98.56 11.136-20.928 27.52-37.312 48.384-48.448 20.864-11.136 41.6-17.088 98.56-17.088H795.84c57.088 0 77.76 5.952 98.56 17.088 20.928 11.136 37.312 27.52 48.448 48.384 11.136 20.864 17.088 41.6 17.088 98.56z"}),e("path",{fill:"currentColor",d:"M64 320h896v64H64zm0 128h896v64H64zm128 192h256v64H192z"})]))}}),T0=R0,O0=a({name:"Crop",__name:"crop",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 768h672a32 32 0 1 1 0 64H224a32 32 0 0 1-32-32V96a32 32 0 0 1 64 0z"}),e("path",{fill:"currentColor",d:"M832 224v704a32 32 0 1 1-64 0V256H96a32 32 0 0 1 0-64h704a32 32 0 0 1 32 32"})]))}}),G0=O0,I0=a({name:"DArrowLeft",__name:"d-arrow-left",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M529.408 149.376a29.12 29.12 0 0 1 41.728 0 30.592 30.592 0 0 1 0 42.688L259.264 511.936l311.872 319.936a30.592 30.592 0 0 1-.512 43.264 29.12 29.12 0 0 1-41.216-.512L197.76 534.272a32 32 0 0 1 0-44.672l331.648-340.224zm256 0a29.12 29.12 0 0 1 41.728 0 30.592 30.592 0 0 1 0 42.688L515.264 511.936l311.872 319.936a30.592 30.592 0 0 1-.512 43.264 29.12 29.12 0 0 1-41.216-.512L453.76 534.272a32 32 0 0 1 0-44.672l331.648-340.224z"})]))}}),U0=I0,W0=a({name:"DArrowRight",__name:"d-arrow-right",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M452.864 149.312a29.12 29.12 0 0 1 41.728.064L826.24 489.664a32 32 0 0 1 0 44.672L494.592 874.624a29.12 29.12 0 0 1-41.728 0 30.592 30.592 0 0 1 0-42.752L764.736 512 452.864 192a30.592 30.592 0 0 1 0-42.688m-256 0a29.12 29.12 0 0 1 41.728.064L570.24 489.664a32 32 0 0 1 0 44.672L238.592 874.624a29.12 29.12 0 0 1-41.728 0 30.592 30.592 0 0 1 0-42.752L508.736 512 196.864 192a30.592 30.592 0 0 1 0-42.688z"})]))}}),E0=W0,N0=a({name:"DCaret",__name:"d-caret",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m512 128 288 320H224zM224 576h576L512 896z"})]))}}),Z0=N0,K0=a({name:"DataAnalysis",__name:"data-analysis",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m665.216 768 110.848 192h-73.856L591.36 768H433.024L322.176 960H248.32l110.848-192H160a32 32 0 0 1-32-32V192H64a32 32 0 0 1 0-64h896a32 32 0 1 1 0 64h-64v544a32 32 0 0 1-32 32zM832 192H192v512h640zM352 448a32 32 0 0 1 32 32v64a32 32 0 0 1-64 0v-64a32 32 0 0 1 32-32m160-64a32 32 0 0 1 32 32v128a32 32 0 0 1-64 0V416a32 32 0 0 1 32-32m160-64a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V352a32 32 0 0 1 32-32"})]))}}),Q0=K0,j0=a({name:"DataBoard",__name:"data-board",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M32 128h960v64H32z"}),e("path",{fill:"currentColor",d:"M192 192v512h640V192zm-64-64h768v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32z"}),e("path",{fill:"currentColor",d:"M322.176 960H248.32l144.64-250.56 55.424 32zm453.888 0h-73.856L576 741.44l55.424-32z"})]))}}),J0=j0,X0=a({name:"DataLine",__name:"data-line",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M359.168 768H160a32 32 0 0 1-32-32V192H64a32 32 0 0 1 0-64h896a32 32 0 1 1 0 64h-64v544a32 32 0 0 1-32 32H665.216l110.848 192h-73.856L591.36 768H433.024L322.176 960H248.32zM832 192H192v512h640zM342.656 534.656a32 32 0 1 1-45.312-45.312L444.992 341.76l125.44 94.08L679.04 300.032a32 32 0 1 1 49.92 39.936L581.632 524.224 451.008 426.24 342.656 534.592z"})]))}}),Y0=X0,$0=a({name:"DeleteFilled",__name:"delete-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M352 192V95.936a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V192h256a32 32 0 1 1 0 64H96a32 32 0 0 1 0-64zm64 0h192v-64H416zM192 960a32 32 0 0 1-32-32V256h704v672a32 32 0 0 1-32 32zm224-192a32 32 0 0 0 32-32V416a32 32 0 0 0-64 0v320a32 32 0 0 0 32 32m192 0a32 32 0 0 0 32-32V416a32 32 0 0 0-64 0v320a32 32 0 0 0 32 32"})]))}}),e1=$0,a1=a({name:"DeleteLocation",__name:"delete-location",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M288 896h448q32 0 32 32t-32 32H288q-32 0-32-32t32-32"}),e("path",{fill:"currentColor",d:"M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416M512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544"}),e("path",{fill:"currentColor",d:"M384 384h256q32 0 32 32t-32 32H384q-32 0-32-32t32-32"})]))}}),t1=a1,_1=a({name:"Delete",__name:"delete",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M160 256H96a32 32 0 0 1 0-64h256V95.936a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V192h256a32 32 0 1 1 0 64h-64v672a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32zm448-64v-64H416v64zM224 896h576V256H224zm192-128a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32m192 0a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32"})]))}}),r1=_1,l1=a({name:"Dessert",__name:"dessert",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 416v-48a144 144 0 0 1 168.64-141.888 224.128 224.128 0 0 1 430.72 0A144 144 0 0 1 896 368v48a384 384 0 0 1-352 382.72V896h-64v-97.28A384 384 0 0 1 128 416m287.104-32.064h193.792a143.808 143.808 0 0 1 58.88-132.736 160.064 160.064 0 0 0-311.552 0 143.808 143.808 0 0 1 58.88 132.8zm-72.896 0a72 72 0 1 0-140.48 0h140.48m339.584 0h140.416a72 72 0 1 0-140.48 0zM512 736a320 320 0 0 0 318.4-288.064H193.6A320 320 0 0 0 512 736M384 896.064h256a32 32 0 1 1 0 64H384a32 32 0 1 1 0-64"})]))}}),u1=l1,p1=a({name:"Discount",__name:"discount",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M224 704h576V318.336L552.512 115.84a64 64 0 0 0-81.024 0L224 318.336zm0 64v128h576V768zM593.024 66.304l259.2 212.096A32 32 0 0 1 864 303.168V928a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32V303.168a32 32 0 0 1 11.712-24.768l259.2-212.096a128 128 0 0 1 162.112 0"}),e("path",{fill:"currentColor",d:"M512 448a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"})]))}}),o1=p1,v1=a({name:"DishDot",__name:"dish-dot",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m384.064 274.56.064-50.688A128 128 0 0 1 512.128 96c70.528 0 127.68 57.152 127.68 127.68v50.752A448.192 448.192 0 0 1 955.392 768H68.544A448.192 448.192 0 0 1 384 274.56zM96 832h832a32 32 0 1 1 0 64H96a32 32 0 1 1 0-64m32-128h768a384 384 0 1 0-768 0m447.808-448v-32.32a63.68 63.68 0 0 0-63.68-63.68 64 64 0 0 0-64 63.936V256z"})]))}}),s1=v1,c1=a({name:"Dish",__name:"dish",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M480 257.152V192h-96a32 32 0 0 1 0-64h256a32 32 0 1 1 0 64h-96v65.152A448 448 0 0 1 955.52 768H68.48A448 448 0 0 1 480 257.152M128 704h768a384 384 0 1 0-768 0M96 832h832a32 32 0 1 1 0 64H96a32 32 0 1 1 0-64"})]))}}),n1=c1,h1=a({name:"DocumentAdd",__name:"document-add",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M832 384H576V128H192v768h640zm-26.496-64L640 154.496V320zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m320 512V448h64v128h128v64H544v128h-64V640H352v-64z"})]))}}),i1=h1,m1=a({name:"DocumentChecked",__name:"document-checked",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M805.504 320 640 154.496V320zM832 384H576V128H192v768h640zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m318.4 582.144 180.992-180.992L704.64 510.4 478.4 736.64 320 578.304l45.248-45.312z"})]))}}),w1=m1,d1=a({name:"DocumentCopy",__name:"document-copy",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 320v576h576V320zm-32-64h640a32 32 0 0 1 32 32v640a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V288a32 32 0 0 1 32-32M960 96v704a32 32 0 0 1-32 32h-96v-64h64V128H384v64h-64V96a32 32 0 0 1 32-32h576a32 32 0 0 1 32 32M256 672h320v64H256zm0-192h320v64H256z"})]))}}),g1=d1,f1=a({name:"DocumentDelete",__name:"document-delete",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M805.504 320 640 154.496V320zM832 384H576V128H192v768h640zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m308.992 546.304-90.496-90.624 45.248-45.248 90.56 90.496 90.496-90.432 45.248 45.248-90.496 90.56 90.496 90.496-45.248 45.248-90.496-90.496-90.56 90.496-45.248-45.248 90.496-90.496z"})]))}}),x1=f1,z1=a({name:"DocumentRemove",__name:"document-remove",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M805.504 320 640 154.496V320zM832 384H576V128H192v768h640zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m192 512h320v64H352z"})]))}}),M1=z1,C1=a({name:"Document",__name:"document",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M832 384H576V128H192v768h640zm-26.496-64L640 154.496V320zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m160 448h384v64H320zm0-192h160v64H320zm0 384h384v64H320z"})]))}}),H1=C1,V1=a({name:"Download",__name:"download",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M160 832h704a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64m384-253.696 236.288-236.352 45.248 45.248L508.8 704 192 387.2l45.248-45.248L480 584.704V128h64z"})]))}}),y1=V1,B1=a({name:"Drizzling",__name:"drizzling",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m739.328 291.328-35.2-6.592-12.8-33.408a192.064 192.064 0 0 0-365.952 23.232l-9.92 40.896-41.472 7.04a176.32 176.32 0 0 0-146.24 173.568c0 97.28 78.72 175.936 175.808 175.936h400a192 192 0 0 0 35.776-380.672zM959.552 480a256 256 0 0 1-256 256h-400A239.808 239.808 0 0 1 63.744 496.192a240.32 240.32 0 0 1 199.488-236.8 256.128 256.128 0 0 1 487.872-30.976A256.064 256.064 0 0 1 959.552 480M288 800h64v64h-64zm192 0h64v64h-64zm-96 96h64v64h-64zm192 0h64v64h-64zm96-96h64v64h-64z"})]))}}),L1=B1,A1=a({name:"EditPen",__name:"edit-pen",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m199.04 672.64 193.984 112 224-387.968-193.92-112-224 388.032zm-23.872 60.16 32.896 148.288 144.896-45.696zM455.04 229.248l193.92 112 56.704-98.112-193.984-112-56.64 98.112zM104.32 708.8l384-665.024 304.768 175.936L409.152 884.8h.064l-248.448 78.336zm384 254.272v-64h448v64h-448z"})]))}}),b1=A1,k1=a({name:"Edit",__name:"edit",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M832 512a32 32 0 1 1 64 0v352a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h352a32 32 0 0 1 0 64H192v640h640z"}),e("path",{fill:"currentColor",d:"m469.952 554.24 52.8-7.552L847.104 222.4a32 32 0 1 0-45.248-45.248L477.44 501.44l-7.552 52.8zm422.4-422.4a96 96 0 0 1 0 135.808l-331.84 331.84a32 32 0 0 1-18.112 9.088L436.8 623.68a32 32 0 0 1-36.224-36.224l15.104-105.6a32 32 0 0 1 9.024-18.112l331.904-331.84a96 96 0 0 1 135.744 0z"})]))}}),S1=k1,q1=a({name:"ElemeFilled",__name:"eleme-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M176 64h672c61.824 0 112 50.176 112 112v672a112 112 0 0 1-112 112H176A112 112 0 0 1 64 848V176c0-61.824 50.176-112 112-112m150.528 173.568c-152.896 99.968-196.544 304.064-97.408 456.96a330.688 330.688 0 0 0 456.96 96.64c9.216-5.888 17.6-11.776 25.152-18.56a18.24 18.24 0 0 0 4.224-24.32L700.352 724.8a47.552 47.552 0 0 0-65.536-14.272A234.56 234.56 0 0 1 310.592 641.6C240 533.248 271.104 387.968 379.456 316.48a234.304 234.304 0 0 1 276.352 15.168c1.664.832 2.56 2.56 3.392 4.224 5.888 8.384 3.328 19.328-5.12 25.216L456.832 489.6a47.552 47.552 0 0 0-14.336 65.472l16 24.384c5.888 8.384 16.768 10.88 25.216 5.056l308.224-199.936a19.584 19.584 0 0 0 6.72-23.488v-.896c-4.992-9.216-10.048-17.6-15.104-26.88-99.968-151.168-304.064-194.88-456.96-95.744zM786.88 504.704l-62.208 40.32c-8.32 5.888-10.88 16.768-4.992 25.216L760 632.32c5.888 8.448 16.768 11.008 25.152 5.12l31.104-20.16a55.36 55.36 0 0 0 16-76.48l-20.224-31.04a19.52 19.52 0 0 0-25.152-5.12z"})]))}}),F1=q1,D1=a({name:"Eleme",__name:"eleme",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M300.032 188.8c174.72-113.28 408-63.36 522.24 109.44 5.76 10.56 11.52 20.16 17.28 30.72v.96a22.4 22.4 0 0 1-7.68 26.88l-352.32 228.48c-9.6 6.72-22.08 3.84-28.8-5.76l-18.24-27.84a54.336 54.336 0 0 1 16.32-74.88l225.6-146.88c9.6-6.72 12.48-19.2 5.76-28.8-.96-1.92-1.92-3.84-3.84-4.8a267.84 267.84 0 0 0-315.84-17.28c-123.84 81.6-159.36 247.68-78.72 371.52a268.096 268.096 0 0 0 370.56 78.72 54.336 54.336 0 0 1 74.88 16.32l17.28 26.88c5.76 9.6 3.84 21.12-4.8 27.84-8.64 7.68-18.24 14.4-28.8 21.12a377.92 377.92 0 0 1-522.24-110.4c-113.28-174.72-63.36-408 111.36-522.24zm526.08 305.28a22.336 22.336 0 0 1 28.8 5.76l23.04 35.52a63.232 63.232 0 0 1-18.24 87.36l-35.52 23.04c-9.6 6.72-22.08 3.84-28.8-5.76l-46.08-71.04c-6.72-9.6-3.84-22.08 5.76-28.8l71.04-46.08z"})]))}}),P1=D1,R1=a({name:"ElementPlus",__name:"element-plus",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M839.7 734.7c0 33.3-17.9 41-17.9 41S519.7 949.8 499.2 960c-10.2 5.1-20.5 5.1-30.7 0 0 0-314.9-184.3-325.1-192-5.1-5.1-10.2-12.8-12.8-20.5V368.6c0-17.9 20.5-28.2 20.5-28.2L466 158.6c12.8-5.1 25.6-5.1 38.4 0 0 0 279 161.3 309.8 179.2 17.9 7.7 28.2 25.6 25.6 46.1-.1-5-.1 317.5-.1 350.8M714.2 371.2c-64-35.8-217.6-125.4-217.6-125.4-7.7-5.1-20.5-5.1-30.7 0L217.6 389.1s-17.9 10.2-17.9 23v297c0 5.1 5.1 12.8 7.7 17.9 7.7 5.1 256 148.5 256 148.5 7.7 5.1 17.9 5.1 25.6 0 15.4-7.7 250.9-145.9 250.9-145.9s12.8-5.1 12.8-30.7v-74.2l-276.5 169v-64c0-17.9 7.7-30.7 20.5-46.1L745 535c5.1-7.7 10.2-20.5 10.2-30.7v-66.6l-279 169v-69.1c0-15.4 5.1-30.7 17.9-38.4l220.1-128zM919 135.7c0-5.1-5.1-7.7-7.7-7.7h-58.9V66.6c0-5.1-5.1-5.1-10.2-5.1l-30.7 5.1c-5.1 0-5.1 2.6-5.1 5.1V128h-56.3c-5.1 0-5.1 5.1-7.7 5.1v38.4h69.1v64c0 5.1 5.1 5.1 10.2 5.1l30.7-5.1c5.1 0 5.1-2.6 5.1-5.1v-56.3h64l-2.5-38.4z"})]))}}),T1=R1,O1=a({name:"Expand",__name:"expand",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 192h768v128H128zm0 256h512v128H128zm0 256h768v128H128zm576-352 192 160-192 128z"})]))}}),G1=O1,I1=a({name:"Failed",__name:"failed",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m557.248 608 135.744-135.744-45.248-45.248-135.68 135.744-135.808-135.68-45.248 45.184L466.752 608l-135.68 135.68 45.184 45.312L512 653.248l135.744 135.744 45.248-45.248L557.312 608zM704 192h160v736H160V192h160v64h384zm-320 0V96h256v96z"})]))}}),U1=I1,W1=a({name:"Female",__name:"female",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 640a256 256 0 1 0 0-512 256 256 0 0 0 0 512m0 64a320 320 0 1 1 0-640 320 320 0 0 1 0 640"}),e("path",{fill:"currentColor",d:"M512 640q32 0 32 32v256q0 32-32 32t-32-32V672q0-32 32-32"}),e("path",{fill:"currentColor",d:"M352 800h320q32 0 32 32t-32 32H352q-32 0-32-32t32-32"})]))}}),E1=W1,N1=a({name:"Files",__name:"files",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 384v448h768V384zm-32-64h832a32 32 0 0 1 32 32v512a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V352a32 32 0 0 1 32-32m64-128h704v64H160zm96-128h512v64H256z"})]))}}),Z1=N1,K1=a({name:"Film",__name:"film",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M160 160v704h704V160zm-32-64h768a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32H128a32 32 0 0 1-32-32V128a32 32 0 0 1 32-32"}),e("path",{fill:"currentColor",d:"M320 288V128h64v352h256V128h64v160h160v64H704v128h160v64H704v128h160v64H704v160h-64V544H384v352h-64V736H128v-64h192V544H128v-64h192V352H128v-64z"})]))}}),Q1=K1,j1=a({name:"Filter",__name:"filter",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M384 523.392V928a32 32 0 0 0 46.336 28.608l192-96A32 32 0 0 0 640 832V523.392l280.768-343.104a32 32 0 1 0-49.536-40.576l-288 352A32 32 0 0 0 576 512v300.224l-128 64V512a32 32 0 0 0-7.232-20.288L195.52 192H704a32 32 0 1 0 0-64H128a32 32 0 0 0-24.768 52.288z"})]))}}),J1=j1,X1=a({name:"Finished",__name:"finished",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M280.768 753.728 691.456 167.04a32 32 0 1 1 52.416 36.672L314.24 817.472a32 32 0 0 1-45.44 7.296l-230.4-172.8a32 32 0 0 1 38.4-51.2l203.968 152.96zM736 448a32 32 0 1 1 0-64h192a32 32 0 1 1 0 64zM608 640a32 32 0 0 1 0-64h319.936a32 32 0 1 1 0 64zM480 832a32 32 0 1 1 0-64h447.936a32 32 0 1 1 0 64z"})]))}}),Y1=X1,$1=a({name:"FirstAidKit",__name:"first-aid-kit",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M192 256a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h640a64 64 0 0 0 64-64V320a64 64 0 0 0-64-64zm0-64h640a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H192A128 128 0 0 1 64 768V320a128 128 0 0 1 128-128"}),e("path",{fill:"currentColor",d:"M544 512h96a32 32 0 0 1 0 64h-96v96a32 32 0 0 1-64 0v-96h-96a32 32 0 0 1 0-64h96v-96a32 32 0 0 1 64 0zM352 128v64h320v-64zm-32-64h384a32 32 0 0 1 32 32v128a32 32 0 0 1-32 32H320a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32"})]))}}),e4=$1,a4=a({name:"Flag",__name:"flag",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M288 128h608L736 384l160 256H288v320h-96V64h96z"})]))}}),t4=a4,_4=a({name:"Fold",__name:"fold",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M896 192H128v128h768zm0 256H384v128h512zm0 256H128v128h768zM320 384 128 512l192 128z"})]))}}),r4=_4,l4=a({name:"FolderAdd",__name:"folder-add",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 192v640h768V320H485.76L357.504 192zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32m384 416V416h64v128h128v64H544v128h-64V608H352v-64z"})]))}}),u4=l4,p4=a({name:"FolderChecked",__name:"folder-checked",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 192v640h768V320H485.76L357.504 192zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32m414.08 502.144 180.992-180.992L736.32 494.4 510.08 720.64l-158.4-158.336 45.248-45.312z"})]))}}),o4=p4,v4=a({name:"FolderDelete",__name:"folder-delete",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 192v640h768V320H485.76L357.504 192zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32m370.752 448-90.496-90.496 45.248-45.248L512 530.752l90.496-90.496 45.248 45.248L557.248 576l90.496 90.496-45.248 45.248L512 621.248l-90.496 90.496-45.248-45.248z"})]))}}),s4=v4,c4=a({name:"FolderOpened",__name:"folder-opened",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M878.08 448H241.92l-96 384h636.16l96-384zM832 384v-64H485.76L357.504 192H128v448l57.92-231.744A32 32 0 0 1 216.96 384zm-24.96 512H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h287.872l128.384 128H864a32 32 0 0 1 32 32v96h23.04a32 32 0 0 1 31.04 39.744l-112 448A32 32 0 0 1 807.04 896"})]))}}),n4=c4,h4=a({name:"FolderRemove",__name:"folder-remove",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 192v640h768V320H485.76L357.504 192zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32m256 416h320v64H352z"})]))}}),i4=h4,m4=a({name:"Folder",__name:"folder",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 192v640h768V320H485.76L357.504 192zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32"})]))}}),w4=m4,d4=a({name:"Food",__name:"food",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 352.576V352a288 288 0 0 1 491.072-204.224 192 192 0 0 1 274.24 204.48 64 64 0 0 1 57.216 74.24C921.6 600.512 850.048 710.656 736 756.992V800a96 96 0 0 1-96 96H384a96 96 0 0 1-96-96v-43.008c-114.048-46.336-185.6-156.48-214.528-330.496A64 64 0 0 1 128 352.64zm64-.576h64a160 160 0 0 1 320 0h64a224 224 0 0 0-448 0m128 0h192a96 96 0 0 0-192 0m439.424 0h68.544A128.256 128.256 0 0 0 704 192c-15.36 0-29.952 2.688-43.52 7.616 11.328 18.176 20.672 37.76 27.84 58.304A64.128 64.128 0 0 1 759.424 352M672 768H352v32a32 32 0 0 0 32 32h256a32 32 0 0 0 32-32zm-342.528-64h365.056c101.504-32.64 165.76-124.928 192.896-288H136.576c27.136 163.072 91.392 255.36 192.896 288"})]))}}),g4=d4,f4=a({name:"Football",__name:"football",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 960a448 448 0 1 1 0-896 448 448 0 0 1 0 896m0-64a384 384 0 1 0 0-768 384 384 0 0 0 0 768"}),e("path",{fill:"currentColor",d:"M186.816 268.288c16-16.384 31.616-31.744 46.976-46.08 17.472 30.656 39.808 58.112 65.984 81.28l-32.512 56.448a385.984 385.984 0 0 1-80.448-91.648zm653.696-5.312a385.92 385.92 0 0 1-83.776 96.96l-32.512-56.384a322.923 322.923 0 0 0 68.48-85.76c15.552 14.08 31.488 29.12 47.808 45.184zM465.984 445.248l11.136-63.104a323.584 323.584 0 0 0 69.76 0l11.136 63.104a387.968 387.968 0 0 1-92.032 0m-62.72-12.8A381.824 381.824 0 0 1 320 396.544l32-55.424a319.885 319.885 0 0 0 62.464 27.712l-11.2 63.488zm300.8-35.84a381.824 381.824 0 0 1-83.328 35.84l-11.2-63.552A319.885 319.885 0 0 0 672 341.184l32 55.424zm-520.768 364.8a385.92 385.92 0 0 1 83.968-97.28l32.512 56.32c-26.88 23.936-49.856 52.352-67.52 84.032-16-13.44-32.32-27.712-48.96-43.072zm657.536.128a1442.759 1442.759 0 0 1-49.024 43.072 321.408 321.408 0 0 0-67.584-84.16l32.512-56.32c33.216 27.456 61.696 60.352 84.096 97.408zM465.92 578.752a387.968 387.968 0 0 1 92.032 0l-11.136 63.104a323.584 323.584 0 0 0-69.76 0zm-62.72 12.8 11.2 63.552a319.885 319.885 0 0 0-62.464 27.712L320 627.392a381.824 381.824 0 0 1 83.264-35.84zm300.8 35.84-32 55.424a318.272 318.272 0 0 0-62.528-27.712l11.2-63.488c29.44 8.64 57.28 20.736 83.264 35.776z"})]))}}),x4=f4,z4=a({name:"ForkSpoon",__name:"fork-spoon",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 410.304V96a32 32 0 0 1 64 0v314.304a96 96 0 0 0 64-90.56V96a32 32 0 0 1 64 0v223.744a160 160 0 0 1-128 156.8V928a32 32 0 1 1-64 0V476.544a160 160 0 0 1-128-156.8V96a32 32 0 0 1 64 0v223.744a96 96 0 0 0 64 90.56zM672 572.48C581.184 552.128 512 446.848 512 320c0-141.44 85.952-256 192-256s192 114.56 192 256c0 126.848-69.184 232.128-160 252.48V928a32 32 0 1 1-64 0zM704 512c66.048 0 128-82.56 128-192s-61.952-192-128-192-128 82.56-128 192 61.952 192 128 192"})]))}}),M4=z4,C4=a({name:"Fries",__name:"fries",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M608 224v-64a32 32 0 0 0-64 0v336h26.88A64 64 0 0 0 608 484.096zm101.12 160A64 64 0 0 0 672 395.904V384h64V224a32 32 0 1 0-64 0v160zm74.88 0a92.928 92.928 0 0 1 91.328 110.08l-60.672 323.584A96 96 0 0 1 720.32 896H303.68a96 96 0 0 1-94.336-78.336L148.672 494.08A92.928 92.928 0 0 1 240 384h-16V224a96 96 0 0 1 188.608-25.28A95.744 95.744 0 0 1 480 197.44V160a96 96 0 0 1 188.608-25.28A96 96 0 0 1 800 224v160zM670.784 512a128 128 0 0 1-99.904 48H453.12a128 128 0 0 1-99.84-48H352v-1.536a128.128 128.128 0 0 1-9.984-14.976L314.88 448H240a28.928 28.928 0 0 0-28.48 34.304L241.088 640h541.824l29.568-157.696A28.928 28.928 0 0 0 784 448h-74.88l-27.136 47.488A132.405 132.405 0 0 1 672 510.464V512zM480 288a32 32 0 0 0-64 0v196.096A64 64 0 0 0 453.12 496H480zm-128 96V224a32 32 0 0 0-64 0v160zh-37.12A64 64 0 0 1 352 395.904zm-98.88 320 19.072 101.888A32 32 0 0 0 303.68 832h416.64a32 32 0 0 0 31.488-26.112L770.88 704z"})]))}}),H4=C4,V4=a({name:"FullScreen",__name:"full-screen",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m160 96.064 192 .192a32 32 0 0 1 0 64l-192-.192V352a32 32 0 0 1-64 0V96h64zm0 831.872V928H96V672a32 32 0 1 1 64 0v191.936l192-.192a32 32 0 1 1 0 64zM864 96.064V96h64v256a32 32 0 1 1-64 0V160.064l-192 .192a32 32 0 1 1 0-64l192-.192zm0 831.872-192-.192a32 32 0 0 1 0-64l192 .192V672a32 32 0 1 1 64 0v256h-64z"})]))}}),y4=V4,B4=a({name:"GobletFull",__name:"goblet-full",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 320h512c0-78.592-12.608-142.4-36.928-192h-434.24C269.504 192.384 256 256.256 256 320m503.936 64H264.064a256.128 256.128 0 0 0 495.872 0zM544 638.4V896h96a32 32 0 1 1 0 64H384a32 32 0 1 1 0-64h96V638.4A320 320 0 0 1 192 320c0-85.632 21.312-170.944 64-256h512c42.688 64.32 64 149.632 64 256a320 320 0 0 1-288 318.4"})]))}}),L4=B4,A4=a({name:"GobletSquareFull",__name:"goblet-square-full",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 270.912c10.048 6.72 22.464 14.912 28.992 18.624a220.16 220.16 0 0 0 114.752 30.72c30.592 0 49.408-9.472 91.072-41.152l.64-.448c52.928-40.32 82.368-55.04 132.288-54.656 55.552.448 99.584 20.8 142.72 57.408l1.536 1.28V128H256v142.912zm.96 76.288C266.368 482.176 346.88 575.872 512 576c157.44.064 237.952-85.056 253.248-209.984a952.32 952.32 0 0 1-40.192-35.712c-32.704-27.776-63.36-41.92-101.888-42.24-31.552-.256-50.624 9.28-93.12 41.6l-.576.448c-52.096 39.616-81.024 54.208-129.792 54.208-54.784 0-100.48-13.376-142.784-37.056zM480 638.848C250.624 623.424 192 442.496 192 319.68V96a32 32 0 0 1 32-32h576a32 32 0 0 1 32 32v224c0 122.816-58.624 303.68-288 318.912V896h96a32 32 0 1 1 0 64H384a32 32 0 1 1 0-64h96z"})]))}}),b4=A4,k4=a({name:"GobletSquare",__name:"goblet-square",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M544 638.912V896h96a32 32 0 1 1 0 64H384a32 32 0 1 1 0-64h96V638.848C250.624 623.424 192 442.496 192 319.68V96a32 32 0 0 1 32-32h576a32 32 0 0 1 32 32v224c0 122.816-58.624 303.68-288 318.912M256 319.68c0 149.568 80 256.192 256 256.256C688.128 576 768 469.568 768 320V128H256z"})]))}}),S4=k4,q4=a({name:"Goblet",__name:"goblet",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M544 638.4V896h96a32 32 0 1 1 0 64H384a32 32 0 1 1 0-64h96V638.4A320 320 0 0 1 192 320c0-85.632 21.312-170.944 64-256h512c42.688 64.32 64 149.632 64 256a320 320 0 0 1-288 318.4M256 320a256 256 0 1 0 512 0c0-78.592-12.608-142.4-36.928-192h-434.24C269.504 192.384 256 256.256 256 320"})]))}}),F4=q4,D4=a({name:"GoldMedal",__name:"gold-medal",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",style:{"enable-background":"new 0 0 1024 1024"},viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m772.13 452.84 53.86-351.81c1.32-10.01-1.17-18.68-7.49-26.02S804.35 64 795.01 64H228.99v-.01h-.06c-9.33 0-17.15 3.67-23.49 11.01s-8.83 16.01-7.49 26.02l53.87 351.89C213.54 505.73 193.59 568.09 192 640c2 90.67 33.17 166.17 93.5 226.5S421.33 957.99 512 960c90.67-2 166.17-33.17 226.5-93.5 60.33-60.34 91.49-135.83 93.5-226.5-1.59-71.94-21.56-134.32-59.87-187.16zM640.01 128h117.02l-39.01 254.02c-20.75-10.64-40.74-19.73-59.94-27.28-5.92-3-11.95-5.8-18.08-8.41V128h.01zM576 128v198.76c-13.18-2.58-26.74-4.43-40.67-5.55-8.07-.8-15.85-1.2-23.33-1.2-10.54 0-21.09.66-31.64 1.96a359.844 359.844 0 0 0-32.36 4.79V128zm-192 0h.04v218.3c-6.22 2.66-12.34 5.5-18.36 8.56-19.13 7.54-39.02 16.6-59.66 27.16L267.01 128zm308.99 692.99c-48 48-108.33 73-180.99 75.01-72.66-2.01-132.99-27.01-180.99-75.01S258.01 712.66 256 640c2.01-72.66 27.01-132.99 75.01-180.99 19.67-19.67 41.41-35.47 65.22-47.41 38.33-15.04 71.15-23.92 98.44-26.65 5.07-.41 10.2-.7 15.39-.88.63-.01 1.28-.03 1.91-.03.66 0 1.35.03 2.02.04 5.11.17 10.15.46 15.13.86 27.4 2.71 60.37 11.65 98.91 26.79 23.71 11.93 45.36 27.69 64.96 47.29 48 48 73 108.33 75.01 180.99-2.01 72.65-27.01 132.98-75.01 180.98z"}),e("path",{fill:"currentColor",d:"M544 480H416v64h64v192h-64v64h192v-64h-64z"})]))}}),P4=D4,R4=a({name:"GoodsFilled",__name:"goods-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M192 352h640l64 544H128zm128 224h64V448h-64zm320 0h64V448h-64zM384 288h-64a192 192 0 1 1 384 0h-64a128 128 0 1 0-256 0"})]))}}),T4=R4,O4=a({name:"Goods",__name:"goods",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M320 288v-22.336C320 154.688 405.504 64 512 64s192 90.688 192 201.664v22.4h131.072a32 32 0 0 1 31.808 28.8l57.6 576a32 32 0 0 1-31.808 35.2H131.328a32 32 0 0 1-31.808-35.2l57.6-576a32 32 0 0 1 31.808-28.8H320zm64 0h256v-22.336C640 189.248 582.272 128 512 128c-70.272 0-128 61.248-128 137.664v22.4zm-64 64H217.92l-51.2 512h690.56l-51.264-512H704v96a32 32 0 1 1-64 0v-96H384v96a32 32 0 0 1-64 0z"})]))}}),G4=O4,I4=a({name:"Grape",__name:"grape",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M544 195.2a160 160 0 0 1 96 60.8 160 160 0 1 1 146.24 254.976 160 160 0 0 1-128 224 160 160 0 1 1-292.48 0 160 160 0 0 1-128-224A160 160 0 1 1 384 256a160 160 0 0 1 96-60.8V128h-64a32 32 0 0 1 0-64h192a32 32 0 0 1 0 64h-64zM512 448a96 96 0 1 0 0-192 96 96 0 0 0 0 192m-256 0a96 96 0 1 0 0-192 96 96 0 0 0 0 192m128 224a96 96 0 1 0 0-192 96 96 0 0 0 0 192m128 224a96 96 0 1 0 0-192 96 96 0 0 0 0 192m128-224a96 96 0 1 0 0-192 96 96 0 0 0 0 192m128-224a96 96 0 1 0 0-192 96 96 0 0 0 0 192"})]))}}),U4=I4,W4=a({name:"Grid",__name:"grid",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M640 384v256H384V384zm64 0h192v256H704zm-64 512H384V704h256zm64 0V704h192v192zm-64-768v192H384V128zm64 0h192v192H704zM320 384v256H128V384zm0 512H128V704h192zm0-768v192H128V128z"})]))}}),E4=W4,N4=a({name:"Guide",__name:"guide",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M640 608h-64V416h64zm0 160v160a32 32 0 0 1-32 32H416a32 32 0 0 1-32-32V768h64v128h128V768zM384 608V416h64v192zm256-352h-64V128H448v128h-64V96a32 32 0 0 1 32-32h192a32 32 0 0 1 32 32z"}),e("path",{fill:"currentColor",d:"m220.8 256-71.232 80 71.168 80H768V256H220.8zm-14.4-64H800a32 32 0 0 1 32 32v224a32 32 0 0 1-32 32H206.4a32 32 0 0 1-23.936-10.752l-99.584-112a32 32 0 0 1 0-42.496l99.584-112A32 32 0 0 1 206.4 192m678.784 496-71.104 80H266.816V608h547.2l71.168 80zm-56.768-144H234.88a32 32 0 0 0-32 32v224a32 32 0 0 0 32 32h593.6a32 32 0 0 0 23.936-10.752l99.584-112a32 32 0 0 0 0-42.496l-99.584-112A32 32 0 0 0 828.48 544z"})]))}}),Z4=N4,K4=a({name:"Handbag",__name:"handbag",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",style:{"enable-background":"new 0 0 1024 1024"},viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M887.01 264.99c-6-5.99-13.67-8.99-23.01-8.99H704c-1.34-54.68-20.01-100.01-56-136s-81.32-54.66-136-56c-54.68 1.34-100.01 20.01-136 56s-54.66 81.32-56 136H160c-9.35 0-17.02 3-23.01 8.99-5.99 6-8.99 13.67-8.99 23.01v640c0 9.35 2.99 17.02 8.99 23.01S150.66 960 160 960h704c9.35 0 17.02-2.99 23.01-8.99S896 937.34 896 928V288c0-9.35-2.99-17.02-8.99-23.01M421.5 165.5c24.32-24.34 54.49-36.84 90.5-37.5 35.99.68 66.16 13.18 90.5 37.5s36.84 54.49 37.5 90.5H384c.68-35.99 13.18-66.16 37.5-90.5M832 896H192V320h128v128h64V320h256v128h64V320h128z"})]))}}),Q4=K4,j4=a({name:"Headset",__name:"headset",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M896 529.152V512a384 384 0 1 0-768 0v17.152A128 128 0 0 1 320 640v128a128 128 0 1 1-256 0V512a448 448 0 1 1 896 0v256a128 128 0 1 1-256 0V640a128 128 0 0 1 192-110.848M896 640a64 64 0 0 0-128 0v128a64 64 0 0 0 128 0zm-768 0v128a64 64 0 0 0 128 0V640a64 64 0 1 0-128 0"})]))}}),J4=j4,X4=a({name:"HelpFilled",__name:"help-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M926.784 480H701.312A192.512 192.512 0 0 0 544 322.688V97.216A416.064 416.064 0 0 1 926.784 480m0 64A416.064 416.064 0 0 1 544 926.784V701.312A192.512 192.512 0 0 0 701.312 544zM97.28 544h225.472A192.512 192.512 0 0 0 480 701.312v225.472A416.064 416.064 0 0 1 97.216 544zm0-64A416.064 416.064 0 0 1 480 97.216v225.472A192.512 192.512 0 0 0 322.688 480H97.216z"})]))}}),Y4=X4,$4=a({name:"Help",__name:"help",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m759.936 805.248-90.944-91.008A254.912 254.912 0 0 1 512 768a254.912 254.912 0 0 1-156.992-53.76l-90.944 91.008A382.464 382.464 0 0 0 512 896c94.528 0 181.12-34.176 247.936-90.752m45.312-45.312A382.464 382.464 0 0 0 896 512c0-94.528-34.176-181.12-90.752-247.936l-91.008 90.944C747.904 398.4 768 452.864 768 512c0 59.136-20.096 113.6-53.76 156.992l91.008 90.944zm-45.312-541.184A382.464 382.464 0 0 0 512 128c-94.528 0-181.12 34.176-247.936 90.752l90.944 91.008A254.912 254.912 0 0 1 512 256c59.136 0 113.6 20.096 156.992 53.76l90.944-91.008zm-541.184 45.312A382.464 382.464 0 0 0 128 512c0 94.528 34.176 181.12 90.752 247.936l91.008-90.944A254.912 254.912 0 0 1 256 512c0-59.136 20.096-113.6 53.76-156.992zm417.28 394.496a194.56 194.56 0 0 0 22.528-22.528C686.912 602.56 704 559.232 704 512a191.232 191.232 0 0 0-67.968-146.56A191.296 191.296 0 0 0 512 320a191.232 191.232 0 0 0-146.56 67.968C337.088 421.44 320 464.768 320 512a191.232 191.232 0 0 0 67.968 146.56C421.44 686.912 464.768 704 512 704c47.296 0 90.56-17.088 124.032-45.44zM512 960a448 448 0 1 1 0-896 448 448 0 0 1 0 896"})]))}}),e6=$4,a6=a({name:"Hide",__name:"hide",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M876.8 156.8c0-9.6-3.2-16-9.6-22.4-6.4-6.4-12.8-9.6-22.4-9.6-9.6 0-16 3.2-22.4 9.6L736 220.8c-64-32-137.6-51.2-224-60.8-160 16-288 73.6-377.6 176C44.8 438.4 0 496 0 512s48 73.6 134.4 176c22.4 25.6 44.8 48 73.6 67.2l-86.4 89.6c-6.4 6.4-9.6 12.8-9.6 22.4 0 9.6 3.2 16 9.6 22.4 6.4 6.4 12.8 9.6 22.4 9.6 9.6 0 16-3.2 22.4-9.6l704-710.4c3.2-6.4 6.4-12.8 6.4-22.4Zm-646.4 528c-76.8-70.4-128-128-153.6-172.8 28.8-48 80-105.6 153.6-172.8C304 272 400 230.4 512 224c64 3.2 124.8 19.2 176 44.8l-54.4 54.4C598.4 300.8 560 288 512 288c-64 0-115.2 22.4-160 64s-64 96-64 160c0 48 12.8 89.6 35.2 124.8L256 707.2c-9.6-6.4-19.2-16-25.6-22.4Zm140.8-96c-12.8-22.4-19.2-48-19.2-76.8 0-44.8 16-83.2 48-112 32-28.8 67.2-48 112-48 28.8 0 54.4 6.4 73.6 19.2zM889.599 336c-12.8-16-28.8-28.8-41.6-41.6l-48 48c73.6 67.2 124.8 124.8 150.4 169.6-28.8 48-80 105.6-153.6 172.8-73.6 67.2-172.8 108.8-284.8 115.2-51.2-3.2-99.2-12.8-140.8-28.8l-48 48c57.6 22.4 118.4 38.4 188.8 44.8 160-16 288-73.6 377.6-176C979.199 585.6 1024 528 1024 512s-48.001-73.6-134.401-176Z"}),e("path",{fill:"currentColor",d:"M511.998 672c-12.8 0-25.6-3.2-38.4-6.4l-51.2 51.2c28.8 12.8 57.6 19.2 89.6 19.2 64 0 115.2-22.4 160-64 41.6-41.6 64-96 64-160 0-32-6.4-64-19.2-89.6l-51.2 51.2c3.2 12.8 6.4 25.6 6.4 38.4 0 44.8-16 83.2-48 112-32 28.8-67.2 48-112 48Z"})]))}}),t6=a6,_6=a({name:"Histogram",__name:"histogram",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M416 896V128h192v768zm-288 0V448h192v448zm576 0V320h192v576z"})]))}}),r6=_6,l6=a({name:"HomeFilled",__name:"home-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 128 128 447.936V896h255.936V640H640v256h255.936V447.936z"})]))}}),u6=l6,p6=a({name:"HotWater",__name:"hot-water",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M273.067 477.867h477.866V409.6H273.067zm0 68.266v51.2A187.733 187.733 0 0 0 460.8 785.067h102.4a187.733 187.733 0 0 0 187.733-187.734v-51.2H273.067zm-34.134-204.8h546.134a34.133 34.133 0 0 1 34.133 34.134v221.866a256 256 0 0 1-256 256H460.8a256 256 0 0 1-256-256V375.467a34.133 34.133 0 0 1 34.133-34.134zM512 34.133a34.133 34.133 0 0 1 34.133 34.134v170.666a34.133 34.133 0 0 1-68.266 0V68.267A34.133 34.133 0 0 1 512 34.133zM375.467 102.4a34.133 34.133 0 0 1 34.133 34.133v102.4a34.133 34.133 0 0 1-68.267 0v-102.4a34.133 34.133 0 0 1 34.134-34.133m273.066 0a34.133 34.133 0 0 1 34.134 34.133v102.4a34.133 34.133 0 1 1-68.267 0v-102.4a34.133 34.133 0 0 1 34.133-34.133M170.667 921.668h682.666a34.133 34.133 0 1 1 0 68.267H170.667a34.133 34.133 0 1 1 0-68.267z"})]))}}),o6=p6,v6=a({name:"House",__name:"house",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M192 413.952V896h640V413.952L512 147.328zM139.52 374.4l352-293.312a32 32 0 0 1 40.96 0l352 293.312A32 32 0 0 1 896 398.976V928a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V398.976a32 32 0 0 1 11.52-24.576"})]))}}),s6=v6,c6=a({name:"IceCreamRound",__name:"ice-cream-round",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m308.352 489.344 226.304 226.304a32 32 0 0 0 45.248 0L783.552 512A192 192 0 1 0 512 240.448L308.352 444.16a32 32 0 0 0 0 45.248zm135.744 226.304L308.352 851.392a96 96 0 0 1-135.744-135.744l135.744-135.744-45.248-45.248a96 96 0 0 1 0-135.808L466.752 195.2A256 256 0 0 1 828.8 557.248L625.152 760.96a96 96 0 0 1-135.808 0l-45.248-45.248zM398.848 670.4 353.6 625.152 217.856 760.896a32 32 0 0 0 45.248 45.248zm248.96-384.64a32 32 0 0 1 0 45.248L466.624 512a32 32 0 1 1-45.184-45.248l180.992-181.056a32 32 0 0 1 45.248 0zm90.496 90.496a32 32 0 0 1 0 45.248L557.248 602.496A32 32 0 1 1 512 557.248l180.992-180.992a32 32 0 0 1 45.312 0z"})]))}}),n6=c6,h6=a({name:"IceCreamSquare",__name:"ice-cream-square",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M416 640h256a32 32 0 0 0 32-32V160a32 32 0 0 0-32-32H352a32 32 0 0 0-32 32v448a32 32 0 0 0 32 32zm192 64v160a96 96 0 0 1-192 0V704h-64a96 96 0 0 1-96-96V160a96 96 0 0 1 96-96h320a96 96 0 0 1 96 96v448a96 96 0 0 1-96 96zm-64 0h-64v160a32 32 0 1 0 64 0z"})]))}}),i6=h6,m6=a({name:"IceCream",__name:"ice-cream",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128.64 448a208 208 0 0 1 193.536-191.552 224 224 0 0 1 445.248 15.488A208.128 208.128 0 0 1 894.784 448H896L548.8 983.68a32 32 0 0 1-53.248.704L128 448zm64.256 0h286.208a144 144 0 0 0-286.208 0zm351.36 0h286.272a144 144 0 0 0-286.272 0zm-294.848 64 271.808 396.608L778.24 512H249.408zM511.68 352.64a207.872 207.872 0 0 1 189.184-96.192 160 160 0 0 0-314.752 5.632c52.608 12.992 97.28 46.08 125.568 90.56"})]))}}),w6=m6,d6=a({name:"IceDrink",__name:"ice-drink",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 448v128h239.68l16.064-128zm-64 0H256.256l16.064 128H448zm64-255.36V384h247.744A256.128 256.128 0 0 0 512 192.64m-64 8.064A256.448 256.448 0 0 0 264.256 384H448zm64-72.064A320.128 320.128 0 0 1 825.472 384H896a32 32 0 1 1 0 64h-64v1.92l-56.96 454.016A64 64 0 0 1 711.552 960H312.448a64 64 0 0 1-63.488-56.064L192 449.92V448h-64a32 32 0 0 1 0-64h70.528A320.384 320.384 0 0 1 448 135.04V96a96 96 0 0 1 96-96h128a32 32 0 1 1 0 64H544a32 32 0 0 0-32 32zM743.68 640H280.32l32.128 256h399.104z"})]))}}),g6=d6,f6=a({name:"IceTea",__name:"ice-tea",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M197.696 259.648a320.128 320.128 0 0 1 628.608 0A96 96 0 0 1 896 352v64a96 96 0 0 1-71.616 92.864l-49.408 395.072A64 64 0 0 1 711.488 960H312.512a64 64 0 0 1-63.488-56.064l-49.408-395.072A96 96 0 0 1 128 416v-64a96 96 0 0 1 69.696-92.352M264.064 256h495.872a256.128 256.128 0 0 0-495.872 0m495.424 256H264.512l48 384h398.976zM224 448h576a32 32 0 0 0 32-32v-64a32 32 0 0 0-32-32H224a32 32 0 0 0-32 32v64a32 32 0 0 0 32 32m160 192h64v64h-64zm192 64h64v64h-64zm-128 64h64v64h-64zm64-192h64v64h-64z"})]))}}),x6=f6,z6=a({name:"InfoFilled",__name:"info-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896.064A448 448 0 0 1 512 64m67.2 275.072c33.28 0 60.288-23.104 60.288-57.344s-27.072-57.344-60.288-57.344c-33.28 0-60.16 23.104-60.16 57.344s26.88 57.344 60.16 57.344M590.912 699.2c0-6.848 2.368-24.64 1.024-34.752l-52.608 60.544c-10.88 11.456-24.512 19.392-30.912 17.28a12.992 12.992 0 0 1-8.256-14.72l87.68-276.992c7.168-35.136-12.544-67.2-54.336-71.296-44.096 0-108.992 44.736-148.48 101.504 0 6.784-1.28 23.68.064 33.792l52.544-60.608c10.88-11.328 23.552-19.328 29.952-17.152a12.8 12.8 0 0 1 7.808 16.128L388.48 728.576c-10.048 32.256 8.96 63.872 55.04 71.04 67.84 0 107.904-43.648 147.456-100.416z"})]))}}),M6=z6,C6=a({name:"Iphone",__name:"iphone",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M224 768v96.064a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64V768zm0-64h576V160a64 64 0 0 0-64-64H288a64 64 0 0 0-64 64zm32 288a96 96 0 0 1-96-96V128a96 96 0 0 1 96-96h512a96 96 0 0 1 96 96v768a96 96 0 0 1-96 96zm304-144a48 48 0 1 1-96 0 48 48 0 0 1 96 0"})]))}}),H6=C6,V6=a({name:"Key",__name:"key",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M448 456.064V96a32 32 0 0 1 32-32.064L672 64a32 32 0 0 1 0 64H512v128h160a32 32 0 0 1 0 64H512v128a256 256 0 1 1-64 8.064M512 896a192 192 0 1 0 0-384 192 192 0 0 0 0 384"})]))}}),y6=V6,B6=a({name:"KnifeFork",__name:"knife-fork",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 410.56V96a32 32 0 0 1 64 0v314.56A96 96 0 0 0 384 320V96a32 32 0 0 1 64 0v224a160 160 0 0 1-128 156.8V928a32 32 0 1 1-64 0V476.8A160 160 0 0 1 128 320V96a32 32 0 0 1 64 0v224a96 96 0 0 0 64 90.56m384-250.24V544h126.72c-3.328-78.72-12.928-147.968-28.608-207.744-14.336-54.528-46.848-113.344-98.112-175.872zM640 608v320a32 32 0 1 1-64 0V64h64c85.312 89.472 138.688 174.848 160 256 21.312 81.152 32 177.152 32 288z"})]))}}),L6=B6,A6=a({name:"Lightning",__name:"lightning",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M288 671.36v64.128A239.808 239.808 0 0 1 63.744 496.192a240.32 240.32 0 0 1 199.488-236.8 256.128 256.128 0 0 1 487.872-30.976A256.064 256.064 0 0 1 736 734.016v-64.768a192 192 0 0 0 3.328-377.92l-35.2-6.592-12.8-33.408a192.064 192.064 0 0 0-365.952 23.232l-9.92 40.896-41.472 7.04a176.32 176.32 0 0 0-146.24 173.568c0 91.968 70.464 167.36 160.256 175.232z"}),e("path",{fill:"currentColor",d:"M416 736a32 32 0 0 1-27.776-47.872l128-224a32 32 0 1 1 55.552 31.744L471.168 672H608a32 32 0 0 1 27.776 47.872l-128 224a32 32 0 1 1-55.68-31.744L552.96 736z"})]))}}),b6=A6,k6=a({name:"Link",__name:"link",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M715.648 625.152 670.4 579.904l90.496-90.56c75.008-74.944 85.12-186.368 22.656-248.896-62.528-62.464-173.952-52.352-248.96 22.656L444.16 353.6l-45.248-45.248 90.496-90.496c100.032-99.968 251.968-110.08 339.456-22.656 87.488 87.488 77.312 239.424-22.656 339.456l-90.496 90.496zm-90.496 90.496-90.496 90.496C434.624 906.112 282.688 916.224 195.2 828.8c-87.488-87.488-77.312-239.424 22.656-339.456l90.496-90.496 45.248 45.248-90.496 90.56c-75.008 74.944-85.12 186.368-22.656 248.896 62.528 62.464 173.952 52.352 248.96-22.656l90.496-90.496zm0-362.048 45.248 45.248L398.848 670.4 353.6 625.152z"})]))}}),S6=k6,q6=a({name:"List",__name:"list",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M704 192h160v736H160V192h160v64h384zM288 512h448v-64H288zm0 256h448v-64H288zm96-576V96h256v96z"})]))}}),F6=q6,D6=a({name:"Loading",__name:"loading",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32m0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32m448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32m-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32M195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248zm452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248zM828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0m-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0z"})]))}}),P6=D6,R6=a({name:"LocationFilled",__name:"location-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 928c23.936 0 117.504-68.352 192.064-153.152C803.456 661.888 864 535.808 864 416c0-189.632-155.84-320-352-320S160 226.368 160 416c0 120.32 60.544 246.4 159.936 359.232C394.432 859.84 488 928 512 928m0-435.2a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 140.8a204.8 204.8 0 1 1 0-409.6 204.8 204.8 0 0 1 0 409.6"})]))}}),T6=R6,O6=a({name:"LocationInformation",__name:"location-information",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M288 896h448q32 0 32 32t-32 32H288q-32 0-32-32t32-32"}),e("path",{fill:"currentColor",d:"M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416M512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544"}),e("path",{fill:"currentColor",d:"M512 512a96 96 0 1 0 0-192 96 96 0 0 0 0 192m0 64a160 160 0 1 1 0-320 160 160 0 0 1 0 320"})]))}}),G6=O6,I6=a({name:"Location",__name:"location",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416M512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544"}),e("path",{fill:"currentColor",d:"M512 512a96 96 0 1 0 0-192 96 96 0 0 0 0 192m0 64a160 160 0 1 1 0-320 160 160 0 0 1 0 320"})]))}}),U6=I6,W6=a({name:"Lock",__name:"lock",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M224 448a32 32 0 0 0-32 32v384a32 32 0 0 0 32 32h576a32 32 0 0 0 32-32V480a32 32 0 0 0-32-32zm0-64h576a96 96 0 0 1 96 96v384a96 96 0 0 1-96 96H224a96 96 0 0 1-96-96V480a96 96 0 0 1 96-96"}),e("path",{fill:"currentColor",d:"M512 544a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V576a32 32 0 0 1 32-32m192-160v-64a192 192 0 1 0-384 0v64zM512 64a256 256 0 0 1 256 256v128H256V320A256 256 0 0 1 512 64"})]))}}),E6=W6,N6=a({name:"Lollipop",__name:"lollipop",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M513.28 448a64 64 0 1 1 76.544 49.728A96 96 0 0 0 768 448h64a160 160 0 0 1-320 0zm-126.976-29.696a256 256 0 1 0 43.52-180.48A256 256 0 0 1 832 448h-64a192 192 0 0 0-381.696-29.696m105.664 249.472L285.696 874.048a96 96 0 0 1-135.68-135.744l206.208-206.272a320 320 0 1 1 135.744 135.744zm-54.464-36.032a321.92 321.92 0 0 1-45.248-45.248L195.2 783.552a32 32 0 1 0 45.248 45.248l197.056-197.12z"})]))}}),Z6=N6,K6=a({name:"MagicStick",__name:"magic-stick",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64h64v192h-64zm0 576h64v192h-64zM160 480v-64h192v64zm576 0v-64h192v64zM249.856 199.04l45.248-45.184L430.848 289.6 385.6 334.848 249.856 199.104zM657.152 606.4l45.248-45.248 135.744 135.744-45.248 45.248zM114.048 923.2 68.8 877.952l316.8-316.8 45.248 45.248zM702.4 334.848 657.152 289.6l135.744-135.744 45.248 45.248z"})]))}}),Q6=K6,j6=a({name:"Magnet",__name:"magnet",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M832 320V192H704v320a192 192 0 1 1-384 0V192H192v128h128v64H192v128a320 320 0 0 0 640 0V384H704v-64zM640 512V128h256v384a384 384 0 1 1-768 0V128h256v384a128 128 0 1 0 256 0"})]))}}),J6=j6,X6=a({name:"Male",__name:"male",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M399.5 849.5a225 225 0 1 0 0-450 225 225 0 0 0 0 450m0 56.25a281.25 281.25 0 1 1 0-562.5 281.25 281.25 0 0 1 0 562.5m253.125-787.5h225q28.125 0 28.125 28.125T877.625 174.5h-225q-28.125 0-28.125-28.125t28.125-28.125"}),e("path",{fill:"currentColor",d:"M877.625 118.25q28.125 0 28.125 28.125v225q0 28.125-28.125 28.125T849.5 371.375v-225q0-28.125 28.125-28.125"}),e("path",{fill:"currentColor",d:"M604.813 458.9 565.1 419.131l292.613-292.668 39.825 39.824z"})]))}}),Y6=X6,$6=a({name:"Management",__name:"management",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M576 128v288l96-96 96 96V128h128v768H320V128zm-448 0h128v768H128z"})]))}}),e3=$6,a3=a({name:"MapLocation",__name:"map-location",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416M512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544"}),e("path",{fill:"currentColor",d:"M512 448a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256m345.6 192L960 960H672v-64H352v64H64l102.4-256zm-68.928 0H235.328l-76.8 192h706.944z"})]))}}),t3=a3,_3=a({name:"Medal",__name:"medal",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 896a256 256 0 1 0 0-512 256 256 0 0 0 0 512m0 64a320 320 0 1 1 0-640 320 320 0 0 1 0 640"}),e("path",{fill:"currentColor",d:"M576 128H448v200a286.72 286.72 0 0 1 64-8c19.52 0 40.832 2.688 64 8zm64 0v219.648c24.448 9.088 50.56 20.416 78.4 33.92L757.44 128zm-256 0H266.624l39.04 253.568c27.84-13.504 53.888-24.832 78.336-33.92V128zM229.312 64h565.376a32 32 0 0 1 31.616 36.864L768 480c-113.792-64-199.104-96-256-96-56.896 0-142.208 32-256 96l-58.304-379.136A32 32 0 0 1 229.312 64"})]))}}),r3=_3,l3=a({name:"Memo",__name:"memo",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",style:{"enable-background":"new 0 0 1024 1024"},viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M480 320h192c21.33 0 32-10.67 32-32s-10.67-32-32-32H480c-21.33 0-32 10.67-32 32s10.67 32 32 32"}),e("path",{fill:"currentColor",d:"M887.01 72.99C881.01 67 873.34 64 864 64H160c-9.35 0-17.02 3-23.01 8.99C131 78.99 128 86.66 128 96v832c0 9.35 2.99 17.02 8.99 23.01S150.66 960 160 960h704c9.35 0 17.02-2.99 23.01-8.99S896 937.34 896 928V96c0-9.35-3-17.02-8.99-23.01M192 896V128h96v768zm640 0H352V128h480z"}),e("path",{fill:"currentColor",d:"M480 512h192c21.33 0 32-10.67 32-32s-10.67-32-32-32H480c-21.33 0-32 10.67-32 32s10.67 32 32 32m0 192h192c21.33 0 32-10.67 32-32s-10.67-32-32-32H480c-21.33 0-32 10.67-32 32s10.67 32 32 32"})]))}}),u3=l3,p3=a({name:"Menu",__name:"menu",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M160 448a32 32 0 0 1-32-32V160.064a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V416a32 32 0 0 1-32 32zm448 0a32 32 0 0 1-32-32V160.064a32 32 0 0 1 32-32h255.936a32 32 0 0 1 32 32V416a32 32 0 0 1-32 32zM160 896a32 32 0 0 1-32-32V608a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32v256a32 32 0 0 1-32 32zm448 0a32 32 0 0 1-32-32V608a32 32 0 0 1 32-32h255.936a32 32 0 0 1 32 32v256a32 32 0 0 1-32 32z"})]))}}),o3=p3,v3=a({name:"MessageBox",__name:"message-box",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M288 384h448v64H288zm96-128h256v64H384zM131.456 512H384v128h256V512h252.544L721.856 192H302.144zM896 576H704v128H320V576H128v256h768zM275.776 128h472.448a32 32 0 0 1 28.608 17.664l179.84 359.552A32 32 0 0 1 960 519.552V864a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V519.552a32 32 0 0 1 3.392-14.336l179.776-359.552A32 32 0 0 1 275.776 128z"})]))}}),s3=v3,c3=a({name:"Message",__name:"message",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 224v512a64 64 0 0 0 64 64h640a64 64 0 0 0 64-64V224zm0-64h768a64 64 0 0 1 64 64v512a128 128 0 0 1-128 128H192A128 128 0 0 1 64 736V224a64 64 0 0 1 64-64"}),e("path",{fill:"currentColor",d:"M904 224 656.512 506.88a192 192 0 0 1-289.024 0L120 224zm-698.944 0 210.56 240.704a128 128 0 0 0 192.704 0L818.944 224H205.056"})]))}}),n3=c3,h3=a({name:"Mic",__name:"mic",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M480 704h160a64 64 0 0 0 64-64v-32h-96a32 32 0 0 1 0-64h96v-96h-96a32 32 0 0 1 0-64h96v-96h-96a32 32 0 0 1 0-64h96v-32a64 64 0 0 0-64-64H384a64 64 0 0 0-64 64v32h96a32 32 0 0 1 0 64h-96v96h96a32 32 0 0 1 0 64h-96v96h96a32 32 0 0 1 0 64h-96v32a64 64 0 0 0 64 64zm64 64v128h192a32 32 0 1 1 0 64H288a32 32 0 1 1 0-64h192V768h-96a128 128 0 0 1-128-128V192A128 128 0 0 1 384 64h256a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128z"})]))}}),i3=h3,m3=a({name:"Microphone",__name:"microphone",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 128a128 128 0 0 0-128 128v256a128 128 0 1 0 256 0V256a128 128 0 0 0-128-128m0-64a192 192 0 0 1 192 192v256a192 192 0 1 1-384 0V256A192 192 0 0 1 512 64m-32 832v-64a288 288 0 0 1-288-288v-32a32 32 0 0 1 64 0v32a224 224 0 0 0 224 224h64a224 224 0 0 0 224-224v-32a32 32 0 1 1 64 0v32a288 288 0 0 1-288 288v64h64a32 32 0 1 1 0 64H416a32 32 0 1 1 0-64z"})]))}}),w3=m3,d3=a({name:"MilkTea",__name:"milk-tea",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M416 128V96a96 96 0 0 1 96-96h128a32 32 0 1 1 0 64H512a32 32 0 0 0-32 32v32h320a96 96 0 0 1 11.712 191.296l-39.68 581.056A64 64 0 0 1 708.224 960H315.776a64 64 0 0 1-63.872-59.648l-39.616-581.056A96 96 0 0 1 224 128zM276.48 320l39.296 576h392.448l4.8-70.784a224.064 224.064 0 0 1 30.016-439.808L747.52 320zM224 256h576a32 32 0 1 0 0-64H224a32 32 0 0 0 0 64m493.44 503.872 21.12-309.12a160 160 0 0 0-21.12 309.12"})]))}}),g3=d3,f3=a({name:"Minus",__name:"minus",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 544h768a32 32 0 1 0 0-64H128a32 32 0 0 0 0 64"})]))}}),x3=f3,z3=a({name:"Money",__name:"money",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 640v192h640V384H768v-64h150.976c14.272 0 19.456 1.472 24.64 4.288a29.056 29.056 0 0 1 12.16 12.096c2.752 5.184 4.224 10.368 4.224 24.64v493.952c0 14.272-1.472 19.456-4.288 24.64a29.056 29.056 0 0 1-12.096 12.16c-5.184 2.752-10.368 4.224-24.64 4.224H233.024c-14.272 0-19.456-1.472-24.64-4.288a29.056 29.056 0 0 1-12.16-12.096c-2.688-5.184-4.224-10.368-4.224-24.576V640z"}),e("path",{fill:"currentColor",d:"M768 192H128v448h640zm64-22.976v493.952c0 14.272-1.472 19.456-4.288 24.64a29.056 29.056 0 0 1-12.096 12.16c-5.184 2.752-10.368 4.224-24.64 4.224H105.024c-14.272 0-19.456-1.472-24.64-4.288a29.056 29.056 0 0 1-12.16-12.096C65.536 682.432 64 677.248 64 663.04V169.024c0-14.272 1.472-19.456 4.288-24.64a29.056 29.056 0 0 1 12.096-12.16C85.568 129.536 90.752 128 104.96 128h685.952c14.272 0 19.456 1.472 24.64 4.288a29.056 29.056 0 0 1 12.16 12.096c2.752 5.184 4.224 10.368 4.224 24.64z"}),e("path",{fill:"currentColor",d:"M448 576a160 160 0 1 1 0-320 160 160 0 0 1 0 320m0-64a96 96 0 1 0 0-192 96 96 0 0 0 0 192"})]))}}),M3=z3,C3=a({name:"Monitor",__name:"monitor",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M544 768v128h192a32 32 0 1 1 0 64H288a32 32 0 1 1 0-64h192V768H192A128 128 0 0 1 64 640V256a128 128 0 0 1 128-128h640a128 128 0 0 1 128 128v384a128 128 0 0 1-128 128zM192 192a64 64 0 0 0-64 64v384a64 64 0 0 0 64 64h640a64 64 0 0 0 64-64V256a64 64 0 0 0-64-64z"})]))}}),H3=C3,V3=a({name:"MoonNight",__name:"moon-night",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M384 512a448 448 0 0 1 215.872-383.296A384 384 0 0 0 213.76 640h188.8A448.256 448.256 0 0 1 384 512M171.136 704a448 448 0 0 1 636.992-575.296A384 384 0 0 0 499.328 704h-328.32z"}),e("path",{fill:"currentColor",d:"M32 640h960q32 0 32 32t-32 32H32q-32 0-32-32t32-32m128 128h384a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64m160 127.68 224 .256a32 32 0 0 1 32 32V928a32 32 0 0 1-32 32l-224-.384a32 32 0 0 1-32-32v-.064a32 32 0 0 1 32-32z"})]))}}),y3=V3,B3=a({name:"Moon",__name:"moon",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M240.448 240.448a384 384 0 1 0 559.424 525.696 448 448 0 0 1-542.016-542.08 390.592 390.592 0 0 0-17.408 16.384zm181.056 362.048a384 384 0 0 0 525.632 16.384A448 448 0 1 1 405.056 76.8a384 384 0 0 0 16.448 525.696"})]))}}),L3=B3,A3=a({name:"MoreFilled",__name:"more-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M176 416a112 112 0 1 1 0 224 112 112 0 0 1 0-224m336 0a112 112 0 1 1 0 224 112 112 0 0 1 0-224m336 0a112 112 0 1 1 0 224 112 112 0 0 1 0-224"})]))}}),b3=A3,k3=a({name:"More",__name:"more",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M176 416a112 112 0 1 0 0 224 112 112 0 0 0 0-224m0 64a48 48 0 1 1 0 96 48 48 0 0 1 0-96m336-64a112 112 0 1 1 0 224 112 112 0 0 1 0-224m0 64a48 48 0 1 0 0 96 48 48 0 0 0 0-96m336-64a112 112 0 1 1 0 224 112 112 0 0 1 0-224m0 64a48 48 0 1 0 0 96 48 48 0 0 0 0-96"})]))}}),S3=k3,q3=a({name:"MostlyCloudy",__name:"mostly-cloudy",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M737.216 357.952 704 349.824l-11.776-32a192.064 192.064 0 0 0-367.424 23.04l-8.96 39.04-39.04 8.96A192.064 192.064 0 0 0 320 768h368a207.808 207.808 0 0 0 207.808-208 208.32 208.32 0 0 0-158.592-202.048m15.168-62.208A272.32 272.32 0 0 1 959.744 560a271.808 271.808 0 0 1-271.552 272H320a256 256 0 0 1-57.536-505.536 256.128 256.128 0 0 1 489.92-30.72"})]))}}),F3=q3,D3=a({name:"Mouse",__name:"mouse",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M438.144 256c-68.352 0-92.736 4.672-117.76 18.112-20.096 10.752-35.52 26.176-46.272 46.272C260.672 345.408 256 369.792 256 438.144v275.712c0 68.352 4.672 92.736 18.112 117.76 10.752 20.096 26.176 35.52 46.272 46.272C345.408 891.328 369.792 896 438.144 896h147.712c68.352 0 92.736-4.672 117.76-18.112 20.096-10.752 35.52-26.176 46.272-46.272C763.328 806.592 768 782.208 768 713.856V438.144c0-68.352-4.672-92.736-18.112-117.76a110.464 110.464 0 0 0-46.272-46.272C678.592 260.672 654.208 256 585.856 256zm0-64h147.712c85.568 0 116.608 8.96 147.904 25.6 31.36 16.768 55.872 41.344 72.576 72.64C823.104 321.536 832 352.576 832 438.08v275.84c0 85.504-8.96 116.544-25.6 147.84a174.464 174.464 0 0 1-72.64 72.576C702.464 951.104 671.424 960 585.92 960H438.08c-85.504 0-116.544-8.96-147.84-25.6a174.464 174.464 0 0 1-72.64-72.704c-16.768-31.296-25.664-62.336-25.664-147.84v-275.84c0-85.504 8.96-116.544 25.6-147.84a174.464 174.464 0 0 1 72.768-72.576c31.232-16.704 62.272-25.6 147.776-25.6z"}),e("path",{fill:"currentColor",d:"M512 320q32 0 32 32v128q0 32-32 32t-32-32V352q0-32 32-32m32-96a32 32 0 0 1-64 0v-64a32 32 0 0 0-32-32h-96a32 32 0 0 1 0-64h96a96 96 0 0 1 96 96z"})]))}}),P3=D3,R3=a({name:"Mug",__name:"mug",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M736 800V160H160v640a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64m64-544h63.552a96 96 0 0 1 96 96v224a96 96 0 0 1-96 96H800v128a128 128 0 0 1-128 128H224A128 128 0 0 1 96 800V128a32 32 0 0 1 32-32h640a32 32 0 0 1 32 32zm0 64v288h63.552a32 32 0 0 0 32-32V352a32 32 0 0 0-32-32z"})]))}}),T3=R3,O3=a({name:"MuteNotification",__name:"mute-notification",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m241.216 832 63.616-64H768V448c0-42.368-10.24-82.304-28.48-117.504l46.912-47.232C815.36 331.392 832 387.84 832 448v320h96a32 32 0 1 1 0 64zm-90.24 0H96a32 32 0 1 1 0-64h96V448a320.128 320.128 0 0 1 256-313.6V128a64 64 0 1 1 128 0v6.4a319.552 319.552 0 0 1 171.648 97.088l-45.184 45.44A256 256 0 0 0 256 448v278.336L151.04 832zM448 896h128a64 64 0 0 1-128 0"}),e("path",{fill:"currentColor",d:"M150.72 859.072a32 32 0 0 1-45.44-45.056l704-708.544a32 32 0 0 1 45.44 45.056l-704 708.544z"})]))}}),G3=O3,I3=a({name:"Mute",__name:"mute",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m412.16 592.128-45.44 45.44A191.232 191.232 0 0 1 320 512V256a192 192 0 1 1 384 0v44.352l-64 64V256a128 128 0 1 0-256 0v256c0 30.336 10.56 58.24 28.16 80.128m51.968 38.592A128 128 0 0 0 640 512v-57.152l64-64V512a192 192 0 0 1-287.68 166.528zM314.88 779.968l46.144-46.08A222.976 222.976 0 0 0 480 768h64a224 224 0 0 0 224-224v-32a32 32 0 1 1 64 0v32a288 288 0 0 1-288 288v64h64a32 32 0 1 1 0 64H416a32 32 0 1 1 0-64h64v-64c-61.44 0-118.4-19.2-165.12-52.032M266.752 737.6A286.976 286.976 0 0 1 192 544v-32a32 32 0 0 1 64 0v32c0 56.832 21.184 108.8 56.064 148.288z"}),e("path",{fill:"currentColor",d:"M150.72 859.072a32 32 0 0 1-45.44-45.056l704-708.544a32 32 0 0 1 45.44 45.056l-704 708.544z"})]))}}),U3=I3,W3=a({name:"NoSmoking",__name:"no-smoking",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M440.256 576H256v128h56.256l-64 64H224a32 32 0 0 1-32-32V544a32 32 0 0 1 32-32h280.256zm143.488 128H704V583.744L775.744 512H928a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32H519.744zM768 576v128h128V576zm-29.696-207.552 45.248 45.248-497.856 497.856-45.248-45.248zM256 64h64v320h-64zM128 192h64v192h-64zM64 512h64v256H64z"})]))}}),E3=W3,N3=a({name:"Notebook",__name:"notebook",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M192 128v768h640V128zm-32-64h704a32 32 0 0 1 32 32v832a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32"}),e("path",{fill:"currentColor",d:"M672 128h64v768h-64zM96 192h128q32 0 32 32t-32 32H96q-32 0-32-32t32-32m0 192h128q32 0 32 32t-32 32H96q-32 0-32-32t32-32m0 192h128q32 0 32 32t-32 32H96q-32 0-32-32t32-32m0 192h128q32 0 32 32t-32 32H96q-32 0-32-32t32-32"})]))}}),Z3=N3,K3=a({name:"Notification",__name:"notification",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 128v64H256a64 64 0 0 0-64 64v512a64 64 0 0 0 64 64h512a64 64 0 0 0 64-64V512h64v256a128 128 0 0 1-128 128H256a128 128 0 0 1-128-128V256a128 128 0 0 1 128-128z"}),e("path",{fill:"currentColor",d:"M768 384a128 128 0 1 0 0-256 128 128 0 0 0 0 256m0 64a192 192 0 1 1 0-384 192 192 0 0 1 0 384"})]))}}),Q3=K3,j3=a({name:"Odometer",__name:"odometer",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),e("path",{fill:"currentColor",d:"M192 512a320 320 0 1 1 640 0 32 32 0 1 1-64 0 256 256 0 1 0-512 0 32 32 0 0 1-64 0"}),e("path",{fill:"currentColor",d:"M570.432 627.84A96 96 0 1 1 509.568 608l60.992-187.776A32 32 0 1 1 631.424 440l-60.992 187.776zM502.08 734.464a32 32 0 1 0 19.84-60.928 32 32 0 0 0-19.84 60.928"})]))}}),J3=j3,X3=a({name:"OfficeBuilding",__name:"office-building",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M192 128v704h384V128zm-32-64h448a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32"}),e("path",{fill:"currentColor",d:"M256 256h256v64H256zm0 192h256v64H256zm0 192h256v64H256zm384-128h128v64H640zm0 128h128v64H640zM64 832h896v64H64z"}),e("path",{fill:"currentColor",d:"M640 384v448h192V384zm-32-64h256a32 32 0 0 1 32 32v512a32 32 0 0 1-32 32H608a32 32 0 0 1-32-32V352a32 32 0 0 1 32-32"})]))}}),Y3=X3,$3=a({name:"Open",__name:"open",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M329.956 257.138a254.862 254.862 0 0 0 0 509.724h364.088a254.862 254.862 0 0 0 0-509.724zm0-72.818h364.088a327.68 327.68 0 1 1 0 655.36H329.956a327.68 327.68 0 1 1 0-655.36z"}),e("path",{fill:"currentColor",d:"M694.044 621.227a109.227 109.227 0 1 0 0-218.454 109.227 109.227 0 0 0 0 218.454m0 72.817a182.044 182.044 0 1 1 0-364.088 182.044 182.044 0 0 1 0 364.088"})]))}}),e8=$3,a8=a({name:"Operation",__name:"operation",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M389.44 768a96.064 96.064 0 0 1 181.12 0H896v64H570.56a96.064 96.064 0 0 1-181.12 0H128v-64zm192-288a96.064 96.064 0 0 1 181.12 0H896v64H762.56a96.064 96.064 0 0 1-181.12 0H128v-64zm-320-288a96.064 96.064 0 0 1 181.12 0H896v64H442.56a96.064 96.064 0 0 1-181.12 0H128v-64z"})]))}}),t8=a8,_8=a({name:"Opportunity",__name:"opportunity",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M384 960v-64h192.064v64zm448-544a350.656 350.656 0 0 1-128.32 271.424C665.344 719.04 640 763.776 640 813.504V832H320v-14.336c0-48-19.392-95.36-57.216-124.992a351.552 351.552 0 0 1-128.448-344.256c25.344-136.448 133.888-248.128 269.76-276.48A352.384 352.384 0 0 1 832 416m-544 32c0-132.288 75.904-224 192-224v-64c-154.432 0-256 122.752-256 288z"})]))}}),r8=_8,l8=a({name:"Orange",__name:"orange",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M544 894.72a382.336 382.336 0 0 0 215.936-89.472L577.024 622.272c-10.24 6.016-21.248 10.688-33.024 13.696v258.688zm261.248-134.784A382.336 382.336 0 0 0 894.656 544H635.968c-3.008 11.776-7.68 22.848-13.696 33.024l182.976 182.912zM894.656 480a382.336 382.336 0 0 0-89.408-215.936L622.272 446.976c6.016 10.24 10.688 21.248 13.696 33.024h258.688zm-134.72-261.248A382.336 382.336 0 0 0 544 129.344v258.688c11.776 3.008 22.848 7.68 33.024 13.696zM480 129.344a382.336 382.336 0 0 0-215.936 89.408l182.912 182.976c10.24-6.016 21.248-10.688 33.024-13.696zm-261.248 134.72A382.336 382.336 0 0 0 129.344 480h258.688c3.008-11.776 7.68-22.848 13.696-33.024zM129.344 544a382.336 382.336 0 0 0 89.408 215.936l182.976-182.912A127.232 127.232 0 0 1 388.032 544zm134.72 261.248A382.336 382.336 0 0 0 480 894.656V635.968a127.232 127.232 0 0 1-33.024-13.696zM512 960a448 448 0 1 1 0-896 448 448 0 0 1 0 896m0-384a64 64 0 1 0 0-128 64 64 0 0 0 0 128"})]))}}),u8=l8,p8=a({name:"Paperclip",__name:"paperclip",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M602.496 240.448A192 192 0 1 1 874.048 512l-316.8 316.8A256 256 0 0 1 195.2 466.752L602.496 59.456l45.248 45.248L240.448 512A192 192 0 0 0 512 783.552l316.8-316.8a128 128 0 1 0-181.056-181.056L353.6 579.904a32 32 0 1 0 45.248 45.248l294.144-294.144 45.312 45.248L444.096 670.4a96 96 0 1 1-135.744-135.744l294.144-294.208z"})]))}}),o8=p8,v8=a({name:"PartlyCloudy",__name:"partly-cloudy",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M598.4 895.872H328.192a256 256 0 0 1-34.496-510.528A352 352 0 1 1 598.4 895.872m-271.36-64h272.256a288 288 0 1 0-248.512-417.664L335.04 445.44l-34.816 3.584a192 192 0 0 0 26.88 382.848z"}),e("path",{fill:"currentColor",d:"M139.84 501.888a256 256 0 1 1 417.856-277.12c-17.728 2.176-38.208 8.448-61.504 18.816A192 192 0 1 0 189.12 460.48a6003.84 6003.84 0 0 0-49.28 41.408z"})]))}}),s8=v8,c8=a({name:"Pear",__name:"pear",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M542.336 258.816a443.255 443.255 0 0 0-9.024 25.088 32 32 0 1 1-60.8-20.032l1.088-3.328a162.688 162.688 0 0 0-122.048 131.392l-17.088 102.72-20.736 15.36C256.192 552.704 224 610.88 224 672c0 120.576 126.4 224 288 224s288-103.424 288-224c0-61.12-32.192-119.296-89.728-161.92l-20.736-15.424-17.088-102.72a162.688 162.688 0 0 0-130.112-133.12zm-40.128-66.56c7.936-15.552 16.576-30.08 25.92-43.776 23.296-33.92 49.408-59.776 78.528-77.12a32 32 0 1 1 32.704 55.04c-20.544 12.224-40.064 31.552-58.432 58.304a316.608 316.608 0 0 0-9.792 15.104 226.688 226.688 0 0 1 164.48 181.568l12.8 77.248C819.456 511.36 864 587.392 864 672c0 159.04-157.568 288-352 288S160 831.04 160 672c0-84.608 44.608-160.64 115.584-213.376l12.8-77.248a226.624 226.624 0 0 1 213.76-189.184z"})]))}}),n8=c8,h8=a({name:"PhoneFilled",__name:"phone-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M199.232 125.568 90.624 379.008a32 32 0 0 0 6.784 35.2l512.384 512.384a32 32 0 0 0 35.2 6.784l253.44-108.608a32 32 0 0 0 10.048-52.032L769.6 633.92a32 32 0 0 0-36.928-5.952l-130.176 65.088-271.488-271.552 65.024-130.176a32 32 0 0 0-5.952-36.928L251.2 115.52a32 32 0 0 0-51.968 10.048z"})]))}}),i8=h8,m8=a({name:"Phone",__name:"phone",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M79.36 432.256 591.744 944.64a32 32 0 0 0 35.2 6.784l253.44-108.544a32 32 0 0 0 9.984-52.032l-153.856-153.92a32 32 0 0 0-36.928-6.016l-69.888 34.944L358.08 394.24l35.008-69.888a32 32 0 0 0-5.952-36.928L233.152 133.568a32 32 0 0 0-52.032 10.048L72.512 397.056a32 32 0 0 0 6.784 35.2zm60.48-29.952 81.536-190.08L325.568 316.48l-24.64 49.216-20.608 41.216 32.576 32.64 271.552 271.552 32.64 32.64 41.216-20.672 49.28-24.576 104.192 104.128-190.08 81.472L139.84 402.304zM512 320v-64a256 256 0 0 1 256 256h-64a192 192 0 0 0-192-192m0-192V64a448 448 0 0 1 448 448h-64a384 384 0 0 0-384-384"})]))}}),w8=m8,d8=a({name:"PictureFilled",__name:"picture-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M96 896a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h832a32 32 0 0 1 32 32v704a32 32 0 0 1-32 32zm315.52-228.48-68.928-68.928a32 32 0 0 0-45.248 0L128 768.064h778.688l-242.112-290.56a32 32 0 0 0-49.216 0L458.752 665.408a32 32 0 0 1-47.232 2.112M256 384a96 96 0 1 0 192.064-.064A96 96 0 0 0 256 384"})]))}}),g8=d8,f8=a({name:"PictureRounded",__name:"picture-rounded",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 128a384 384 0 1 0 0 768 384 384 0 0 0 0-768m0-64a448 448 0 1 1 0 896 448 448 0 0 1 0-896"}),e("path",{fill:"currentColor",d:"M640 288q64 0 64 64t-64 64q-64 0-64-64t64-64M214.656 790.656l-45.312-45.312 185.664-185.6a96 96 0 0 1 123.712-10.24l138.24 98.688a32 32 0 0 0 39.872-2.176L906.688 422.4l42.624 47.744L699.52 693.696a96 96 0 0 1-119.808 6.592l-138.24-98.752a32 32 0 0 0-41.152 3.456l-185.664 185.6z"})]))}}),x8=f8,z8=a({name:"Picture",__name:"picture",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M160 160v704h704V160zm-32-64h768a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32H128a32 32 0 0 1-32-32V128a32 32 0 0 1 32-32"}),e("path",{fill:"currentColor",d:"M384 288q64 0 64 64t-64 64q-64 0-64-64t64-64M185.408 876.992l-50.816-38.912L350.72 556.032a96 96 0 0 1 134.592-17.856l1.856 1.472 122.88 99.136a32 32 0 0 0 44.992-4.864l216-269.888 49.92 39.936-215.808 269.824-.256.32a96 96 0 0 1-135.04 14.464l-122.88-99.072-.64-.512a32 32 0 0 0-44.8 5.952z"})]))}}),M8=z8,C8=a({name:"PieChart",__name:"pie-chart",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M448 68.48v64.832A384.128 384.128 0 0 0 512 896a384.128 384.128 0 0 0 378.688-320h64.768A448.128 448.128 0 0 1 64 512 448.128 448.128 0 0 1 448 68.48z"}),e("path",{fill:"currentColor",d:"M576 97.28V448h350.72A384.064 384.064 0 0 0 576 97.28zM512 64V33.152A448 448 0 0 1 990.848 512H512z"})]))}}),H8=C8,V8=a({name:"Place",__name:"place",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 512a192 192 0 1 0 0-384 192 192 0 0 0 0 384m0 64a256 256 0 1 1 0-512 256 256 0 0 1 0 512"}),e("path",{fill:"currentColor",d:"M512 512a32 32 0 0 1 32 32v256a32 32 0 1 1-64 0V544a32 32 0 0 1 32-32"}),e("path",{fill:"currentColor",d:"M384 649.088v64.96C269.76 732.352 192 771.904 192 800c0 37.696 139.904 96 320 96s320-58.304 320-96c0-28.16-77.76-67.648-192-85.952v-64.96C789.12 671.04 896 730.368 896 800c0 88.32-171.904 160-384 160s-384-71.68-384-160c0-69.696 106.88-128.96 256-150.912"})]))}}),y8=V8,B8=a({name:"Platform",__name:"platform",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M448 832v-64h128v64h192v64H256v-64zM128 704V128h768v576z"})]))}}),L8=B8,A8=a({name:"Plus",__name:"plus",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M480 480V128a32 32 0 0 1 64 0v352h352a32 32 0 1 1 0 64H544v352a32 32 0 1 1-64 0V544H128a32 32 0 0 1 0-64z"})]))}}),b8=A8,k8=a({name:"Pointer",__name:"pointer",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M511.552 128c-35.584 0-64.384 28.8-64.384 64.448v516.48L274.048 570.88a94.272 94.272 0 0 0-112.896-3.456 44.416 44.416 0 0 0-8.96 62.208L332.8 870.4A64 64 0 0 0 384 896h512V575.232a64 64 0 0 0-45.632-61.312l-205.952-61.76A96 96 0 0 1 576 360.192V192.448C576 156.8 547.2 128 511.552 128M359.04 556.8l24.128 19.2V192.448a128.448 128.448 0 1 1 256.832 0v167.744a32 32 0 0 0 22.784 30.656l206.016 61.76A128 128 0 0 1 960 575.232V896a64 64 0 0 1-64 64H384a128 128 0 0 1-102.4-51.2L101.056 668.032A108.416 108.416 0 0 1 128 512.512a158.272 158.272 0 0 1 185.984 8.32z"})]))}}),S8=k8,q8=a({name:"Position",__name:"position",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m249.6 417.088 319.744 43.072 39.168 310.272L845.12 178.88 249.6 417.088zm-129.024 47.168a32 32 0 0 1-7.68-61.44l777.792-311.04a32 32 0 0 1 41.6 41.6l-310.336 775.68a32 32 0 0 1-61.44-7.808L512 516.992l-391.424-52.736z"})]))}}),F8=q8,D8=a({name:"Postcard",__name:"postcard",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M160 224a32 32 0 0 0-32 32v512a32 32 0 0 0 32 32h704a32 32 0 0 0 32-32V256a32 32 0 0 0-32-32zm0-64h704a96 96 0 0 1 96 96v512a96 96 0 0 1-96 96H160a96 96 0 0 1-96-96V256a96 96 0 0 1 96-96"}),e("path",{fill:"currentColor",d:"M704 320a64 64 0 1 1 0 128 64 64 0 0 1 0-128M288 448h256q32 0 32 32t-32 32H288q-32 0-32-32t32-32m0 128h256q32 0 32 32t-32 32H288q-32 0-32-32t32-32"})]))}}),P8=D8,R8=a({name:"Pouring",__name:"pouring",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m739.328 291.328-35.2-6.592-12.8-33.408a192.064 192.064 0 0 0-365.952 23.232l-9.92 40.896-41.472 7.04a176.32 176.32 0 0 0-146.24 173.568c0 97.28 78.72 175.936 175.808 175.936h400a192 192 0 0 0 35.776-380.672zM959.552 480a256 256 0 0 1-256 256h-400A239.808 239.808 0 0 1 63.744 496.192a240.32 240.32 0 0 1 199.488-236.8 256.128 256.128 0 0 1 487.872-30.976A256.064 256.064 0 0 1 959.552 480M224 800a32 32 0 0 1 32 32v96a32 32 0 1 1-64 0v-96a32 32 0 0 1 32-32m192 0a32 32 0 0 1 32 32v96a32 32 0 1 1-64 0v-96a32 32 0 0 1 32-32m192 0a32 32 0 0 1 32 32v96a32 32 0 1 1-64 0v-96a32 32 0 0 1 32-32m192 0a32 32 0 0 1 32 32v96a32 32 0 1 1-64 0v-96a32 32 0 0 1 32-32"})]))}}),T8=R8,O8=a({name:"Present",__name:"present",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M480 896V640H192v-64h288V320H192v576zm64 0h288V320H544v256h288v64H544zM128 256h768v672a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32z"}),e("path",{fill:"currentColor",d:"M96 256h832q32 0 32 32t-32 32H96q-32 0-32-32t32-32"}),e("path",{fill:"currentColor",d:"M416 256a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"}),e("path",{fill:"currentColor",d:"M608 256a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"})]))}}),G8=O8,I8=a({name:"PriceTag",__name:"price-tag",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M224 318.336V896h576V318.336L552.512 115.84a64 64 0 0 0-81.024 0zM593.024 66.304l259.2 212.096A32 32 0 0 1 864 303.168V928a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32V303.168a32 32 0 0 1 11.712-24.768l259.2-212.096a128 128 0 0 1 162.112 0z"}),e("path",{fill:"currentColor",d:"M512 448a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"})]))}}),U8=I8,W8=a({name:"Printer",__name:"printer",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 768H105.024c-14.272 0-19.456-1.472-24.64-4.288a29.056 29.056 0 0 1-12.16-12.096C65.536 746.432 64 741.248 64 727.04V379.072c0-42.816 4.48-58.304 12.8-73.984 8.384-15.616 20.672-27.904 36.288-36.288 15.68-8.32 31.168-12.8 73.984-12.8H256V64h512v192h68.928c42.816 0 58.304 4.48 73.984 12.8 15.616 8.384 27.904 20.672 36.288 36.288 8.32 15.68 12.8 31.168 12.8 73.984v347.904c0 14.272-1.472 19.456-4.288 24.64a29.056 29.056 0 0 1-12.096 12.16c-5.184 2.752-10.368 4.224-24.64 4.224H768v192H256zm64-192v320h384V576zm-64 128V512h512v192h128V379.072c0-29.376-1.408-36.48-5.248-43.776a23.296 23.296 0 0 0-10.048-10.048c-7.232-3.84-14.4-5.248-43.776-5.248H187.072c-29.376 0-36.48 1.408-43.776 5.248a23.296 23.296 0 0 0-10.048 10.048c-3.84 7.232-5.248 14.4-5.248 43.776V704zm64-448h384V128H320zm-64 128h64v64h-64zm128 0h64v64h-64z"})]))}}),E8=W8,N8=a({name:"Promotion",__name:"promotion",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m64 448 832-320-128 704-446.08-243.328L832 192 242.816 545.472zm256 512V657.024L512 768z"})]))}}),Z8=N8,K8=a({name:"QuartzWatch",__name:"quartz-watch",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",style:{"enable-background":"new 0 0 1024 1024"},viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M422.02 602.01v-.03c-6.68-5.99-14.35-8.83-23.01-8.51-8.67.32-16.17 3.66-22.5 10.02-6.33 6.36-9.5 13.7-9.5 22.02s3 15.82 8.99 22.5c8.68 8.68 19.02 11.35 31.01 8s19.49-10.85 22.5-22.5c3.01-11.65.51-22.15-7.49-31.49zM384 512c0-9.35-3-17.02-8.99-23.01-6-5.99-13.66-8.99-23.01-8.99-9.35 0-17.02 3-23.01 8.99-5.99 6-8.99 13.66-8.99 23.01s3 17.02 8.99 23.01c6 5.99 13.66 8.99 23.01 8.99 9.35 0 17.02-3 23.01-8.99 5.99-6 8.99-13.67 8.99-23.01m6.53-82.49c11.65 3.01 22.15.51 31.49-7.49h.04c5.99-6.68 8.83-14.34 8.51-23.01-.32-8.67-3.66-16.16-10.02-22.5-6.36-6.33-13.7-9.5-22.02-9.5s-15.82 3-22.5 8.99c-8.68 8.69-11.35 19.02-8 31.01 3.35 11.99 10.85 19.49 22.5 22.5zm242.94 0c11.67-3.03 19.01-10.37 22.02-22.02 3.01-11.65.51-22.15-7.49-31.49h.01c-6.68-5.99-14.18-8.99-22.5-8.99s-15.66 3.16-22.02 9.5c-6.36 6.34-9.7 13.84-10.02 22.5-.32 8.66 2.52 16.33 8.51 23.01 9.32 8.02 19.82 10.52 31.49 7.49M512 640c-9.35 0-17.02 3-23.01 8.99-5.99 6-8.99 13.66-8.99 23.01s3 17.02 8.99 23.01c6 5.99 13.67 8.99 23.01 8.99 9.35 0 17.02-3 23.01-8.99 5.99-6 8.99-13.66 8.99-23.01s-3-17.02-8.99-23.01c-6-5.99-13.66-8.99-23.01-8.99m183.01-151.01c-6-5.99-13.66-8.99-23.01-8.99s-17.02 3-23.01 8.99c-5.99 6-8.99 13.66-8.99 23.01s3 17.02 8.99 23.01c6 5.99 13.66 8.99 23.01 8.99s17.02-3 23.01-8.99c5.99-6 8.99-13.67 8.99-23.01 0-9.35-3-17.02-8.99-23.01"}),e("path",{fill:"currentColor",d:"M832 512c-2-90.67-33.17-166.17-93.5-226.5-20.43-20.42-42.6-37.49-66.5-51.23V64H352v170.26c-23.9 13.74-46.07 30.81-66.5 51.24-60.33 60.33-91.49 135.83-93.5 226.5 2 90.67 33.17 166.17 93.5 226.5 20.43 20.43 42.6 37.5 66.5 51.24V960h320V789.74c23.9-13.74 46.07-30.81 66.5-51.24 60.33-60.34 91.49-135.83 93.5-226.5M416 128h192v78.69c-29.85-9.03-61.85-13.93-96-14.69-34.15.75-66.15 5.65-96 14.68zm192 768H416v-78.68c29.85 9.03 61.85 13.93 96 14.68 34.15-.75 66.15-5.65 96-14.68zm-96-128c-72.66-2.01-132.99-27.01-180.99-75.01S258.01 584.66 256 512c2.01-72.66 27.01-132.99 75.01-180.99S439.34 258.01 512 256c72.66 2.01 132.99 27.01 180.99 75.01S765.99 439.34 768 512c-2.01 72.66-27.01 132.99-75.01 180.99S584.66 765.99 512 768"}),e("path",{fill:"currentColor",d:"M512 320c-9.35 0-17.02 3-23.01 8.99-5.99 6-8.99 13.66-8.99 23.01 0 9.35 3 17.02 8.99 23.01 6 5.99 13.67 8.99 23.01 8.99 9.35 0 17.02-3 23.01-8.99 5.99-6 8.99-13.66 8.99-23.01 0-9.35-3-17.02-8.99-23.01-6-5.99-13.66-8.99-23.01-8.99m112.99 273.5c-8.66-.32-16.33 2.52-23.01 8.51-7.98 9.32-10.48 19.82-7.49 31.49s10.49 19.17 22.5 22.5 22.35.66 31.01-8v.04c5.99-6.68 8.99-14.18 8.99-22.5s-3.16-15.66-9.5-22.02-13.84-9.7-22.5-10.02"})]))}}),Q8=K8,j8=a({name:"QuestionFilled",__name:"question-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m23.744 191.488c-52.096 0-92.928 14.784-123.2 44.352-30.976 29.568-45.76 70.4-45.76 122.496h80.256c0-29.568 5.632-52.8 17.6-68.992 13.376-19.712 35.2-28.864 66.176-28.864 23.936 0 42.944 6.336 56.32 19.712 12.672 13.376 19.712 31.68 19.712 54.912 0 17.6-6.336 34.496-19.008 49.984l-8.448 9.856c-45.76 40.832-73.216 70.4-82.368 89.408-9.856 19.008-14.08 42.24-14.08 68.992v9.856h80.96v-9.856c0-16.896 3.52-31.68 10.56-45.76 6.336-12.672 15.488-24.64 28.16-35.2 33.792-29.568 54.208-48.576 60.544-55.616 16.896-22.528 26.048-51.392 26.048-86.592 0-42.944-14.08-76.736-42.24-101.376-28.16-25.344-65.472-37.312-111.232-37.312zm-12.672 406.208a54.272 54.272 0 0 0-38.72 14.784 49.408 49.408 0 0 0-15.488 38.016c0 15.488 4.928 28.16 15.488 38.016A54.848 54.848 0 0 0 523.072 768c15.488 0 28.16-4.928 38.72-14.784a51.52 51.52 0 0 0 16.192-38.72 51.968 51.968 0 0 0-15.488-38.016 55.936 55.936 0 0 0-39.424-14.784z"})]))}}),J8=j8,X8=a({name:"Rank",__name:"rank",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m186.496 544 41.408 41.344a32 32 0 1 1-45.248 45.312l-96-96a32 32 0 0 1 0-45.312l96-96a32 32 0 1 1 45.248 45.312L186.496 480h290.816V186.432l-41.472 41.472a32 32 0 1 1-45.248-45.184l96-96.128a32 32 0 0 1 45.312 0l96 96.064a32 32 0 0 1-45.248 45.184l-41.344-41.28V480H832l-41.344-41.344a32 32 0 0 1 45.248-45.312l96 96a32 32 0 0 1 0 45.312l-96 96a32 32 0 0 1-45.248-45.312L832 544H541.312v293.44l41.344-41.28a32 32 0 1 1 45.248 45.248l-96 96a32 32 0 0 1-45.312 0l-96-96a32 32 0 1 1 45.312-45.248l41.408 41.408V544H186.496z"})]))}}),Y8=X8,$8=a({name:"ReadingLamp",__name:"reading-lamp",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M352 896h320q32 0 32 32t-32 32H352q-32 0-32-32t32-32m-44.672-768-99.52 448h608.384l-99.52-448zm-25.6-64h460.608a32 32 0 0 1 31.232 25.088l113.792 512A32 32 0 0 1 856.128 640H167.872a32 32 0 0 1-31.232-38.912l113.792-512A32 32 0 0 1 281.664 64z"}),e("path",{fill:"currentColor",d:"M672 576q32 0 32 32v128q0 32-32 32t-32-32V608q0-32 32-32m-192-.064h64V960h-64z"})]))}}),ee=$8,ae=a({name:"Reading",__name:"reading",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m512 863.36 384-54.848v-638.72L525.568 222.72a96 96 0 0 1-27.136 0L128 169.792v638.72zM137.024 106.432l370.432 52.928a32 32 0 0 0 9.088 0l370.432-52.928A64 64 0 0 1 960 169.792v638.72a64 64 0 0 1-54.976 63.36l-388.48 55.488a32 32 0 0 1-9.088 0l-388.48-55.488A64 64 0 0 1 64 808.512v-638.72a64 64 0 0 1 73.024-63.36z"}),e("path",{fill:"currentColor",d:"M480 192h64v704h-64z"})]))}}),te=ae,_e=a({name:"RefreshLeft",__name:"refresh-left",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M289.088 296.704h92.992a32 32 0 0 1 0 64H232.96a32 32 0 0 1-32-32V179.712a32 32 0 0 1 64 0v50.56a384 384 0 0 1 643.84 282.88 384 384 0 0 1-383.936 384 384 384 0 0 1-384-384h64a320 320 0 1 0 640 0 320 320 0 0 0-555.712-216.448z"})]))}}),re=_e,le=a({name:"RefreshRight",__name:"refresh-right",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M784.512 230.272v-50.56a32 32 0 1 1 64 0v149.056a32 32 0 0 1-32 32H667.52a32 32 0 1 1 0-64h92.992A320 320 0 1 0 524.8 833.152a320 320 0 0 0 320-320h64a384 384 0 0 1-384 384 384 384 0 0 1-384-384 384 384 0 0 1 643.712-282.88z"})]))}}),ue=le,pe=a({name:"Refresh",__name:"refresh",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M771.776 794.88A384 384 0 0 1 128 512h64a320 320 0 0 0 555.712 216.448H654.72a32 32 0 1 1 0-64h149.056a32 32 0 0 1 32 32v148.928a32 32 0 1 1-64 0v-50.56zM276.288 295.616h92.992a32 32 0 0 1 0 64H220.16a32 32 0 0 1-32-32V178.56a32 32 0 0 1 64 0v50.56A384 384 0 0 1 896.128 512h-64a320 320 0 0 0-555.776-216.384z"})]))}}),oe=pe,ve=a({name:"Refrigerator",__name:"refrigerator",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 448h512V160a32 32 0 0 0-32-32H288a32 32 0 0 0-32 32zm0 64v352a32 32 0 0 0 32 32h448a32 32 0 0 0 32-32V512zm32-448h448a96 96 0 0 1 96 96v704a96 96 0 0 1-96 96H288a96 96 0 0 1-96-96V160a96 96 0 0 1 96-96m32 224h64v96h-64zm0 288h64v96h-64z"})]))}}),se=ve,ce=a({name:"RemoveFilled",__name:"remove-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896M288 512a38.4 38.4 0 0 0 38.4 38.4h371.2a38.4 38.4 0 0 0 0-76.8H326.4A38.4 38.4 0 0 0 288 512"})]))}}),ne=ce,he=a({name:"Remove",__name:"remove",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M352 480h320a32 32 0 1 1 0 64H352a32 32 0 0 1 0-64"}),e("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"})]))}}),ie=he,me=a({name:"Right",__name:"right",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M754.752 480H160a32 32 0 1 0 0 64h594.752L521.344 777.344a32 32 0 0 0 45.312 45.312l288-288a32 32 0 0 0 0-45.312l-288-288a32 32 0 1 0-45.312 45.312z"})]))}}),we=me,de=a({name:"ScaleToOriginal",__name:"scale-to-original",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M813.176 180.706a60.235 60.235 0 0 1 60.236 60.235v481.883a60.235 60.235 0 0 1-60.236 60.235H210.824a60.235 60.235 0 0 1-60.236-60.235V240.94a60.235 60.235 0 0 1 60.236-60.235h602.352zm0-60.235H210.824A120.47 120.47 0 0 0 90.353 240.94v481.883a120.47 120.47 0 0 0 120.47 120.47h602.353a120.47 120.47 0 0 0 120.471-120.47V240.94a120.47 120.47 0 0 0-120.47-120.47zm-120.47 180.705a30.118 30.118 0 0 0-30.118 30.118v301.177a30.118 30.118 0 0 0 60.236 0V331.294a30.118 30.118 0 0 0-30.118-30.118zm-361.412 0a30.118 30.118 0 0 0-30.118 30.118v301.177a30.118 30.118 0 1 0 60.236 0V331.294a30.118 30.118 0 0 0-30.118-30.118M512 361.412a30.118 30.118 0 0 0-30.118 30.117v30.118a30.118 30.118 0 0 0 60.236 0V391.53A30.118 30.118 0 0 0 512 361.412M512 512a30.118 30.118 0 0 0-30.118 30.118v30.117a30.118 30.118 0 0 0 60.236 0v-30.117A30.118 30.118 0 0 0 512 512"})]))}}),ge=de,fe=a({name:"School",__name:"school",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M224 128v704h576V128zm-32-64h640a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32"}),e("path",{fill:"currentColor",d:"M64 832h896v64H64zm256-640h128v96H320z"}),e("path",{fill:"currentColor",d:"M384 832h256v-64a128 128 0 1 0-256 0zm128-256a192 192 0 0 1 192 192v128H320V768a192 192 0 0 1 192-192M320 384h128v96H320zm256-192h128v96H576zm0 192h128v96H576z"})]))}}),xe=fe,ze=a({name:"Scissor",__name:"scissor",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m512.064 578.368-106.88 152.768a160 160 0 1 1-23.36-78.208L472.96 522.56 196.864 128.256a32 32 0 1 1 52.48-36.736l393.024 561.344a160 160 0 1 1-23.36 78.208l-106.88-152.704zm54.4-189.248 208.384-297.6a32 32 0 0 1 52.48 36.736l-221.76 316.672-39.04-55.808zm-376.32 425.856a96 96 0 1 0 110.144-157.248 96 96 0 0 0-110.08 157.248zm643.84 0a96 96 0 1 0-110.08-157.248 96 96 0 0 0 110.08 157.248"})]))}}),Me=ze,Ce=a({name:"Search",__name:"search",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704"})]))}}),He=Ce,Ve=a({name:"Select",__name:"select",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M77.248 415.04a64 64 0 0 1 90.496 0l226.304 226.304L846.528 188.8a64 64 0 1 1 90.56 90.496l-543.04 543.04-316.8-316.8a64 64 0 0 1 0-90.496z"})]))}}),ye=Ve,Be=a({name:"Sell",__name:"sell",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M704 288h131.072a32 32 0 0 1 31.808 28.8L886.4 512h-64.384l-16-160H704v96a32 32 0 1 1-64 0v-96H384v96a32 32 0 0 1-64 0v-96H217.92l-51.2 512H512v64H131.328a32 32 0 0 1-31.808-35.2l57.6-576a32 32 0 0 1 31.808-28.8H320v-22.336C320 154.688 405.504 64 512 64s192 90.688 192 201.664v22.4zm-64 0v-22.336C640 189.248 582.272 128 512 128c-70.272 0-128 61.248-128 137.664v22.4h256zm201.408 483.84L768 698.496V928a32 32 0 1 1-64 0V698.496l-73.344 73.344a32 32 0 1 1-45.248-45.248l128-128a32 32 0 0 1 45.248 0l128 128a32 32 0 1 1-45.248 45.248z"})]))}}),Le=Be,Ae=a({name:"SemiSelect",__name:"semi-select",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 448h768q64 0 64 64t-64 64H128q-64 0-64-64t64-64"})]))}}),be=Ae,ke=a({name:"Service",__name:"service",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M864 409.6a192 192 0 0 1-37.888 349.44A256.064 256.064 0 0 1 576 960h-96a32 32 0 1 1 0-64h96a192.064 192.064 0 0 0 181.12-128H736a32 32 0 0 1-32-32V416a32 32 0 0 1 32-32h32c10.368 0 20.544.832 30.528 2.432a288 288 0 0 0-573.056 0A193.235 193.235 0 0 1 256 384h32a32 32 0 0 1 32 32v320a32 32 0 0 1-32 32h-32a192 192 0 0 1-96-358.4 352 352 0 0 1 704 0M256 448a128 128 0 1 0 0 256zm640 128a128 128 0 0 0-128-128v256a128 128 0 0 0 128-128"})]))}}),Se=ke,qe=a({name:"SetUp",__name:"set-up",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M224 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V224a64 64 0 0 0-64-64zm0-64h576a128 128 0 0 1 128 128v576a128 128 0 0 1-128 128H224A128 128 0 0 1 96 800V224A128 128 0 0 1 224 96"}),e("path",{fill:"currentColor",d:"M384 416a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"}),e("path",{fill:"currentColor",d:"M480 320h256q32 0 32 32t-32 32H480q-32 0-32-32t32-32m160 416a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"}),e("path",{fill:"currentColor",d:"M288 640h256q32 0 32 32t-32 32H288q-32 0-32-32t32-32"})]))}}),Fe=qe,De=a({name:"Setting",__name:"setting",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M600.704 64a32 32 0 0 1 30.464 22.208l35.2 109.376c14.784 7.232 28.928 15.36 42.432 24.512l112.384-24.192a32 32 0 0 1 34.432 15.36L944.32 364.8a32 32 0 0 1-4.032 37.504l-77.12 85.12a357.12 357.12 0 0 1 0 49.024l77.12 85.248a32 32 0 0 1 4.032 37.504l-88.704 153.6a32 32 0 0 1-34.432 15.296L708.8 803.904c-13.44 9.088-27.648 17.28-42.368 24.512l-35.264 109.376A32 32 0 0 1 600.704 960H423.296a32 32 0 0 1-30.464-22.208L357.696 828.48a351.616 351.616 0 0 1-42.56-24.64l-112.32 24.256a32 32 0 0 1-34.432-15.36L79.68 659.2a32 32 0 0 1 4.032-37.504l77.12-85.248a357.12 357.12 0 0 1 0-48.896l-77.12-85.248A32 32 0 0 1 79.68 364.8l88.704-153.6a32 32 0 0 1 34.432-15.296l112.32 24.256c13.568-9.152 27.776-17.408 42.56-24.64l35.2-109.312A32 32 0 0 1 423.232 64H600.64zm-23.424 64H446.72l-36.352 113.088-24.512 11.968a294.113 294.113 0 0 0-34.816 20.096l-22.656 15.36-116.224-25.088-65.28 113.152 79.68 88.192-1.92 27.136a293.12 293.12 0 0 0 0 40.192l1.92 27.136-79.808 88.192 65.344 113.152 116.224-25.024 22.656 15.296a294.113 294.113 0 0 0 34.816 20.096l24.512 11.968L446.72 896h130.688l36.48-113.152 24.448-11.904a288.282 288.282 0 0 0 34.752-20.096l22.592-15.296 116.288 25.024 65.28-113.152-79.744-88.192 1.92-27.136a293.12 293.12 0 0 0 0-40.256l-1.92-27.136 79.808-88.128-65.344-113.152-116.288 24.96-22.592-15.232a287.616 287.616 0 0 0-34.752-20.096l-24.448-11.904L577.344 128zM512 320a192 192 0 1 1 0 384 192 192 0 0 1 0-384m0 64a128 128 0 1 0 0 256 128 128 0 0 0 0-256"})]))}}),Pe=De,Re=a({name:"Share",__name:"share",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m679.872 348.8-301.76 188.608a127.808 127.808 0 0 1 5.12 52.16l279.936 104.96a128 128 0 1 1-22.464 59.904l-279.872-104.96a128 128 0 1 1-16.64-166.272l301.696-188.608a128 128 0 1 1 33.92 54.272z"})]))}}),Te=Re,Oe=a({name:"Ship",__name:"ship",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 386.88V448h405.568a32 32 0 0 1 30.72 40.768l-76.48 267.968A192 192 0 0 1 687.168 896H336.832a192 192 0 0 1-184.64-139.264L75.648 488.768A32 32 0 0 1 106.368 448H448V117.888a32 32 0 0 1 47.36-28.096l13.888 7.616L512 96v2.88l231.68 126.4a32 32 0 0 1-2.048 57.216zm0-70.272 144.768-65.792L512 171.84zM512 512H148.864l18.24 64H856.96l18.24-64zM185.408 640l28.352 99.2A128 128 0 0 0 336.832 832h350.336a128 128 0 0 0 123.072-92.8l28.352-99.2H185.408"})]))}}),Ge=Oe,Ie=a({name:"Shop",__name:"shop",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M704 704h64v192H256V704h64v64h384zm188.544-152.192C894.528 559.616 896 567.616 896 576a96 96 0 1 1-192 0 96 96 0 1 1-192 0 96 96 0 1 1-192 0 96 96 0 1 1-192 0c0-8.384 1.408-16.384 3.392-24.192L192 128h640z"})]))}}),Ue=Ie,We=a({name:"ShoppingBag",__name:"shopping-bag",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M704 320v96a32 32 0 0 1-32 32h-32V320H384v128h-32a32 32 0 0 1-32-32v-96H192v576h640V320zm-384-64a192 192 0 1 1 384 0h160a32 32 0 0 1 32 32v640a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V288a32 32 0 0 1 32-32zm64 0h256a128 128 0 1 0-256 0"}),e("path",{fill:"currentColor",d:"M192 704h640v64H192z"})]))}}),Ee=We,Ne=a({name:"ShoppingCartFull",__name:"shopping-cart-full",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M432 928a48 48 0 1 1 0-96 48 48 0 0 1 0 96m320 0a48 48 0 1 1 0-96 48 48 0 0 1 0 96M96 128a32 32 0 0 1 0-64h160a32 32 0 0 1 31.36 25.728L320.64 256H928a32 32 0 0 1 31.296 38.72l-96 448A32 32 0 0 1 832 768H384a32 32 0 0 1-31.36-25.728L229.76 128zm314.24 576h395.904l82.304-384H333.44l76.8 384z"}),e("path",{fill:"currentColor",d:"M699.648 256 608 145.984 516.352 256h183.296zm-140.8-151.04a64 64 0 0 1 98.304 0L836.352 320H379.648l179.2-215.04"})]))}}),Ze=Ne,Ke=a({name:"ShoppingCart",__name:"shopping-cart",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M432 928a48 48 0 1 1 0-96 48 48 0 0 1 0 96m320 0a48 48 0 1 1 0-96 48 48 0 0 1 0 96M96 128a32 32 0 0 1 0-64h160a32 32 0 0 1 31.36 25.728L320.64 256H928a32 32 0 0 1 31.296 38.72l-96 448A32 32 0 0 1 832 768H384a32 32 0 0 1-31.36-25.728L229.76 128zm314.24 576h395.904l82.304-384H333.44l76.8 384z"})]))}}),Qe=Ke,je=a({name:"ShoppingTrolley",__name:"shopping-trolley",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",style:{"enable-background":"new 0 0 1024 1024"},viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M368 833c-13.3 0-24.5 4.5-33.5 13.5S321 866.7 321 880s4.5 24.5 13.5 33.5 20.2 13.8 33.5 14.5c13.3-.7 24.5-5.5 33.5-14.5S415 893.3 415 880s-4.5-24.5-13.5-33.5S381.3 833 368 833m439-193c7.4 0 13.8-2.2 19.5-6.5S836 623.3 838 616l112-448c2-10-.2-19.2-6.5-27.5S929 128 919 128H96c-9.3 0-17 3-23 9s-9 13.7-9 23 3 17 9 23 13.7 9 23 9h96v576h672c9.3 0 17-3 23-9s9-13.7 9-23-3-17-9-23-13.7-9-23-9H256v-64zM256 192h622l-96 384H256zm432 641c-13.3 0-24.5 4.5-33.5 13.5S641 866.7 641 880s4.5 24.5 13.5 33.5 20.2 13.8 33.5 14.5c13.3-.7 24.5-5.5 33.5-14.5S735 893.3 735 880s-4.5-24.5-13.5-33.5S701.3 833 688 833"})]))}}),Je=je,Xe=a({name:"Smoking",__name:"smoking",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 576v128h640V576zm-32-64h704a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32H224a32 32 0 0 1-32-32V544a32 32 0 0 1 32-32"}),e("path",{fill:"currentColor",d:"M704 576h64v128h-64zM256 64h64v320h-64zM128 192h64v192h-64zM64 512h64v256H64z"})]))}}),Ye=Xe,$e=a({name:"Soccer",__name:"soccer",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M418.496 871.04 152.256 604.8c-16.512 94.016-2.368 178.624 42.944 224 44.928 44.928 129.344 58.752 223.296 42.24m72.32-18.176a573.056 573.056 0 0 0 224.832-137.216 573.12 573.12 0 0 0 137.216-224.832L533.888 171.84a578.56 578.56 0 0 0-227.52 138.496A567.68 567.68 0 0 0 170.432 532.48l320.384 320.384zM871.04 418.496c16.512-93.952 2.688-178.368-42.24-223.296-44.544-44.544-128.704-58.048-222.592-41.536zM149.952 874.048c-112.96-112.96-88.832-408.96 111.168-608.96C461.056 65.152 760.96 36.928 874.048 149.952c113.024 113.024 86.784 411.008-113.152 610.944-199.936 199.936-497.92 226.112-610.944 113.152m452.544-497.792 22.656-22.656a32 32 0 0 1 45.248 45.248l-22.656 22.656 45.248 45.248A32 32 0 1 1 647.744 512l-45.248-45.248L557.248 512l45.248 45.248a32 32 0 1 1-45.248 45.248L512 557.248l-45.248 45.248L512 647.744a32 32 0 1 1-45.248 45.248l-45.248-45.248-22.656 22.656a32 32 0 1 1-45.248-45.248l22.656-22.656-45.248-45.248A32 32 0 1 1 376.256 512l45.248 45.248L466.752 512l-45.248-45.248a32 32 0 1 1 45.248-45.248L512 466.752l45.248-45.248L512 376.256a32 32 0 0 1 45.248-45.248l45.248 45.248z"})]))}}),ea=$e,aa=a({name:"SoldOut",__name:"sold-out",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M704 288h131.072a32 32 0 0 1 31.808 28.8L886.4 512h-64.384l-16-160H704v96a32 32 0 1 1-64 0v-96H384v96a32 32 0 0 1-64 0v-96H217.92l-51.2 512H512v64H131.328a32 32 0 0 1-31.808-35.2l57.6-576a32 32 0 0 1 31.808-28.8H320v-22.336C320 154.688 405.504 64 512 64s192 90.688 192 201.664v22.4zm-64 0v-22.336C640 189.248 582.272 128 512 128c-70.272 0-128 61.248-128 137.664v22.4h256zm201.408 476.16a32 32 0 1 1 45.248 45.184l-128 128a32 32 0 0 1-45.248 0l-128-128a32 32 0 1 1 45.248-45.248L704 837.504V608a32 32 0 1 1 64 0v229.504l73.408-73.408z"})]))}}),ta=aa,_a=a({name:"SortDown",__name:"sort-down",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M576 96v709.568L333.312 562.816A32 32 0 1 0 288 608l297.408 297.344A32 32 0 0 0 640 882.688V96a32 32 0 0 0-64 0"})]))}}),ra=_a,la=a({name:"SortUp",__name:"sort-up",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M384 141.248V928a32 32 0 1 0 64 0V218.56l242.688 242.688A32 32 0 1 0 736 416L438.592 118.656A32 32 0 0 0 384 141.248"})]))}}),ua=la,pa=a({name:"Sort",__name:"sort",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M384 96a32 32 0 0 1 64 0v786.752a32 32 0 0 1-54.592 22.656L95.936 608a32 32 0 0 1 0-45.312h.128a32 32 0 0 1 45.184 0L384 805.632zm192 45.248a32 32 0 0 1 54.592-22.592L928.064 416a32 32 0 0 1 0 45.312h-.128a32 32 0 0 1-45.184 0L640 218.496V928a32 32 0 1 1-64 0V141.248z"})]))}}),oa=pa,va=a({name:"Stamp",__name:"stamp",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M624 475.968V640h144a128 128 0 0 1 128 128H128a128 128 0 0 1 128-128h144V475.968a192 192 0 1 1 224 0M128 896v-64h768v64z"})]))}}),sa=va,ca=a({name:"StarFilled",__name:"star-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M283.84 867.84 512 747.776l228.16 119.936a6.4 6.4 0 0 0 9.28-6.72l-43.52-254.08 184.512-179.904a6.4 6.4 0 0 0-3.52-10.88l-255.104-37.12L517.76 147.904a6.4 6.4 0 0 0-11.52 0L392.192 379.072l-255.104 37.12a6.4 6.4 0 0 0-3.52 10.88L318.08 606.976l-43.584 254.08a6.4 6.4 0 0 0 9.28 6.72z"})]))}}),na=ca,ha=a({name:"Star",__name:"star",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m512 747.84 228.16 119.936a6.4 6.4 0 0 0 9.28-6.72l-43.52-254.08 184.512-179.904a6.4 6.4 0 0 0-3.52-10.88l-255.104-37.12L517.76 147.904a6.4 6.4 0 0 0-11.52 0L392.192 379.072l-255.104 37.12a6.4 6.4 0 0 0-3.52 10.88L318.08 606.976l-43.584 254.08a6.4 6.4 0 0 0 9.28 6.72zM313.6 924.48a70.4 70.4 0 0 1-102.144-74.24l37.888-220.928L88.96 472.96A70.4 70.4 0 0 1 128 352.896l221.76-32.256 99.2-200.96a70.4 70.4 0 0 1 126.208 0l99.2 200.96 221.824 32.256a70.4 70.4 0 0 1 39.04 120.064L774.72 629.376l37.888 220.928a70.4 70.4 0 0 1-102.144 74.24L512 820.096l-198.4 104.32z"})]))}}),ia=ha,ma=a({name:"Stopwatch",__name:"stopwatch",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),e("path",{fill:"currentColor",d:"M672 234.88c-39.168 174.464-80 298.624-122.688 372.48-64 110.848-202.624 30.848-138.624-80C453.376 453.44 540.48 355.968 672 234.816z"})]))}}),wa=ma,da=a({name:"SuccessFilled",__name:"success-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m-55.808 536.384-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.272 38.272 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336z"})]))}}),ga=da,fa=a({name:"Sugar",__name:"sugar",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m801.728 349.184 4.48 4.48a128 128 0 0 1 0 180.992L534.656 806.144a128 128 0 0 1-181.056 0l-4.48-4.48-19.392 109.696a64 64 0 0 1-108.288 34.176L78.464 802.56a64 64 0 0 1 34.176-108.288l109.76-19.328-4.544-4.544a128 128 0 0 1 0-181.056l271.488-271.488a128 128 0 0 1 181.056 0l4.48 4.48 19.392-109.504a64 64 0 0 1 108.352-34.048l142.592 143.04a64 64 0 0 1-34.24 108.16l-109.248 19.2zm-548.8 198.72h447.168v2.24l60.8-60.8a63.808 63.808 0 0 0 18.752-44.416h-426.88l-89.664 89.728a64.064 64.064 0 0 0-10.24 13.248zm0 64c2.752 4.736 6.144 9.152 10.176 13.248l135.744 135.744a64 64 0 0 0 90.496 0L638.4 611.904zm490.048-230.976L625.152 263.104a64 64 0 0 0-90.496 0L416.768 380.928zM123.712 757.312l142.976 142.976 24.32-137.6a25.6 25.6 0 0 0-29.696-29.632l-137.6 24.256zm633.6-633.344-24.32 137.472a25.6 25.6 0 0 0 29.632 29.632l137.28-24.064-142.656-143.04z"})]))}}),xa=fa,za=a({name:"SuitcaseLine",__name:"suitcase-line",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",style:{"enable-background":"new 0 0 1024 1024"},viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M922.5 229.5c-24.32-24.34-54.49-36.84-90.5-37.5H704v-64c-.68-17.98-7.02-32.98-19.01-44.99S658.01 64.66 640 64H384c-17.98.68-32.98 7.02-44.99 19.01S320.66 110 320 128v64H192c-35.99.68-66.16 13.18-90.5 37.5C77.16 253.82 64.66 283.99 64 320v448c.68 35.99 13.18 66.16 37.5 90.5s54.49 36.84 90.5 37.5h640c35.99-.68 66.16-13.18 90.5-37.5s36.84-54.49 37.5-90.5V320c-.68-35.99-13.18-66.16-37.5-90.5M384 128h256v64H384zM256 832h-64c-17.98-.68-32.98-7.02-44.99-19.01S128.66 786.01 128 768V448h128zm448 0H320V448h384zm192-64c-.68 17.98-7.02 32.98-19.01 44.99S850.01 831.34 832 832h-64V448h128zm0-384H128v-64c.69-17.98 7.02-32.98 19.01-44.99S173.99 256.66 192 256h640c17.98.69 32.98 7.02 44.99 19.01S895.34 301.99 896 320z"})]))}}),Ma=za,Ca=a({name:"Suitcase",__name:"suitcase",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 384h768v-64a64 64 0 0 0-64-64H192a64 64 0 0 0-64 64zm0 64v320a64 64 0 0 0 64 64h640a64 64 0 0 0 64-64V448zm64-256h640a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H192A128 128 0 0 1 64 768V320a128 128 0 0 1 128-128"}),e("path",{fill:"currentColor",d:"M384 128v64h256v-64zm0-64h256a64 64 0 0 1 64 64v64a64 64 0 0 1-64 64H384a64 64 0 0 1-64-64v-64a64 64 0 0 1 64-64"})]))}}),Ha=Ca,Va=a({name:"Sunny",__name:"sunny",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 704a192 192 0 1 0 0-384 192 192 0 0 0 0 384m0 64a256 256 0 1 1 0-512 256 256 0 0 1 0 512m0-704a32 32 0 0 1 32 32v64a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32m0 768a32 32 0 0 1 32 32v64a32 32 0 1 1-64 0v-64a32 32 0 0 1 32-32M195.2 195.2a32 32 0 0 1 45.248 0l45.248 45.248a32 32 0 1 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248zm543.104 543.104a32 32 0 0 1 45.248 0l45.248 45.248a32 32 0 0 1-45.248 45.248l-45.248-45.248a32 32 0 0 1 0-45.248M64 512a32 32 0 0 1 32-32h64a32 32 0 0 1 0 64H96a32 32 0 0 1-32-32m768 0a32 32 0 0 1 32-32h64a32 32 0 1 1 0 64h-64a32 32 0 0 1-32-32M195.2 828.8a32 32 0 0 1 0-45.248l45.248-45.248a32 32 0 0 1 45.248 45.248L240.448 828.8a32 32 0 0 1-45.248 0zm543.104-543.104a32 32 0 0 1 0-45.248l45.248-45.248a32 32 0 0 1 45.248 45.248l-45.248 45.248a32 32 0 0 1-45.248 0"})]))}}),ya=Va,Ba=a({name:"Sunrise",__name:"sunrise",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M32 768h960a32 32 0 1 1 0 64H32a32 32 0 1 1 0-64m129.408-96a352 352 0 0 1 701.184 0h-64.32a288 288 0 0 0-572.544 0h-64.32zM512 128a32 32 0 0 1 32 32v96a32 32 0 0 1-64 0v-96a32 32 0 0 1 32-32m407.296 168.704a32 32 0 0 1 0 45.248l-67.84 67.84a32 32 0 1 1-45.248-45.248l67.84-67.84a32 32 0 0 1 45.248 0zm-814.592 0a32 32 0 0 1 45.248 0l67.84 67.84a32 32 0 1 1-45.248 45.248l-67.84-67.84a32 32 0 0 1 0-45.248"})]))}}),La=Ba,Aa=a({name:"Sunset",__name:"sunset",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M82.56 640a448 448 0 1 1 858.88 0h-67.2a384 384 0 1 0-724.288 0zM32 704h960q32 0 32 32t-32 32H32q-32 0-32-32t32-32m256 128h448q32 0 32 32t-32 32H288q-32 0-32-32t32-32"})]))}}),ba=Aa,ka=a({name:"SwitchButton",__name:"switch-button",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M352 159.872V230.4a352 352 0 1 0 320 0v-70.528A416.128 416.128 0 0 1 512 960a416 416 0 0 1-160-800.128z"}),e("path",{fill:"currentColor",d:"M512 64q32 0 32 32v320q0 32-32 32t-32-32V96q0-32 32-32"})]))}}),Sa=ka,qa=a({name:"SwitchFilled",__name:"switch-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",style:{"enable-background":"new 0 0 1024 1024"},viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M247.47 358.4v.04c.07 19.17 7.72 37.53 21.27 51.09s31.92 21.2 51.09 21.27c39.86 0 72.41-32.6 72.41-72.4s-32.6-72.36-72.41-72.36-72.36 32.55-72.36 72.36z"}),e("path",{fill:"currentColor",d:"M492.38 128H324.7c-52.16 0-102.19 20.73-139.08 57.61a196.655 196.655 0 0 0-57.61 139.08V698.7c-.01 25.84 5.08 51.42 14.96 75.29s24.36 45.56 42.63 63.83 39.95 32.76 63.82 42.65a196.67 196.67 0 0 0 75.28 14.98h167.68c3.03 0 5.46-2.43 5.46-5.42V133.42c.6-2.99-1.83-5.42-5.46-5.42zm-56.11 705.88H324.7c-17.76.13-35.36-3.33-51.75-10.18s-31.22-16.94-43.61-29.67c-25.3-25.35-39.81-59.1-39.81-95.32V324.69c-.13-17.75 3.33-35.35 10.17-51.74a131.695 131.695 0 0 1 29.64-43.62c25.39-25.3 59.14-39.81 95.36-39.81h111.57zm402.12-647.67a196.655 196.655 0 0 0-139.08-57.61H580.48c-3.03 0-4.82 2.43-4.82 4.82v757.16c-.6 2.99 1.79 5.42 5.42 5.42h118.23a196.69 196.69 0 0 0 139.08-57.61A196.655 196.655 0 0 0 896 699.31V325.29a196.69 196.69 0 0 0-57.61-139.08zm-111.3 441.92c-42.83 0-77.82-34.99-77.82-77.82s34.98-77.82 77.82-77.82c42.83 0 77.82 34.99 77.82 77.82s-34.99 77.82-77.82 77.82z"})]))}}),Fa=qa,Da=a({name:"Switch",__name:"switch",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M118.656 438.656a32 32 0 0 1 0-45.248L416 96l4.48-3.776A32 32 0 0 1 461.248 96l3.712 4.48a32.064 32.064 0 0 1-3.712 40.832L218.56 384H928a32 32 0 1 1 0 64H141.248a32 32 0 0 1-22.592-9.344zM64 608a32 32 0 0 1 32-32h786.752a32 32 0 0 1 22.656 54.592L608 928l-4.48 3.776a32.064 32.064 0 0 1-40.832-49.024L805.632 640H96a32 32 0 0 1-32-32"})]))}}),Pa=Da,Ra=a({name:"TakeawayBox",__name:"takeaway-box",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M832 384H192v448h640zM96 320h832V128H96zm800 64v480a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V384H64a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32h896a32 32 0 0 1 32 32v256a32 32 0 0 1-32 32zM416 512h192a32 32 0 0 1 0 64H416a32 32 0 0 1 0-64"})]))}}),Ta=Ra,Oa=a({name:"Ticket",__name:"ticket",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M640 832H64V640a128 128 0 1 0 0-256V192h576v160h64V192h256v192a128 128 0 1 0 0 256v192H704V672h-64zm0-416v192h64V416z"})]))}}),Ga=Oa,Ia=a({name:"Tickets",__name:"tickets",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M192 128v768h640V128zm-32-64h704a32 32 0 0 1 32 32v832a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m160 448h384v64H320zm0-192h192v64H320zm0 384h384v64H320z"})]))}}),Ua=Ia,Wa=a({name:"Timer",__name:"timer",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 896a320 320 0 1 0 0-640 320 320 0 0 0 0 640m0 64a384 384 0 1 1 0-768 384 384 0 0 1 0 768"}),e("path",{fill:"currentColor",d:"M512 320a32 32 0 0 1 32 32l-.512 224a32 32 0 1 1-64 0L480 352a32 32 0 0 1 32-32"}),e("path",{fill:"currentColor",d:"M448 576a64 64 0 1 0 128 0 64 64 0 1 0-128 0m96-448v128h-64V128h-96a32 32 0 0 1 0-64h256a32 32 0 1 1 0 64z"})]))}}),Ea=Wa,Na=a({name:"ToiletPaper",__name:"toilet-paper",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M595.2 128H320a192 192 0 0 0-192 192v576h384V352c0-90.496 32.448-171.2 83.2-224M736 64c123.712 0 224 128.96 224 288S859.712 640 736 640H576v320H64V320A256 256 0 0 1 320 64zM576 352v224h160c84.352 0 160-97.28 160-224s-75.648-224-160-224-160 97.28-160 224"}),e("path",{fill:"currentColor",d:"M736 448c-35.328 0-64-43.008-64-96s28.672-96 64-96 64 43.008 64 96-28.672 96-64 96"})]))}}),Za=Na,Ka=a({name:"Tools",__name:"tools",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M764.416 254.72a351.68 351.68 0 0 1 86.336 149.184H960v192.064H850.752a351.68 351.68 0 0 1-86.336 149.312l54.72 94.72-166.272 96-54.592-94.72a352.64 352.64 0 0 1-172.48 0L371.136 936l-166.272-96 54.72-94.72a351.68 351.68 0 0 1-86.336-149.312H64v-192h109.248a351.68 351.68 0 0 1 86.336-149.312L204.8 160l166.208-96h.192l54.656 94.592a352.64 352.64 0 0 1 172.48 0L652.8 64h.128L819.2 160l-54.72 94.72zM704 499.968a192 192 0 1 0-384 0 192 192 0 0 0 384 0"})]))}}),Qa=Ka,ja=a({name:"TopLeft",__name:"top-left",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M256 256h416a32 32 0 1 0 0-64H224a32 32 0 0 0-32 32v448a32 32 0 0 0 64 0z"}),e("path",{fill:"currentColor",d:"M246.656 201.344a32 32 0 0 0-45.312 45.312l544 544a32 32 0 0 0 45.312-45.312l-544-544z"})]))}}),Ja=ja,Xa=a({name:"TopRight",__name:"top-right",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M768 256H353.6a32 32 0 1 1 0-64H800a32 32 0 0 1 32 32v448a32 32 0 0 1-64 0z"}),e("path",{fill:"currentColor",d:"M777.344 201.344a32 32 0 0 1 45.312 45.312l-544 544a32 32 0 0 1-45.312-45.312l544-544z"})]))}}),Ya=Xa,$a=a({name:"Top",__name:"top",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M572.235 205.282v600.365a30.118 30.118 0 1 1-60.235 0V205.282L292.382 438.633a28.913 28.913 0 0 1-42.646 0 33.43 33.43 0 0 1 0-45.236l271.058-288.045a28.913 28.913 0 0 1 42.647 0L834.5 393.397a33.43 33.43 0 0 1 0 45.176 28.913 28.913 0 0 1-42.647 0l-219.618-233.23z"})]))}}),et=$a,at=a({name:"TrendCharts",__name:"trend-charts",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128 896V128h768v768zm291.712-327.296 128 102.4 180.16-201.792-47.744-42.624-139.84 156.608-128-102.4-180.16 201.792 47.744 42.624 139.84-156.608zM816 352a48 48 0 1 0-96 0 48 48 0 0 0 96 0"})]))}}),tt=at,_t=a({name:"TrophyBase",__name:"trophy-base",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",style:{"enable-background":"new 0 0 1024 1024"},viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M918.4 201.6c-6.4-6.4-12.8-9.6-22.4-9.6H768V96c0-9.6-3.2-16-9.6-22.4C752 67.2 745.6 64 736 64H288c-9.6 0-16 3.2-22.4 9.6C259.2 80 256 86.4 256 96v96H128c-9.6 0-16 3.2-22.4 9.6-6.4 6.4-9.6 16-9.6 22.4 3.2 108.8 25.6 185.6 64 224 34.4 34.4 77.56 55.65 127.65 61.99 10.91 20.44 24.78 39.25 41.95 56.41 40.86 40.86 91 65.47 150.4 71.9V768h-96c-9.6 0-16 3.2-22.4 9.6-6.4 6.4-9.6 12.8-9.6 22.4s3.2 16 9.6 22.4c6.4 6.4 12.8 9.6 22.4 9.6h256c9.6 0 16-3.2 22.4-9.6 6.4-6.4 9.6-12.8 9.6-22.4s-3.2-16-9.6-22.4c-6.4-6.4-12.8-9.6-22.4-9.6h-96V637.26c59.4-7.71 109.54-30.01 150.4-70.86 17.2-17.2 31.51-36.06 42.81-56.55 48.93-6.51 90.02-27.7 126.79-61.85 38.4-38.4 60.8-112 64-224 0-6.4-3.2-16-9.6-22.4zM256 438.4c-19.2-6.4-35.2-19.2-51.2-35.2-22.4-22.4-35.2-70.4-41.6-147.2H256zm390.4 80C608 553.6 566.4 576 512 576s-99.2-19.2-134.4-57.6C342.4 480 320 438.4 320 384V128h384v256c0 54.4-19.2 99.2-57.6 134.4m172.8-115.2c-16 16-32 25.6-51.2 35.2V256h92.8c-6.4 76.8-19.2 124.8-41.6 147.2zM768 896H256c-9.6 0-16 3.2-22.4 9.6-6.4 6.4-9.6 12.8-9.6 22.4s3.2 16 9.6 22.4c6.4 6.4 12.8 9.6 22.4 9.6h512c9.6 0 16-3.2 22.4-9.6 6.4-6.4 9.6-12.8 9.6-22.4s-3.2-16-9.6-22.4c-6.4-6.4-12.8-9.6-22.4-9.6"})]))}}),rt=_t,lt=a({name:"Trophy",__name:"trophy",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M480 896V702.08A256.256 256.256 0 0 1 264.064 512h-32.64a96 96 0 0 1-91.968-68.416L93.632 290.88a76.8 76.8 0 0 1 73.6-98.88H256V96a32 32 0 0 1 32-32h448a32 32 0 0 1 32 32v96h88.768a76.8 76.8 0 0 1 73.6 98.88L884.48 443.52A96 96 0 0 1 792.576 512h-32.64A256.256 256.256 0 0 1 544 702.08V896h128a32 32 0 1 1 0 64H352a32 32 0 1 1 0-64zm224-448V128H320v320a192 192 0 1 0 384 0m64 0h24.576a32 32 0 0 0 30.656-22.784l45.824-152.768A12.8 12.8 0 0 0 856.768 256H768zm-512 0V256h-88.768a12.8 12.8 0 0 0-12.288 16.448l45.824 152.768A32 32 0 0 0 231.424 448z"})]))}}),ut=lt,pt=a({name:"TurnOff",__name:"turn-off",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M329.956 257.138a254.862 254.862 0 0 0 0 509.724h364.088a254.862 254.862 0 0 0 0-509.724zm0-72.818h364.088a327.68 327.68 0 1 1 0 655.36H329.956a327.68 327.68 0 1 1 0-655.36z"}),e("path",{fill:"currentColor",d:"M329.956 621.227a109.227 109.227 0 1 0 0-218.454 109.227 109.227 0 0 0 0 218.454m0 72.817a182.044 182.044 0 1 1 0-364.088 182.044 182.044 0 0 1 0 364.088"})]))}}),ot=pt,vt=a({name:"Umbrella",__name:"umbrella",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M320 768a32 32 0 1 1 64 0 64 64 0 0 0 128 0V512H64a448 448 0 1 1 896 0H576v256a128 128 0 1 1-256 0m570.688-320a384.128 384.128 0 0 0-757.376 0z"})]))}}),st=vt,ct=a({name:"Unlock",__name:"unlock",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M224 448a32 32 0 0 0-32 32v384a32 32 0 0 0 32 32h576a32 32 0 0 0 32-32V480a32 32 0 0 0-32-32zm0-64h576a96 96 0 0 1 96 96v384a96 96 0 0 1-96 96H224a96 96 0 0 1-96-96V480a96 96 0 0 1 96-96"}),e("path",{fill:"currentColor",d:"M512 544a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V576a32 32 0 0 1 32-32m178.304-295.296A192.064 192.064 0 0 0 320 320v64h352l96 38.4V448H256V320a256 256 0 0 1 493.76-95.104z"})]))}}),nt=ct,ht=a({name:"UploadFilled",__name:"upload-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M544 864V672h128L512 480 352 672h128v192H320v-1.6c-5.376.32-10.496 1.6-16 1.6A240 240 0 0 1 64 624c0-123.136 93.12-223.488 212.608-237.248A239.808 239.808 0 0 1 512 192a239.872 239.872 0 0 1 235.456 194.752c119.488 13.76 212.48 114.112 212.48 237.248a240 240 0 0 1-240 240c-5.376 0-10.56-1.28-16-1.6v1.6z"})]))}}),it=ht,mt=a({name:"Upload",__name:"upload",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M160 832h704a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64m384-578.304V704h-64V247.296L237.248 490.048 192 444.8 508.8 128l316.8 316.8-45.312 45.248z"})]))}}),wt=mt,dt=a({name:"UserFilled",__name:"user-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M288 320a224 224 0 1 0 448 0 224 224 0 1 0-448 0m544 608H160a32 32 0 0 1-32-32v-96a160 160 0 0 1 160-160h448a160 160 0 0 1 160 160v96a32 32 0 0 1-32 32z"})]))}}),gt=dt,ft=a({name:"User",__name:"user",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 512a192 192 0 1 0 0-384 192 192 0 0 0 0 384m0 64a256 256 0 1 1 0-512 256 256 0 0 1 0 512m320 320v-96a96 96 0 0 0-96-96H288a96 96 0 0 0-96 96v96a32 32 0 1 1-64 0v-96a160 160 0 0 1 160-160h448a160 160 0 0 1 160 160v96a32 32 0 1 1-64 0"})]))}}),xt=ft,zt=a({name:"Van",__name:"van",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M128.896 736H96a32 32 0 0 1-32-32V224a32 32 0 0 1 32-32h576a32 32 0 0 1 32 32v96h164.544a32 32 0 0 1 31.616 27.136l54.144 352A32 32 0 0 1 922.688 736h-91.52a144 144 0 1 1-286.272 0H415.104a144 144 0 1 1-286.272 0zm23.36-64a143.872 143.872 0 0 1 239.488 0H568.32c17.088-25.6 42.24-45.376 71.744-55.808V256H128v416zm655.488 0h77.632l-19.648-128H704v64.896A144 144 0 0 1 807.744 672m48.128-192-14.72-96H704v96h151.872M688 832a80 80 0 1 0 0-160 80 80 0 0 0 0 160m-416 0a80 80 0 1 0 0-160 80 80 0 0 0 0 160"})]))}}),Mt=zt,Ct=a({name:"VideoCameraFilled",__name:"video-camera-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m768 576 192-64v320l-192-64v96a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V480a32 32 0 0 1 32-32h640a32 32 0 0 1 32 32zM192 768v64h384v-64zm192-480a160 160 0 0 1 320 0 160 160 0 0 1-320 0m64 0a96 96 0 1 0 192.064-.064A96 96 0 0 0 448 288m-320 32a128 128 0 1 1 256.064.064A128 128 0 0 1 128 320m64 0a64 64 0 1 0 128 0 64 64 0 0 0-128 0"})]))}}),Ht=Ct,Vt=a({name:"VideoCamera",__name:"video-camera",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M704 768V256H128v512zm64-416 192-96v512l-192-96v128a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V224a32 32 0 0 1 32-32h640a32 32 0 0 1 32 32zm0 71.552v176.896l128 64V359.552zM192 320h192v64H192z"})]))}}),yt=Vt,Bt=a({name:"VideoPause",__name:"video-pause",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 832a384 384 0 0 0 0-768 384 384 0 0 0 0 768m-96-544q32 0 32 32v256q0 32-32 32t-32-32V384q0-32 32-32m192 0q32 0 32 32v256q0 32-32 32t-32-32V384q0-32 32-32"})]))}}),Lt=Bt,At=a({name:"VideoPlay",__name:"video-play",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 832a384 384 0 0 0 0-768 384 384 0 0 0 0 768m-48-247.616L668.608 512 464 375.616zm10.624-342.656 249.472 166.336a48 48 0 0 1 0 79.872L474.624 718.272A48 48 0 0 1 400 678.336V345.6a48 48 0 0 1 74.624-39.936z"})]))}}),bt=At,kt=a({name:"View",__name:"view",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 160c320 0 512 352 512 352S832 864 512 864 0 512 0 512s192-352 512-352m0 64c-225.28 0-384.128 208.064-436.8 288 52.608 79.872 211.456 288 436.8 288 225.28 0 384.128-208.064 436.8-288-52.608-79.872-211.456-288-436.8-288zm0 64a224 224 0 1 1 0 448 224 224 0 0 1 0-448m0 64a160.192 160.192 0 0 0-160 160c0 88.192 71.744 160 160 160s160-71.808 160-160-71.744-160-160-160"})]))}}),St=kt,qt=a({name:"WalletFilled",__name:"wallet-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M688 512a112 112 0 1 0 0 224h208v160H128V352h768v160zm32 160h-32a48 48 0 0 1 0-96h32a48 48 0 0 1 0 96m-80-544 128 160H384z"})]))}}),Ft=qt,Dt=a({name:"Wallet",__name:"wallet",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M640 288h-64V128H128v704h384v32a32 32 0 0 0 32 32H96a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32h512a32 32 0 0 1 32 32z"}),e("path",{fill:"currentColor",d:"M128 320v512h768V320zm-32-64h832a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V288a32 32 0 0 1 32-32"}),e("path",{fill:"currentColor",d:"M704 640a64 64 0 1 1 0-128 64 64 0 0 1 0 128"})]))}}),Pt=Dt,Rt=a({name:"WarnTriangleFilled",__name:"warn-triangle-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",style:{"enable-background":"new 0 0 1024 1024"},viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M928.99 755.83 574.6 203.25c-12.89-20.16-36.76-32.58-62.6-32.58s-49.71 12.43-62.6 32.58L95.01 755.83c-12.91 20.12-12.9 44.91.01 65.03 12.92 20.12 36.78 32.51 62.59 32.49h708.78c25.82.01 49.68-12.37 62.59-32.49 12.91-20.12 12.92-44.91.01-65.03M554.67 768h-85.33v-85.33h85.33zm0-426.67v298.66h-85.33V341.32z"})]))}}),Tt=Rt,Ot=a({name:"WarningFilled",__name:"warning-filled",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 192a58.432 58.432 0 0 0-58.24 63.744l23.36 256.384a35.072 35.072 0 0 0 69.76 0l23.296-256.384A58.432 58.432 0 0 0 512 256m0 512a51.2 51.2 0 1 0 0-102.4 51.2 51.2 0 0 0 0 102.4"})]))}}),Gt=Ot,It=a({name:"Warning",__name:"warning",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 832a384 384 0 0 0 0-768 384 384 0 0 0 0 768m48-176a48 48 0 1 1-96 0 48 48 0 0 1 96 0m-48-464a32 32 0 0 1 32 32v288a32 32 0 0 1-64 0V288a32 32 0 0 1 32-32"})]))}}),Ut=It,Wt=a({name:"Watch",__name:"watch",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M512 768a256 256 0 1 0 0-512 256 256 0 0 0 0 512m0 64a320 320 0 1 1 0-640 320 320 0 0 1 0 640"}),e("path",{fill:"currentColor",d:"M480 352a32 32 0 0 1 32 32v160a32 32 0 0 1-64 0V384a32 32 0 0 1 32-32"}),e("path",{fill:"currentColor",d:"M480 512h128q32 0 32 32t-32 32H480q-32 0-32-32t32-32m128-256V128H416v128h-64V64h320v192zM416 768v128h192V768h64v192H352V768z"})]))}}),Et=Wt,Nt=a({name:"Watermelon",__name:"watermelon",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m683.072 600.32-43.648 162.816-61.824-16.512 53.248-198.528L576 493.248l-158.4 158.4-45.248-45.248 158.4-158.4-55.616-55.616-198.528 53.248-16.512-61.824 162.816-43.648L282.752 200A384 384 0 0 0 824 741.248zm231.552 141.056a448 448 0 1 1-632-632l632 632"})]))}}),Zt=Nt,Kt=a({name:"WindPower",__name:"wind-power",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"M160 64q32 0 32 32v832q0 32-32 32t-32-32V96q0-32 32-32m416 354.624 128-11.584V168.96l-128-11.52v261.12zm-64 5.824V151.552L320 134.08V160h-64V64l616.704 56.064A96 96 0 0 1 960 215.68v144.64a96 96 0 0 1-87.296 95.616L256 512V224h64v217.92zm256-23.232 98.88-8.96A32 32 0 0 0 896 360.32V215.68a32 32 0 0 0-29.12-31.872l-98.88-8.96z"})]))}}),Qt=Kt,jt=a({name:"ZoomIn",__name:"zoom-in",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704m-32-384v-96a32 32 0 0 1 64 0v96h96a32 32 0 0 1 0 64h-96v96a32 32 0 0 1-64 0v-96h-96a32 32 0 0 1 0-64z"})]))}}),Jt=jt,Xt=a({name:"ZoomOut",__name:"zoom-out",setup(r){return(l,u)=>(_(),t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[e("path",{fill:"currentColor",d:"m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704M352 448h256a32 32 0 0 1 0 64H352a32 32 0 0 1 0-64"})]))}}),Yt=Xt;const e_=Object.freeze(Object.defineProperty({__proto__:null,AddLocation:o,Aim:s,AlarmClock:n,Apple:i,ArrowDown:g,ArrowDownBold:w,ArrowLeft:M,ArrowLeftBold:x,ArrowRight:y,ArrowRightBold:H,ArrowUp:b,ArrowUpBold:L,Avatar:S,Back:F,Baseball:P,Basketball:T,Bell:U,BellFilled:G,Bicycle:E,Bottom:J,BottomLeft:Z,BottomRight:Q,Bowl:Y,Box:e2,Briefcase:t2,Brush:u2,BrushFilled:r2,Burger:o2,Calendar:s2,Camera:i2,CameraFilled:n2,CaretBottom:w2,CaretLeft:g2,CaretRight:x2,CaretTop:M2,Cellphone:H2,ChatDotRound:y2,ChatDotSquare:L2,ChatLineRound:b2,ChatLineSquare:S2,ChatRound:F2,ChatSquare:P2,Check:T2,Checked:G2,Cherry:U2,Chicken:E2,ChromeFilled:Z2,CircleCheck:J2,CircleCheckFilled:Q2,CircleClose:e0,CircleCloseFilled:Y2,CirclePlus:r0,CirclePlusFilled:t0,Clock:u0,Close:s0,CloseBold:o0,Cloudy:n0,Coffee:w0,CoffeeCup:i0,Coin:g0,ColdDrink:x0,Collection:H0,CollectionTag:M0,Comment:y0,Compass:L0,Connection:b0,Coordinate:S0,CopyDocument:F0,Cpu:P0,CreditCard:T0,Crop:G0,DArrowLeft:U0,DArrowRight:E0,DCaret:Z0,DataAnalysis:Q0,DataBoard:J0,DataLine:Y0,Delete:r1,DeleteFilled:e1,DeleteLocation:t1,Dessert:u1,Discount:o1,Dish:n1,DishDot:s1,Document:H1,DocumentAdd:i1,DocumentChecked:w1,DocumentCopy:g1,DocumentDelete:x1,DocumentRemove:M1,Download:y1,Drizzling:L1,Edit:S1,EditPen:b1,Eleme:P1,ElemeFilled:F1,ElementPlus:T1,Expand:G1,Failed:U1,Female:E1,Files:Z1,Film:Q1,Filter:J1,Finished:Y1,FirstAidKit:e4,Flag:t4,Fold:r4,Folder:w4,FolderAdd:u4,FolderChecked:o4,FolderDelete:s4,FolderOpened:n4,FolderRemove:i4,Food:g4,Football:x4,ForkSpoon:M4,Fries:H4,FullScreen:y4,Goblet:F4,GobletFull:L4,GobletSquare:S4,GobletSquareFull:b4,GoldMedal:P4,Goods:G4,GoodsFilled:T4,Grape:U4,Grid:E4,Guide:Z4,Handbag:Q4,Headset:J4,Help:e6,HelpFilled:Y4,Hide:t6,Histogram:r6,HomeFilled:u6,HotWater:o6,House:s6,IceCream:w6,IceCreamRound:n6,IceCreamSquare:i6,IceDrink:g6,IceTea:x6,InfoFilled:M6,Iphone:H6,Key:y6,KnifeFork:L6,Lightning:b6,Link:S6,List:F6,Loading:P6,Location:U6,LocationFilled:T6,LocationInformation:G6,Lock:E6,Lollipop:Z6,MagicStick:Q6,Magnet:J6,Male:Y6,Management:e3,MapLocation:t3,Medal:r3,Memo:u3,Menu:o3,Message:n3,MessageBox:s3,Mic:i3,Microphone:w3,MilkTea:g3,Minus:x3,Money:M3,Monitor:H3,Moon:L3,MoonNight:y3,More:S3,MoreFilled:b3,MostlyCloudy:F3,Mouse:P3,Mug:T3,Mute:U3,MuteNotification:G3,NoSmoking:E3,Notebook:Z3,Notification:Q3,Odometer:J3,OfficeBuilding:Y3,Open:e8,Operation:t8,Opportunity:r8,Orange:u8,Paperclip:o8,PartlyCloudy:s8,Pear:n8,Phone:w8,PhoneFilled:i8,Picture:M8,PictureFilled:g8,PictureRounded:x8,PieChart:H8,Place:y8,Platform:L8,Plus:b8,Pointer:S8,Position:F8,Postcard:P8,Pouring:T8,Present:G8,PriceTag:U8,Printer:E8,Promotion:Z8,QuartzWatch:Q8,QuestionFilled:J8,Rank:Y8,Reading:te,ReadingLamp:ee,Refresh:oe,RefreshLeft:re,RefreshRight:ue,Refrigerator:se,Remove:ie,RemoveFilled:ne,Right:we,ScaleToOriginal:ge,School:xe,Scissor:Me,Search:He,Select:ye,Sell:Le,SemiSelect:be,Service:Se,SetUp:Fe,Setting:Pe,Share:Te,Ship:Ge,Shop:Ue,ShoppingBag:Ee,ShoppingCart:Qe,ShoppingCartFull:Ze,ShoppingTrolley:Je,Smoking:Ye,Soccer:ea,SoldOut:ta,Sort:oa,SortDown:ra,SortUp:ua,Stamp:sa,Star:ia,StarFilled:na,Stopwatch:wa,SuccessFilled:ga,Sugar:xa,Suitcase:Ha,SuitcaseLine:Ma,Sunny:ya,Sunrise:La,Sunset:ba,Switch:Pa,SwitchButton:Sa,SwitchFilled:Fa,TakeawayBox:Ta,Ticket:Ga,Tickets:Ua,Timer:Ea,ToiletPaper:Za,Tools:Qa,Top:et,TopLeft:Ja,TopRight:Ya,TrendCharts:tt,Trophy:ut,TrophyBase:rt,TurnOff:ot,Umbrella:st,Unlock:nt,Upload:wt,UploadFilled:it,User:xt,UserFilled:gt,Van:Mt,VideoCamera:yt,VideoCameraFilled:Ht,VideoPause:Lt,VideoPlay:bt,View:St,Wallet:Pt,WalletFilled:Ft,WarnTriangleFilled:Tt,Warning:Ut,WarningFilled:Gt,Watch:Et,Watermelon:Zt,WindPower:Qt,ZoomIn:Jt,ZoomOut:Yt},Symbol.toStringTag,{value:"Module"}));export{S3 as A,F as B,b3 as C,T2 as D,x2 as E,H1 as F,r1 as G,e_ as H,He as I,Pe as J,L as K,w as L,J2 as a,s0 as b,e0 as c,Y2 as d,y as e,g as f,u0 as g,t6 as h,M6 as i,s2 as j,b as k,P6 as l,U0 as m,M as n,E0 as o,ge as p,y4 as q,Jt as r,ga as s,re as t,ue as u,St as v,Gt as w,x3 as x,b8 as y,Yt as z}; diff --git a/server/public/admin/assets/@floating-ui-l0sNRNKZ.js b/server/public/admin/assets/@floating-ui-l0sNRNKZ.js new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/server/public/admin/assets/@floating-ui-l0sNRNKZ.js @@ -0,0 +1 @@ + diff --git a/server/public/admin/assets/@highlightjs-DcY7_PRf.js b/server/public/admin/assets/@highlightjs-DcY7_PRf.js new file mode 100644 index 0000000..fabcf9a --- /dev/null +++ b/server/public/admin/assets/@highlightjs-DcY7_PRf.js @@ -0,0 +1 @@ +import{H as u}from"./highlight.js--xG_hASo.js";import{d as c,h as g,r as d,w as h,b as n}from"./@vue-DjVelRet.js";var i=c({props:{code:{type:String,required:!0},language:{type:String,default:""},autodetect:{type:Boolean,default:!0},ignoreIllegals:{type:Boolean,default:!0}},setup:function(e){var t=d(e.language);h(function(){return e.language},function(a){t.value=a});var r=n(function(){return e.autodetect||!t.value}),o=n(function(){return!r.value&&!u.getLanguage(t.value)});return{className:n(function(){return o.value?"":"hljs "+t.value}),highlightedCode:n(function(){var a;if(o.value)return console.warn('The language "'+t.value+'" you specified could not be found.'),e.code.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'");if(r.value){var l=u.highlightAuto(e.code);return t.value=(a=l.language)!==null&&a!==void 0?a:"",l.value}return(l=u.highlight(e.code,{language:t.value,ignoreIllegals:e.ignoreIllegals})).value})}},render:function(){return g("pre",{},[g("code",{class:this.className,innerHTML:this.highlightedCode})])}}),f={install:function(e){e.component("highlightjs",i)},component:i};export{f as o}; diff --git a/server/public/admin/assets/@popperjs-D_chPuIy.js b/server/public/admin/assets/@popperjs-D_chPuIy.js new file mode 100644 index 0000000..cca52d1 --- /dev/null +++ b/server/public/admin/assets/@popperjs-D_chPuIy.js @@ -0,0 +1 @@ +var W="top",L="bottom",S="right",P="left",je="auto",fe=[W,L,S,P],Q="start",ae="end",mt="clippingParents",Ze="viewport",re="popper",ht="reference",Ve=fe.reduce(function(t,e){return t.concat([e+"-"+Q,e+"-"+ae])},[]),Ye=[].concat(fe,[je]).reduce(function(t,e){return t.concat([e,e+"-"+Q,e+"-"+ae])},[]),vt="beforeRead",gt="read",yt="afterRead",bt="beforeMain",xt="main",wt="afterMain",Ot="beforeWrite",jt="write",Et="afterWrite",At=[vt,gt,yt,bt,xt,wt,Ot,jt,Et];function T(t){return t?(t.nodeName||"").toLowerCase():null}function q(t){if(t==null)return window;if(t.toString()!=="[object Window]"){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function Z(t){var e=q(t).Element;return t instanceof e||t instanceof Element}function R(t){var e=q(t).HTMLElement;return t instanceof e||t instanceof HTMLElement}function Ee(t){if(typeof ShadowRoot>"u")return!1;var e=q(t).ShadowRoot;return t instanceof e||t instanceof ShadowRoot}function Dt(t){var e=t.state;Object.keys(e.elements).forEach(function(n){var r=e.styles[n]||{},o=e.attributes[n]||{},i=e.elements[n];!R(i)||!T(i)||(Object.assign(i.style,r),Object.keys(o).forEach(function(a){var s=o[a];s===!1?i.removeAttribute(a):i.setAttribute(a,s===!0?"":s)}))})}function kt(t){var e=t.state,n={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,n.popper),e.styles=n,e.elements.arrow&&Object.assign(e.elements.arrow.style,n.arrow),function(){Object.keys(e.elements).forEach(function(r){var o=e.elements[r],i=e.attributes[r]||{},a=Object.keys(e.styles.hasOwnProperty(r)?e.styles[r]:n[r]),s=a.reduce(function(f,c){return f[c]="",f},{});!R(o)||!T(o)||(Object.assign(o.style,s),Object.keys(i).forEach(function(f){o.removeAttribute(f)}))})}}var $e={name:"applyStyles",enabled:!0,phase:"write",fn:Dt,effect:kt,requires:["computeStyles"]};function N(t){return t.split("-")[0]}var J=Math.max,ge=Math.min,Y=Math.round;function $(t,e){e===void 0&&(e=!1);var n=t.getBoundingClientRect(),r=1,o=1;if(R(t)&&e){var i=t.offsetHeight,a=t.offsetWidth;a>0&&(r=Y(n.width)/a||1),i>0&&(o=Y(n.height)/i||1)}return{width:n.width/r,height:n.height/o,top:n.top/o,right:n.right/r,bottom:n.bottom/o,left:n.left/r,x:n.left/r,y:n.top/o}}function Ae(t){var e=$(t),n=t.offsetWidth,r=t.offsetHeight;return Math.abs(e.width-n)<=1&&(n=e.width),Math.abs(e.height-r)<=1&&(r=e.height),{x:t.offsetLeft,y:t.offsetTop,width:n,height:r}}function _e(t,e){var n=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(n&&Ee(n)){var r=e;do{if(r&&t.isSameNode(r))return!0;r=r.parentNode||r.host}while(r)}return!1}function I(t){return q(t).getComputedStyle(t)}function Wt(t){return["table","td","th"].indexOf(T(t))>=0}function U(t){return((Z(t)?t.ownerDocument:t.document)||window.document).documentElement}function ye(t){return T(t)==="html"?t:t.assignedSlot||t.parentNode||(Ee(t)?t.host:null)||U(t)}function Ie(t){return!R(t)||I(t).position==="fixed"?null:t.offsetParent}function Pt(t){var e=navigator.userAgent.toLowerCase().indexOf("firefox")!==-1,n=navigator.userAgent.indexOf("Trident")!==-1;if(n&&R(t)){var r=I(t);if(r.position==="fixed")return null}var o=ye(t);for(Ee(o)&&(o=o.host);R(o)&&["html","body"].indexOf(T(o))<0;){var i=I(o);if(i.transform!=="none"||i.perspective!=="none"||i.contain==="paint"||["transform","perspective"].indexOf(i.willChange)!==-1||e&&i.willChange==="filter"||e&&i.filter&&i.filter!=="none")return o;o=o.parentNode}return null}function ce(t){for(var e=q(t),n=Ie(t);n&&Wt(n)&&I(n).position==="static";)n=Ie(n);return n&&(T(n)==="html"||T(n)==="body"&&I(n).position==="static")?e:n||Pt(t)||e}function De(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function oe(t,e,n){return J(t,ge(e,n))}function Bt(t,e,n){var r=oe(t,e,n);return r>n?n:r}function Fe(){return{top:0,right:0,bottom:0,left:0}}function et(t){return Object.assign({},Fe(),t)}function tt(t,e){return e.reduce(function(n,r){return n[r]=t,n},{})}var Ht=function(t,e){return t=typeof t=="function"?t(Object.assign({},e.rects,{placement:e.placement})):t,et(typeof t!="number"?t:tt(t,fe))};function Rt(t){var e,n=t.state,r=t.name,o=t.options,i=n.elements.arrow,a=n.modifiersData.popperOffsets,s=N(n.placement),f=De(s),c=[P,S].indexOf(s)>=0,p=c?"height":"width";if(!(!i||!a)){var h=Ht(o.padding,n),v=Ae(i),l=f==="y"?W:P,m=f==="y"?L:S,u=n.rects.reference[p]+n.rects.reference[f]-a[f]-n.rects.popper[p],g=a[f]-n.rects.reference[f],w=ce(i),y=w?f==="y"?w.clientHeight||0:w.clientWidth||0:0,j=u/2-g/2,d=h[l],b=y-v[p]-h[m],x=y/2-v[p]/2+j,O=oe(d,x,b),E=f;n.modifiersData[r]=(e={},e[E]=O,e.centerOffset=O-x,e)}}function Lt(t){var e=t.state,n=t.options,r=n.element,o=r===void 0?"[data-popper-arrow]":r;o!=null&&(typeof o=="string"&&(o=e.elements.popper.querySelector(o),!o)||!_e(e.elements.popper,o)||(e.elements.arrow=o))}var St={name:"arrow",enabled:!0,phase:"main",fn:Rt,effect:Lt,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function _(t){return t.split("-")[1]}var Ct={top:"auto",right:"auto",bottom:"auto",left:"auto"};function Mt(t){var e=t.x,n=t.y,r=window,o=r.devicePixelRatio||1;return{x:Y(e*o)/o||0,y:Y(n*o)/o||0}}function Ue(t){var e,n=t.popper,r=t.popperRect,o=t.placement,i=t.variation,a=t.offsets,s=t.position,f=t.gpuAcceleration,c=t.adaptive,p=t.roundOffsets,h=t.isFixed,v=a.x,l=v===void 0?0:v,m=a.y,u=m===void 0?0:m,g=typeof p=="function"?p({x:l,y:u}):{x:l,y:u};l=g.x,u=g.y;var w=a.hasOwnProperty("x"),y=a.hasOwnProperty("y"),j=P,d=W,b=window;if(c){var x=ce(n),O="clientHeight",E="clientWidth";if(x===q(n)&&(x=U(n),I(x).position!=="static"&&s==="absolute"&&(O="scrollHeight",E="scrollWidth")),x=x,o===W||(o===P||o===S)&&i===ae){d=L;var D=h&&x===b&&b.visualViewport?b.visualViewport.height:x[O];u-=D-r.height,u*=f?1:-1}if(o===P||(o===W||o===L)&&i===ae){j=S;var k=h&&x===b&&b.visualViewport?b.visualViewport.width:x[E];l-=k-r.width,l*=f?1:-1}}var A=Object.assign({position:s},c&&Ct),C=p===!0?Mt({x:l,y:u}):{x:l,y:u};if(l=C.x,u=C.y,f){var B;return Object.assign({},A,(B={},B[d]=y?"0":"",B[j]=w?"0":"",B.transform=(b.devicePixelRatio||1)<=1?"translate("+l+"px, "+u+"px)":"translate3d("+l+"px, "+u+"px, 0)",B))}return Object.assign({},A,(e={},e[d]=y?u+"px":"",e[j]=w?l+"px":"",e.transform="",e))}function qt(t){var e=t.state,n=t.options,r=n.gpuAcceleration,o=r===void 0?!0:r,i=n.adaptive,a=i===void 0?!0:i,s=n.roundOffsets,f=s===void 0?!0:s,c={placement:N(e.placement),variation:_(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:o,isFixed:e.options.strategy==="fixed"};e.modifiersData.popperOffsets!=null&&(e.styles.popper=Object.assign({},e.styles.popper,Ue(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:a,roundOffsets:f})))),e.modifiersData.arrow!=null&&(e.styles.arrow=Object.assign({},e.styles.arrow,Ue(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:f})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})}var nt={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:qt,data:{}},he={passive:!0};function Nt(t){var e=t.state,n=t.instance,r=t.options,o=r.scroll,i=o===void 0?!0:o,a=r.resize,s=a===void 0?!0:a,f=q(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return i&&c.forEach(function(p){p.addEventListener("scroll",n.update,he)}),s&&f.addEventListener("resize",n.update,he),function(){i&&c.forEach(function(p){p.removeEventListener("scroll",n.update,he)}),s&&f.removeEventListener("resize",n.update,he)}}var rt={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:Nt,data:{}},Tt={left:"right",right:"left",bottom:"top",top:"bottom"};function ve(t){return t.replace(/left|right|bottom|top/g,function(e){return Tt[e]})}var Vt={start:"end",end:"start"};function Xe(t){return t.replace(/start|end/g,function(e){return Vt[e]})}function ke(t){var e=q(t),n=e.pageXOffset,r=e.pageYOffset;return{scrollLeft:n,scrollTop:r}}function We(t){return $(U(t)).left+ke(t).scrollLeft}function It(t){var e=q(t),n=U(t),r=e.visualViewport,o=n.clientWidth,i=n.clientHeight,a=0,s=0;return r&&(o=r.width,i=r.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(a=r.offsetLeft,s=r.offsetTop)),{width:o,height:i,x:a+We(t),y:s}}function Ut(t){var e,n=U(t),r=ke(t),o=(e=t.ownerDocument)==null?void 0:e.body,i=J(n.scrollWidth,n.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),a=J(n.scrollHeight,n.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),s=-r.scrollLeft+We(t),f=-r.scrollTop;return I(o||n).direction==="rtl"&&(s+=J(n.clientWidth,o?o.clientWidth:0)-i),{width:i,height:a,x:s,y:f}}function Pe(t){var e=I(t),n=e.overflow,r=e.overflowX,o=e.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+r)}function ot(t){return["html","body","#document"].indexOf(T(t))>=0?t.ownerDocument.body:R(t)&&Pe(t)?t:ot(ye(t))}function ie(t,e){var n;e===void 0&&(e=[]);var r=ot(t),o=r===((n=t.ownerDocument)==null?void 0:n.body),i=q(r),a=o?[i].concat(i.visualViewport||[],Pe(r)?r:[]):r,s=e.concat(a);return o?s:s.concat(ie(ye(a)))}function Oe(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Xt(t){var e=$(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}function ze(t,e){return e===Ze?Oe(It(t)):Z(e)?Xt(e):Oe(Ut(U(t)))}function zt(t){var e=ie(ye(t)),n=["absolute","fixed"].indexOf(I(t).position)>=0,r=n&&R(t)?ce(t):t;return Z(r)?e.filter(function(o){return Z(o)&&_e(o,r)&&T(o)!=="body"}):[]}function Gt(t,e,n){var r=e==="clippingParents"?zt(t):[].concat(e),o=[].concat(r,[n]),i=o[0],a=o.reduce(function(s,f){var c=ze(t,f);return s.top=J(c.top,s.top),s.right=ge(c.right,s.right),s.bottom=ge(c.bottom,s.bottom),s.left=J(c.left,s.left),s},ze(t,i));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}function it(t){var e=t.reference,n=t.element,r=t.placement,o=r?N(r):null,i=r?_(r):null,a=e.x+e.width/2-n.width/2,s=e.y+e.height/2-n.height/2,f;switch(o){case W:f={x:a,y:e.y-n.height};break;case L:f={x:a,y:e.y+e.height};break;case S:f={x:e.x+e.width,y:s};break;case P:f={x:e.x-n.width,y:s};break;default:f={x:e.x,y:e.y}}var c=o?De(o):null;if(c!=null){var p=c==="y"?"height":"width";switch(i){case Q:f[c]=f[c]-(e[p]/2-n[p]/2);break;case ae:f[c]=f[c]+(e[p]/2-n[p]/2);break}}return f}function se(t,e){e===void 0&&(e={});var n=e,r=n.placement,o=r===void 0?t.placement:r,i=n.boundary,a=i===void 0?mt:i,s=n.rootBoundary,f=s===void 0?Ze:s,c=n.elementContext,p=c===void 0?re:c,h=n.altBoundary,v=h===void 0?!1:h,l=n.padding,m=l===void 0?0:l,u=et(typeof m!="number"?m:tt(m,fe)),g=p===re?ht:re,w=t.rects.popper,y=t.elements[v?g:p],j=Gt(Z(y)?y:y.contextElement||U(t.elements.popper),a,f),d=$(t.elements.reference),b=it({reference:d,element:w,placement:o}),x=Oe(Object.assign({},w,b)),O=p===re?x:d,E={top:j.top-O.top+u.top,bottom:O.bottom-j.bottom+u.bottom,left:j.left-O.left+u.left,right:O.right-j.right+u.right},D=t.modifiersData.offset;if(p===re&&D){var k=D[o];Object.keys(E).forEach(function(A){var C=[S,L].indexOf(A)>=0?1:-1,B=[W,L].indexOf(A)>=0?"y":"x";E[A]+=k[B]*C})}return E}function Jt(t,e){e===void 0&&(e={});var n=e,r=n.placement,o=n.boundary,i=n.rootBoundary,a=n.padding,s=n.flipVariations,f=n.allowedAutoPlacements,c=f===void 0?Ye:f,p=_(r),h=p?s?Ve:Ve.filter(function(m){return _(m)===p}):fe,v=h.filter(function(m){return c.indexOf(m)>=0});v.length===0&&(v=h);var l=v.reduce(function(m,u){return m[u]=se(t,{placement:u,boundary:o,rootBoundary:i,padding:a})[N(u)],m},{});return Object.keys(l).sort(function(m,u){return l[m]-l[u]})}function Kt(t){if(N(t)===je)return[];var e=ve(t);return[Xe(t),e,Xe(e)]}function Qt(t){var e=t.state,n=t.options,r=t.name;if(!e.modifiersData[r]._skip){for(var o=n.mainAxis,i=o===void 0?!0:o,a=n.altAxis,s=a===void 0?!0:a,f=n.fallbackPlacements,c=n.padding,p=n.boundary,h=n.rootBoundary,v=n.altBoundary,l=n.flipVariations,m=l===void 0?!0:l,u=n.allowedAutoPlacements,g=e.options.placement,w=N(g),y=w===g,j=f||(y||!m?[ve(g)]:Kt(g)),d=[g].concat(j).reduce(function(z,V){return z.concat(N(V)===je?Jt(e,{placement:V,boundary:p,rootBoundary:h,padding:c,flipVariations:m,allowedAutoPlacements:u}):V)},[]),b=e.rects.reference,x=e.rects.popper,O=new Map,E=!0,D=d[0],k=0;k=0,ee=F?"width":"height",H=se(e,{placement:A,boundary:p,rootBoundary:h,altBoundary:v,padding:c}),M=F?B?S:P:B?L:W;b[ee]>x[ee]&&(M=ve(M));var ue=ve(M),X=[];if(i&&X.push(H[C]<=0),s&&X.push(H[M]<=0,H[ue]<=0),X.every(function(z){return z})){D=A,E=!1;break}O.set(A,X)}if(E)for(var pe=m?3:1,be=function(z){var V=d.find(function(de){var ne=O.get(de);if(ne)return ne.slice(0,z).every(function(K){return K})});if(V)return D=V,"break"},te=pe;te>0;te--){var le=be(te);if(le==="break")break}e.placement!==D&&(e.modifiersData[r]._skip=!0,e.placement=D,e.reset=!0)}}var Zt={name:"flip",enabled:!0,phase:"main",fn:Qt,requiresIfExists:["offset"],data:{_skip:!1}};function Ge(t,e,n){return n===void 0&&(n={x:0,y:0}),{top:t.top-e.height-n.y,right:t.right-e.width+n.x,bottom:t.bottom-e.height+n.y,left:t.left-e.width-n.x}}function Je(t){return[W,S,L,P].some(function(e){return t[e]>=0})}function Yt(t){var e=t.state,n=t.name,r=e.rects.reference,o=e.rects.popper,i=e.modifiersData.preventOverflow,a=se(e,{elementContext:"reference"}),s=se(e,{altBoundary:!0}),f=Ge(a,r),c=Ge(s,o,i),p=Je(f),h=Je(c);e.modifiersData[n]={referenceClippingOffsets:f,popperEscapeOffsets:c,isReferenceHidden:p,hasPopperEscaped:h},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":p,"data-popper-escaped":h})}var $t={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:Yt};function _t(t,e,n){var r=N(t),o=[P,W].indexOf(r)>=0?-1:1,i=typeof n=="function"?n(Object.assign({},e,{placement:t})):n,a=i[0],s=i[1];return a=a||0,s=(s||0)*o,[P,S].indexOf(r)>=0?{x:s,y:a}:{x:a,y:s}}function Ft(t){var e=t.state,n=t.options,r=t.name,o=n.offset,i=o===void 0?[0,0]:o,a=Ye.reduce(function(p,h){return p[h]=_t(h,e.rects,i),p},{}),s=a[e.placement],f=s.x,c=s.y;e.modifiersData.popperOffsets!=null&&(e.modifiersData.popperOffsets.x+=f,e.modifiersData.popperOffsets.y+=c),e.modifiersData[r]=a}var en={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Ft};function tn(t){var e=t.state,n=t.name;e.modifiersData[n]=it({reference:e.rects.reference,element:e.rects.popper,placement:e.placement})}var at={name:"popperOffsets",enabled:!0,phase:"read",fn:tn,data:{}};function nn(t){return t==="x"?"y":"x"}function rn(t){var e=t.state,n=t.options,r=t.name,o=n.mainAxis,i=o===void 0?!0:o,a=n.altAxis,s=a===void 0?!1:a,f=n.boundary,c=n.rootBoundary,p=n.altBoundary,h=n.padding,v=n.tether,l=v===void 0?!0:v,m=n.tetherOffset,u=m===void 0?0:m,g=se(e,{boundary:f,rootBoundary:c,padding:h,altBoundary:p}),w=N(e.placement),y=_(e.placement),j=!y,d=De(w),b=nn(d),x=e.modifiersData.popperOffsets,O=e.rects.reference,E=e.rects.popper,D=typeof u=="function"?u(Object.assign({},e.rects,{placement:e.placement})):u,k=typeof D=="number"?{mainAxis:D,altAxis:D}:Object.assign({mainAxis:0,altAxis:0},D),A=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,C={x:0,y:0};if(x){if(i){var B,F=d==="y"?W:P,ee=d==="y"?L:S,H=d==="y"?"height":"width",M=x[d],ue=M+g[F],X=M-g[ee],pe=l?-E[H]/2:0,be=y===Q?O[H]:E[H],te=y===Q?-E[H]:-O[H],le=e.elements.arrow,z=l&&le?Ae(le):{width:0,height:0},V=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:Fe(),de=V[F],ne=V[ee],K=oe(0,O[H],z[H]),st=j?O[H]/2-pe-K-de-k.mainAxis:be-K-de-k.mainAxis,ft=j?-O[H]/2+pe+K+ne+k.mainAxis:te+K+ne+k.mainAxis,xe=e.elements.arrow&&ce(e.elements.arrow),ct=xe?d==="y"?xe.clientTop||0:xe.clientLeft||0:0,He=(B=A==null?void 0:A[d])!=null?B:0,ut=M+st-He-ct,pt=M+ft-He,Re=oe(l?ge(ue,ut):ue,M,l?J(X,pt):X);x[d]=Re,C[d]=Re-M}if(s){var Le,lt=d==="x"?W:P,dt=d==="x"?L:S,G=x[b],me=b==="y"?"height":"width",Se=G+g[lt],Ce=G-g[dt],we=[W,P].indexOf(w)!==-1,Me=(Le=A==null?void 0:A[b])!=null?Le:0,qe=we?Se:G-O[me]-E[me]-Me+k.altAxis,Ne=we?G+O[me]+E[me]-Me-k.altAxis:Ce,Te=l&&we?Bt(qe,G,Ne):oe(l?qe:Se,G,l?Ne:Ce);x[b]=Te,C[b]=Te-G}e.modifiersData[r]=C}}var on={name:"preventOverflow",enabled:!0,phase:"main",fn:rn,requiresIfExists:["offset"]};function an(t){return{scrollLeft:t.scrollLeft,scrollTop:t.scrollTop}}function sn(t){return t===q(t)||!R(t)?ke(t):an(t)}function fn(t){var e=t.getBoundingClientRect(),n=Y(e.width)/t.offsetWidth||1,r=Y(e.height)/t.offsetHeight||1;return n!==1||r!==1}function cn(t,e,n){n===void 0&&(n=!1);var r=R(e),o=R(e)&&fn(e),i=U(e),a=$(t,o),s={scrollLeft:0,scrollTop:0},f={x:0,y:0};return(r||!r&&!n)&&((T(e)!=="body"||Pe(i))&&(s=sn(e)),R(e)?(f=$(e,!0),f.x+=e.clientLeft,f.y+=e.clientTop):i&&(f.x=We(i))),{x:a.left+s.scrollLeft-f.x,y:a.top+s.scrollTop-f.y,width:a.width,height:a.height}}function un(t){var e=new Map,n=new Set,r=[];t.forEach(function(i){e.set(i.name,i)});function o(i){n.add(i.name);var a=[].concat(i.requires||[],i.requiresIfExists||[]);a.forEach(function(s){if(!n.has(s)){var f=e.get(s);f&&o(f)}}),r.push(i)}return t.forEach(function(i){n.has(i.name)||o(i)}),r}function pn(t){var e=un(t);return At.reduce(function(n,r){return n.concat(e.filter(function(o){return o.phase===r}))},[])}function ln(t){var e;return function(){return e||(e=new Promise(function(n){Promise.resolve().then(function(){e=void 0,n(t())})})),e}}function dn(t){var e=t.reduce(function(n,r){var o=n[r.name];return n[r.name]=o?Object.assign({},o,r,{options:Object.assign({},o.options,r.options),data:Object.assign({},o.data,r.data)}):r,n},{});return Object.keys(e).map(function(n){return e[n]})}var Ke={placement:"bottom",modifiers:[],strategy:"absolute"};function Qe(){for(var t=arguments.length,e=new Array(t),n=0;nn in t}const Z={},sn=[],Ae=()=>{},An=()=>!1,qt=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Ri=e=>e.startsWith("onUpdate:"),ee=Object.assign,Mi=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},ku=Object.prototype.hasOwnProperty,ie=(e,t)=>ku.call(e,t),H=Array.isArray,rn=e=>En(e)==="[object Map]",Yt=e=>En(e)==="[object Set]",Pr=e=>En(e)==="[object Date]",_c=e=>En(e)==="[object RegExp]",G=e=>typeof e=="function",Y=e=>typeof e=="string",Xe=e=>typeof e=="symbol",oe=e=>e!==null&&typeof e=="object",Pi=e=>(oe(e)||G(e))&&G(e.then)&&G(e.catch),io=Object.prototype.toString,En=e=>io.call(e),Ec=e=>En(e).slice(8,-1),As=e=>En(e)==="[object Object]",Li=e=>Y(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,vt=Ie(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),yc=Ie("bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo"),xi=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},Fu=/-(\w)/g,fe=xi(e=>e.replace(Fu,(t,n)=>n?n.toUpperCase():"")),Vu=/\B([A-Z])/g,De=xi(e=>e.replace(Vu,"-$1").toLowerCase()),Jt=xi(e=>e.charAt(0).toUpperCase()+e.slice(1)),on=xi(e=>e?`on${Jt(e)}`:""),Le=(e,t)=>!Object.is(e,t),ln=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},cs=e=>{const t=parseFloat(e);return isNaN(t)?e:t},as=e=>{const t=Y(e)?Number(e):NaN;return isNaN(t)?e:t};let dl;const Is=()=>dl||(dl=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{}),Uu=/^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/;function Hu(e){return Uu.test(e)?`__props.${e}`:`__props[${JSON.stringify(e)}]`}function Bu(e,t){return e+JSON.stringify(t,(n,s)=>typeof s=="function"?s.toString():s)}const $u={TEXT:1,1:"TEXT",CLASS:2,2:"CLASS",STYLE:4,4:"STYLE",PROPS:8,8:"PROPS",FULL_PROPS:16,16:"FULL_PROPS",NEED_HYDRATION:32,32:"NEED_HYDRATION",STABLE_FRAGMENT:64,64:"STABLE_FRAGMENT",KEYED_FRAGMENT:128,128:"KEYED_FRAGMENT",UNKEYED_FRAGMENT:256,256:"UNKEYED_FRAGMENT",NEED_PATCH:512,512:"NEED_PATCH",DYNAMIC_SLOTS:1024,1024:"DYNAMIC_SLOTS",DEV_ROOT_FRAGMENT:2048,2048:"DEV_ROOT_FRAGMENT",CACHED:-1,"-1":"CACHED",BAIL:-2,"-2":"BAIL"},Xu={1:"TEXT",2:"CLASS",4:"STYLE",8:"PROPS",16:"FULL_PROPS",32:"NEED_HYDRATION",64:"STABLE_FRAGMENT",128:"KEYED_FRAGMENT",256:"UNKEYED_FRAGMENT",512:"NEED_PATCH",1024:"DYNAMIC_SLOTS",2048:"DEV_ROOT_FRAGMENT",[-1]:"HOISTED",[-2]:"BAIL"},ju={ELEMENT:1,1:"ELEMENT",FUNCTIONAL_COMPONENT:2,2:"FUNCTIONAL_COMPONENT",STATEFUL_COMPONENT:4,4:"STATEFUL_COMPONENT",TEXT_CHILDREN:8,8:"TEXT_CHILDREN",ARRAY_CHILDREN:16,16:"ARRAY_CHILDREN",SLOTS_CHILDREN:32,32:"SLOTS_CHILDREN",TELEPORT:64,64:"TELEPORT",SUSPENSE:128,128:"SUSPENSE",COMPONENT_SHOULD_KEEP_ALIVE:256,256:"COMPONENT_SHOULD_KEEP_ALIVE",COMPONENT_KEPT_ALIVE:512,512:"COMPONENT_KEPT_ALIVE",COMPONENT:6,6:"COMPONENT"},Ku={STABLE:1,1:"STABLE",DYNAMIC:2,2:"DYNAMIC",FORWARDED:3,3:"FORWARDED"},Gu={1:"STABLE",2:"DYNAMIC",3:"FORWARDED"},Wu="Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol",oo=Ie(Wu),qu=oo,gl=2;function Tc(e,t=0,n=e.length){if(t=Math.max(0,Math.min(t,e.length)),n=Math.max(0,Math.min(n,e.length)),t>n)return"";let s=e.split(/(\r?\n)/);const i=s.filter((l,c)=>c%2===1);s=s.filter((l,c)=>c%2===0);let r=0;const o=[];for(let l=0;l=t){for(let c=l-gl;c<=l+gl||n>r;c++){if(c<0||c>=s.length)continue;const f=c+1;o.push(`${f}${" ".repeat(Math.max(3-String(f).length,0))}| ${s[c]}`);const a=s[c].length,u=i[c]&&i[c].length||0;if(c===l){const p=t-(r-(a+u)),d=Math.max(1,n>r?a-p:n-t);o.push(" | "+" ".repeat(p)+"^".repeat(d))}else if(c>l){if(n>r){const p=Math.max(Math.min(n-r,a),1);o.push(" | "+"^".repeat(p))}r+=a+u}}break}return o.join(` +`)}function $n(e){if(H(e)){const t={};for(let n=0;n{if(n){const s=n.split(Ju);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function Qu(e){if(!e)return"";if(Y(e))return e;let t="";for(const n in e){const s=e[n];if(Y(s)||typeof s=="number"){const i=n.startsWith("--")?n:De(n);t+=`${i}:${s};`}}return t}function Xn(e){let t="";if(Y(e))t=e;else if(H(e))for(let n=0;n/="'\u0009\u000a\u000c\u0020]/,_r={};function rh(e){if(_r.hasOwnProperty(e))return _r[e];const t=ih.test(e);return t&&console.error(`unsafe attribute name: ${e}`),_r[e]=!t}const oh={acceptCharset:"accept-charset",className:"class",htmlFor:"for",httpEquiv:"http-equiv"},lh=Ie("accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap"),ch=Ie("xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,color-interpolation-filters,color-profile,color-rendering,contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,overflow,overline-position,overline-thickness,panose-1,paint-order,path,pathLength,patternContentUnits,patternTransform,patternUnits,ping,pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,specularConstant,specularExponent,speed,spreadMethod,startOffset,stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,string,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,text-decoration,text-rendering,textLength,to,transform,transform-origin,type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan"),ah=Ie("accent,accentunder,actiontype,align,alignmentscope,altimg,altimg-height,altimg-valign,altimg-width,alttext,bevelled,close,columnsalign,columnlines,columnspan,denomalign,depth,dir,display,displaystyle,encoding,equalcolumns,equalrows,fence,fontstyle,fontweight,form,frame,framespacing,groupalign,height,href,id,indentalign,indentalignfirst,indentalignlast,indentshift,indentshiftfirst,indentshiftlast,indextype,justify,largetop,largeop,lquote,lspace,mathbackground,mathcolor,mathsize,mathvariant,maxsize,minlabelspacing,mode,other,overflow,position,rowalign,rowlines,rowspan,rquote,rspace,scriptlevel,scriptminsize,scriptsizemultiplier,selection,separator,separators,shift,side,src,stackalign,stretchy,subscriptshift,superscriptshift,symmetric,voffset,width,widths,xlink:href,xlink:show,xlink:type,xmlns");function fh(e){if(e==null)return!1;const t=typeof e;return t==="string"||t==="number"||t==="boolean"}const uh=/["'&<>]/;function hh(e){const t=""+e,n=uh.exec(t);if(!n)return t;let s="",i,r,o=0;for(r=n.index;r||--!>|?@[\\\]^`{|}~]/g;function gh(e,t){return e.replace(Oc,n=>t?n==='"'?'\\\\\\"':`\\\\${n}`:`\\${n}`)}function mh(e,t){if(e.length!==t.length)return!1;let n=!0;for(let s=0;n&&sOt(n,t))}const Rc=e=>!!(e&&e.__v_isRef===!0),ao=e=>Y(e)?e:e==null?"":H(e)||oe(e)&&(e.toString===io||!G(e.toString))?Rc(e)?ao(e.value):JSON.stringify(e,Mc,2):String(e),Mc=(e,t)=>Rc(t)?Mc(e,t.value):rn(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,i],r)=>(n[Er(s,r)+" =>"]=i,n),{})}:Yt(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>Er(n))}:Xe(t)?Er(t):oe(t)&&!H(t)&&!As(t)?String(t):t,Er=(e,t="")=>{var n;return Xe(e)?`Symbol(${(n=e.description)!=null?n:t})`:e},_h=Object.freeze(Object.defineProperty({__proto__:null,EMPTY_ARR:sn,EMPTY_OBJ:Z,NO:An,NOOP:Ae,PatchFlagNames:Xu,PatchFlags:$u,ShapeFlags:ju,SlotFlags:Ku,camelize:fe,capitalize:Jt,cssVarNameEscapeSymbolsRE:Oc,def:ro,escapeHtml:hh,escapeHtmlComment:dh,extend:ee,genCacheKey:Bu,genPropsAccessExp:Hu,generateCodeFrame:Tc,getEscapedCssVarName:gh,getGlobalThis:Is,hasChanged:Le,hasOwn:ie,hyphenate:De,includeBooleanAttr:co,invokeArrayFns:ln,isArray:H,isBooleanAttr:sh,isBuiltInDirective:yc,isDate:Pr,isFunction:G,isGloballyAllowed:oo,isGloballyWhitelisted:qu,isHTMLTag:bc,isIntegerKey:Li,isKnownHtmlAttr:lh,isKnownMathMLAttr:ah,isKnownSvgAttr:ch,isMap:rn,isMathMLTag:Cc,isModelListener:Ri,isObject:oe,isOn:qt,isPlainObject:As,isPromise:Pi,isRegExp:_c,isRenderableAttrValue:fh,isReservedProp:vt,isSSRSafeAttrName:rh,isSVGTag:Nc,isSet:Yt,isSpecialBooleanAttr:Ic,isString:Y,isSymbol:Xe,isVoidTag:vc,looseEqual:Ot,looseIndexOf:Os,looseToNumber:cs,makeMap:Ie,normalizeClass:Xn,normalizeProps:Sc,normalizeStyle:$n,objectToString:io,parseStringStyle:lo,propsToAttrMap:oh,remove:Mi,slotFlagsText:Gu,stringifyStyle:Qu,toDisplayString:ao,toHandlerKey:on,toNumber:as,toRawType:Ec,toTypeString:En},Symbol.toStringTag,{value:"Module"}));/** +* @vue/reactivity v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Ue;class fo{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=Ue,!t&&Ue&&(this.index=(Ue.scopes||(Ue.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0)return;if(ts){let t=ts;for(ts=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;es;){let t=es;for(es=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function wc(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Dc(e){let t,n=e.depsTail,s=n;for(;s;){const i=s.prevDep;s.version===-1?(s===n&&(n=i),po(s),Th(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=i}e.deps=t,e.depsTail=n}function Lr(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(kc(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function kc(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===us))return;e.globalVersion=us;const t=e.dep;if(e.flags|=2,t.version>0&&!e.isSSR&&e.deps&&!Lr(e)){e.flags&=-3;return}const n=ae,s=at;ae=e,at=!0;try{wc(e);const i=e.fn(e._value);(t.version===0||Le(i,e._value))&&(e._value=i,t.version++)}catch(i){throw t.version++,i}finally{ae=n,at=s,Dc(e),e.flags&=-3}}function po(e,t=!1){const{dep:n,prevSub:s,nextSub:i}=e;if(s&&(s.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let r=n.computed.deps;r;r=r.nextDep)po(r,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Th(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}function Sh(e,t){e.effect instanceof fs&&(e=e.effect.fn);const n=new fs(e);t&&ee(n,t);try{n.run()}catch(i){throw n.stop(),i}const s=n.run.bind(n);return s.effect=n,s}function bh(e){e.effect.stop()}let at=!0;const Fc=[];function zt(){Fc.push(at),at=!1}function Qt(){const e=Fc.pop();at=e===void 0?!0:e}function ml(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=ae;ae=void 0;try{t()}finally{ae=n}}}let us=0;class Nh{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class wi{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0}track(t){if(!ae||!at||ae===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==ae)n=this.activeLink=new Nh(ae,this),ae.deps?(n.prevDep=ae.depsTail,ae.depsTail.nextDep=n,ae.depsTail=n):ae.deps=ae.depsTail=n,Vc(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=ae.depsTail,n.nextDep=void 0,ae.depsTail.nextDep=n,ae.depsTail=n,ae.deps===n&&(ae.deps=s)}return n}trigger(t){this.version++,us++,this.notify(t)}notify(t){uo();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{ho()}}}function Vc(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)Vc(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const ci=new WeakMap,cn=Symbol(""),xr=Symbol(""),hs=Symbol("");function xe(e,t,n){if(at&&ae){let s=ci.get(e);s||ci.set(e,s=new Map);let i=s.get(n);i||(s.set(n,i=new wi),i.map=s,i.key=n),i.track()}}function St(e,t,n,s,i,r){const o=ci.get(e);if(!o){us++;return}const l=c=>{c&&c.trigger()};if(uo(),t==="clear")o.forEach(l);else{const c=H(e),f=c&&Li(n);if(c&&n==="length"){const a=Number(s);o.forEach((u,p)=>{(p==="length"||p===hs||!Xe(p)&&p>=a)&&l(u)})}else switch((n!==void 0||o.has(void 0))&&l(o.get(n)),f&&l(o.get(hs)),t){case"add":c?f&&l(o.get("length")):(l(o.get(cn)),rn(e)&&l(o.get(xr)));break;case"delete":c||(l(o.get(cn)),rn(e)&&l(o.get(xr)));break;case"set":rn(e)&&l(o.get(cn));break}}ho()}function Ch(e,t){const n=ci.get(e);return n&&n.get(t)}function Sn(e){const t=se(e);return t===e?t:(xe(t,"iterate",hs),Ze(e)?t:t.map(we))}function Di(e){return xe(e=se(e),"iterate",hs),e}const vh={__proto__:null,[Symbol.iterator](){return Tr(this,Symbol.iterator,we)},concat(...e){return Sn(this).concat(...e.map(t=>H(t)?Sn(t):t))},entries(){return Tr(this,"entries",e=>(e[1]=we(e[1]),e))},every(e,t){return _t(this,"every",e,t,void 0,arguments)},filter(e,t){return _t(this,"filter",e,t,n=>n.map(we),arguments)},find(e,t){return _t(this,"find",e,t,we,arguments)},findIndex(e,t){return _t(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return _t(this,"findLast",e,t,we,arguments)},findLastIndex(e,t){return _t(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return _t(this,"forEach",e,t,void 0,arguments)},includes(...e){return Sr(this,"includes",e)},indexOf(...e){return Sr(this,"indexOf",e)},join(e){return Sn(this).join(e)},lastIndexOf(...e){return Sr(this,"lastIndexOf",e)},map(e,t){return _t(this,"map",e,t,void 0,arguments)},pop(){return Yn(this,"pop")},push(...e){return Yn(this,"push",e)},reduce(e,...t){return _l(this,"reduce",e,t)},reduceRight(e,...t){return _l(this,"reduceRight",e,t)},shift(){return Yn(this,"shift")},some(e,t){return _t(this,"some",e,t,void 0,arguments)},splice(...e){return Yn(this,"splice",e)},toReversed(){return Sn(this).toReversed()},toSorted(e){return Sn(this).toSorted(e)},toSpliced(...e){return Sn(this).toSpliced(...e)},unshift(...e){return Yn(this,"unshift",e)},values(){return Tr(this,"values",we)}};function Tr(e,t,n){const s=Di(e),i=s[t]();return s!==e&&!Ze(e)&&(i._next=i.next,i.next=()=>{const r=i._next();return r.value&&(r.value=n(r.value)),r}),i}const Ah=Array.prototype;function _t(e,t,n,s,i,r){const o=Di(e),l=o!==e&&!Ze(e),c=o[t];if(c!==Ah[t]){const u=c.apply(e,r);return l?we(u):u}let f=n;o!==e&&(l?f=function(u,p){return n.call(this,we(u),p,e)}:n.length>2&&(f=function(u,p){return n.call(this,u,p,e)}));const a=c.call(o,f,s);return l&&i?i(a):a}function _l(e,t,n,s){const i=Di(e);let r=n;return i!==e&&(Ze(e)?n.length>3&&(r=function(o,l,c){return n.call(this,o,l,c,e)}):r=function(o,l,c){return n.call(this,o,we(l),c,e)}),i[t](r,...s)}function Sr(e,t,n){const s=se(e);xe(s,"iterate",hs);const i=s[t](...n);return(i===-1||i===!1)&&Ui(n[0])?(n[0]=se(n[0]),s[t](...n)):i}function Yn(e,t,n=[]){zt(),uo();const s=se(e)[t].apply(e,n);return ho(),Qt(),s}const Ih=Ie("__proto__,__v_isRef,__isVue"),Uc=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Xe));function Oh(e){Xe(e)||(e=String(e));const t=se(this);return xe(t,"has",e),t.hasOwnProperty(e)}class Hc{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const i=this._isReadonly,r=this._isShallow;if(n==="__v_isReactive")return!i;if(n==="__v_isReadonly")return i;if(n==="__v_isShallow")return r;if(n==="__v_raw")return s===(i?r?Gc:Kc:r?jc:Xc).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=H(t);if(!i){let c;if(o&&(c=vh[n]))return c;if(n==="hasOwnProperty")return Oh}const l=Reflect.get(t,n,Ne(t)?t:s);return(Xe(n)?Uc.has(n):Ih(n))||(i||xe(t,"get",n),r)?l:Ne(l)?o&&Li(n)?l:l.value:oe(l)?i?go(l):Fi(l):l}}class Bc extends Hc{constructor(t=!1){super(!1,t)}set(t,n,s,i){let r=t[n];if(!this._isShallow){const c=Kt(r);if(!Ze(s)&&!Kt(s)&&(r=se(r),s=se(s)),!H(t)&&Ne(r)&&!Ne(s))return c?!1:(r.value=s,!0)}const o=H(t)&&Li(n)?Number(n)e,Us=e=>Reflect.getPrototypeOf(e);function xh(e,t,n){return function(...s){const i=this.__v_raw,r=se(i),o=rn(r),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,f=i[e](...s),a=n?wr:t?Dr:we;return!t&&xe(r,"iterate",c?xr:cn),{next(){const{value:u,done:p}=f.next();return p?{value:u,done:p}:{value:l?[a(u[0]),a(u[1])]:a(u),done:p}},[Symbol.iterator](){return this}}}}function Hs(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function wh(e,t){const n={get(i){const r=this.__v_raw,o=se(r),l=se(i);e||(Le(i,l)&&xe(o,"get",i),xe(o,"get",l));const{has:c}=Us(o),f=t?wr:e?Dr:we;if(c.call(o,i))return f(r.get(i));if(c.call(o,l))return f(r.get(l));r!==o&&r.get(i)},get size(){const i=this.__v_raw;return!e&&xe(se(i),"iterate",cn),Reflect.get(i,"size",i)},has(i){const r=this.__v_raw,o=se(r),l=se(i);return e||(Le(i,l)&&xe(o,"has",i),xe(o,"has",l)),i===l?r.has(i):r.has(i)||r.has(l)},forEach(i,r){const o=this,l=o.__v_raw,c=se(l),f=t?wr:e?Dr:we;return!e&&xe(c,"iterate",cn),l.forEach((a,u)=>i.call(r,f(a),f(u),o))}};return ee(n,e?{add:Hs("add"),set:Hs("set"),delete:Hs("delete"),clear:Hs("clear")}:{add(i){!t&&!Ze(i)&&!Kt(i)&&(i=se(i));const r=se(this);return Us(r).has.call(r,i)||(r.add(i),St(r,"add",i,i)),this},set(i,r){!t&&!Ze(r)&&!Kt(r)&&(r=se(r));const o=se(this),{has:l,get:c}=Us(o);let f=l.call(o,i);f||(i=se(i),f=l.call(o,i));const a=c.call(o,i);return o.set(i,r),f?Le(r,a)&&St(o,"set",i,r):St(o,"add",i,r),this},delete(i){const r=se(this),{has:o,get:l}=Us(r);let c=o.call(r,i);c||(i=se(i),c=o.call(r,i)),l&&l.call(r,i);const f=r.delete(i);return c&&St(r,"delete",i,void 0),f},clear(){const i=se(this),r=i.size!==0,o=i.clear();return r&&St(i,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(i=>{n[i]=xh(i,e,t)}),n}function ki(e,t){const n=wh(e,t);return(s,i,r)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?s:Reflect.get(ie(n,i)&&i in s?n:s,i,r)}const Dh={get:ki(!1,!1)},kh={get:ki(!1,!0)},Fh={get:ki(!0,!1)},Vh={get:ki(!0,!0)},Xc=new WeakMap,jc=new WeakMap,Kc=new WeakMap,Gc=new WeakMap;function Uh(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Hh(e){return e.__v_skip||!Object.isExtensible(e)?0:Uh(Ec(e))}function Fi(e){return Kt(e)?e:Vi(e,!1,Rh,Dh,Xc)}function Wc(e){return Vi(e,!1,Ph,kh,jc)}function go(e){return Vi(e,!0,Mh,Fh,Kc)}function Bh(e){return Vi(e,!0,Lh,Vh,Gc)}function Vi(e,t,n,s,i){if(!oe(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const r=i.get(e);if(r)return r;const o=Hh(e);if(o===0)return e;const l=new Proxy(e,o===2?s:n);return i.set(e,l),l}function Bt(e){return Kt(e)?Bt(e.__v_raw):!!(e&&e.__v_isReactive)}function Kt(e){return!!(e&&e.__v_isReadonly)}function Ze(e){return!!(e&&e.__v_isShallow)}function Ui(e){return e?!!e.__v_raw:!1}function se(e){const t=e&&e.__v_raw;return t?se(t):e}function qc(e){return!ie(e,"__v_skip")&&Object.isExtensible(e)&&ro(e,"__v_skip",!0),e}const we=e=>oe(e)?Fi(e):e,Dr=e=>oe(e)?go(e):e;function Ne(e){return e?e.__v_isRef===!0:!1}function ns(e){return Jc(e,!1)}function Yc(e){return Jc(e,!0)}function Jc(e,t){return Ne(e)?e:new $h(e,t)}class $h{constructor(t,n){this.dep=new wi,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:se(t),this._value=n?t:we(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Ze(t)||Kt(t);t=s?t:se(t),Le(t,n)&&(this._rawValue=t,this._value=s?t:we(t),this.dep.trigger())}}function Xh(e){e.dep&&e.dep.trigger()}function Hi(e){return Ne(e)?e.value:e}function jh(e){return G(e)?e():Hi(e)}const Kh={get:(e,t,n)=>t==="__v_raw"?e:Hi(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const i=e[t];return Ne(i)&&!Ne(n)?(i.value=n,!0):Reflect.set(e,t,n,s)}};function mo(e){return Bt(e)?e:new Proxy(e,Kh)}class Gh{constructor(t){this.__v_isRef=!0,this._value=void 0;const n=this.dep=new wi,{get:s,set:i}=t(n.track.bind(n),n.trigger.bind(n));this._get=s,this._set=i}get value(){return this._value=this._get()}set value(t){this._set(t)}}function zc(e){return new Gh(e)}function Wh(e){const t=H(e)?new Array(e.length):{};for(const n in e)t[n]=Qc(e,n);return t}class qh{constructor(t,n,s){this._object=t,this._key=n,this._defaultValue=s,this.__v_isRef=!0,this._value=void 0}get value(){const t=this._object[this._key];return this._value=t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return Ch(se(this._object),this._key)}}class Yh{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function Jh(e,t,n){return Ne(e)?e:G(e)?new Yh(e):oe(e)&&arguments.length>1?Qc(e,t,n):ns(e)}function Qc(e,t,n){const s=e[t];return Ne(s)?s:new qh(e,t,n)}class zh{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new wi(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=us-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&ae!==this)return xc(this,!0),!0}get value(){const t=this.dep.track();return kc(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Qh(e,t,n=!1){let s,i;return G(e)?s=e:(s=e.get,i=e.set),new zh(s,i,n)}const Zh={GET:"get",HAS:"has",ITERATE:"iterate"},ep={SET:"set",ADD:"add",DELETE:"delete",CLEAR:"clear"},Bs={},ai=new WeakMap;let Dt;function tp(){return Dt}function Zc(e,t=!1,n=Dt){if(n){let s=ai.get(n);s||ai.set(n,s=[]),s.push(e)}}function np(e,t,n=Z){const{immediate:s,deep:i,once:r,scheduler:o,augmentJob:l,call:c}=n,f=_=>i?_:Ze(_)||i===!1||i===0?bt(_,1):bt(_);let a,u,p,d,y=!1,E=!1;if(Ne(e)?(u=()=>e.value,y=Ze(e)):Bt(e)?(u=()=>f(e),y=!0):H(e)?(E=!0,y=e.some(_=>Bt(_)||Ze(_)),u=()=>e.map(_=>{if(Ne(_))return _.value;if(Bt(_))return f(_);if(G(_))return c?c(_,2):_()})):G(e)?t?u=c?()=>c(e,2):e:u=()=>{if(p){zt();try{p()}finally{Qt()}}const _=Dt;Dt=a;try{return c?c(e,3,[d]):e(d)}finally{Dt=_}}:u=Ae,t&&i){const _=u,S=i===!0?1/0:i;u=()=>bt(_(),S)}const w=Pc(),I=()=>{a.stop(),w&&w.active&&Mi(w.effects,a)};if(r&&t){const _=t;t=(...S)=>{_(...S),I()}}let v=E?new Array(e.length).fill(Bs):Bs;const g=_=>{if(!(!(a.flags&1)||!a.dirty&&!_))if(t){const S=a.run();if(i||y||(E?S.some((O,V)=>Le(O,v[V])):Le(S,v))){p&&p();const O=Dt;Dt=a;try{const V=[S,v===Bs?void 0:E&&v[0]===Bs?[]:v,d];c?c(t,3,V):t(...V),v=S}finally{Dt=O}}}else a.run()};return l&&l(g),a=new fs(u),a.scheduler=o?()=>o(g,!1):g,d=_=>Zc(_,!1,a),p=a.onStop=()=>{const _=ai.get(a);if(_){if(c)c(_,4);else for(const S of _)S();ai.delete(a)}},t?s?g(!0):v=a.run():o?o(g.bind(null,!0),!0):a.run(),I.pause=a.pause.bind(a),I.resume=a.resume.bind(a),I.stop=I,I}function bt(e,t=1/0,n){if(t<=0||!oe(e)||e.__v_skip||(n=n||new Set,n.has(e)))return e;if(n.add(e),t--,Ne(e))bt(e.value,t,n);else if(H(e))for(let s=0;s{bt(s,t,n)});else if(As(e)){for(const s in e)bt(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&bt(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/const ea=[];function sp(e){ea.push(e)}function ip(){ea.pop()}function rp(e,t){}const op={SETUP_FUNCTION:0,0:"SETUP_FUNCTION",RENDER_FUNCTION:1,1:"RENDER_FUNCTION",NATIVE_EVENT_HANDLER:5,5:"NATIVE_EVENT_HANDLER",COMPONENT_EVENT_HANDLER:6,6:"COMPONENT_EVENT_HANDLER",VNODE_HOOK:7,7:"VNODE_HOOK",DIRECTIVE_HOOK:8,8:"DIRECTIVE_HOOK",TRANSITION_HOOK:9,9:"TRANSITION_HOOK",APP_ERROR_HANDLER:10,10:"APP_ERROR_HANDLER",APP_WARN_HANDLER:11,11:"APP_WARN_HANDLER",FUNCTION_REF:12,12:"FUNCTION_REF",ASYNC_COMPONENT_LOADER:13,13:"ASYNC_COMPONENT_LOADER",SCHEDULER:14,14:"SCHEDULER",COMPONENT_UPDATE:15,15:"COMPONENT_UPDATE",APP_UNMOUNT_CLEANUP:16,16:"APP_UNMOUNT_CLEANUP"},lp={sp:"serverPrefetch hook",bc:"beforeCreate hook",c:"created hook",bm:"beforeMount hook",m:"mounted hook",bu:"beforeUpdate hook",u:"updated",bum:"beforeUnmount hook",um:"unmounted hook",a:"activated hook",da:"deactivated hook",ec:"errorCaptured hook",rtc:"renderTracked hook",rtg:"renderTriggered hook",0:"setup function",1:"render function",2:"watcher getter",3:"watcher callback",4:"watcher cleanup function",5:"native event handler",6:"component event handler",7:"vnode hook",8:"directive hook",9:"transition hook",10:"app errorHandler",11:"app warnHandler",12:"ref function",13:"async component loader",14:"scheduler flush",15:"component update",16:"app unmount cleanup function"};function jn(e,t,n,s){try{return s?e(...s):e()}catch(i){yn(i,t,n)}}function st(e,t,n,s){if(G(e)){const i=jn(e,t,n,s);return i&&Pi(i)&&i.catch(r=>{yn(r,t,n)}),i}if(H(e)){const i=[];for(let r=0;r>>1,i=He[s],r=ds(i);r=ds(n)?He.push(e):He.splice(ap(t),0,e),e.flags|=1,na()}}function na(){fi||(fi=ta.then(sa))}function ps(e){H(e)?On.push(...e):kt&&e.id===-1?kt.splice(Cn+1,0,e):e.flags&1||(On.push(e),e.flags|=1),na()}function El(e,t,n=dt+1){for(;nds(n)-ds(s));if(On.length=0,kt){kt.push(...t);return}for(kt=t,Cn=0;Cne.id==null?e.flags&2?-1:1/0:e.id;function sa(e){try{for(dt=0;dtvn.emit(i,...r)),$s=[]):typeof window<"u"&&window.HTMLElement&&!((s=(n=window.navigator)==null?void 0:n.userAgent)!=null&&s.includes("jsdom"))?((t.__VUE_DEVTOOLS_HOOK_REPLAY__=t.__VUE_DEVTOOLS_HOOK_REPLAY__||[]).push(r=>{ia(r,t)}),setTimeout(()=>{vn||(t.__VUE_DEVTOOLS_HOOK_REPLAY__=null,$s=[])},3e3)):$s=[]}let be=null,$i=null;function gs(e){const t=be;return be=e,$i=e&&e.type.__scopeId||null,t}function fp(e){$i=e}function up(){$i=null}const hp=e=>Eo;function Eo(e,t=be,n){if(!t||e._n)return e;const s=(...i)=>{s._d&&Xr(-1);const r=gs(t);let o;try{o=e(...i)}finally{gs(r),s._d&&Xr(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function pp(e,t){if(be===null)return e;const n=Ls(be),s=e.dirs||(e.dirs=[]);for(let i=0;ie.__isTeleport,ss=e=>e&&(e.disabled||e.disabled===""),yl=e=>e&&(e.defer||e.defer===""),Tl=e=>typeof SVGElement<"u"&&e instanceof SVGElement,Sl=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,kr=(e,t)=>{const n=e&&e.to;return Y(n)?t?t(n):null:n},la={name:"Teleport",__isTeleport:!0,process(e,t,n,s,i,r,o,l,c,f){const{mc:a,pc:u,pbc:p,o:{insert:d,querySelector:y,createText:E,createComment:w}}=f,I=ss(t.props);let{shapeFlag:v,children:g,dynamicChildren:_}=t;if(e==null){const S=t.el=E(""),O=t.anchor=E("");d(S,n,s),d(O,n,s);const V=(T,C)=>{v&16&&(i&&i.isCE&&(i.ce._teleportTarget=T),a(g,T,C,i,r,o,l,c))},M=()=>{const T=t.target=kr(t.props,y),C=ca(T,t,E,d);T&&(o!=="svg"&&Tl(T)?o="svg":o!=="mathml"&&Sl(T)&&(o="mathml"),I||(V(T,C),Qs(t,!1)))};I&&(V(n,O),Qs(t,!0)),yl(t.props)?Te(()=>{M(),t.el.__isMounted=!0},r):M()}else{if(yl(t.props)&&!e.el.__isMounted){Te(()=>{la.process(e,t,n,s,i,r,o,l,c,f),delete e.el.__isMounted},r);return}t.el=e.el,t.targetStart=e.targetStart;const S=t.anchor=e.anchor,O=t.target=e.target,V=t.targetAnchor=e.targetAnchor,M=ss(e.props),T=M?n:O,C=M?S:V;if(o==="svg"||Tl(O)?o="svg":(o==="mathml"||Sl(O))&&(o="mathml"),_?(p(e.dynamicChildren,_,T,i,r,o,l),Ro(e,t,!0)):c||u(e,t,T,C,i,r,o,l,!1),I)M?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Xs(t,n,S,f,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const x=t.target=kr(t.props,y);x&&Xs(t,x,null,f,0)}else M&&Xs(t,O,V,f,1);Qs(t,I)}},remove(e,t,n,{um:s,o:{remove:i}},r){const{shapeFlag:o,children:l,anchor:c,targetStart:f,targetAnchor:a,target:u,props:p}=e;if(u&&(i(f),i(a)),r&&i(c),o&16){const d=r||!ss(p);for(let y=0;y{e.isMounted=!0}),Gi(()=>{e.isUnmounting=!0}),e}const et=[Function,Array],To={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:et,onEnter:et,onAfterEnter:et,onEnterCancelled:et,onBeforeLeave:et,onLeave:et,onAfterLeave:et,onLeaveCancelled:et,onBeforeAppear:et,onAppear:et,onAfterAppear:et,onAppearCancelled:et},aa=e=>{const t=e.subTree;return t.component?aa(t.component):t},mp={name:"BaseTransition",props:To,setup(e,{slots:t}){const n=it(),s=yo();return()=>{const i=t.default&&Xi(t.default(),!0);if(!i||!i.length)return;const r=fa(i),o=se(e),{mode:l}=o;if(s.isLeaving)return br(r);const c=bl(r);if(!c)return br(r);let f=Ln(c,o,s,n,u=>f=u);c.type!==ye&&Rt(c,f);let a=n.subTree&&bl(n.subTree);if(a&&a.type!==ye&&!ct(c,a)&&aa(n).type!==ye){let u=Ln(a,o,s,n);if(Rt(a,u),l==="out-in"&&c.type!==ye)return s.isLeaving=!0,u.afterLeave=()=>{s.isLeaving=!1,n.job.flags&8||n.update(),delete u.afterLeave,a=void 0},br(r);l==="in-out"&&c.type!==ye?u.delayLeave=(p,d,y)=>{const E=ha(s,a);E[String(a.key)]=a,p[Ft]=()=>{d(),p[Ft]=void 0,delete f.delayedLeave,a=void 0},f.delayedLeave=()=>{y(),delete f.delayedLeave,a=void 0}}:a=void 0}else a&&(a=void 0);return r}}};function fa(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==ye){t=n;break}}return t}const ua=mp;function ha(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function Ln(e,t,n,s,i){const{appear:r,mode:o,persisted:l=!1,onBeforeEnter:c,onEnter:f,onAfterEnter:a,onEnterCancelled:u,onBeforeLeave:p,onLeave:d,onAfterLeave:y,onLeaveCancelled:E,onBeforeAppear:w,onAppear:I,onAfterAppear:v,onAppearCancelled:g}=t,_=String(e.key),S=ha(n,e),O=(T,C)=>{T&&st(T,s,9,C)},V=(T,C)=>{const x=C[1];O(T,C),H(T)?T.every(b=>b.length<=1)&&x():T.length<=1&&x()},M={mode:o,persisted:l,beforeEnter(T){let C=c;if(!n.isMounted)if(r)C=w||c;else return;T[Ft]&&T[Ft](!0);const x=S[_];x&&ct(e,x)&&x.el[Ft]&&x.el[Ft](),O(C,[T])},enter(T){let C=f,x=a,b=u;if(!n.isMounted)if(r)C=I||f,x=v||a,b=g||u;else return;let k=!1;const K=T[js]=q=>{k||(k=!0,q?O(b,[T]):O(x,[T]),M.delayedLeave&&M.delayedLeave(),T[js]=void 0)};C?V(C,[T,K]):K()},leave(T,C){const x=String(e.key);if(T[js]&&T[js](!0),n.isUnmounting)return C();O(p,[T]);let b=!1;const k=T[Ft]=K=>{b||(b=!0,C(),K?O(E,[T]):O(y,[T]),T[Ft]=void 0,S[x]===e&&delete S[x])};S[x]=e,d?V(d,[T,k]):k()},clone(T){const C=Ln(T,t,n,s,i);return i&&i(C),C}};return M}function br(e){if(Rs(e))return e=mt(e),e.children=null,e}function bl(e){if(!Rs(e))return oa(e.type)&&e.children?fa(e.children):e;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&G(n.default))return n.default()}}function Rt(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Rt(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Xi(e,t=!1,n){let s=[],i=0;for(let r=0;r1)for(let r=0;rn.value,set:r=>n.value=r})}return n}function ms(e,t,n,s,i=!1){if(H(e)){e.forEach((y,E)=>ms(y,t&&(H(t)?t[E]:t),n,s,i));return}if($t(s)&&!i){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&ms(e,t,n,s.component.subTree);return}const r=s.shapeFlag&4?Ls(s.component):s.el,o=i?null:r,{i:l,r:c}=e,f=t&&t.r,a=l.refs===Z?l.refs={}:l.refs,u=l.setupState,p=se(u),d=u===Z?()=>!1:y=>ie(p,y);if(f!=null&&f!==c&&(Y(f)?(a[f]=null,d(f)&&(u[f]=null)):Ne(f)&&(f.value=null)),G(c))jn(c,l,12,[o,a]);else{const y=Y(c),E=Ne(c);if(y||E){const w=()=>{if(e.f){const I=y?d(c)?u[c]:a[c]:c.value;i?H(I)&&Mi(I,r):H(I)?I.includes(r)||I.push(r):y?(a[c]=[r],d(c)&&(u[c]=a[c])):(c.value=[r],e.k&&(a[e.k]=c.value))}else y?(a[c]=o,d(c)&&(u[c]=o)):E&&(c.value=o,e.k&&(a[e.k]=o))};o?(w.id=-1,Te(w,n)):w()}}}let Nl=!1;const bn=()=>{Nl||(console.error("Hydration completed but contains mismatches."),Nl=!0)},yp=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",Tp=e=>e.namespaceURI.includes("MathML"),Ks=e=>{if(e.nodeType===1){if(yp(e))return"svg";if(Tp(e))return"mathml"}},In=e=>e.nodeType===8;function Sp(e){const{mt:t,p:n,o:{patchProp:s,createText:i,nextSibling:r,parentNode:o,remove:l,insert:c,createComment:f}}=e,a=(g,_)=>{if(!_.hasChildNodes()){n(null,g,_),ui(),_._vnode=g;return}u(_.firstChild,g,null,null,null),ui(),_._vnode=g},u=(g,_,S,O,V,M=!1)=>{M=M||!!_.dynamicChildren;const T=In(g)&&g.data==="[",C=()=>E(g,_,S,O,V,T),{type:x,ref:b,shapeFlag:k,patchFlag:K}=_;let q=g.nodeType;_.el=g,K===-2&&(M=!1,_.dynamicChildren=null);let $=null;switch(x){case Xt:q!==3?_.children===""?(c(_.el=i(""),o(g),g),$=g):$=C():(g.data!==_.children&&(bn(),g.data=_.children),$=r(g));break;case ye:v(g)?($=r(g),I(_.el=g.content.firstChild,g,S)):q!==8||T?$=C():$=r(g);break;case fn:if(T&&(g=r(g),q=g.nodeType),q===1||q===3){$=g;const J=!_.children.length;for(let X=0;X<_.staticCount;X++)J&&(_.children+=$.nodeType===1?$.outerHTML:$.data),X===_.staticCount-1&&(_.anchor=$),$=r($);return T?r($):$}else C();break;case ve:T?$=y(g,_,S,O,V,M):$=C();break;default:if(k&1)(q!==1||_.type.toLowerCase()!==g.tagName.toLowerCase())&&!v(g)?$=C():$=p(g,_,S,O,V,M);else if(k&6){_.slotScopeIds=V;const J=o(g);if(T?$=w(g):In(g)&&g.data==="teleport start"?$=w(g,g.data,"teleport end"):$=r(g),t(_,J,null,S,O,Ks(J),M),$t(_)&&!_.type.__asyncResolved){let X;T?(X=he(ve),X.anchor=$?$.previousSibling:J.lastChild):X=g.nodeType===3?Po(""):he("div"),X.el=g,_.component.subTree=X}}else k&64?q!==8?$=C():$=_.type.hydrate(g,_,S,O,V,M,e,d):k&128&&($=_.type.hydrate(g,_,S,O,Ks(o(g)),V,M,e,u))}return b!=null&&ms(b,null,O,_),$},p=(g,_,S,O,V,M)=>{M=M||!!_.dynamicChildren;const{type:T,props:C,patchFlag:x,shapeFlag:b,dirs:k,transition:K}=_,q=T==="input"||T==="option";if(q||x!==-1){k&>(_,null,S,"created");let $=!1;if(v(g)){$=Va(null,K)&&S&&S.vnode.props&&S.vnode.props.appear;const X=g.content.firstChild;$&&K.beforeEnter(X),I(X,g,S),_.el=g=X}if(b&16&&!(C&&(C.innerHTML||C.textContent))){let X=d(g.firstChild,_,g,S,O,V,M);for(;X;){Gs(g,1)||bn();const _e=X;X=X.nextSibling,l(_e)}}else if(b&8){let X=_.children;X[0]===` +`&&(g.tagName==="PRE"||g.tagName==="TEXTAREA")&&(X=X.slice(1)),g.textContent!==X&&(Gs(g,0)||bn(),g.textContent=_.children)}if(C){if(q||!M||x&48){const X=g.tagName.includes("-");for(const _e in C)(q&&(_e.endsWith("value")||_e==="indeterminate")||qt(_e)&&!vt(_e)||_e[0]==="."||X)&&s(g,_e,null,C[_e],void 0,S)}else if(C.onClick)s(g,"onClick",null,C.onClick,void 0,S);else if(x&4&&Bt(C.style))for(const X in C.style)C.style[X]}let J;(J=C&&C.onVnodeBeforeMount)&&Ke(J,S,_),k&>(_,null,S,"beforeMount"),((J=C&&C.onVnodeMounted)||k||$)&&Wa(()=>{J&&Ke(J,S,_),$&&K.enter(g),k&>(_,null,S,"mounted")},O)}return g.nextSibling},d=(g,_,S,O,V,M,T)=>{T=T||!!_.dynamicChildren;const C=_.children,x=C.length;for(let b=0;b{const{slotScopeIds:T}=_;T&&(V=V?V.concat(T):T);const C=o(g),x=d(r(g),_,C,S,O,V,M);return x&&In(x)&&x.data==="]"?r(_.anchor=x):(bn(),c(_.anchor=f("]"),C,x),x)},E=(g,_,S,O,V,M)=>{if(Gs(g.parentElement,1)||bn(),_.el=null,M){const x=w(g);for(;;){const b=r(g);if(b&&b!==x)l(b);else break}}const T=r(g),C=o(g);return l(g),n(null,_,C,T,S,O,Ks(C),V),S&&(S.vnode.el=_.el,Yi(S,_.el)),T},w=(g,_="[",S="]")=>{let O=0;for(;g;)if(g=r(g),g&&In(g)&&(g.data===_&&O++,g.data===S)){if(O===0)return r(g);O--}return g},I=(g,_,S)=>{const O=_.parentNode;O&&O.replaceChild(g,_);let V=S;for(;V;)V.vnode.el===_&&(V.vnode.el=V.subTree.el=g),V=V.parent},v=g=>g.nodeType===1&&g.tagName==="TEMPLATE";return[a,u]}const Cl="data-allow-mismatch",bp={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function Gs(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(Cl);)e=e.parentElement;const n=e&&e.getAttribute(Cl);if(n==null)return!1;if(n==="")return!0;{const s=n.split(",");return t===0&&s.includes("children")?!0:n.split(",").includes(bp[t])}}const Np=Is().requestIdleCallback||(e=>setTimeout(e,1)),Cp=Is().cancelIdleCallback||(e=>clearTimeout(e)),vp=(e=1e4)=>t=>{const n=Np(t,{timeout:e});return()=>Cp(n)};function Ap(e){const{top:t,left:n,bottom:s,right:i}=e.getBoundingClientRect(),{innerHeight:r,innerWidth:o}=window;return(t>0&&t0&&s0&&n0&&i(t,n)=>{const s=new IntersectionObserver(i=>{for(const r of i)if(r.isIntersecting){s.disconnect(),t();break}},e);return n(i=>{if(i instanceof Element){if(Ap(i))return t(),s.disconnect(),!1;s.observe(i)}}),()=>s.disconnect()},Op=e=>t=>{if(e){const n=matchMedia(e);if(n.matches)t();else return n.addEventListener("change",t,{once:!0}),()=>n.removeEventListener("change",t)}},Rp=(e=[])=>(t,n)=>{Y(e)&&(e=[e]);let s=!1;const i=o=>{s||(s=!0,r(),t(),o.target.dispatchEvent(new o.constructor(o.type,o)))},r=()=>{n(o=>{for(const l of e)o.removeEventListener(l,i)})};return n(o=>{for(const l of e)o.addEventListener(l,i,{once:!0})}),r};function Mp(e,t){if(In(e)&&e.data==="["){let n=1,s=e.nextSibling;for(;s;){if(s.nodeType===1){if(t(s)===!1)break}else if(In(s))if(s.data==="]"){if(--n===0)break}else s.data==="["&&n++;s=s.nextSibling}}else t(e)}const $t=e=>!!e.type.__asyncLoader;/*! #__NO_SIDE_EFFECTS__ */function Pp(e){G(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:s,delay:i=200,hydrate:r,timeout:o,suspensible:l=!0,onError:c}=e;let f=null,a,u=0;const p=()=>(u++,f=null,d()),d=()=>{let y;return f||(y=f=t().catch(E=>{if(E=E instanceof Error?E:new Error(String(E)),c)return new Promise((w,I)=>{c(E,()=>w(p()),()=>I(E),u+1)});throw E}).then(E=>y!==f&&f?f:(E&&(E.__esModule||E[Symbol.toStringTag]==="Module")&&(E=E.default),a=E,E)))};return So({name:"AsyncComponentWrapper",__asyncLoader:d,__asyncHydrate(y,E,w){const I=r?()=>{const v=r(w,g=>Mp(y,g));v&&(E.bum||(E.bum=[])).push(v)}:w;a?I():d().then(()=>!E.isUnmounted&&I())},get __asyncResolved(){return a},setup(){const y=Se;if(bo(y),a)return()=>Nr(a,y);const E=g=>{f=null,yn(g,y,13,!s)};if(l&&y.suspense||xn)return d().then(g=>()=>Nr(g,y)).catch(g=>(E(g),()=>s?he(s,{error:g}):null));const w=ns(!1),I=ns(),v=ns(!!i);return i&&setTimeout(()=>{v.value=!1},i),o!=null&&setTimeout(()=>{if(!w.value&&!I.value){const g=new Error(`Async component timed out after ${o}ms.`);E(g),I.value=g}},o),d().then(()=>{w.value=!0,y.parent&&Rs(y.parent.vnode)&&y.parent.update()}).catch(g=>{E(g),I.value=g}),()=>{if(w.value&&a)return Nr(a,y);if(I.value&&s)return he(s,{error:I.value});if(n&&!v.value)return he(n)}}})}function Nr(e,t){const{ref:n,props:s,children:i,ce:r}=t.vnode,o=he(e,s,i);return o.ref=n,o.ce=r,delete t.vnode.ce,o}const Rs=e=>e.type.__isKeepAlive,Lp={name:"KeepAlive",__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){const n=it(),s=n.ctx;if(!s.renderer)return()=>{const v=t.default&&t.default();return v&&v.length===1?v[0]:v};const i=new Map,r=new Set;let o=null;const l=n.suspense,{renderer:{p:c,m:f,um:a,o:{createElement:u}}}=s,p=u("div");s.activate=(v,g,_,S,O)=>{const V=v.component;f(v,g,_,0,l),c(V.vnode,v,g,_,V,l,S,v.slotScopeIds,O),Te(()=>{V.isDeactivated=!1,V.a&&ln(V.a);const M=v.props&&v.props.onVnodeMounted;M&&Ke(M,V.parent,v)},l)},s.deactivate=v=>{const g=v.component;pi(g.m),pi(g.a),f(v,p,null,1,l),Te(()=>{g.da&&ln(g.da);const _=v.props&&v.props.onVnodeUnmounted;_&&Ke(_,g.parent,v),g.isDeactivated=!0},l)};function d(v){Cr(v),a(v,n,l,!0)}function y(v){i.forEach((g,_)=>{const S=qr(g.type);S&&!v(S)&&E(_)})}function E(v){const g=i.get(v);g&&(!o||!ct(g,o))?d(g):o&&Cr(o),i.delete(v),r.delete(v)}Rn(()=>[e.include,e.exclude],([v,g])=>{v&&y(_=>Qn(v,_)),g&&y(_=>!Qn(g,_))},{flush:"post",deep:!0});let w=null;const I=()=>{w!=null&&(di(n.subTree.type)?Te(()=>{i.set(w,Ws(n.subTree))},n.subTree.suspense):i.set(w,Ws(n.subTree)))};return Ms(I),Ki(I),Gi(()=>{i.forEach(v=>{const{subTree:g,suspense:_}=n,S=Ws(g);if(v.type===S.type&&v.key===S.key){Cr(S);const O=S.component.da;O&&Te(O,_);return}d(v)})}),()=>{if(w=null,!t.default)return o=null;const v=t.default(),g=v[0];if(v.length>1)return o=null,v;if(!Mt(g)||!(g.shapeFlag&4)&&!(g.shapeFlag&128))return o=null,g;let _=Ws(g);if(_.type===ye)return o=null,_;const S=_.type,O=qr($t(_)?_.type.__asyncResolved||{}:S),{include:V,exclude:M,max:T}=e;if(V&&(!O||!Qn(V,O))||M&&O&&Qn(M,O))return _.shapeFlag&=-257,o=_,g;const C=_.key==null?S:_.key,x=i.get(C);return _.el&&(_=mt(_),g.shapeFlag&128&&(g.ssContent=_)),w=C,x?(_.el=x.el,_.component=x.component,_.transition&&Rt(_,_.transition),_.shapeFlag|=512,r.delete(C),r.add(C)):(r.add(C),T&&r.size>parseInt(T,10)&&E(r.values().next().value)),_.shapeFlag|=256,o=_,di(g.type)?g:_}}},xp=Lp;function Qn(e,t){return H(e)?e.some(n=>Qn(n,t)):Y(e)?e.split(",").includes(t):_c(e)?(e.lastIndex=0,e.test(t)):!1}function pa(e,t){ga(e,"a",t)}function da(e,t){ga(e,"da",t)}function ga(e,t,n=Se){const s=e.__wdc||(e.__wdc=()=>{let i=n;for(;i;){if(i.isDeactivated)return;i=i.parent}return e()});if(ji(t,s,n),n){let i=n.parent;for(;i&&i.parent;)Rs(i.parent.vnode)&&wp(s,t,n,i),i=i.parent}}function wp(e,t,n,s){const i=ji(t,e,s,!0);Wi(()=>{Mi(s[t],i)},n)}function Cr(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Ws(e){return e.shapeFlag&128?e.ssContent:e}function ji(e,t,n=Se,s=!1){if(n){const i=n[e]||(n[e]=[]),r=t.__weh||(t.__weh=(...o)=>{zt();const l=pn(n),c=st(t,n,e,o);return l(),Qt(),c});return s?i.unshift(r):i.push(r),r}}const Pt=e=>(t,n=Se)=>{(!xn||e==="sp")&&ji(e,(...s)=>t(...s),n)},ma=Pt("bm"),Ms=Pt("m"),No=Pt("bu"),Ki=Pt("u"),Gi=Pt("bum"),Wi=Pt("um"),_a=Pt("sp"),Ea=Pt("rtg"),ya=Pt("rtc");function Ta(e,t=Se){ji("ec",e,t)}const Co="components",Dp="directives";function kp(e,t){return vo(Co,e,!0,t)||e}const Sa=Symbol.for("v-ndc");function Fp(e){return Y(e)?vo(Co,e,!1)||e:e||Sa}function Vp(e){return vo(Dp,e)}function vo(e,t,n=!0,s=!1){const i=be||Se;if(i){const r=i.type;if(e===Co){const l=qr(r,!1);if(l&&(l===t||l===fe(t)||l===Jt(fe(t))))return r}const o=vl(i[e]||r[e],t)||vl(i.appContext[e],t);return!o&&s?r:o}}function vl(e,t){return e&&(e[t]||e[fe(t)]||e[Jt(fe(t))])}function Up(e,t,n,s){let i;const r=n&&n[s],o=H(e);if(o||Y(e)){const l=o&&Bt(e);let c=!1;l&&(c=!Ze(e),e=Di(e)),i=new Array(e.length);for(let f=0,a=e.length;ft(l,c,void 0,r&&r[c]));else{const l=Object.keys(e);i=new Array(l.length);for(let c=0,f=l.length;c{const r=s.fn(...i);return r&&(r.key=s.key),r}:s.fn)}return e}function Bp(e,t,n={},s,i){if(be.ce||be.parent&&$t(be.parent)&&be.parent.ce)return t!=="default"&&(n.name=t),ys(),gi(ve,null,[he("slot",n,s&&s())],64);let r=e[t];r&&r._c&&(r._d=!1),ys();const o=r&&Ao(r(n)),l=n.key||o&&o.key,c=gi(ve,{key:(l&&!Xe(l)?l:`_${t}`)+(!o&&s?"_fb":"")},o||(s?s():[]),o&&e._===1?64:-2);return!i&&c.scopeId&&(c.slotScopeIds=[c.scopeId+"-s"]),r&&r._c&&(r._d=!0),c}function Ao(e){return e.some(t=>Mt(t)?!(t.type===ye||t.type===ve&&!Ao(t.children)):!0)?e:null}function $p(e,t){const n={};for(const s in e)n[t&&/[A-Z]/.test(s)?`on:${s}`:on(s)]=e[s];return n}const Fr=e=>e?ef(e)?Ls(e):Fr(e.parent):null,is=ee(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Fr(e.parent),$root:e=>Fr(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Io(e),$forceUpdate:e=>e.f||(e.f=()=>{_o(e.update)}),$nextTick:e=>e.n||(e.n=Bi.bind(e.proxy)),$watch:e=>Td.bind(e)}),vr=(e,t)=>e!==Z&&!e.__isScriptSetup&&ie(e,t),Vr={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:i,props:r,accessCache:o,type:l,appContext:c}=e;let f;if(t[0]!=="$"){const d=o[t];if(d!==void 0)switch(d){case 1:return s[t];case 2:return i[t];case 4:return n[t];case 3:return r[t]}else{if(vr(s,t))return o[t]=1,s[t];if(i!==Z&&ie(i,t))return o[t]=2,i[t];if((f=e.propsOptions[0])&&ie(f,t))return o[t]=3,r[t];if(n!==Z&&ie(n,t))return o[t]=4,n[t];Ur&&(o[t]=0)}}const a=is[t];let u,p;if(a)return t==="$attrs"&&xe(e.attrs,"get",""),a(e);if((u=l.__cssModules)&&(u=u[t]))return u;if(n!==Z&&ie(n,t))return o[t]=4,n[t];if(p=c.config.globalProperties,ie(p,t))return p[t]},set({_:e},t,n){const{data:s,setupState:i,ctx:r}=e;return vr(i,t)?(i[t]=n,!0):s!==Z&&ie(s,t)?(s[t]=n,!0):ie(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(r[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:i,propsOptions:r}},o){let l;return!!n[o]||e!==Z&&ie(e,o)||vr(t,o)||(l=r[0])&&ie(l,o)||ie(s,o)||ie(is,o)||ie(i.config.globalProperties,o)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:ie(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}},Xp=ee({},Vr,{get(e,t){if(t!==Symbol.unscopables)return Vr.get(e,t,e)},has(e,t){return t[0]!=="_"&&!oo(t)}});function jp(){return null}function Kp(){return null}function Gp(e){}function Wp(e){}function qp(){return null}function Yp(){}function Jp(e,t){return null}function zp(){return ba().slots}function Qp(){return ba().attrs}function ba(){const e=it();return e.setupContext||(e.setupContext=sf(e))}function _s(e){return H(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}function Zp(e,t){const n=_s(e);for(const s in t){if(s.startsWith("__skip"))continue;let i=n[s];i?H(i)||G(i)?i=n[s]={type:i,default:t[s]}:i.default=t[s]:i===null&&(i=n[s]={default:t[s]}),i&&t[`__skip_${s}`]&&(i.skipFactory=!0)}return n}function ed(e,t){return!e||!t?e||t:H(e)&&H(t)?e.concat(t):ee({},_s(e),_s(t))}function td(e,t){const n={};for(const s in e)t.includes(s)||Object.defineProperty(n,s,{enumerable:!0,get:()=>e[s]});return n}function nd(e){const t=it();let n=e();return Kr(),Pi(n)&&(n=n.catch(s=>{throw pn(t),s})),[n,()=>pn(t)]}let Ur=!0;function sd(e){const t=Io(e),n=e.proxy,s=e.ctx;Ur=!1,t.beforeCreate&&Al(t.beforeCreate,e,"bc");const{data:i,computed:r,methods:o,watch:l,provide:c,inject:f,created:a,beforeMount:u,mounted:p,beforeUpdate:d,updated:y,activated:E,deactivated:w,beforeDestroy:I,beforeUnmount:v,destroyed:g,unmounted:_,render:S,renderTracked:O,renderTriggered:V,errorCaptured:M,serverPrefetch:T,expose:C,inheritAttrs:x,components:b,directives:k,filters:K}=t;if(f&&id(f,s,null),o)for(const J in o){const X=o[J];G(X)&&(s[J]=X.bind(n))}if(i){const J=i.call(n,n);oe(J)&&(e.data=Fi(J))}if(Ur=!0,r)for(const J in r){const X=r[J],_e=G(X)?X.bind(n,n):G(X.get)?X.get.bind(n,n):Ae,ft=!G(X)&&G(X.set)?X.set.bind(n):Ae,rt=rf({get:_e,set:ft});Object.defineProperty(s,J,{enumerable:!0,configurable:!0,get:()=>rt.value,set:ut=>rt.value=ut})}if(l)for(const J in l)Na(l[J],s,n,J);if(c){const J=G(c)?c.call(n):c;Reflect.ownKeys(J).forEach(X=>{va(X,J[X])})}a&&Al(a,e,"c");function $(J,X){H(X)?X.forEach(_e=>J(_e.bind(n))):X&&J(X.bind(n))}if($(ma,u),$(Ms,p),$(No,d),$(Ki,y),$(pa,E),$(da,w),$(Ta,M),$(ya,O),$(Ea,V),$(Gi,v),$(Wi,_),$(_a,T),H(C))if(C.length){const J=e.exposed||(e.exposed={});C.forEach(X=>{Object.defineProperty(J,X,{get:()=>n[X],set:_e=>n[X]=_e})})}else e.exposed||(e.exposed={});S&&e.render===Ae&&(e.render=S),x!=null&&(e.inheritAttrs=x),b&&(e.components=b),k&&(e.directives=k),T&&bo(e)}function id(e,t,n=Ae){H(e)&&(e=Hr(e));for(const s in e){const i=e[s];let r;oe(i)?"default"in i?r=rs(i.from||s,i.default,!0):r=rs(i.from||s):r=rs(i),Ne(r)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>r.value,set:o=>r.value=o}):t[s]=r}}function Al(e,t,n){st(H(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Na(e,t,n,s){let i=s.includes(".")?Xa(n,s):()=>n[s];if(Y(e)){const r=t[e];G(r)&&Rn(i,r)}else if(G(e))Rn(i,e.bind(n));else if(oe(e))if(H(e))e.forEach(r=>Na(r,t,n,s));else{const r=G(e.handler)?e.handler.bind(n):t[e.handler];G(r)&&Rn(i,r,e)}}function Io(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:i,optionsCache:r,config:{optionMergeStrategies:o}}=e.appContext,l=r.get(t);let c;return l?c=l:!i.length&&!n&&!s?c=t:(c={},i.length&&i.forEach(f=>hi(c,f,o,!0)),hi(c,t,o)),oe(t)&&r.set(t,c),c}function hi(e,t,n,s=!1){const{mixins:i,extends:r}=t;r&&hi(e,r,n,!0),i&&i.forEach(o=>hi(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=rd[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const rd={data:Il,props:Ol,emits:Ol,methods:Zn,computed:Zn,beforeCreate:Ve,created:Ve,beforeMount:Ve,mounted:Ve,beforeUpdate:Ve,updated:Ve,beforeDestroy:Ve,beforeUnmount:Ve,destroyed:Ve,unmounted:Ve,activated:Ve,deactivated:Ve,errorCaptured:Ve,serverPrefetch:Ve,components:Zn,directives:Zn,watch:ld,provide:Il,inject:od};function Il(e,t){return t?e?function(){return ee(G(e)?e.call(this,this):e,G(t)?t.call(this,this):t)}:t:e}function od(e,t){return Zn(Hr(e),Hr(t))}function Hr(e){if(H(e)){const t={};for(let n=0;n1)return n&&G(t)?t.call(s&&s.proxy):t}}function fd(){return!!(Se||be||an)}const Aa={},Ia=()=>Object.create(Aa),Oa=e=>Object.getPrototypeOf(e)===Aa;function ud(e,t,n,s=!1){const i={},r=Ia();e.propsDefaults=Object.create(null),Ra(e,t,i,r);for(const o in e.propsOptions[0])o in i||(i[o]=void 0);n?e.props=s?i:Wc(i):e.type.props?e.props=i:e.props=r,e.attrs=r}function hd(e,t,n,s){const{props:i,attrs:r,vnode:{patchFlag:o}}=e,l=se(i),[c]=e.propsOptions;let f=!1;if((s||o>0)&&!(o&16)){if(o&8){const a=e.vnode.dynamicProps;for(let u=0;u{c=!0;const[p,d]=Ma(u,t,!0);ee(o,p),d&&l.push(...d)};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!r&&!c)return oe(e)&&s.set(e,sn),sn;if(H(r))for(let a=0;ae[0]==="_"||e==="$stable",Oo=e=>H(e)?e.map(Ge):[Ge(e)],dd=(e,t,n)=>{if(t._n)return t;const s=Eo((...i)=>Oo(t(...i)),n);return s._c=!1,s},La=(e,t,n)=>{const s=e._ctx;for(const i in e){if(Pa(i))continue;const r=e[i];if(G(r))t[i]=dd(i,r,s);else if(r!=null){const o=Oo(r);t[i]=()=>o}}},xa=(e,t)=>{const n=Oo(t);e.slots.default=()=>n},wa=(e,t,n)=>{for(const s in t)(n||s!=="_")&&(e[s]=t[s])},gd=(e,t,n)=>{const s=e.slots=Ia();if(e.vnode.shapeFlag&32){const i=t._;i?(wa(s,t,n),n&&ro(s,"_",i,!0)):La(t,s)}else t&&xa(e,t)},md=(e,t,n)=>{const{vnode:s,slots:i}=e;let r=!0,o=Z;if(s.shapeFlag&32){const l=t._;l?n&&l===1?r=!1:wa(i,t,n):(r=!t.$stable,La(t,i)),o=t}else t&&(xa(e,t),o={default:1});if(r)for(const l in i)!Pa(l)&&o[l]==null&&delete i[l]},Te=Wa;function Da(e){return Fa(e)}function ka(e){return Fa(e,Sp)}function Fa(e,t){const n=Is();n.__VUE__=!0;const{insert:s,remove:i,patchProp:r,createElement:o,createText:l,createComment:c,setText:f,setElementText:a,parentNode:u,nextSibling:p,setScopeId:d=Ae,insertStaticContent:y}=e,E=(h,m,N,P=null,A=null,R=null,U=void 0,F=null,D=!!m.dynamicChildren)=>{if(h===m)return;h&&!ct(h,m)&&(P=Vs(h),ut(h,A,R,!0),h=null),m.patchFlag===-2&&(D=!1,m.dynamicChildren=null);const{type:L,ref:W,shapeFlag:B}=m;switch(L){case Xt:w(h,m,N,P);break;case ye:I(h,m,N,P);break;case fn:h==null&&v(m,N,P,U);break;case ve:b(h,m,N,P,A,R,U,F,D);break;default:B&1?S(h,m,N,P,A,R,U,F,D):B&6?k(h,m,N,P,A,R,U,F,D):(B&64||B&128)&&L.process(h,m,N,P,A,R,U,F,D,Tn)}W!=null&&A&&ms(W,h&&h.ref,R,m||h,!m)},w=(h,m,N,P)=>{if(h==null)s(m.el=l(m.children),N,P);else{const A=m.el=h.el;m.children!==h.children&&f(A,m.children)}},I=(h,m,N,P)=>{h==null?s(m.el=c(m.children||""),N,P):m.el=h.el},v=(h,m,N,P)=>{[h.el,h.anchor]=y(h.children,m,N,P,h.el,h.anchor)},g=({el:h,anchor:m},N,P)=>{let A;for(;h&&h!==m;)A=p(h),s(h,N,P),h=A;s(m,N,P)},_=({el:h,anchor:m})=>{let N;for(;h&&h!==m;)N=p(h),i(h),h=N;i(m)},S=(h,m,N,P,A,R,U,F,D)=>{m.type==="svg"?U="svg":m.type==="math"&&(U="mathml"),h==null?O(m,N,P,A,R,U,F,D):T(h,m,A,R,U,F,D)},O=(h,m,N,P,A,R,U,F)=>{let D,L;const{props:W,shapeFlag:B,transition:j,dirs:z}=h;if(D=h.el=o(h.type,R,W&&W.is,W),B&8?a(D,h.children):B&16&&M(h.children,D,null,P,A,Ar(h,R),U,F),z&>(h,null,P,"created"),V(D,h,h.scopeId,U,P),W){for(const ce in W)ce!=="value"&&!vt(ce)&&r(D,ce,null,W[ce],R,P);"value"in W&&r(D,"value",null,W.value,R),(L=W.onVnodeBeforeMount)&&Ke(L,P,h)}z&>(h,null,P,"beforeMount");const te=Va(A,j);te&&j.beforeEnter(D),s(D,m,N),((L=W&&W.onVnodeMounted)||te||z)&&Te(()=>{L&&Ke(L,P,h),te&&j.enter(D),z&>(h,null,P,"mounted")},A)},V=(h,m,N,P,A)=>{if(N&&d(h,N),P)for(let R=0;R{for(let L=D;L{const F=m.el=h.el;let{patchFlag:D,dynamicChildren:L,dirs:W}=m;D|=h.patchFlag&16;const B=h.props||Z,j=m.props||Z;let z;if(N&&Zt(N,!1),(z=j.onVnodeBeforeUpdate)&&Ke(z,N,m,h),W&>(m,h,N,"beforeUpdate"),N&&Zt(N,!0),(B.innerHTML&&j.innerHTML==null||B.textContent&&j.textContent==null)&&a(F,""),L?C(h.dynamicChildren,L,F,N,P,Ar(m,A),R):U||X(h,m,F,null,N,P,Ar(m,A),R,!1),D>0){if(D&16)x(F,B,j,N,A);else if(D&2&&B.class!==j.class&&r(F,"class",null,j.class,A),D&4&&r(F,"style",B.style,j.style,A),D&8){const te=m.dynamicProps;for(let ce=0;ce{z&&Ke(z,N,m,h),W&>(m,h,N,"updated")},P)},C=(h,m,N,P,A,R,U)=>{for(let F=0;F{if(m!==N){if(m!==Z)for(const R in m)!vt(R)&&!(R in N)&&r(h,R,m[R],null,A,P);for(const R in N){if(vt(R))continue;const U=N[R],F=m[R];U!==F&&R!=="value"&&r(h,R,F,U,A,P)}"value"in N&&r(h,"value",m.value,N.value,A)}},b=(h,m,N,P,A,R,U,F,D)=>{const L=m.el=h?h.el:l(""),W=m.anchor=h?h.anchor:l("");let{patchFlag:B,dynamicChildren:j,slotScopeIds:z}=m;z&&(F=F?F.concat(z):z),h==null?(s(L,N,P),s(W,N,P),M(m.children||[],N,W,A,R,U,F,D)):B>0&&B&64&&j&&h.dynamicChildren?(C(h.dynamicChildren,j,N,A,R,U,F),(m.key!=null||A&&m===A.subTree)&&Ro(h,m,!0)):X(h,m,N,W,A,R,U,F,D)},k=(h,m,N,P,A,R,U,F,D)=>{m.slotScopeIds=F,h==null?m.shapeFlag&512?A.ctx.activate(m,N,P,U,D):K(m,N,P,A,R,U,D):q(h,m,D)},K=(h,m,N,P,A,R,U)=>{const F=h.component=Za(h,P,A);if(Rs(h)&&(F.ctx.renderer=Tn),tf(F,!1,U),F.asyncDep){if(A&&A.registerDep(F,$,U),!h.el){const D=F.subTree=he(ye);I(null,D,m,N)}}else $(F,h,m,N,A,R,U)},q=(h,m,N)=>{const P=m.component=h.component;if(Ad(h,m,N))if(P.asyncDep&&!P.asyncResolved){J(P,m,N);return}else P.next=m,P.update();else m.el=h.el,P.vnode=m},$=(h,m,N,P,A,R,U)=>{const F=()=>{if(h.isMounted){let{next:B,bu:j,u:z,parent:te,vnode:ce}=h;{const Ye=Ua(h);if(Ye){B&&(B.el=ce.el,J(h,B,U)),Ye.asyncDep.then(()=>{h.isUnmounted||F()});return}}let le=B,qe;Zt(h,!1),B?(B.el=ce.el,J(h,B,U)):B=ce,j&&ln(j),(qe=B.props&&B.props.onVnodeBeforeUpdate)&&Ke(qe,te,B,ce),Zt(h,!0);const Re=Zs(h),ot=h.subTree;h.subTree=Re,E(ot,Re,u(ot.el),Vs(ot),h,A,R),B.el=Re.el,le===null&&Yi(h,Re.el),z&&Te(z,A),(qe=B.props&&B.props.onVnodeUpdated)&&Te(()=>Ke(qe,te,B,ce),A)}else{let B;const{el:j,props:z}=m,{bm:te,m:ce,parent:le,root:qe,type:Re}=h,ot=$t(m);if(Zt(h,!1),te&&ln(te),!ot&&(B=z&&z.onVnodeBeforeMount)&&Ke(B,le,m),Zt(h,!0),j&&mr){const Ye=()=>{h.subTree=Zs(h),mr(j,h.subTree,h,A,null)};ot&&Re.__asyncHydrate?Re.__asyncHydrate(j,h,Ye):Ye()}else{qe.ce&&qe.ce._injectChildStyle(Re);const Ye=h.subTree=Zs(h);E(null,Ye,N,P,h,A,R),m.el=Ye.el}if(ce&&Te(ce,A),!ot&&(B=z&&z.onVnodeMounted)){const Ye=m;Te(()=>Ke(B,le,Ye),A)}(m.shapeFlag&256||le&&$t(le.vnode)&&le.vnode.shapeFlag&256)&&h.a&&Te(h.a,A),h.isMounted=!0,m=N=P=null}};h.scope.on();const D=h.effect=new fs(F);h.scope.off();const L=h.update=D.run.bind(D),W=h.job=D.runIfDirty.bind(D);W.i=h,W.id=h.uid,D.scheduler=()=>_o(W),Zt(h,!0),L()},J=(h,m,N)=>{m.component=h;const P=h.vnode.props;h.vnode=m,h.next=null,hd(h,m.props,P,N),md(h,m.children,N),zt(),El(h),Qt()},X=(h,m,N,P,A,R,U,F,D=!1)=>{const L=h&&h.children,W=h?h.shapeFlag:0,B=m.children,{patchFlag:j,shapeFlag:z}=m;if(j>0){if(j&128){ft(L,B,N,P,A,R,U,F,D);return}else if(j&256){_e(L,B,N,P,A,R,U,F,D);return}}z&8?(W&16&&Wn(L,A,R),B!==L&&a(N,B)):W&16?z&16?ft(L,B,N,P,A,R,U,F,D):Wn(L,A,R,!0):(W&8&&a(N,""),z&16&&M(B,N,P,A,R,U,F,D))},_e=(h,m,N,P,A,R,U,F,D)=>{h=h||sn,m=m||sn;const L=h.length,W=m.length,B=Math.min(L,W);let j;for(j=0;jW?Wn(h,A,R,!0,!1,B):M(m,N,P,A,R,U,F,D,B)},ft=(h,m,N,P,A,R,U,F,D)=>{let L=0;const W=m.length;let B=h.length-1,j=W-1;for(;L<=B&&L<=j;){const z=h[L],te=m[L]=D?Vt(m[L]):Ge(m[L]);if(ct(z,te))E(z,te,N,null,A,R,U,F,D);else break;L++}for(;L<=B&&L<=j;){const z=h[B],te=m[j]=D?Vt(m[j]):Ge(m[j]);if(ct(z,te))E(z,te,N,null,A,R,U,F,D);else break;B--,j--}if(L>B){if(L<=j){const z=j+1,te=zj)for(;L<=B;)ut(h[L],A,R,!0),L++;else{const z=L,te=L,ce=new Map;for(L=te;L<=j;L++){const Je=m[L]=D?Vt(m[L]):Ge(m[L]);Je.key!=null&&ce.set(Je.key,L)}let le,qe=0;const Re=j-te+1;let ot=!1,Ye=0;const qn=new Array(Re);for(L=0;L=Re){ut(Je,A,R,!0);continue}let ht;if(Je.key!=null)ht=ce.get(Je.key);else for(le=te;le<=j;le++)if(qn[le-te]===0&&ct(Je,m[le])){ht=le;break}ht===void 0?ut(Je,A,R,!0):(qn[ht-te]=L+1,ht>=Ye?Ye=ht:ot=!0,E(Je,m[ht],N,null,A,R,U,F,D),qe++)}const hl=ot?_d(qn):sn;for(le=hl.length-1,L=Re-1;L>=0;L--){const Je=te+L,ht=m[Je],pl=Je+1{const{el:R,type:U,transition:F,children:D,shapeFlag:L}=h;if(L&6){rt(h.component.subTree,m,N,P);return}if(L&128){h.suspense.move(m,N,P);return}if(L&64){U.move(h,m,N,Tn);return}if(U===ve){s(R,m,N);for(let B=0;BF.enter(R),A);else{const{leave:B,delayLeave:j,afterLeave:z}=F,te=()=>s(R,m,N),ce=()=>{B(R,()=>{te(),z&&z()})};j?j(R,te,ce):ce()}else s(R,m,N)},ut=(h,m,N,P=!1,A=!1)=>{const{type:R,props:U,ref:F,children:D,dynamicChildren:L,shapeFlag:W,patchFlag:B,dirs:j,cacheIndex:z}=h;if(B===-2&&(A=!1),F!=null&&ms(F,null,N,h,!0),z!=null&&(m.renderCache[z]=void 0),W&256){m.ctx.deactivate(h);return}const te=W&1&&j,ce=!$t(h);let le;if(ce&&(le=U&&U.onVnodeBeforeUnmount)&&Ke(le,m,h),W&6)Du(h.component,N,P);else{if(W&128){h.suspense.unmount(N,P);return}te&>(h,null,m,"beforeUnmount"),W&64?h.type.remove(h,m,N,Tn,P):L&&!L.hasOnce&&(R!==ve||B>0&&B&64)?Wn(L,m,N,!1,!0):(R===ve&&B&384||!A&&W&16)&&Wn(D,m,N),P&&fl(h)}(ce&&(le=U&&U.onVnodeUnmounted)||te)&&Te(()=>{le&&Ke(le,m,h),te&>(h,null,m,"unmounted")},N)},fl=h=>{const{type:m,el:N,anchor:P,transition:A}=h;if(m===ve){wu(N,P);return}if(m===fn){_(h);return}const R=()=>{i(N),A&&!A.persisted&&A.afterLeave&&A.afterLeave()};if(h.shapeFlag&1&&A&&!A.persisted){const{leave:U,delayLeave:F}=A,D=()=>U(N,R);F?F(h.el,R,D):D()}else R()},wu=(h,m)=>{let N;for(;h!==m;)N=p(h),i(h),h=N;i(m)},Du=(h,m,N)=>{const{bum:P,scope:A,job:R,subTree:U,um:F,m:D,a:L}=h;pi(D),pi(L),P&&ln(P),A.stop(),R&&(R.flags|=8,ut(U,h,m,N)),F&&Te(F,m),Te(()=>{h.isUnmounted=!0},m),m&&m.pendingBranch&&!m.isUnmounted&&h.asyncDep&&!h.asyncResolved&&h.suspenseId===m.pendingId&&(m.deps--,m.deps===0&&m.resolve())},Wn=(h,m,N,P=!1,A=!1,R=0)=>{for(let U=R;U{if(h.shapeFlag&6)return Vs(h.component.subTree);if(h.shapeFlag&128)return h.suspense.next();const m=p(h.anchor||h.el),N=m&&m[ra];return N?p(N):m};let dr=!1;const ul=(h,m,N)=>{h==null?m._vnode&&ut(m._vnode,null,null,!0):E(m._vnode||null,h,m,null,null,null,N),m._vnode=h,dr||(dr=!0,El(),ui(),dr=!1)},Tn={p:E,um:ut,m:rt,r:fl,mt:K,mc:M,pc:X,pbc:C,n:Vs,o:e};let gr,mr;return t&&([gr,mr]=t(Tn)),{render:ul,hydrate:gr,createApp:ad(ul,gr)}}function Ar({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function Zt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Va(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Ro(e,t,n=!1){const s=e.children,i=t.children;if(H(s)&&H(i))for(let r=0;r>1,e[n[l]]0&&(t[s]=n[r-1]),n[r]=s)}}for(r=n.length,o=n[r-1];r-- >0;)n[r]=o,o=t[o];return n}function Ua(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Ua(t)}function pi(e){if(e)for(let t=0;trs(Ha);function Ed(e,t){return Ps(e,null,t)}function yd(e,t){return Ps(e,null,{flush:"post"})}function $a(e,t){return Ps(e,null,{flush:"sync"})}function Rn(e,t,n){return Ps(e,t,n)}function Ps(e,t,n=Z){const{immediate:s,deep:i,flush:r,once:o}=n,l=ee({},n),c=t&&s||!t&&r!=="post";let f;if(xn){if(r==="sync"){const d=Ba();f=d.__watcherHandles||(d.__watcherHandles=[])}else if(!c){const d=()=>{};return d.stop=Ae,d.resume=Ae,d.pause=Ae,d}}const a=Se;l.call=(d,y,E)=>st(d,a,y,E);let u=!1;r==="post"?l.scheduler=d=>{Te(d,a&&a.suspense)}:r!=="sync"&&(u=!0,l.scheduler=(d,y)=>{y?d():_o(d)}),l.augmentJob=d=>{t&&(d.flags|=4),u&&(d.flags|=2,a&&(d.id=a.uid,d.i=a))};const p=np(e,t,l);return xn&&(f?f.push(p):c&&p()),p}function Td(e,t,n){const s=this.proxy,i=Y(e)?e.includes(".")?Xa(s,e):()=>s[e]:e.bind(s,s);let r;G(t)?r=t:(r=t.handler,n=t);const o=pn(this),l=Ps(i,r.bind(s),n);return o(),l}function Xa(e,t){const n=t.split(".");return()=>{let s=e;for(let i=0;i{let a,u=Z,p;return $a(()=>{const d=e[i];Le(a,d)&&(a=d,f())}),{get(){return c(),n.get?n.get(a):a},set(d){const y=n.set?n.set(d):d;if(!Le(y,a)&&!(u!==Z&&Le(d,u)))return;const E=s.vnode.props;E&&(t in E||i in E||r in E)&&(`onUpdate:${t}`in E||`onUpdate:${i}`in E||`onUpdate:${r}`in E)||(a=d,f()),s.emit(`update:${t}`,y),Le(d,y)&&Le(d,u)&&!Le(y,p)&&f(),u=d,p=y}}});return l[Symbol.iterator]=()=>{let c=0;return{next(){return c<2?{value:c++?o||Z:l,done:!1}:{done:!0}}}},l}const ja=(e,t)=>t==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${fe(t)}Modifiers`]||e[`${De(t)}Modifiers`];function bd(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||Z;let i=n;const r=t.startsWith("update:"),o=r&&ja(s,t.slice(7));o&&(o.trim&&(i=n.map(a=>Y(a)?a.trim():a)),o.number&&(i=n.map(cs)));let l,c=s[l=on(t)]||s[l=on(fe(t))];!c&&r&&(c=s[l=on(De(t))]),c&&st(c,e,6,i);const f=s[l+"Once"];if(f){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,st(f,e,6,i)}}function Ka(e,t,n=!1){const s=t.emitsCache,i=s.get(e);if(i!==void 0)return i;const r=e.emits;let o={},l=!1;if(!G(e)){const c=f=>{const a=Ka(f,t,!0);a&&(l=!0,ee(o,a))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!r&&!l?(oe(e)&&s.set(e,null),null):(H(r)?r.forEach(c=>o[c]=null):ee(o,r),oe(e)&&s.set(e,o),o)}function qi(e,t){return!e||!qt(t)?!1:(t=t.slice(2).replace(/Once$/,""),ie(e,t[0].toLowerCase()+t.slice(1))||ie(e,De(t))||ie(e,t))}function Zs(e){const{type:t,vnode:n,proxy:s,withProxy:i,propsOptions:[r],slots:o,attrs:l,emit:c,render:f,renderCache:a,props:u,data:p,setupState:d,ctx:y,inheritAttrs:E}=e,w=gs(e);let I,v;try{if(n.shapeFlag&4){const _=i||s,S=_;I=Ge(f.call(S,_,a,u,d,p,y)),v=l}else{const _=t;I=Ge(_.length>1?_(u,{attrs:l,slots:o,emit:c}):_(u,null)),v=t.props?l:Cd(l)}}catch(_){os.length=0,yn(_,e,1),I=he(ye)}let g=I;if(v&&E!==!1){const _=Object.keys(v),{shapeFlag:S}=g;_.length&&S&7&&(r&&_.some(Ri)&&(v=vd(v,r)),g=mt(g,v,!1,!0))}return n.dirs&&(g=mt(g,null,!1,!0),g.dirs=g.dirs?g.dirs.concat(n.dirs):n.dirs),n.transition&&Rt(g,n.transition),I=g,gs(w),I}function Nd(e,t=!0){let n;for(let s=0;s{let t;for(const n in e)(n==="class"||n==="style"||qt(n))&&((t||(t={}))[n]=e[n]);return t},vd=(e,t)=>{const n={};for(const s in e)(!Ri(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function Ad(e,t,n){const{props:s,children:i,component:r}=e,{props:o,children:l,patchFlag:c}=t,f=r.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?Ml(s,o,f):!!o;if(c&8){const a=t.dynamicProps;for(let u=0;ue.__isSuspense;let $r=0;const Id={name:"Suspense",__isSuspense:!0,process(e,t,n,s,i,r,o,l,c,f){if(e==null)Rd(t,n,s,i,r,o,l,c,f);else{if(r&&r.deps>0&&!e.suspense.isInFallback){t.suspense=e.suspense,t.suspense.vnode=t,t.el=e.el;return}Md(e,t,n,s,i,o,l,c,f)}},hydrate:Pd,normalize:Ld},Od=Id;function Es(e,t){const n=e.props&&e.props[t];G(n)&&n()}function Rd(e,t,n,s,i,r,o,l,c){const{p:f,o:{createElement:a}}=c,u=a("div"),p=e.suspense=Ga(e,i,s,t,u,n,r,o,l,c);f(null,p.pendingBranch=e.ssContent,u,null,s,p,r,o),p.deps>0?(Es(e,"onPending"),Es(e,"onFallback"),f(null,e.ssFallback,t,n,s,null,r,o),Mn(p,e.ssFallback)):p.resolve(!1,!0)}function Md(e,t,n,s,i,r,o,l,{p:c,um:f,o:{createElement:a}}){const u=t.suspense=e.suspense;u.vnode=t,t.el=e.el;const p=t.ssContent,d=t.ssFallback,{activeBranch:y,pendingBranch:E,isInFallback:w,isHydrating:I}=u;if(E)u.pendingBranch=p,ct(p,E)?(c(E,p,u.hiddenContainer,null,i,u,r,o,l),u.deps<=0?u.resolve():w&&(I||(c(y,d,n,s,i,null,r,o,l),Mn(u,d)))):(u.pendingId=$r++,I?(u.isHydrating=!1,u.activeBranch=E):f(E,i,u),u.deps=0,u.effects.length=0,u.hiddenContainer=a("div"),w?(c(null,p,u.hiddenContainer,null,i,u,r,o,l),u.deps<=0?u.resolve():(c(y,d,n,s,i,null,r,o,l),Mn(u,d))):y&&ct(p,y)?(c(y,p,n,s,i,u,r,o,l),u.resolve(!0)):(c(null,p,u.hiddenContainer,null,i,u,r,o,l),u.deps<=0&&u.resolve()));else if(y&&ct(p,y))c(y,p,n,s,i,u,r,o,l),Mn(u,p);else if(Es(t,"onPending"),u.pendingBranch=p,p.shapeFlag&512?u.pendingId=p.component.suspenseId:u.pendingId=$r++,c(null,p,u.hiddenContainer,null,i,u,r,o,l),u.deps<=0)u.resolve();else{const{timeout:v,pendingId:g}=u;v>0?setTimeout(()=>{u.pendingId===g&&u.fallback(d)},v):v===0&&u.fallback(d)}}function Ga(e,t,n,s,i,r,o,l,c,f,a=!1){const{p:u,m:p,um:d,n:y,o:{parentNode:E,remove:w}}=f;let I;const v=xd(e);v&&t&&t.pendingBranch&&(I=t.pendingId,t.deps++);const g=e.props?as(e.props.timeout):void 0,_=r,S={vnode:e,parent:t,parentComponent:n,namespace:o,container:s,hiddenContainer:i,deps:0,pendingId:$r++,timeout:typeof g=="number"?g:-1,activeBranch:null,pendingBranch:null,isInFallback:!a,isHydrating:a,isUnmounted:!1,effects:[],resolve(O=!1,V=!1){const{vnode:M,activeBranch:T,pendingBranch:C,pendingId:x,effects:b,parentComponent:k,container:K}=S;let q=!1;S.isHydrating?S.isHydrating=!1:O||(q=T&&C.transition&&C.transition.mode==="out-in",q&&(T.transition.afterLeave=()=>{x===S.pendingId&&(p(C,K,r===_?y(T):r,0),ps(b))}),T&&(E(T.el)===K&&(r=y(T)),d(T,k,S,!0)),q||p(C,K,r,0)),Mn(S,C),S.pendingBranch=null,S.isInFallback=!1;let $=S.parent,J=!1;for(;$;){if($.pendingBranch){$.effects.push(...b),J=!0;break}$=$.parent}!J&&!q&&ps(b),S.effects=[],v&&t&&t.pendingBranch&&I===t.pendingId&&(t.deps--,t.deps===0&&!V&&t.resolve()),Es(M,"onResolve")},fallback(O){if(!S.pendingBranch)return;const{vnode:V,activeBranch:M,parentComponent:T,container:C,namespace:x}=S;Es(V,"onFallback");const b=y(M),k=()=>{S.isInFallback&&(u(null,O,C,b,T,null,x,l,c),Mn(S,O))},K=O.transition&&O.transition.mode==="out-in";K&&(M.transition.afterLeave=k),S.isInFallback=!0,d(M,T,null,!0),K||k()},move(O,V,M){S.activeBranch&&p(S.activeBranch,O,V,M),S.container=O},next(){return S.activeBranch&&y(S.activeBranch)},registerDep(O,V,M){const T=!!S.pendingBranch;T&&S.deps++;const C=O.vnode.el;O.asyncDep.catch(x=>{yn(x,O,0)}).then(x=>{if(O.isUnmounted||S.isUnmounted||S.pendingId!==O.suspenseId)return;O.asyncResolved=!0;const{vnode:b}=O;Gr(O,x,!1),C&&(b.el=C);const k=!C&&O.subTree.el;V(O,b,E(C||O.subTree.el),C?null:y(O.subTree),S,o,M),k&&w(k),Yi(O,b.el),T&&--S.deps===0&&S.resolve()})},unmount(O,V){S.isUnmounted=!0,S.activeBranch&&d(S.activeBranch,n,O,V),S.pendingBranch&&d(S.pendingBranch,n,O,V)}};return S}function Pd(e,t,n,s,i,r,o,l,c){const f=t.suspense=Ga(t,s,n,e.parentNode,document.createElement("div"),null,i,r,o,l,!0),a=c(e,f.pendingBranch=t.ssContent,n,f,r,o);return f.deps===0&&f.resolve(!1,!0),a}function Ld(e){const{shapeFlag:t,children:n}=e,s=t&32;e.ssContent=Pl(s?n.default:n),e.ssFallback=s?Pl(n.fallback):he(ye)}function Pl(e){let t;if(G(e)){const n=hn&&e._c;n&&(e._d=!1,ys()),e=e(),n&&(e._d=!0,t=ke,qa())}return H(e)&&(e=Nd(e)),e=Ge(e),t&&!e.dynamicChildren&&(e.dynamicChildren=t.filter(n=>n!==e)),e}function Wa(e,t){t&&t.pendingBranch?H(e)?t.effects.push(...e):t.effects.push(e):ps(e)}function Mn(e,t){e.activeBranch=t;const{vnode:n,parentComponent:s}=e;let i=t.el;for(;!i&&t.component;)t=t.component.subTree,i=t.el;n.el=i,s&&s.subTree===n&&(s.vnode.el=i,Yi(s,i))}function xd(e){const t=e.props&&e.props.suspensible;return t!=null&&t!==!1}const ve=Symbol.for("v-fgt"),Xt=Symbol.for("v-txt"),ye=Symbol.for("v-cmt"),fn=Symbol.for("v-stc"),os=[];let ke=null;function ys(e=!1){os.push(ke=e?null:[])}function qa(){os.pop(),ke=os[os.length-1]||null}let hn=1;function Xr(e,t=!1){hn+=e,e<0&&ke&&t&&(ke.hasOnce=!0)}function Ya(e){return e.dynamicChildren=hn>0?ke||sn:null,qa(),hn>0&&ke&&ke.push(e),e}function wd(e,t,n,s,i,r){return Ya(Mo(e,t,n,s,i,r,!0))}function gi(e,t,n,s,i){return Ya(he(e,t,n,s,i,!0))}function Mt(e){return e?e.__v_isVNode===!0:!1}function ct(e,t){return e.type===t.type&&e.key===t.key}function Dd(e){}const Ja=({key:e})=>e??null,ei=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?Y(e)||Ne(e)||G(e)?{i:be,r:e,k:t,f:!!n}:e:null);function Mo(e,t=null,n=null,s=0,i=null,r=e===ve?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Ja(t),ref:t&&ei(t),scopeId:$i,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:r,patchFlag:s,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:be};return l?(Lo(c,n),r&128&&e.normalize(c)):n&&(c.shapeFlag|=Y(n)?8:16),hn>0&&!o&&ke&&(c.patchFlag>0||r&6)&&c.patchFlag!==32&&ke.push(c),c}const he=kd;function kd(e,t=null,n=null,s=0,i=null,r=!1){if((!e||e===Sa)&&(e=ye),Mt(e)){const l=mt(e,t,!0);return n&&Lo(l,n),hn>0&&!r&&ke&&(l.shapeFlag&6?ke[ke.indexOf(e)]=l:ke.push(l)),l.patchFlag=-2,l}if(Kd(e)&&(e=e.__vccOpts),t){t=za(t);let{class:l,style:c}=t;l&&!Y(l)&&(t.class=Xn(l)),oe(c)&&(Ui(c)&&!H(c)&&(c=ee({},c)),t.style=$n(c))}const o=Y(e)?1:di(e)?128:oa(e)?64:oe(e)?4:G(e)?2:0;return Mo(e,t,n,s,i,o,r,!0)}function za(e){return e?Ui(e)||Oa(e)?ee({},e):e:null}function mt(e,t,n=!1,s=!1){const{props:i,ref:r,patchFlag:o,children:l,transition:c}=e,f=t?Qa(i||{},t):i,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:f,key:f&&Ja(f),ref:t&&t.ref?n&&r?H(r)?r.concat(ei(t)):[r,ei(t)]:ei(t):r,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ve?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&mt(e.ssContent),ssFallback:e.ssFallback&&mt(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&Rt(a,c.clone(a)),a}function Po(e=" ",t=0){return he(Xt,null,e,t)}function Fd(e,t){const n=he(fn,null,e);return n.staticCount=t,n}function Vd(e="",t=!1){return t?(ys(),gi(ye,null,e)):he(ye,null,e)}function Ge(e){return e==null||typeof e=="boolean"?he(ye):H(e)?he(ve,null,e.slice()):Mt(e)?Vt(e):he(Xt,null,String(e))}function Vt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:mt(e)}function Lo(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(H(t))n=16;else if(typeof t=="object")if(s&65){const i=t.default;i&&(i._c&&(i._d=!1),Lo(e,i()),i._c&&(i._d=!0));return}else{n=32;const i=t._;!i&&!Oa(t)?t._ctx=be:i===3&&be&&(be.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else G(t)?(t={default:t,_ctx:be},n=32):(t=String(t),s&64?(n=16,t=[Po(t)]):n=8);e.children=t,e.shapeFlag|=n}function Qa(...e){const t={};for(let n=0;nSe||be;let mi,jr;{const e=Is(),t=(n,s)=>{let i;return(i=e[n])||(i=e[n]=[]),i.push(s),r=>{i.length>1?i.forEach(o=>o(r)):i[0](r)}};mi=t("__VUE_INSTANCE_SETTERS__",n=>Se=n),jr=t("__VUE_SSR_SETTERS__",n=>xn=n)}const pn=e=>{const t=Se;return mi(e),e.scope.on(),()=>{e.scope.off(),mi(t)}},Kr=()=>{Se&&Se.scope.off(),mi(null)};function ef(e){return e.vnode.shapeFlag&4}let xn=!1;function tf(e,t=!1,n=!1){t&&jr(t);const{props:s,children:i}=e.vnode,r=ef(e);ud(e,s,r,t),gd(e,i,n);const o=r?Bd(e,t):void 0;return t&&jr(!1),o}function Bd(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Vr);const{setup:s}=n;if(s){zt();const i=e.setupContext=s.length>1?sf(e):null,r=pn(e),o=jn(s,e,0,[e.props,i]),l=Pi(o);if(Qt(),r(),(l||e.sp)&&!$t(e)&&bo(e),l){if(o.then(Kr,Kr),t)return o.then(c=>{Gr(e,c,t)}).catch(c=>{yn(c,e,0)});e.asyncDep=o}else Gr(e,o,t)}else nf(e,t)}function Gr(e,t,n){G(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:oe(t)&&(e.setupState=mo(t)),nf(e,n)}let _i,Wr;function $d(e){_i=e,Wr=t=>{t.render._rc&&(t.withProxy=new Proxy(t.ctx,Xp))}}const Xd=()=>!_i;function nf(e,t,n){const s=e.type;if(!e.render){if(!t&&_i&&!s.render){const i=s.template||Io(e).template;if(i){const{isCustomElement:r,compilerOptions:o}=e.appContext.config,{delimiters:l,compilerOptions:c}=s,f=ee(ee({isCustomElement:r,delimiters:l},o),c);s.render=_i(i,f)}}e.render=s.render||Ae,Wr&&Wr(e)}{const i=pn(e);zt();try{sd(e)}finally{Qt(),i()}}}const jd={get(e,t){return xe(e,"get",""),e[t]}};function sf(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,jd),slots:e.slots,emit:e.emit,expose:t}}function Ls(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(mo(qc(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in is)return is[n](e)},has(t,n){return n in t||n in is}})):e.proxy}function qr(e,t=!0){return G(e)?e.displayName||e.name:e.name||t&&e.__name}function Kd(e){return G(e)&&"__vccOpts"in e}const rf=(e,t)=>Qh(e,t,xn);function of(e,t,n){const s=arguments.length;return s===2?oe(t)&&!H(t)?Mt(t)?he(e,null,[t]):he(e,t):he(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&Mt(n)&&(n=[n]),he(e,t,n))}function Gd(){}function Wd(e,t,n,s){const i=n[s];if(i&&lf(i,e))return i;const r=t();return r.memo=e.slice(),r.cacheIndex=s,n[s]=r}function lf(e,t){const n=e.memo;if(n.length!=t.length)return!1;for(let s=0;s0&&ke&&ke.push(e),!0}const cf="3.5.13",qd=Ae,Yd=lp,Jd=vn,zd=ia,Qd={createComponentInstance:Za,setupComponent:tf,renderComponentRoot:Zs,setCurrentRenderingInstance:gs,isVNode:Mt,normalizeVNode:Ge,getComponentPublicInstance:Ls,ensureValidVNode:Ao,pushWarningContext:sp,popWarningContext:ip},Zd=Qd,eg=null,tg=null,ng=null;/** +* @vue/runtime-dom v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Yr;const Ll=typeof window<"u"&&window.trustedTypes;if(Ll)try{Yr=Ll.createPolicy("vue",{createHTML:e=>e})}catch{}const af=Yr?e=>Yr.createHTML(e):e=>e,sg="http://www.w3.org/2000/svg",ig="http://www.w3.org/1998/Math/MathML",Tt=typeof document<"u"?document:null,xl=Tt&&Tt.createElement("template"),rg={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const i=t==="svg"?Tt.createElementNS(sg,e):t==="mathml"?Tt.createElementNS(ig,e):n?Tt.createElement(e,{is:n}):Tt.createElement(e);return e==="select"&&s&&s.multiple!=null&&i.setAttribute("multiple",s.multiple),i},createText:e=>Tt.createTextNode(e),createComment:e=>Tt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Tt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,i,r){const o=n?n.previousSibling:t.lastChild;if(i&&(i===r||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),n),!(i===r||!(i=i.nextSibling)););else{xl.innerHTML=af(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=xl.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Lt="transition",Jn="animation",wn=Symbol("_vtc"),ff={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},uf=ee({},To,ff),og=e=>(e.displayName="Transition",e.props=uf,e),lg=og((e,{slots:t})=>of(ua,hf(e),t)),en=(e,t=[])=>{H(e)?e.forEach(n=>n(...t)):e&&e(...t)},wl=e=>e?H(e)?e.some(t=>t.length>1):e.length>1:!1;function hf(e){const t={};for(const b in e)b in ff||(t[b]=e[b]);if(e.css===!1)return t;const{name:n="v",type:s,duration:i,enterFromClass:r=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:c=r,appearActiveClass:f=o,appearToClass:a=l,leaveFromClass:u=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:d=`${n}-leave-to`}=e,y=cg(i),E=y&&y[0],w=y&&y[1],{onBeforeEnter:I,onEnter:v,onEnterCancelled:g,onLeave:_,onLeaveCancelled:S,onBeforeAppear:O=I,onAppear:V=v,onAppearCancelled:M=g}=t,T=(b,k,K,q)=>{b._enterCancelled=q,wt(b,k?a:l),wt(b,k?f:o),K&&K()},C=(b,k)=>{b._isLeaving=!1,wt(b,u),wt(b,d),wt(b,p),k&&k()},x=b=>(k,K)=>{const q=b?V:v,$=()=>T(k,b,K);en(q,[k,$]),Dl(()=>{wt(k,b?c:r),pt(k,b?a:l),wl(q)||kl(k,s,E,$)})};return ee(t,{onBeforeEnter(b){en(I,[b]),pt(b,r),pt(b,o)},onBeforeAppear(b){en(O,[b]),pt(b,c),pt(b,f)},onEnter:x(!1),onAppear:x(!0),onLeave(b,k){b._isLeaving=!0;const K=()=>C(b,k);pt(b,u),b._enterCancelled?(pt(b,p),Jr()):(Jr(),pt(b,p)),Dl(()=>{b._isLeaving&&(wt(b,u),pt(b,d),wl(_)||kl(b,s,w,K))}),en(_,[b,K])},onEnterCancelled(b){T(b,!1,void 0,!0),en(g,[b])},onAppearCancelled(b){T(b,!0,void 0,!0),en(M,[b])},onLeaveCancelled(b){C(b),en(S,[b])}})}function cg(e){if(e==null)return null;if(oe(e))return[Ir(e.enter),Ir(e.leave)];{const t=Ir(e);return[t,t]}}function Ir(e){return as(e)}function pt(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[wn]||(e[wn]=new Set)).add(t)}function wt(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const n=e[wn];n&&(n.delete(t),n.size||(e[wn]=void 0))}function Dl(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let ag=0;function kl(e,t,n,s){const i=e._endId=++ag,r=()=>{i===e._endId&&s()};if(n!=null)return setTimeout(r,n);const{type:o,timeout:l,propCount:c}=pf(e,t);if(!o)return s();const f=o+"end";let a=0;const u=()=>{e.removeEventListener(f,p),r()},p=d=>{d.target===e&&++a>=c&&u()};setTimeout(()=>{a(n[y]||"").split(", "),i=s(`${Lt}Delay`),r=s(`${Lt}Duration`),o=Fl(i,r),l=s(`${Jn}Delay`),c=s(`${Jn}Duration`),f=Fl(l,c);let a=null,u=0,p=0;t===Lt?o>0&&(a=Lt,u=o,p=r.length):t===Jn?f>0&&(a=Jn,u=f,p=c.length):(u=Math.max(o,f),a=u>0?o>f?Lt:Jn:null,p=a?a===Lt?r.length:c.length:0);const d=a===Lt&&/\b(transform|all)(,|$)/.test(s(`${Lt}Property`).toString());return{type:a,timeout:u,propCount:p,hasTransform:d}}function Fl(e,t){for(;e.lengthVl(n)+Vl(e[s])))}function Vl(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Jr(){return document.body.offsetHeight}function fg(e,t,n){const s=e[wn];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Ei=Symbol("_vod"),df=Symbol("_vsh"),gf={beforeMount(e,{value:t},{transition:n}){e[Ei]=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):zn(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:s}){!t!=!n&&(s?t?(s.beforeEnter(e),zn(e,!0),s.enter(e)):s.leave(e,()=>{zn(e,!1)}):zn(e,t))},beforeUnmount(e,{value:t}){zn(e,t)}};function zn(e,t){e.style.display=t?e[Ei]:"none",e[df]=!t}function ug(){gf.getSSRProps=({value:e})=>{if(!e)return{style:{display:"none"}}}}const mf=Symbol("");function hg(e){const t=it();if(!t)return;const n=t.ut=(i=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach(r=>yi(r,i))},s=()=>{const i=e(t.proxy);t.ce?yi(t.ce,i):zr(t.subTree,i),n(i)};No(()=>{ps(s)}),Ms(()=>{Rn(s,Ae,{flush:"post"});const i=new MutationObserver(s);i.observe(t.subTree.el.parentNode,{childList:!0}),Wi(()=>i.disconnect())})}function zr(e,t){if(e.shapeFlag&128){const n=e.suspense;e=n.activeBranch,n.pendingBranch&&!n.isHydrating&&n.effects.push(()=>{zr(n.activeBranch,t)})}for(;e.component;)e=e.component.subTree;if(e.shapeFlag&1&&e.el)yi(e.el,t);else if(e.type===ve)e.children.forEach(n=>zr(n,t));else if(e.type===fn){let{el:n,anchor:s}=e;for(;n&&(yi(n,t),n!==s);)n=n.nextSibling}}function yi(e,t){if(e.nodeType===1){const n=e.style;let s="";for(const i in t)n.setProperty(`--${i}`,t[i]),s+=`--${i}: ${t[i]};`;n[mf]=s}}const pg=/(^|;)\s*display\s*:/;function dg(e,t,n){const s=e.style,i=Y(n);let r=!1;if(n&&!i){if(t)if(Y(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();n[l]==null&&ti(s,l,"")}else for(const o in t)n[o]==null&&ti(s,o,"");for(const o in n)o==="display"&&(r=!0),ti(s,o,n[o])}else if(i){if(t!==n){const o=s[mf];o&&(n+=";"+o),s.cssText=n,r=pg.test(n)}}else t&&e.removeAttribute("style");Ei in e&&(e[Ei]=r?s.display:"",e[df]&&(s.display="none"))}const Ul=/\s*!important$/;function ti(e,t,n){if(H(n))n.forEach(s=>ti(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=gg(e,t);Ul.test(n)?e.setProperty(De(s),n.replace(Ul,""),"important"):e[s]=n}}const Hl=["Webkit","Moz","ms"],Or={};function gg(e,t){const n=Or[t];if(n)return n;let s=fe(t);if(s!=="filter"&&s in e)return Or[t]=s;s=Jt(s);for(let i=0;iRr||(yg.then(()=>Rr=0),Rr=Date.now());function Sg(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;st(bg(s,n.value),t,5,[s])};return n.value=e,n.attached=Tg(),n}function bg(e,t){if(H(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>i=>!i._stopped&&s&&s(i))}else return t}const Gl=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Ng=(e,t,n,s,i,r)=>{const o=i==="svg";t==="class"?fg(e,s,o):t==="style"?dg(e,n,s):qt(t)?Ri(t)||_g(e,t,n,s,r):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Cg(e,t,s,o))?(Xl(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&$l(e,t,s,o,r,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!Y(s))?Xl(e,fe(t),s,r,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),$l(e,t,s,o))};function Cg(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Gl(t)&&G(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const i=e.tagName;if(i==="IMG"||i==="VIDEO"||i==="CANVAS"||i==="SOURCE")return!1}return Gl(t)&&Y(n)?!1:t in e}const Wl={};/*! #__NO_SIDE_EFFECTS__ */function _f(e,t,n){const s=So(e,t);As(s)&&ee(s,t);class i extends Ji{constructor(o){super(s,o,n)}}return i.def=s,i}/*! #__NO_SIDE_EFFECTS__ */const vg=(e,t)=>_f(e,t,Rf),Ag=typeof HTMLElement<"u"?HTMLElement:class{};class Ji extends Ag{constructor(t,n={},s=Qr){super(),this._def=t,this._props=n,this._createApp=s,this._isVueCE=!0,this._instance=null,this._app=null,this._nonce=this._def.nonce,this._connected=!1,this._resolved=!1,this._numberProps=null,this._styleChildren=new WeakSet,this._ob=null,this.shadowRoot&&s!==Qr?this._root=this.shadowRoot:t.shadowRoot!==!1?(this.attachShadow({mode:"open"}),this._root=this.shadowRoot):this._root=this,this._def.__asyncLoader||this._resolveProps(this._def)}connectedCallback(){if(!this.isConnected)return;this.shadowRoot||this._parseSlots(),this._connected=!0;let t=this;for(;t=t&&(t.parentNode||t.host);)if(t instanceof Ji){this._parent=t;break}this._instance||(this._resolved?(this._setParent(),this._update()):t&&t._pendingResolve?this._pendingResolve=t._pendingResolve.then(()=>{this._pendingResolve=void 0,this._resolveDef()}):this._resolveDef())}_setParent(t=this._parent){t&&(this._instance.parent=t._instance,this._instance.provides=t._instance.provides)}disconnectedCallback(){this._connected=!1,Bi(()=>{this._connected||(this._ob&&(this._ob.disconnect(),this._ob=null),this._app&&this._app.unmount(),this._instance&&(this._instance.ce=void 0),this._app=this._instance=null)})}_resolveDef(){if(this._pendingResolve)return;for(let s=0;s{for(const i of s)this._setAttr(i.attributeName)}),this._ob.observe(this,{attributes:!0});const t=(s,i=!1)=>{this._resolved=!0,this._pendingResolve=void 0;const{props:r,styles:o}=s;let l;if(r&&!H(r))for(const c in r){const f=r[c];(f===Number||f&&f.type===Number)&&(c in this._props&&(this._props[c]=as(this._props[c])),(l||(l=Object.create(null)))[fe(c)]=!0)}this._numberProps=l,i&&this._resolveProps(s),this.shadowRoot&&this._applyStyles(o),this._mount(s)},n=this._def.__asyncLoader;n?this._pendingResolve=n().then(s=>t(this._def=s,!0)):t(this._def)}_mount(t){this._app=this._createApp(t),t.configureApp&&t.configureApp(this._app),this._app._ceVNode=this._createVNode(),this._app.mount(this._root);const n=this._instance&&this._instance.exposed;if(n)for(const s in n)ie(this,s)||Object.defineProperty(this,s,{get:()=>Hi(n[s])})}_resolveProps(t){const{props:n}=t,s=H(n)?n:Object.keys(n||{});for(const i of Object.keys(this))i[0]!=="_"&&s.includes(i)&&this._setProp(i,this[i]);for(const i of s.map(fe))Object.defineProperty(this,i,{get(){return this._getProp(i)},set(r){this._setProp(i,r,!0,!0)}})}_setAttr(t){if(t.startsWith("data-v-"))return;const n=this.hasAttribute(t);let s=n?this.getAttribute(t):Wl;const i=fe(t);n&&this._numberProps&&this._numberProps[i]&&(s=as(s)),this._setProp(i,s,!1,!0)}_getProp(t){return this._props[t]}_setProp(t,n,s=!0,i=!1){if(n!==this._props[t]&&(n===Wl?delete this._props[t]:(this._props[t]=n,t==="key"&&this._app&&(this._app._ceVNode.key=n)),i&&this._instance&&this._update(),s)){const r=this._ob;r&&r.disconnect(),n===!0?this.setAttribute(De(t),""):typeof n=="string"||typeof n=="number"?this.setAttribute(De(t),n+""):n||this.removeAttribute(De(t)),r&&r.observe(this,{attributes:!0})}}_update(){Of(this._createVNode(),this._root)}_createVNode(){const t={};this.shadowRoot||(t.onVnodeMounted=t.onVnodeUpdated=this._renderSlots.bind(this));const n=he(this._def,ee(t,this._props));return this._instance||(n.ce=s=>{this._instance=s,s.ce=this,s.isCE=!0;const i=(r,o)=>{this.dispatchEvent(new CustomEvent(r,As(o[0])?ee({detail:o},o[0]):{detail:o}))};s.emit=(r,...o)=>{i(r,o),De(r)!==r&&i(De(r),o)},this._setParent()}),n}_applyStyles(t,n){if(!t)return;if(n){if(n===this._def||this._styleChildren.has(n))return;this._styleChildren.add(n)}const s=this._nonce;for(let i=t.length-1;i>=0;i--){const r=document.createElement("style");s&&r.setAttribute("nonce",s),r.textContent=t[i],this.shadowRoot.prepend(r)}}_parseSlots(){const t=this._slots={};let n;for(;n=this.firstChild;){const s=n.nodeType===1&&n.getAttribute("slot")||"default";(t[s]||(t[s]=[])).push(n),this.removeChild(n)}}_renderSlots(){const t=(this._teleportTarget||this).querySelectorAll("slot"),n=this._instance.type.__scopeId;for(let s=0;s(delete e.props.mode,e),Mg=Rg({name:"TransitionGroup",props:ee({},uf,{tag:String,moveClass:String}),setup(e,{slots:t}){const n=it(),s=yo();let i,r;return Ki(()=>{if(!i.length)return;const o=e.moveClass||`${e.name||"v"}-move`;if(!Dg(i[0].el,n.vnode.el,o))return;i.forEach(Lg),i.forEach(xg);const l=i.filter(wg);Jr(),l.forEach(c=>{const f=c.el,a=f.style;pt(f,o),a.transform=a.webkitTransform=a.transitionDuration="";const u=f[Ti]=p=>{p&&p.target!==f||(!p||/transform$/.test(p.propertyName))&&(f.removeEventListener("transitionend",u),f[Ti]=null,wt(f,o))};f.addEventListener("transitionend",u)})}),()=>{const o=se(e),l=hf(o);let c=o.tag||ve;if(i=[],r)for(let f=0;f{l.split(/\s+/).forEach(c=>c&&s.classList.remove(c))}),n.split(/\s+/).forEach(l=>l&&s.classList.add(l)),s.style.display="none";const r=t.nodeType===1?t:t.parentNode;r.appendChild(s);const{hasTransform:o}=pf(s);return r.removeChild(s),o}const Gt=e=>{const t=e.props["onUpdate:modelValue"]||!1;return H(t)?n=>ln(t,n):t};function kg(e){e.target.composing=!0}function Yl(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const tt=Symbol("_assign"),Si={created(e,{modifiers:{lazy:t,trim:n,number:s}},i){e[tt]=Gt(i);const r=s||i.props&&i.props.type==="number";Nt(e,t?"change":"input",o=>{if(o.target.composing)return;let l=e.value;n&&(l=l.trim()),r&&(l=cs(l)),e[tt](l)}),n&&Nt(e,"change",()=>{e.value=e.value.trim()}),t||(Nt(e,"compositionstart",kg),Nt(e,"compositionend",Yl),Nt(e,"change",Yl))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:i,number:r}},o){if(e[tt]=Gt(o),e.composing)return;const l=(r||e.type==="number")&&!/^0\d/.test(e.value)?cs(e.value):e.value,c=t??"";l!==c&&(document.activeElement===e&&e.type!=="range"&&(s&&t===n||i&&e.value.trim()===c)||(e.value=c))}},xo={deep:!0,created(e,t,n){e[tt]=Gt(n),Nt(e,"change",()=>{const s=e._modelValue,i=Dn(e),r=e.checked,o=e[tt];if(H(s)){const l=Os(s,i),c=l!==-1;if(r&&!c)o(s.concat(i));else if(!r&&c){const f=[...s];f.splice(l,1),o(f)}}else if(Yt(s)){const l=new Set(s);r?l.add(i):l.delete(i),o(l)}else o(bf(e,r))})},mounted:Jl,beforeUpdate(e,t,n){e[tt]=Gt(n),Jl(e,t,n)}};function Jl(e,{value:t,oldValue:n},s){e._modelValue=t;let i;if(H(t))i=Os(t,s.props.value)>-1;else if(Yt(t))i=t.has(s.props.value);else{if(t===n)return;i=Ot(t,bf(e,!0))}e.checked!==i&&(e.checked=i)}const wo={created(e,{value:t},n){e.checked=Ot(t,n.props.value),e[tt]=Gt(n),Nt(e,"change",()=>{e[tt](Dn(e))})},beforeUpdate(e,{value:t,oldValue:n},s){e[tt]=Gt(s),t!==n&&(e.checked=Ot(t,s.props.value))}},Sf={deep:!0,created(e,{value:t,modifiers:{number:n}},s){const i=Yt(t);Nt(e,"change",()=>{const r=Array.prototype.filter.call(e.options,o=>o.selected).map(o=>n?cs(Dn(o)):Dn(o));e[tt](e.multiple?i?new Set(r):r:r[0]),e._assigning=!0,Bi(()=>{e._assigning=!1})}),e[tt]=Gt(s)},mounted(e,{value:t}){zl(e,t)},beforeUpdate(e,t,n){e[tt]=Gt(n)},updated(e,{value:t}){e._assigning||zl(e,t)}};function zl(e,t){const n=e.multiple,s=H(t);if(!(n&&!s&&!Yt(t))){for(let i=0,r=e.options.length;iString(f)===String(l)):o.selected=Os(t,l)>-1}else o.selected=t.has(l);else if(Ot(Dn(o),t)){e.selectedIndex!==i&&(e.selectedIndex=i);return}}!n&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Dn(e){return"_value"in e?e._value:e.value}function bf(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const Nf={created(e,t,n){qs(e,t,n,null,"created")},mounted(e,t,n){qs(e,t,n,null,"mounted")},beforeUpdate(e,t,n,s){qs(e,t,n,s,"beforeUpdate")},updated(e,t,n,s){qs(e,t,n,s,"updated")}};function Cf(e,t){switch(e){case"SELECT":return Sf;case"TEXTAREA":return Si;default:switch(t){case"checkbox":return xo;case"radio":return wo;default:return Si}}}function qs(e,t,n,s,i){const o=Cf(e.tagName,n.props&&n.props.type)[i];o&&o(e,t,n,s)}function Fg(){Si.getSSRProps=({value:e})=>({value:e}),wo.getSSRProps=({value:e},t)=>{if(t.props&&Ot(t.props.value,e))return{checked:!0}},xo.getSSRProps=({value:e},t)=>{if(H(e)){if(t.props&&Os(e,t.props.value)>-1)return{checked:!0}}else if(Yt(e)){if(t.props&&e.has(t.props.value))return{checked:!0}}else if(e)return{checked:!0}},Nf.getSSRProps=(e,t)=>{if(typeof t.type!="string")return;const n=Cf(t.type.toUpperCase(),t.props&&t.props.type);if(n.getSSRProps)return n.getSSRProps(e,t)}}const Vg=["ctrl","shift","alt","meta"],Ug={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>Vg.some(n=>e[`${n}Key`]&&!t.includes(n))},Hg=(e,t)=>{const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(i,...r)=>{for(let o=0;o{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=i=>{if(!("key"in i))return;const r=De(i.key);if(t.some(o=>o===r||Bg[o]===r))return e(i)})},vf=ee({patchProp:Ng},rg);let ls,Ql=!1;function Af(){return ls||(ls=Da(vf))}function If(){return ls=Ql?ls:ka(vf),Ql=!0,ls}const Of=(...e)=>{Af().render(...e)},Xg=(...e)=>{If().hydrate(...e)},Qr=(...e)=>{const t=Af().createApp(...e),{mount:n}=t;return t.mount=s=>{const i=Pf(s);if(!i)return;const r=t._component;!G(r)&&!r.render&&!r.template&&(r.template=i.innerHTML),i.nodeType===1&&(i.textContent="");const o=n(i,!1,Mf(i));return i instanceof Element&&(i.removeAttribute("v-cloak"),i.setAttribute("data-v-app","")),o},t},Rf=(...e)=>{const t=If().createApp(...e),{mount:n}=t;return t.mount=s=>{const i=Pf(s);if(i)return n(i,!0,Mf(i))},t};function Mf(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Pf(e){return Y(e)?document.querySelector(e):e}let Zl=!1;const jg=()=>{Zl||(Zl=!0,Fg(),ug())},Kg=Object.freeze(Object.defineProperty({__proto__:null,BaseTransition:ua,BaseTransitionPropsValidators:To,Comment:ye,DeprecationTypes:ng,EffectScope:fo,ErrorCodes:op,ErrorTypeStrings:Yd,Fragment:ve,KeepAlive:xp,ReactiveEffect:fs,Static:fn,Suspense:Od,Teleport:gp,Text:Xt,TrackOpTypes:Zh,Transition:lg,TransitionGroup:Pg,TriggerOpTypes:ep,VueElement:Ji,assertNumber:rp,callWithAsyncErrorHandling:st,callWithErrorHandling:jn,camelize:fe,capitalize:Jt,cloneVNode:mt,compatUtils:tg,computed:rf,createApp:Qr,createBlock:gi,createCommentVNode:Vd,createElementBlock:wd,createElementVNode:Mo,createHydrationRenderer:ka,createPropsRestProxy:td,createRenderer:Da,createSSRApp:Rf,createSlots:Hp,createStaticVNode:Fd,createTextVNode:Po,createVNode:he,customRef:zc,defineAsyncComponent:Pp,defineComponent:So,defineCustomElement:_f,defineEmits:Kp,defineExpose:Gp,defineModel:Yp,defineOptions:Wp,defineProps:jp,defineSSRCustomElement:vg,defineSlots:qp,devtools:Jd,effect:Sh,effectScope:Eh,getCurrentInstance:it,getCurrentScope:Pc,getCurrentWatcher:tp,getTransitionRawChildren:Xi,guardReactiveProps:za,h:of,handleError:yn,hasInjectionContext:fd,hydrate:Xg,hydrateOnIdle:vp,hydrateOnInteraction:Rp,hydrateOnMediaQuery:Op,hydrateOnVisible:Ip,initCustomFormatter:Gd,initDirectivesForSSR:jg,inject:rs,isMemoSame:lf,isProxy:Ui,isReactive:Bt,isReadonly:Kt,isRef:Ne,isRuntimeOnly:Xd,isShallow:Ze,isVNode:Mt,markRaw:qc,mergeDefaults:Zp,mergeModels:ed,mergeProps:Qa,nextTick:Bi,normalizeClass:Xn,normalizeProps:Sc,normalizeStyle:$n,onActivated:pa,onBeforeMount:ma,onBeforeUnmount:Gi,onBeforeUpdate:No,onDeactivated:da,onErrorCaptured:Ta,onMounted:Ms,onRenderTracked:ya,onRenderTriggered:Ea,onScopeDispose:yh,onServerPrefetch:_a,onUnmounted:Wi,onUpdated:Ki,onWatcherCleanup:Zc,openBlock:ys,popScopeId:up,provide:va,proxyRefs:mo,pushScopeId:fp,queuePostFlushCb:ps,reactive:Fi,readonly:go,ref:ns,registerRuntimeCompiler:$d,render:Of,renderList:Up,renderSlot:Bp,resolveComponent:kp,resolveDirective:Vp,resolveDynamicComponent:Fp,resolveFilter:eg,resolveTransitionHooks:Ln,setBlockTracking:Xr,setDevtoolsHook:zd,setTransitionHooks:Rt,shallowReactive:Wc,shallowReadonly:Bh,shallowRef:Yc,ssrContextKey:Ha,ssrUtils:Zd,stop:bh,toDisplayString:ao,toHandlerKey:on,toHandlers:$p,toRaw:se,toRef:Jh,toRefs:Wh,toValue:jh,transformVNodeArgs:Dd,triggerRef:Xh,unref:Hi,useAttrs:Qp,useCssModule:Og,useCssVars:hg,useHost:Ef,useId:_p,useModel:Sd,useSSRContext:Ba,useShadowRoot:Ig,useSlots:zp,useTemplateRef:Ep,useTransitionState:yo,vModelCheckbox:xo,vModelDynamic:Nf,vModelRadio:wo,vModelSelect:Sf,vModelText:Si,vShow:gf,version:cf,warn:qd,watch:Rn,watchEffect:Ed,watchPostEffect:yd,watchSyncEffect:$a,withAsyncContext:nd,withCtx:Eo,withDefaults:Jp,withDirectives:pp,withKeys:$g,withMemo:Wd,withModifiers:Hg,withScopeId:hp},Symbol.toStringTag,{value:"Module"}));/** +* @vue/compiler-core v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/const kn=Symbol(""),Pn=Symbol(""),zi=Symbol(""),Ts=Symbol(""),Do=Symbol(""),Wt=Symbol(""),ko=Symbol(""),Fo=Symbol(""),Qi=Symbol(""),Zi=Symbol(""),Kn=Symbol(""),er=Symbol(""),Vo=Symbol(""),tr=Symbol(""),nr=Symbol(""),sr=Symbol(""),ir=Symbol(""),rr=Symbol(""),or=Symbol(""),Uo=Symbol(""),Ho=Symbol(""),xs=Symbol(""),Ss=Symbol(""),lr=Symbol(""),cr=Symbol(""),Fn=Symbol(""),Gn=Symbol(""),ar=Symbol(""),bi=Symbol(""),Lf=Symbol(""),Ni=Symbol(""),bs=Symbol(""),xf=Symbol(""),wf=Symbol(""),fr=Symbol(""),Df=Symbol(""),kf=Symbol(""),ur=Symbol(""),Bo=Symbol(""),dn={[kn]:"Fragment",[Pn]:"Teleport",[zi]:"Suspense",[Ts]:"KeepAlive",[Do]:"BaseTransition",[Wt]:"openBlock",[ko]:"createBlock",[Fo]:"createElementBlock",[Qi]:"createVNode",[Zi]:"createElementVNode",[Kn]:"createCommentVNode",[er]:"createTextVNode",[Vo]:"createStaticVNode",[tr]:"resolveComponent",[nr]:"resolveDynamicComponent",[sr]:"resolveDirective",[ir]:"resolveFilter",[rr]:"withDirectives",[or]:"renderList",[Uo]:"renderSlot",[Ho]:"createSlots",[xs]:"toDisplayString",[Ss]:"mergeProps",[lr]:"normalizeClass",[cr]:"normalizeStyle",[Fn]:"normalizeProps",[Gn]:"guardReactiveProps",[ar]:"toHandlers",[bi]:"camelize",[Lf]:"capitalize",[Ni]:"toHandlerKey",[bs]:"setBlockTracking",[xf]:"pushScopeId",[wf]:"popScopeId",[fr]:"withCtx",[Df]:"unref",[kf]:"isRef",[ur]:"withMemo",[Bo]:"isMemoSame"};function Ff(e){Object.getOwnPropertySymbols(e).forEach(t=>{dn[t]=e[t]})}const Gg={HTML:0,0:"HTML",SVG:1,1:"SVG",MATH_ML:2,2:"MATH_ML"},Wg={ROOT:0,0:"ROOT",ELEMENT:1,1:"ELEMENT",TEXT:2,2:"TEXT",COMMENT:3,3:"COMMENT",SIMPLE_EXPRESSION:4,4:"SIMPLE_EXPRESSION",INTERPOLATION:5,5:"INTERPOLATION",ATTRIBUTE:6,6:"ATTRIBUTE",DIRECTIVE:7,7:"DIRECTIVE",COMPOUND_EXPRESSION:8,8:"COMPOUND_EXPRESSION",IF:9,9:"IF",IF_BRANCH:10,10:"IF_BRANCH",FOR:11,11:"FOR",TEXT_CALL:12,12:"TEXT_CALL",VNODE_CALL:13,13:"VNODE_CALL",JS_CALL_EXPRESSION:14,14:"JS_CALL_EXPRESSION",JS_OBJECT_EXPRESSION:15,15:"JS_OBJECT_EXPRESSION",JS_PROPERTY:16,16:"JS_PROPERTY",JS_ARRAY_EXPRESSION:17,17:"JS_ARRAY_EXPRESSION",JS_FUNCTION_EXPRESSION:18,18:"JS_FUNCTION_EXPRESSION",JS_CONDITIONAL_EXPRESSION:19,19:"JS_CONDITIONAL_EXPRESSION",JS_CACHE_EXPRESSION:20,20:"JS_CACHE_EXPRESSION",JS_BLOCK_STATEMENT:21,21:"JS_BLOCK_STATEMENT",JS_TEMPLATE_LITERAL:22,22:"JS_TEMPLATE_LITERAL",JS_IF_STATEMENT:23,23:"JS_IF_STATEMENT",JS_ASSIGNMENT_EXPRESSION:24,24:"JS_ASSIGNMENT_EXPRESSION",JS_SEQUENCE_EXPRESSION:25,25:"JS_SEQUENCE_EXPRESSION",JS_RETURN_STATEMENT:26,26:"JS_RETURN_STATEMENT"},qg={ELEMENT:0,0:"ELEMENT",COMPONENT:1,1:"COMPONENT",SLOT:2,2:"SLOT",TEMPLATE:3,3:"TEMPLATE"},Yg={NOT_CONSTANT:0,0:"NOT_CONSTANT",CAN_SKIP_PATCH:1,1:"CAN_SKIP_PATCH",CAN_CACHE:2,2:"CAN_CACHE",CAN_STRINGIFY:3,3:"CAN_STRINGIFY"},Ce={start:{line:1,column:1,offset:0},end:{line:1,column:1,offset:0},source:""};function Vf(e,t=""){return{type:0,source:t,children:e,helpers:new Set,components:[],directives:[],hoists:[],imports:[],cached:[],temps:0,codegenNode:void 0,loc:Ce}}function Vn(e,t,n,s,i,r,o,l=!1,c=!1,f=!1,a=Ce){return e&&(l?(e.helper(Wt),e.helper(_n(e.inSSR,f))):e.helper(mn(e.inSSR,f)),o&&e.helper(rr)),{type:13,tag:t,props:n,children:s,patchFlag:i,dynamicProps:r,directives:o,isBlock:l,disableTracking:c,isComponent:f,loc:a}}function jt(e,t=Ce){return{type:17,loc:t,elements:e}}function Qe(e,t=Ce){return{type:15,loc:t,properties:e}}function ge(e,t){return{type:16,loc:Ce,key:Y(e)?Q(e,!0):e,value:t}}function Q(e,t=!1,n=Ce,s=0){return{type:4,loc:n,content:e,isStatic:t,constType:t?3:s}}function Jg(e,t){return{type:5,loc:t,content:Y(e)?Q(e,!1,t):e}}function nt(e,t=Ce){return{type:8,loc:t,children:e}}function Ee(e,t=[],n=Ce){return{type:14,loc:n,callee:e,arguments:t}}function gn(e,t=void 0,n=!1,s=!1,i=Ce){return{type:18,params:e,returns:t,newline:n,isSlot:s,loc:i}}function Ci(e,t,n,s=!0){return{type:19,test:e,consequent:t,alternate:n,newline:s,loc:Ce}}function Uf(e,t,n=!1,s=!1){return{type:20,index:e,value:t,needPauseTracking:n,inVOnce:s,needArraySpread:!1,loc:Ce}}function Hf(e){return{type:21,body:e,loc:Ce}}function zg(e){return{type:22,elements:e,loc:Ce}}function Qg(e,t,n){return{type:23,test:e,consequent:t,alternate:n,loc:Ce}}function Zg(e,t){return{type:24,left:e,right:t,loc:Ce}}function em(e){return{type:25,expressions:e,loc:Ce}}function tm(e){return{type:26,returns:e,loc:Ce}}function mn(e,t){return e||t?Qi:Zi}function _n(e,t){return e||t?ko:Fo}function hr(e,{helper:t,removeHelper:n,inSSR:s}){e.isBlock||(e.isBlock=!0,n(mn(s,e.isComponent)),t(Wt),t(_n(s,e.isComponent)))}const ec=new Uint8Array([123,123]),tc=new Uint8Array([125,125]);function nc(e){return e>=97&&e<=122||e>=65&&e<=90}function ze(e){return e===32||e===10||e===9||e===12||e===13}function xt(e){return e===47||e===62||ze(e)}function vi(e){const t=new Uint8Array(e.length);for(let n=0;n=0;i--){const r=this.newlines[i];if(t>r){n=i+2,s=t-r;break}}return{column:s,line:n,offset:t}}peek(){return this.buffer.charCodeAt(this.index+1)}stateText(t){t===60?(this.index>this.sectionStart&&this.cbs.ontext(this.sectionStart,this.index),this.state=5,this.sectionStart=this.index):!this.inVPre&&t===this.delimiterOpen[0]&&(this.state=2,this.delimiterIndex=0,this.stateInterpolationOpen(t))}stateInterpolationOpen(t){if(t===this.delimiterOpen[this.delimiterIndex])if(this.delimiterIndex===this.delimiterOpen.length-1){const n=this.index+1-this.delimiterOpen.length;n>this.sectionStart&&this.cbs.ontext(this.sectionStart,n),this.state=3,this.sectionStart=n}else this.delimiterIndex++;else this.inRCDATA?(this.state=32,this.stateInRCDATA(t)):(this.state=1,this.stateText(t))}stateInterpolation(t){t===this.delimiterClose[0]&&(this.state=4,this.delimiterIndex=0,this.stateInterpolationClose(t))}stateInterpolationClose(t){t===this.delimiterClose[this.delimiterIndex]?this.delimiterIndex===this.delimiterClose.length-1?(this.cbs.oninterpolation(this.sectionStart,this.index+1),this.inRCDATA?this.state=32:this.state=1,this.sectionStart=this.index+1):this.delimiterIndex++:(this.state=3,this.stateInterpolation(t))}stateSpecialStartSequence(t){const n=this.sequenceIndex===this.currentSequence.length;if(!(n?xt(t):(t|32)===this.currentSequence[this.sequenceIndex]))this.inRCDATA=!1;else if(!n){this.sequenceIndex++;return}this.sequenceIndex=0,this.state=6,this.stateInTagName(t)}stateInRCDATA(t){if(this.sequenceIndex===this.currentSequence.length){if(t===62||ze(t)){const n=this.index-this.currentSequence.length;if(this.sectionStart=t||(this.state===28?this.currentSequence===Me.CdataEnd?this.cbs.oncdata(this.sectionStart,t):this.cbs.oncomment(this.sectionStart,t):this.state===6||this.state===11||this.state===18||this.state===17||this.state===12||this.state===13||this.state===14||this.state===15||this.state===16||this.state===20||this.state===19||this.state===21||this.state===9||this.cbs.ontext(this.sectionStart,t))}emitCodePoint(t,n){}}const sm={COMPILER_IS_ON_ELEMENT:"COMPILER_IS_ON_ELEMENT",COMPILER_V_BIND_SYNC:"COMPILER_V_BIND_SYNC",COMPILER_V_BIND_OBJECT_ORDER:"COMPILER_V_BIND_OBJECT_ORDER",COMPILER_V_ON_NATIVE:"COMPILER_V_ON_NATIVE",COMPILER_V_IF_V_FOR_PRECEDENCE:"COMPILER_V_IF_V_FOR_PRECEDENCE",COMPILER_NATIVE_TEMPLATE:"COMPILER_NATIVE_TEMPLATE",COMPILER_INLINE_TEMPLATE:"COMPILER_INLINE_TEMPLATE",COMPILER_FILTERS:"COMPILER_FILTERS"},im={COMPILER_IS_ON_ELEMENT:{message:'Platform-native elements with "is" prop will no longer be treated as components in Vue 3 unless the "is" value is explicitly prefixed with "vue:".',link:"https://v3-migration.vuejs.org/breaking-changes/custom-elements-interop.html"},COMPILER_V_BIND_SYNC:{message:e=>`.sync modifier for v-bind has been removed. Use v-model with argument instead. \`v-bind:${e}.sync\` should be changed to \`v-model:${e}\`.`,link:"https://v3-migration.vuejs.org/breaking-changes/v-model.html"},COMPILER_V_BIND_OBJECT_ORDER:{message:'v-bind="obj" usage is now order sensitive and behaves like JavaScript object spread: it will now overwrite an existing non-mergeable attribute that appears before v-bind in the case of conflict. To retain 2.x behavior, move v-bind to make it the first attribute. You can also suppress this warning if the usage is intended.',link:"https://v3-migration.vuejs.org/breaking-changes/v-bind.html"},COMPILER_V_ON_NATIVE:{message:".native modifier for v-on has been removed as is no longer necessary.",link:"https://v3-migration.vuejs.org/breaking-changes/v-on-native-modifier-removed.html"},COMPILER_V_IF_V_FOR_PRECEDENCE:{message:"v-if / v-for precedence when used on the same element has changed in Vue 3: v-if now takes higher precedence and will no longer have access to v-for scope variables. It is best to avoid the ambiguity with