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 ...`。
+



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, '
)/is';
+ $videoPreg = '/(